文章目录
一、文件
1.1 格式化输入输出
之前我们经常用到print("%d", 变量)、scanf("%d", 变量地址),其实在%和d之间还可以有一些东西。
printf
%[flags][width][.prec][hlL]type
scanf
%[flag]type
我们今天来彻底研究一下这些东西是什么。
1.1.1 格式化输出printf
首先看printf的格式:[flags]、[width]、[.prec]、[hlL]、type。
1、flags
Flag | 含义 |
---|---|
- | 左对齐 |
+ | 在前面放+或- |
(space) | 正数留空 |
0 | 0填充 |
我们先写段代码来看看 + 和 - 的功能:
#include <stdio.h>
int main(int argc, char const *argv[])
{
printf("%+9d\n",123);
printf("%-+9d\n",-123);
return 0;
}
运行,可以看出我的字符的输出要占据9个字节的空间,但是它是靠右对齐的。当使用了 - 之后,就靠左对齐了。
此外,我们测试填充0的功能,可以看出左对齐时无法填充0。
2、width
width或prec | 含义 |
---|---|
number | 最小字符数 |
* | 下一个参数是字符数 |
.number | 小数点后的位数 |
.* | 下一个参数是小数点后的位数 |
我们写段代码测试一下功能。
#include <stdio.h>
int main(int argc, char const *argv[])
{
printf("%*d\n",6, 123);
printf("%9.2f\n",123.0);
return 0;
}
运行,结果如下图所示。
3、hIL
类型修饰 | 含义 |
---|---|
hh | 单个字节 |
h | short |
I | long |
II | long long |
L | long double |
我们写段代码测试
#include <stdio.h>
int main(int argc, char const *argv[])
{
printf("%hhd\n",12345);
return 0;
}
结果如下
4、type
type | 用于 | type | 用于 |
---|---|---|---|
i或d | int | g | float |
u | unsigned int | G | float |
o | 八进制 | a或A | 十六进制浮点 |
x | 十六进制 | c | char |
X | 字母大写的十六进制 | s | 字符串 |
f或F | float,6位有效数字 | p | 指针 |
e或E | 指数 | n | 读入/写出的个数 |
我们测试下n的作用。
#include <stdio.h>
int main(int argc, char const *argv[])
{
int num;
printf("%dty%n\n", 12345, &num);
printf("%d\n", num);
return 0;
}
运行,n的值为7。它的作用就是统计当前输出字符的个数,这里输出了12345ty,刚好是7个。(注意:Visual Studio中无法通过编译)。
1.1.2 格式化输入scanf
我们接着看看scanf的部分。
1、flag
flag | 含义 | flag | 含义 |
---|---|---|---|
* | 跳过 | I | long,double |
数字 | 最大字符数 | II | long long |
hh | char | L | long double |
h | short |
2、type
type | 用于 | type | 用于 |
---|---|---|---|
d | int | s | 字符串(单词) |
i | 整数,可能为十六进制或八进制 | […] | 所允许的字符 |
u | unsigned int | p | 指针 |
o | 八进制 | ||
x | 十六进制 | ||
a,e,f,g | float | ||
c | char |
我们来看下 i 的用法:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(int argc, char const *argv[])
{
int num;
scanf("%i",&num);
printf("%d\n", num);
return 0;
}
可以看出,当我们以各种进制输入时,均能输出对应十进制的东西。
我们再来看看[ ]的作用。
现在有一些GPS信息:
//$GPRMC,004319.00,A,3016.98468,N,12006.39211,E,
0.047,,130909,,,D*79
输入如下:
scanf("%*[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,]",
sTime,sAV,sLati,&sNW,sLong,&sEW,sSpeed,sAngle,sDate);
其中[^,]表示读取逗号前面的所有东西
1.1.3 printf与scanf的返回值
• scanf返回读入的项目数
• printf返回输出的字符数
• 在要求严格的程序中,应该判断每次调用scanf或printf的返回值,从而了解程序运行中是否存在问题
我们写出代码来测试看看
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(int argc, char const *argv[])
{
int num;
int i1 = scanf("%i",&num);
int i2 = printf("%d\n", num);
printf("%d:%d\n", i1, i2);
return 0;
}
运行,结果如下:
1.2 文件输入输出
在C语言中,对文件读写通常用FILE。这一节我们罗列出FILE的一些函数及操作符。
FILE相关函数
• FILE* fopen(const char * restrict path, const char *restrict mode);
• int fclose(FILE *stream);
• fscanf(FILE*, ...)
• fprintf(FILE*, ...)
打开文件的标准代码
FILE* fp = fopen(“file”,“r”); //打开的路径,打开的方式
if ( fp ) {
fscanf(fp,...);
fclose(fp);
} else {
...
}
fopen
标识 | 打开方式 |
---|---|
r | 打开只读 |
r+ | 打开读写,从文件头开始 |
w | 打开只写。如果不存在则新建,如果存在则清空 |
w+ | 打开读写。如果不存在则新建,如果存在则清空 |
a | 打开追加。如果不存在则新建,如果存在则从文件尾开始 |
…x | 只新建,如果文件已存在则不能打开 |
我们来写段代码看看fopen的使用。首先我有一个文档a.txt,里面有个数字12345。
接着去读取里面的内容。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(int argc, char const *argv[])
{
FILE *fp = fopen("D:\\C\\Exercise\\1\\ConsoleApplication1\\Release\\a.txt", "r");
if (fp) {
int num;
fscanf(fp, "%d", &num);
printf("%d\n", num);
fclose(fp);
}
else
{
printf("无法打开文件\n");
}
return 0;
}
运行,可以看出成功读取到了12345
1.3 二进制文件
二进制文件
• 其实所有的文件最终都是二进制的
• 文本文件无非是用最简单的方式可以读写的文件
• 而二进制文件是需要专门的程序来读写的文件
• 文本文件的输入输出是格式化,可能经过转码
文本VS二进制
• Unix喜欢用文本文件来做数据存储和程序配置
• 交互式终端的出现使得人们喜欢用文本和计算机“talk”
• Unix的shell提供了⼀些读写文本的⼩程序
• Windows喜欢用⼆进制⽂件
• DOS是草根⽂化,并不继承和熟悉Unix文化
• PC刚开始的时候能力有限,DOS的能力更有限,⼆进制更接近底层
• 文本的优势是方便人类读写,而且跨平台
• 文本的缺点是程序输入输出要经过格式化,开销⼤
• ⼆进制的缺点是⼈类读写困难,而且不跨平台
• int的大小不⼀致,大小端的问题...
• ⼆进制的优点是程序读写快
程序为什么要文件
为什么我们写程序要操作文件呢?一般来说有以下三种:
• 配置
• 如窗口多大?字体什么颜色?
• Unix用文本,Windows用注册表
• 数据
• 有数据需要保存,比如保存学生的成绩
• 稍微有点量的数据都放数据库了
• 媒体
• 图片、语音、视频等
• 这个只能是二进制的
• 现实是,程序通过第三方库来读写文件,很少直接读写二进制文件了
二进制读写
size_t fread(void *restrict ptr, size_t size, size_t nitems, FILE *restrict stream);
size_t fwrite(const void *restrict ptr, size_t size, size_t nitems, FILE *restrict stream);
第一个参数是个指针,表示你要读写的那块内存
第二个参数是这块内存有多大
第三个参数是有几个这样的内存
第四个参数是文件指针
• 注意FILE指针是最后一个参数
• 返回的是成功读写的字节数
• 为什么有nitem?
• 因为二进制文件的读写一般都是通过对一个结构变量的操作来进行的
• 于是nitem就是用来说明这次读写几个结构变量
我们写一些代码来测试一下二进制读写的功能。我们定义一个结构体,里面包含学生信息。我们输入一些学生的信息到结构体数组中,并将这个结构体数组以二进制方式保存到文件student.data中。
主函数
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include "student.h"
#include <stdlib.h>
void getList(Student aStu[], int number);
int save(Student aStu[], int number);
int main(int argc, char const *argv[])
{
int number = 0;
printf("输入学生的数量:");
scanf("%d", &number);
Student *aStu = (Student*)malloc(sizeof(Student)*number);
getList(aStu, number);
if (save(aStu, number)) {
printf("保存成功\n");
}
else {
printf("保存失败\n");
}
free(aStu);
return 0;
}
void getList(Student aStu[], int number)
{
char format[20];
sprintf(format, "%%%ds", STR_LEN-1); //format = "%19s"
int i;
for (i = 0; i < number; i++) {
printf("第%d个学生:\n", i);
printf("\t姓名:");
scanf(format, aStu[i].name);
printf("\t性别(0-男,1-女,2-其他):");
scanf("%d", &aStu[i].gender);
printf("\t年龄:");
scanf("%d", &aStu[i].age);
}
}
int save(Student aStu[], int number)
{
int ret = -1;
FILE *fp = fopen("student.data", "w");
if (fp) {
ret = fwrite(aStu, sizeof(Student), number, fp);
fclose(fp);
}
return ret == number;
}
student.h
#pragma once
const int STR_LEN = 20;
typedef struct _student {
char name[20];
int gender;
int age;
}Student;
我们运行,输入两个学生的信息,可以看出成功写入。
然后可以看到刚才写入信息的文件:
在文件中定位
我们可以知道现在在文件中的什么位置
• long ftell(FILE *stream);
• int fseek(FILE *stream, long offset, int whence);
• SEEK_SET:从头开始
• SEEK_CUR:从当前位置开始
• SEEK_END:从尾开始(倒过来)
我们接着上面的程序,来打开文件进行读取。我们这里打开上面保存的student.data文件,然后自定义读取第几个学生的信息。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include "student.h"
#include <stdlib.h>
void read(FILE *fp, int index);
int main(int argc, char const *argv[])
{
FILE *fp = fopen("student.data", "r");
if (fp) {
fseek(fp, 0L, SEEK_END); //移动到文件的尾巴上
long size = ftell(fp); //得到现在的位置,其实就是文件的大小
int number = size/sizeof(Student);
int index=0;
printf("有%d个数据,你要看第几个:", number);
scanf("%d", &index);
read(fp, index-1);
fclose(fp);
}
return 0;
}
void read(FILE *fp, int index)
{
//走到第index个学生的位置上去
fseek(fp, index*sizeof(Student), SEEK_SET);
Student stu;
if (fread(&stu, sizeof(Student), 1, fp) == 1)
{
printf("第%d个学生:", index+1);
printf("\t姓名:%s\n", stu.name);
printf("\t性别:");
switch (stu.gender) {
case 0: printf("男\n"); break;
case 1: printf("女\n"); break;
}
}
}
运行,我们这里读取第二个学生的信息,可以看出正确读到了信息。
可移植性
• 这样的二进制文件不具有可移植性
• 在int为32位的机器上写成的数据文件无法直接在int为64位的机器上正确读出
• 解决方案之一是放弃使用int,而是typedef具有明确大小的类型
• 更好的方案是用文本
二、位运算
2.1 按位运算
C语言提供了许多操作底层的运算,有下面这些:
这些操作符都是将整数当作二进制来做计算
& 按位的与
| 按位的或
~ 按位取反
^ 按位的异或
<< 左移
>> 右移
1、按位与 &
•如果 ( x ) i = = 1 {(x)_i} = = 1 (x)i==1并且 ( y ) i = = 1 {(y)_i} = = 1 (y)i==1,那么 ( x & y ) i = 1 {(x\& y)_i} = 1 (x&y)i=1
•否则的话 ( x & y ) i = 0 {(x\& y)_i} = 0 (x&y)i=0
原理如下图所示:
按位与常用于两种应用:
• 让某一位或某些位为0:x & 0xFE
0与上任何数都为0,所以可以让某些位为0
• 取一个数中的一段:x & 0xFF
1与上另外一个数仍然得到另外那个数,因此假设有一个int类型整数有4个字节,
我们用0x00 00 00 FF与上那个数,可以去取得int最后的1个字节。
2、按位或 |
•如果 ( x ) i = = 1 {(x)_i} = = 1 (x)i==1或 ( y ) i = = 1 {(y)_i} = = 1 (y)i==1,那么 ( x ∣ y ) i = 1 {(x | y)_i} = 1 (x∣y)i=1
•否则的话 ( x ∣ y ) i = 0 {(x| y)_i} = 0 (x∣y)i=0
原理如下图所示:
按位或常用于两种应用:
• 使得一位或几个位为1:x | 0x01
• 把两个数拼起来:0x00FF | 0xFF00
3、按位反 ~
~ ( x ) i = 1 − ( x ) i {(x)_i} = 1 - {(x)_i} (x)i=1−(x)i
•把1位变0,0位变1
•想得到全部位为1的数:~0
•7的二进制是0111,x | 7使得低3位为1,而
•x & ~7,就使得低3位为0
按位取反和补码是不一样的,我们写段代码测试一下:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(int argc, char const *argv[])
{
unsigned char c = 0xAA;
printf(" c=%hhx\n", c);
printf("~c=%hhx\n", (char)~c);
printf("-c=%hhx\n", (char)-c);
return 0;
}
运行,可以看出按位取反和补码结果不一样。
逻辑运算vs按位运算
• 对于逻辑运算,它只看到两个值:0和1
• 可以认为逻辑运算相当于把所有非0值都变成1,然后做按位运算
• 5 & 4 —>4而 5 && 4 —> 1 & 1 —> 1
• 5 | 4 —> 5而 5 || 4 —> 1 | 1 —> 1
• ~4 —> 3而 !4 —> !1 —> 0
4、按位异或^
•如果 ( x ) i = = ( y ) i {(x)_i} = = {(y)_i} (x)i==(y)i,那么 ( x {(x } (x ^ y ) i = 0 {y)_i} = 0 y)i=0
•否则的话, ( x {(x } (x ^ y ) i = 1 {y)_i} = 1 y)i=1
• 如果两个位相等,那么结果为0;不相等,结果为1
• 如果x和y相等,那么x ^ y的结果为0
• 对一个变量用同一个值异或两次,等于什么也没做
• x ^ y ^ y —> x
2.2 移位运算
1、左移 <<
• i << j
• i中所有的位向左移动j个位置,而右边填入0
• 所有小于int的类型,移位以int的方式来做,结果是int
• x <<= 1 等价于 x *= 2
• x <<= n 等价于 x *= 2的n次幂.
我们写段代码看看左移的功能。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(int argc, char const *argv[])
{
unsigned char c = 0xA5;
//以10进制输出c的值
printf(" c=%d\n", c);
printf("c<<2=%d\n", c<<2);
return 0;
}
以十进制的方式输出这两个数,可以看出左移两位后刚好为原来值的4倍。
我们来看看有符号数左移会怎样
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(int argc, char const *argv[])
{
//4字节的int能够表达的最大的负数
int a = 0x80000000;
//100000000...000 二进制的a
//00000...0000000 a左移一位之后
unsigned int b = 0x80000000;
printf("a=%d\n", a);
printf("b=%u\n", b);
printf("a<<1=%d\n", a<<1);
printf("b<<1=%u\n", b<<1);
return 0;
}
运行,移动后a和b的值都为0,可以看出往左移动是不管符号位的。
2、右移 >>
• i >> j
• i中所有的位向右移j位
• 所有小于int的类型,移位以int的方式来做,结果是int
• 对于unsigned的类型,左边填入0
• 对于signed的类型,左边填入原来的最高位(保持符号不变)
• x >>= 1 等价于 x /= 2
• x >>= n 等价于 x /= 2的n次幂
我们来试一下signed类型做右移运算
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(int argc, char const *argv[])
{
//4字节的int能够表达的最大的负数
int a = 0x80000000;
//100000000...000 二进制的a
//110000...000000 a右移一位之后
unsigned int b = 0x80000000;
printf("a=%d\n", a);
printf("b=%u\n", b);
printf("a>>1=%d\n", a>>1);
printf("b>>1=%u\n", b>>1);
return 0;
}
运行,可以看出有符号数b(负数)右移过后仍然为负数,说明b开头的1没有被移动走。可以看出右移是要考虑符号位的。
注意:
• 移位的位数不要用负数,这是没有定义的行为
• x << -2 //!!NO!!
2.3 位运算例子
问题:输出一个数的二进制数
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(int argc, char const *argv[])
{
int number;
//scanf("%d", &number);
number = 0xaaaaaaaa;
unsigned mask = 1u<<31;
for (; mask; mask >>= 1) {
printf("%d", number & mask?1:0);
}
printf("\n");
return 0;
}
运行,测试一下0xaaaaaaaa的二进制数是多少。
2.3 位段
位段:把⼀个int的若干位组合成⼀个结构,如下所示。其中,数字代表占据多少个bit。总的来说,这些成员放在一个int里面,而其中的成员占据某些比特。这样就可以表达单片机中某些位置了。
struct {
unsigned int leading : 3;
unsigned int FLAG1: 1;
unsigned int FLAG2: 1;
int trailing: 11;
};
我们来看个例子。这里有一个位段,其中有4个成员,分别占据3、1、1、27个bit,然后对其赋值,看看这个位段值的变化。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
//输出一个数对应的二进制数
void prtBin(unsigned int number);
//位段,共4个字节,32bit,成员分别占据3、1、1、27个bit
struct U0 {
unsigned int leading: 3;
unsigned int FLAG1: 1;
unsigned int FLAG2: 1;
int trailing: 27;
};
int main(int argc, char const *argv[])
{
struct U0 uu;
uu.leading = 2;
uu.FLAG1 = 0;
uu.FLAG2 = 1;
uu.trailing = 0;
printf("sizeof(uu)=%lu\n", sizeof(uu));
//我们取到uu的地址,然后强制转换成int*指针,再取这个指针所指的int。
prtBin(*(int*)&uu);
return 0;
}
void prtBin(unsigned int number)
{
unsigned mask = 1u << 31;
for (; mask; mask >>= 1) {
printf("%d", number & mask ? 1 : 0);
}
printf("\n");
}
运行,结果正确,详细情况如下:
如果我们做点改动,删除trailing这个成员,则前面27位的值会乱了,这是因为我们没有去做初始化。
我们再试着让trailing这个成员占据32个bit,可以看到这个位段大小变成8个字节了。
总结:
• 可以直接用位段的成员名称来访问
• 比移位、与、或还方便
• 编译器会安排其中的位的排列,不具有可移植性
• 当所需的位超过一个int时会采用多个int