1.数据的存储
1.1 为什么数据在内存中存放的是补码
- 因为CPU只有加法器,而使用补码,就可以将符号位和数值域统一处理(即统一处理加法和减法)且不会需要额外的硬件电路。
1.2 为什么会有大小端
- 这是因为在计算机系统中,是以字节为单位的,比如: 每个地址单元都对应着一个字节
- 而位数大于8位的处理器,比如:16位,32位处理器,由于寄存器宽度大于一个字节,那么必然会存在如何将多个字节安排的问题,这就导致出现的大,小端存储
1.3. 验证机器大小端
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int check_sys()
{
int a = 1;//0x00000001
char* p = (char*)&a;//int*
return *p;//返回1表示小端,返回0表示大端
}
int main()
{
//写代码判断当前机器的字节序
int ret = check_sys();
if (ret == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
- char*类型的指针,解引用访问的是一个char的大小
- vs2022采用的是小端存储模式
1.4 浮点型在内存中的存储
一个数的存入和它的取出是息息相关的
1.4.1 案例展示
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
int n = 9;
float* pFloat = (float*)&n;
printf("n的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);
*pFloat = 9.0;
printf("num的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);
return 0;
}
1.4.2 浮点型在内存中的存储形式
- 根据国际标准IEEE(电气和电子工程协会)754,任意一个二进制浮点数都可以用上面的形式保存
- (1)^S表示符号位,当S=0,V为正数;当S=1,V为负数。
- M表示有效数字,大于等于1,小于2。
- 2^E表示指数位
1.4.3 对于32位的浮点数
- 最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M
1.4.4 对于64位的浮点数
- 最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M
1.4.5 对有效数字M和指数E的特别规定
- 有效数字M的取值范围是[1,2),即M可以写成1.XXXX的形式,其中XXXX表示为小数部分
- IEEE 754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保留后面的XXXX部分
- 以32位浮点数为例,比如保存1.01的时候,
- 将1舍去, 只保存01,M就会有24为有效位
- 等到需要读取的时候,再把第1位的1加上去
1.4.6 指数E在内存中的存储
E为一个无符号整数(unsigned int)
- 如果E为8位,它的取值范围为0~255;
如果E为11位,它的取值范围为0~2047。 - 由于科学计数法中的E是可以出现负数的,
所以IEEE 754规定,存入内存时E的真实值必须再加上一个中间数, - 对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。
比如,2^10的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001。
1.4.7 指数E从内存中取出
情况一:E不全为0或不全为1
- 指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上第一位的1。
情况二:E全为0
- 说明存的时候E加上了127,但还是为0,说明这个2 ^ E特别小
- 规定这时取的时候,有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。
情况三:E全为1
- 存的时候E加上127,居然全部都变成了1,说明这个2 ^ E特别大(正负取决于符号位s)
- 总结IEEE 754规定,得出浮点数的存储形式
1.4.8 案例分析
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int n = 9;
float* pFloat = (float*)&n;
printf("n的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);
*pFloat = 9.0;
printf("num的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);
return 0;
}
- 对于第一个printf,毫无疑问结果是9,不解释
- 对于第二个printf,float* pFloat = (float*)&n;它将n的地址强制转化成float*,并赋给了pFloat,
- 此时pFloat就认为这段二进制: 是float类型存入内存的二进制
- pFloat指向9并解引用,最后又是以%f打印的,所以结果为0.000000
- 对于第三个printf,*pFloat = 9.0;把9的值赋给了n,且pFloat是一个float* 的指针变量,最后又是以%d的形式打印,所以结果为1091567616
- 对于第四个printf,和第三个printf同理,不同之处是
第三个printf以浮点数存入,以%d的形式打印,
第四个printf中也是以浮点数存入,但是却是以%f,
所以结果应该为9.000000
2. 指针
2.1 字符指针
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
char ch = 'q';
char * pc = &ch;
char* ps = "hello bit";
char arr[] = "hello bit";
*ps = 'w';//err
arr[0] = 'w';
printf("%c\n", *ps);//h
printf("%s\n", ps);//hello bit
printf("%s\n", arr);//wello bit
return 0;
}
-
char* ps = "hello bit";不是把字符串 hello bit放到字符指针 pstr 里了,而是把"hello bit"这个字符串的首字符的地址存储在了ps中
-
"hello bit"是一个常量字符串,常量字符串是不能被修改,则*ps = 'w';这个语句就是错的
2.1.1 经典题
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
char str1[] = "hello bit.";
char str2[] = "hello bit.";
const char* str3 = "hello bit.";
const char* str4 = "hello bit.";
//*str3 = 'w';
if (str1 == str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if (str3 == str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
str1和str2不同,str3和str4相同。
- 这其实也很好理解"hello bit.",这是一个常量字符串,不能被修改,又因为str1和str2都是指向同一个常量字符串,自然也就不需要再开辟一段空间放相同的常量字符串
- srt1和str2虽然数组的内容一样,但是str1和str2中的"hello bit."是可以被修改,所以开辟了2个不同数组存放"hello bit."
2.2 二维数组传参
传入的参数是二维数组的首地址
- 第二个test错误,接收时int arr[][],可以用二维数组接收,但不能省略列数
- 第四个test错误,不能用一级指针接收,用指针接收,只能用数组指针(一级)
- 第五个test错误,不能用一级指针数组接收,用数组接收,只能用二维数组,
- 第七个test错误,不能用二级指针接收,用指针接收,只能用数组指针(一级)
2.3 函数指针
2.3.1 函数传参
- 函数名 == &函数名,即函数传参的时候,&可以不写
2.3.2 函数指针解引用
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
//int (*pf)(int, int) = &Add;//OK
int (*pf)(int, int) = Add;//Add === pf
int ret = 0;
ret = (*pf)(3, 5);//1
printf("%d\n", ret);
ret = pf(3, 5);//2
printf("%d\n", ret);
ret = Add(3, 5);//3
printf("%d\n", ret);
//int ret = * pf(3, 5);//err
return 0;
}
- 对于一个函数指针的解引用,*可以不用写
2.3.2 经典题
代码1 : (*(void (*)())0)();// 请问该代码什么意思
- void(*)() - 函数指针类型
- (void(*)())0 - 对0进行强制类型转换,被解释为一个函数地址
- *(void(*)())0 - 对0地址进行解引用操作
- (*(void(*)())0)() - 调用0地址处的函数
代码2 :void (*signal(int , void(*)(int)))(int);// 请问该代码什么意思
- signal 和()先结合,说明signal是函数名
- signal函数的第一个参数的类型是int,第二个参数的类型是函数指针
- signal函数的返回类型也是一个函数指针\
- signal是一个函数的声明,
2.4 函数指针数组的用途
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
void menu()
{
printf("**************************\n");
printf("**** 1. add 2. sub ****\n");
printf("**** 3. mul 4. div ****\n");
printf("**** 0. exit ****\n");
printf("**************************\n");
}
int main()
{
int input = 0;
//计算器-计算整型变量的加、减、乘、除
//a&b a^b a|b a>>b a<<b a>b
do {
menu();
int (*pfArr[5])(int, int) = { NULL, Add, Sub, Mul, Div };
int x = 0;
int y = 0;
int ret = 0;
printf("请选择:>");
scanf("%d", &input);//2
if (input >= 1 && input <= 4)
{
printf("请输入2个操作数>:");
scanf("%d %d", &x, &y);
ret = pfArr[input](x, y);
printf("ret = %d\n", ret);
}
else if (input == 0)
{
printf("退出程序\n");
break;
}
else
{
printf("选择错误\n");
}
} while (input);//只有输入0才退出
return 0;
}
- 函数指针数组更像是一个跳板的作用,可以减少代码冗余
2.4.回调函数
将一个函数A的地址传给另一个函数B(用函数指针接收),该函数B又通过解引用调用其他函数
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
void menu()
{
printf("**************************\n");
printf("**** 1. add 2. sub ****\n");
printf("**** 3. mul 4. div ****\n");
printf("**** 0. exit ****\n");
printf("**************************\n");
}
int Calc(int (*pf)(int, int))
{
int x = 0;
int y = 0;
printf("请输入2个操作数>:");
scanf("%d %d", &x, &y);
return pf(x, y);
}
int main()
{
int input = 0;
//计算器-计算整型变量的加、减、乘、除
//a&b a^b a|b a>>b a<<b a>b
do {
menu();
int ret = 0;
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
ret = Calc(Add);
printf("ret = %d\n", ret);
break;
case 2:
ret = Calc(Sub);
printf("ret = %d\n", ret);
break;
case 3:
ret = Calc(Mul);//
printf("ret = %d\n", ret);
break;
case 4:
ret = Calc(Div);//
printf("ret = %d\n", ret);
break;
case 0:
printf("退出程序\n");
break;
default:
printf("选择错误,重新选择!\n");
break;
}
} while (input);
return 0;
}
- Clac这一个函数就能调用多个函数,减少了代码的冗余,Clac就像一个集成器,
2.5 指针经典题
2.5.1 题一
考查的是:指针类型决定了指针的运算
- p+0x1中p为结构体指针变量,这个结构体的大小为20,0x1实际上就是1,p+1会跳过一个结构体的大小,指向的是数组后面空间的地址0x100000+20=0x100014,结果为0x100014
- (unsigned long)p + 0x1中将p强制类型转换为unsigned long,它加1就是加1,0x100000+1=0x100001
- (unsigned int*)p+0x1中将p强制类型转换为unsigned long*,p变成了无符号整形指针,它加一就是加一个int,0x100000+4=0x100004
2.5.2 题二
- ptr1是一个整形指针,指向的是数组后面空间的地址,&a取出的是数组的地址
- ptr2是一个整形指针,(int)a + 1中a表示首元素的地址,再将其强制类型转换问int,它加一就是加一(地址加1),相当于向后偏移了一个字节,在内存中一个字节给一个地址,如:0x0012ff44-->int+1-->0x0012ff45
在小端机器下, - *ptr2表示对ptr2进行解引用,找到并访问4个字节,ptr1[-1]等价于*(ptr1-1),结果为4,2000000
2.5.3 题三
- (0,1) 叫做逗号表达式,结果是最右边的值
- a[0]表示这个二维数组的首元素的地址,p为整形指针变量,p[0]等价于*(p+0),结果为1
2.5.4 题四
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
int a[5][5];
int(*p)[4];
p = a;
printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);//FFFFFFFC,-4
return 0;
}
- -4以%d的形式打印还是-4
- -4的原码10000000000000000000000000000100
- -4的反码111111111111111111111111111111111011
- -4的补码111111111111111111111111111111111100
- -4在内存中以补码的形式存储,%p的形式打印,会直接将-4的补码当作原码打印出来所以结果为FFFFFFFC
2.5.5 题五
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
char* c[] = { "ENTER","NEW","POINT","FIRST" };
char** cp[] = { c + 3,c + 2,c + 1,c };
char*** cpp = cp;
printf("%s\n", **++cpp);//POINT
printf("%s\n", *-- * ++cpp + 3);//ER
printf("%s\n", *cpp[-2] + 3);//ST
printf("%s\n", cpp[-1][-1] + 1);//EW
return 0;
}
- char*c[],char**cp[],char***cpp这三者之间的指向关系如下:
- **++cpp表示先cpp+1,再解引用,指向c+2的地址,再解引用,指向P的地址,结果为POINT
- *-- * ++cpp + 3表示先cpp+1,由于上面的运算cpp变成了cpp+1,所以这里的cpp变成了cpp+2,再解引用,指向c+1的地址,再减1,指向c的地址,再解引用,指向E的地址,再加3,指向第四个E的地址,结果为ER
- *cpp[-2] + 3等价于*(*(cpp-2))+3表示为cpp-2,由于上面的运算cpp变成了cpp+2,所以这里的cpp变成了cpp,再解引用,指向c+3地址,再解引用,指向F的地址,再加3,指向S的地址,结果为ST
- cpp[-1][-1] + 1等价于*(*(cpp-1)-1)+1,由于上面的运算cpp变成了cpp+2,所以这里的cpp变成了cpp+1,再解引用,指向c+2的地址,再减1,指向c+1的地址,再解引用,指向N的地址,再加一指向E的地址,结果为EW
3.常用库函数
3.1 字符分类函数
3.1.1 模拟实现->求字符串长度strlen
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <assert.h>
// 计数器->实现
int my_strlen(const char* str)
{
int count = 0;
assert(str != NULL);
while (*str != '\0')
{
count++;
str++;
}
return count;
}
int main()
{
char arr[] = "abc";
//char arr[] = { 'a', 'b', 'c' };
int len = my_strlen(arr);
printf("%d\n", len);
return 0;
}
- 模拟实现一共有三种版本:计数器,递归,指针-指针,这里我就不一一的举例说明了
strlen的返回值是size_t
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
#include <assert.h>
int my_strlen(const char* str)
{
int count = 0;
assert(str != NULL);
while (*str != '\0')
{
count++;
str++;
}
return count;
}
int main()
{
if (my_strlen("abc") - my_strlen("abcdef") > 0) {
printf(">\n");
}
else {
printf("<=\n");
}
return 0;
}
-
在这段代码中3-6=-3,但是因为是无符号数,所以-3的补码会被当成一个很大的数来解析
-
易错: strlen函数的返回值为size_t,是无符号的
-
补充一点:我上面的my_strlen和库里面的strlen不一样(返回值),
-
所以用我模拟的my_strlen将会是<=
-
而库里面的strlen结果是>
3.1.2 模拟实现->字符串拷贝strcpy
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<assert.h>
char* my_strcpy(char* des, const char* sou)
{
assert(des && sou);
char* ret = des;
while (*des++ = *sou++)
{
;
}
return ret;
}
int main()
{
char arr1[20] = { "xxxxxxxxxxxxxxxxx" };
char arr2[] = { "hello" };
printf("%s\n", my_strcpy(arr1, arr2));//链式访问
printf("%s\n", arr1);//链式访问
return 0;
}
- 通过strcpy拷贝时,会将字符串的\0一起拷贝过去,或者说会以\0作为结束标志
- 注: 目标空间必须可变,目标空间必须足够大,以确保能存放源字符串。
3.1.3 模拟实现->字符串追加strcat
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
#include <assert.h>
char* my_strcat(char* dest, const char*src)
{
char* ret = dest;
assert(dest && src);//断言
//1. 找目标字符串中的\0
while (*dest)
{
dest++;
}
//2. 追加源字符串,包含\0
while(*dest++ = *src++)
{
;
}
return ret;//返回的目标空间的起始地址
}
int main()
{
char arr1[20] = "hello ";//world
char arr2[] = "world";
printf("%s\n", my_strcat(arr1, arr2));
return 0;
}
- 注意: strcat这个库函数是不能自己追加自己的,因为如果自己追加自己,会找不到\0,导致无法结束,(原字符串的\0会被修改)
3.1.4 模拟实现->字符串比较strcmp
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <assert.h>
#include <string.h>
int my_strcmp(const char* s1, const char* s2)
{
assert(s1 && s2);
while (*s1 == *s2)
{
if (*s1 == '\0')
{
return 0;//相等
}
s1++;
s2++;
}
return *s1 - *s2;
}
int main()
{
const char* p = "abcdef";
const char* q = "abb";
int ret = strcmp(p, q);
if (ret > 0)
{
printf("p > q\n");
}
else if (ret < 0)
{
printf("p < q\n");
}
else
{
printf("p == q\n");
}
return 0;
}
长度不受限制的字符串函数 | 长度受限制的字符串函数 |
strcpy | strncpy |
strcat | strncat |
strcmp | strncmp |
3.1.5 模拟实现->字符串匹配strstr
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<assert.h>
const char* my_strstr(const char* str1, const char* str2)
{
//指向 str2 中指定的整个字符序列在 str1 中第一次出现的指针,
//如果该序列不存在于 str1 中,则为空指针
assert(str1 && str2);
// 三指针实现
const char* s1 = NULL;
const char* s2 = NULL;
const char* cp = str1;//记录地址
if (*str2 == '\0')//如果查找的是空字符
{
return str1;
}
while (*cp)
{
s1 = cp;//相当于归位
s2 = str2;//相当于归位
//abcdef
// cdef
while (*s1 && *s2 && (*s1 == *s2))
{
s1++;
s2++;
}
if (*s2 == '\0')
{
return cp;//找到了返回的地址
}
cp++;//此函数的核心,就是通过cp进行返回地址,相当于归位又加一
}
return NULL;//没找到返回NULL
}
int main()
{
char arr1[] = "abbbcdef";
char arr2[] = "bbc";
//或者是char arr2[]="abc";
//在arr1中查找是否包含arr2数组
const char* ret = my_strstr(arr1, arr2);
if (ret == NULL)
{
printf("没找到\n");
}
else
{
printf("找到了:%s\n", ret);
}
return 0;
}
- 在模拟实现strstr时需要,考虑出现重复的情况,顺便一提:
- 程序运行的结果是:找到了:bbcdef
3.1.6 字符串分割函数->strtok
strtok的返回值的描述
如果找到标记,则指向标记开头的指针。 否则为空指针。 当正在扫描的字符串中到达字符串结尾(即空字符)时,始终返回空指针
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
#include <assert.h>
int main()
{
char arr[] = "zpw@bitedu.tech hehe";
char* p = "@. ";//分隔符的集合
char tmp[30] = { 0 };
strcpy(tmp, arr);//临时拷贝
//zpw\0bitedu\0tech\0
char* ret = NULL;
for (ret = strtok(tmp, p); ret != NULL; ret=strtok(NULL, p))
{
printf("%s\n", ret);
}
return 0;
}
-
strtok函数的第一个参数不为 NULL ,函数将找到str中第一个标记,strtok函数将保存它在字符串中的位置。
-
strtok函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标记
-
如果字符串中不存在更多的标记,则返回 NULL 指针。
3.1.7 strerror VS perror
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
perror("fopen");
return 1;
}
fclose(pf);
pf = NULL;
return 0;
}
- strerror: 返回错误码,所对应的错误信息。
- perror: 返回错误码,所对应的错误信息。(更清晰)
常见错误码介绍
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
int main()
{
printf("%s\n", strerror(0));
printf("%s\n", strerror(1));
printf("%s\n", strerror(2));
printf("%s\n", strerror(3));
printf("%s\n", strerror(4));
printf("%s\n", strerror(5));
return 0;
}
3.2字符转化函数
函数 | 如果他的参数符合下列条件就返回真 |
iscntrl | 任何控制字符 |
isspace | 空白字符:空格‘ ’,换页‘\f’,换行'\n',回车‘\r’,制表符'\t'或者垂直制表符'\v' |
isdigit | 十进制数字 0~9 |
isxdigit | 十六进制数字,包括所有十进制数字,小写字母a~f,大写字母A~F |
islower | 小写字母a~z |
isupper | 大写字母A~Z |
isalpha | 字母a~z或A~Z |
isalnum | 字母或者数字,a~z,A~Z,0~9 |
ispunct | 标点符号,任何不属于数字或者字母的图形字符(可打印) |
isgraph | 任何图形字符 |
isprint | 任何可打印字符,包括图形字符和空白字符 |
3.2.1 islower——判读小写字符
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <ctype.h>
int main()
{
char ch = 'a';
if (islower(ch)) {
printf("小写字母\n");
}
return 0;
}
- 返回值描述: 是小写字符就返回非0
3.2.2 isupper——判读小写字符
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <ctype.h>
int main()
{
char ch = 'A';
if (isupper(ch)) {
printf("大写字母\n");
}
return 0;
}
- 返回值描述: 是大写字符就返回非0
3.3 内存函数
3.3.1 模拟实现->内存拷贝memcpy
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <assert.h>
void* my_memcpy(void* dest, const void* src, size_t num)
{
void* ret = dest;//记录起始地址
assert(dest && src);
while (num--)//4 3 2 1
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
return ret;
}
int main()
{
int arr1[10] = { 1,2,3,4,5,6,7,8,9,10 };
//01 00 00 00 02 00 00 00 ...
int arr2[20] = { 0 };
my_memcpy(arr2, arr1, sizeof(arr1));
return 0;
}
- 如果source和destination有任何的重叠,复制的结果都是未定义的
- 但是 vs2019中的memcpy能自己拷贝自己
3.3.2 模拟实现->内存移动memmove
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
void* my_memmove(void* dest, const void* src, size_t num)
{
void* ret = dest;
//assert(dest && src);
if (dest < src)
{
//前->后
while (num--)
{
// 从前往后拷贝
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
}
else
{
//后->前
while (num--)//19
{
// 从后往前拷贝
*((char*)dest + num) = *((char*)src + num);
}
}
return ret;
}
int main()
{
int arr1[10] = { 1,2,3,4,5,6,7,8,9,10 };
// 1 2 1 2 3 4 5 8 9 10
my_memmove(arr1+2, arr1, 20);
return 0;
}
- 内存的移动需要分三种情况拷贝, 但分成两种情况就行了
- dest 在 src前,从前向后拷贝
- dest 在 src后,从后向前拷贝
3.3.3 模拟实现->内存比较memcmp
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<assert.h>
int my_memcmp(const void* str1, const void* str2, size_t num)
{
assert(str1 && str2);
while (num--)
{
if (*(char*)str1 == *(char*)str2)
{
str1 = (char*)str1 + 1;
str2 = (char*)str2 + 1;
}
else if (*(char*)str1 > *(char*)str2)
{
return 1;
}
else
{
return -1;
}
}
return 0;
}
int main()
{
int arr1[10] = { 1,2,3,4,5,6,7,8,9,10 };
int arr2[10] = { 1,8,3,4,5,6,7,8,9,10 };
int ret = my_memcmp(arr1, arr2, 20);
if (ret == 0)
{
printf("相同");
}
else
{
printf("不同");
}
return 0;
}
- memcmp与strcmp的不同:strcmp只能比较字符串,而memcmp能比较任意类,
4. 自定义类型
4.1 特殊的结构体声明
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], * p;
int main()
{
p = &x;
return 0;
}
- 虽然上面两个结构体的内容是相同的,但是编译器会把上面的两个声明当成完全不同的两个类型,所以是非法的。
4.2 结构体的自引用
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
struct Node
{
int data;
struct Node next;
};
int main()
{
struct Node a;
return 0;
}
- 结构体是不能自己嵌套自己的,因为sizeof(struct Node)是未知的
4.3 匿名结构体的问题
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
typedef struct
{
int data;
Node* next;
}Node;
int main()
{
Node n;
return 0;
}
- 建议 一般不要使用匿名结构体
4.4 一个隐藏问题
- 在编译器中是先定义next,再typedef,此时的结构体是匿名结构体,所以会报一堆错,
4.5 结构体内存对齐
首先得掌握结构体的对齐规则:
- 第一个成员在与结构体变量偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
- 对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 VS中默认的值为8
- 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
printf("%d\n", sizeof(struct S4));
return 0;
}
4.5.1 存在内存对齐的原因
大部分的参考资料都是如是说的:
1. 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特
定类型的数据,否则抛出硬件异常。
2.性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问
比如说:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
struct S
{
char c;
int i;
}s;
int main()
{
return 0;
}
- 存在内存对齐,一次就能拿到变量i的所有字节
- 而如果没有内存对齐,要取变量i,就需要访问两次
4.5.2 修改默认对齐数
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#pragma pack(2)
struct S
{
char c1;
int i;//
char c2;//
};
int main()
{
printf("%d\n", sizeof(struct S));
return 0;
}
- 结构体对齐方式不合适的时候,我们可以用#pragma 这个预处理指令可以改变我们的默认对齐数
4.5.3 小结
- 结构体的内存对齐是拿空间来换取时间的做法。
- 建议-> 让占用空间小的成员尽量集中在一起。这样能在满足对齐的情况下,节省空间
- 注意: Linux中的gcc是没有对齐数的
4.6 结构体经典题
计算结构体中某变量相对于首地址的偏移,并给出说明
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stddef.h>
#pragma pack(2)
struct S
{
char c1;
int i;
char c2;
};
int main()
{
printf("%d\n", offsetof(struct S, c1));
printf("%d\n", offsetof(struct S, i));
printf("%d\n", offsetof(struct S, c2));
return 0;
}
- offsetof是一个宏,能计算结构体的成员变量的偏移量
4.7 位段
4.7.1 位段的内存分配
位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
return 0;
}
- 大小端是字节序才考虑的这里不考虑
- 上面说了:位段涉及很多不确定因素,位段是不跨平台的
- 这里我是在vs2019中测试的,仅供参考
4.7.2 位段的跨平台问题
- int 位段被当成有符号数还是无符号数是不确定的
- 位段中最大位的数目不确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
- 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的
4.7.3 位段的应用
两个比特位有四种组合:00,01,10,11,
- 性别:男,女,保密,用两个比特位足够表示,char a : 2就行了,不用int a
- 有时候用位段,比定义变量,定义结构体更加节省空间
4.7.4 小结
总结:跟结构体相比,位段可以达到同样的效果,虽然可以很好的节省空间,但是有跨平台的问题
4.8.枚举
4.8.1 枚举的优点
- 增加代码的可读性和可维护性
- 和#define定义的标识符相比较,枚举有类型检查更加严谨。
- define定义的宏和字符在预处理就替换了,而enum不会
- 防止了命名污染(封装)
- 便于调试, 使用方便,一次可以定义多个常量
4.8.2 枚举的使用
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
void menu()
{
printf("*****************************\n");
printf("**** 1. add 2. sub *****\n");
printf("**** 3. mul 4. div *****\n");
printf("**** 0. exit *****\n");
printf("*****************************\n");
}
enum Option
{
EXIT,//0
ADD,//1
SUB,//2
MUL,//3
DIV,//4
};
int main()
{
int input = 0;
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case ADD://case 1:
break;
case SUB://case 2:
break;
case MUL://case 3:
break;
case DIV://case 4:
break;
case EXIT://case 5:
break;
default:
break;
}
} while (input);
return 0;
}
- 使用了枚举提高了代码的可读性,简洁性,
4.9 联合(共用体)
4.9.1 联合的特点
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
union Un
{
char c;//1
int i;//4
};
int main()
{
union Un u = {10};
u.i = 1000;
u.c = 100;
printf("%p\n", &u);
printf("%p\n", &(u.c));
printf("%p\n", &(u.i));
printf("%d\n", sizeof(u));//
return 0;
}
4.9.2 经典面试题
判断当前计算机的大小端存储
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int check_sys()
{
union U
{
char c;
int i;
}u;
u.i = 1;
return u.c;
//返回1 就是小端
//返回0 就是大端
}
int main()
{
int ret = check_sys();
if (ret == 1)
printf("小端\n");
else
printf("大端\n");
return 0;
}
- 联合体的地址是共用的,char c和int i的地址是一样的
4.9.3 联合大小的内存对齐
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
union Un
{
char a[5];//1 5
int i;//4
char c;//1
};
int main()
{
union Un u;
printf("%d\n", sizeof(u));
return 0;
}
- 联合的大小至少是最大成员的大小。
- 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
- 最大成员的大小是5,最大对齐数是4,所以结果是8
5.动态内存管理
5.1 realloc
void* realloc (void* ptr, size_t size);
#define _crt_secure_no_warnings 1
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)calloc(10, sizeof(int));
if (p == NULL)
{
perror("main");
return 1;
}
//使用
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = 5;
}
//这里需要p指向的空间更大,需要20个int的空间
//realloc调整空间
int* ptr = (int*)realloc(p, 20 * sizeof(int));
if (ptr != NULL)
{
p = ptr;
}
}
- 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 新 的空间。
5.1.1 调整内存空间的两种情况
int* ptr = (int*)realloc(p, 80);
情况一: 原有空间之后有足够大的空间
- realloc会在原来的空间中添加40字节
- 这种情况下, ptr和p的地址一样
情况二: 原有空间不足
- realloc会在会另找一块空间,开辟80字节
- 这种情况下, ptr和p的地址一样
情况三: 不存在合适的空间
- realloc有可能找不到合适的空间来调整大小,将会返回空指针
5.2 常见的动态内存错误
5.2.1 对空指针的解引用
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
void test()
{
int* p = (int*)malloc(INT_MAX / 4);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}
int main()
{
test();
return 0;
}
- 错误: 没有判断是否为空指针
5.2.2 对动态开辟空间的越界访问
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
void test()
{
int i = 0;
int* p = (int*)malloc(10 * sizeof(int));
if (NULL == p)
{
exit(EXIT_FAILURE);
}
for (i = 0; i <= 10; i++)
{
*(p + i) = i;//当i是10的时候越界访问
}
free(p);
}
int main()
{
test();
return 0;
}
- 错误: 动态内存开辟也存在越界访问的情况
5.2.3 对非动态开辟内存使用free释放
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
void test()
{
int a = 10;
int* p = &a;
free(p);//ok?
}
int main()
{
test();
return 0;
}
- 错误: free只能释放掉动态内存开辟的空间
5.2.4 使用free释放一块动态开辟内存的一部分
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
void test()
{
int* p = (int*)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}
int main()
{
test();
return 0;
}
- 错误: p不再指向动态内存的起始位置,且p也没有置成空指针,
这就导致有一部分空间没被释放,且找不到这块空间了
5.2.5 对同一块动态内存多次释放
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
void test()
{
int* p = (int*)malloc(100);
free(p);
free(p);//重复释放
}
int main()
{
test();
return 0;
}
- 错误: 不能对同一块空间多次free
5.2.6 动态开辟内存忘记释放(内存泄漏)
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
void test()
{
int* p = (int*)malloc(100);
if (NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while (1);
}
- 错误: 忘记释放不再使用的动态开辟的空间会造成内存泄漏
5.3 经典的笔试题
5.3.1 题目一
请问运行Test函数会有怎样的结果
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
int main()
{
Test();
while (1);
}
- str传给GetMemory函数的时候是值传递,所以GetMemory函数的形参p是str的一份临时拷贝
- 在GetMemory函数内部动态申请空间的地址,存放在p中,不会影响外边str,所以当GetMemory函数返回之后,str依然是NULL。所以strcpy会失败。
- 当GetMemory函数返回之后,形参p销毁,使得动态开辟的100个字节存在内存泄漏。无法释放。
5.3.2 题目二
请问运行Test函数会有怎样的结果
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
- GetMemory 函数内部创建的数组是在栈区上创建的
- 出了函数,p数组的空间就还给了操作系统
- 返回的地址是没有实际的意义,如果通过返回的地址,去访问内存就是非法访问内存的
5.3.3 题目三
请问运行Test函数会有怎样的结果
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
int main()
{
Test();
return 0;
}
- 这段代码少了free(str);str = NULL;可能会引发内存泄漏
5.3.3 题目四
请问运行Test函数会有怎样的结果
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
return 0;
}
- 逻辑错误,正确的应该是先判断是否是空指针,再free释放空间
5.4 C/C++程序的内存开辟
5.5 柔性数组
C99 中,结构体中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
struct S
{
int n;//4
int arr[0];//大小是未知
};
int main()
{
//期望arr的大小是10个整形
struct S*ps = (struct S*)malloc(sizeof(struct S)+10*sizeof(int));
if (ps == NULL)
{
perror("main");
return 1;
}
ps->n = 10;
int i = 0;
for (i = 0; i < 10; i++)
{
ps->arr[i] = i;
}
//增加
struct S* ptr = (struct S*)realloc(ps, sizeof(struct S)+20*sizeof(int));
if (ptr != NULL)
{
ps = ptr;
}
//使用
//释放
free(ps);
ps = NULL;
//struct S s = {0};
//printf("%d\n", sizeof(s));//?
return 0;
}
可以通过realloc来调节这个柔性数组的大小
- 结构中的柔性数组成员前面必须至少一个其他成员。
- sizeof 返回的这种结构大小不包括柔性数组的内存。
- 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
5.5.1 柔性数组不是必须的
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
struct S
{
int n;
int* arr;
};
int main()
{
struct S* ps = (struct S*)malloc(sizeof(struct S));
if (ps == NULL)
return 1;
ps->n = 10;
ps->arr = (int*)malloc(10*sizeof(int));
if (ps->arr == NULL)
return 1;
int i = 0;
for (i = 0; i < 10; i++)
{
ps->arr[i] = i;
}
//增加
int*ptr = realloc(ps->arr, 20 * sizeof(int));
if (ptr != NULL)
{
ps->arr = ptr;
}
//使用
//释放
free(ps->arr);
ps->arr = NULL;
free(ps);
ps = NULL;
return 0;
}
- 这种方法用了两次malloc,但是也成功了,说明柔性数组并不是不可缺少的
5.5.2 柔性数组的优势
优势一: 方便内存释放
- 不用柔性数组实现的代码,需要释放两次free
- 柔性数组中用了一次malloc,则一次free就行了
优势二: 提高访问速度
- 连续的内存有益于提高访问速度,也有益于减少内存碎片
6. 文件
6.1 文件的基本使用
FILE * fopen ( const char * filename, const char * mode );// 打开
int fclose ( FILE * stream ); // 关闭
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
FILE* pf = fopen("code11.c", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
- r(只读) 为了输入数据,打开一个已经存在的文本文件
- w(只写) 为了输出数据,打开一个文本文件(没有这个文件,会创建新文件)
- a(追加) 向文本文件尾添加数据
6. 2 文件的顺序读写
功能 | 函数名 | 适用于 |
读取文件(字符) | fgetc | 所有输入流 |
向文件中写入(字符) | fputc | 所有输出流 |
读取文件(字符串) | fgets | 所有输入流 |
向文件中写入(字符串) | fputs | 所有输出流 |
向文件中写入(格式化) | fscanf | 所有输入流 |
读取文件(格式化) | fprintf | 所有输出流 |
读取文件(二进制) | fread | 文件 |
向文件中写入(二进制) | fwrite | 文件 |
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
FILE* pf = fopen("code11.c", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
{
fputc('a', pf);
fputs("2023_5_2", pf);
fscanf(pf, "%s", "星期二");
char s = 'b';
fwrite(&s, sizeof s, 1, pf);
}
// 读文件
{
int ret = fgetc(pf);
printf("%c\n", ret);
char arr[10];
fgets(arr, 1, pf);
printf("%s\n", arr);
char arrr[10];
fprintf(pf, "%s\n", arrr);
char tmp;
fread(&tmp, sizeof tmp, 1, pf);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
6. 3 对比一组函数
scanf | 针对标准输入的格式化输入语句 - stdin |
fscanf | 针对所有输入流的格式化输入语句 - stdin/文件 |
sscanf | 从一个字符串中读取一个格式化的数据 |
printf | 针对标准输入的格式化输出语句 - stdout |
fprintf | 针对所有输入流的格式化输出语句 - stdout/文件 |
sprintf | 把一个格式化的数据,转换成字符串 |
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
struct S
{
char arr[10];
int age;
float f;
};
int main()
{
struct S s = { "hello", 20, 5.5f };
struct S tmp = { 0 };
char buf[100] = {0};
//sprintf 把一个格式化的数据,转换成字符串
sprintf(buf, "%s %d %f", s.arr, s.age, s.f);
printf("%s\n", buf);
//从buf字符串中还原出一个结构体数据
sscanf(buf, "%s %d %f", tmp.arr, &(tmp.age), &(tmp.f));
printf("%s %d %f\n", tmp.arr, tmp.age, tmp.f);
return 0;
}
- 虽然两次的值一样,但是打印的形式却完全不一样,
- 这两个就像是格式化数据和字符的相互转换
6.4 文件的随机读写
6.4.1 fseek
int fseek ( FILE * stream, long int offset, int origin );
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读取文件
int ch = fgetc(pf);
printf("%c\n", ch);
//调整文件指针
fseek(pf, -1, SEEK_CUR);
ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
- feek 根据文件指针的位置和偏移量来定位文件指针
6.4.2 ftell
long int ftell ( FILE * stream );
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读取文件
int ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
//指针相对于起始位置的偏移量
int ret = ftell(pf);
printf("%d\n", ret);
ch = fgetc(pf);
printf("%c\n", ch);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
- ftell : 返回文件指针相对于起始位置的偏移量
6.4.3 rewind
void rewind ( FILE * stream );
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读取文件
int ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
//让文件指针回到起始位置
rewind(pf);
//指针相对于起始位置的偏移量
int ret = ftell(pf);
printf("%d\n", ret);
ch = fgetc(pf);
printf("%c\n", ch);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
- rewind: 让文件指针的位置回到文件的起始位置
6.5 文件读取结束的判定
在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束。
而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
FILE* pfread = fopen("test.txt", "r");
if (pfread == NULL)
{
return 1;
}
FILE* pfwrite = fopen("test2.txt", "w");
if (pfwrite == NULL)
{
fclose(pfread);
pfread = NULL;
return 1;
}
//文件打开成功
//读写文件
int ch = 0;
while ((ch = fgetc(pfread)) != EOF)
{
//写文件
fputc(ch, pfwrite);
}
if (feof(pfread))
{
printf("遇到文件结束标志,文件正常结束\n");
}
else if(ferror(pfread))
{
printf("文件读取失败结束\n");
}
//关闭文件
fclose(pfread);
pfread = NULL;
fclose(pfwrite);
pfwrite = NULL;
return 0;
}
- 这段代码实现的是: 将test.txt的内容拷贝给test2.txt中
6.6 文件缓冲区
-
ANSIC 标准采用“缓冲文件系统”处理的数据文件的,
-
所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上
-
如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)
-
缓冲区的大小根据C编译系统决定的。
6.6.1 fflush
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <windows.h>
//VS2013 WIN10环境测试
int main()
{
FILE* pf = fopen("test.txt", "w");
fputs("abcdef", pf);//先将代码放在输出缓冲区
printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n");
Sleep(10000);
printf("刷新缓冲区\n");
fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)
printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n");
Sleep(10000);
fclose(pf);
//注:fclose在关闭文件的时候,也会刷新缓冲区
pf = NULL;
return 0;
}
- fflush 表示刷新缓冲区
- fclose 也会自动刷新缓冲区
- 注意: 有些笔试题目需要考虑缓冲区的问题
7. 程序环境和预处理
7.1 预处理
- gcc -E test.c -o test.i
- a.头文件的展开,b.去注释,c. 宏替换,d. 条件编译
7.2 编译
- gcc -S test.i -o test.s
- 把C语言变成汇编语言
7.3 汇编
- gcc -c test.s -o test.o
- 通过汇编变成以.o结尾的目标二进制文件(不可执行)
7.4 连接
- 把多个目标文件和连接库进行链接的
7.5 预定义符号
__FILE__ | 进行编译的源文件 |
__LINE__ | 文件当前的行号 |
__DATE__ | 文件被编译的日期 |
__TIME__ | 文件被编译的时间 |
__STDC__ | 如果编译器遵循ANSI C 其值为1,否则未定义 |
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
printf("%s\n", __FILE__);
printf("%d\n", __LINE__);
printf("%s\n", __DATE__);
printf("%s\n", __TIME__);
printf("%s\n", __FUNCTION__);
int i = 0;
FILE* pf = fopen("log.txt", "a+");
if (pf == NULL)
{
perror("fopen\n");
return 1;
}
for (i = 0; i < 10; i++)
{
fprintf(pf, "%s %d %s %s %d\n",\
__FILE__, __LINE__, __DATE__, __TIME__, i);
}
fclose(pf);
pf = NULL;
//printf("%d\n", __STDC__);//不支持
return 0;
}
- 这些预定义符号都是语言内置的
7.6 #define的坑点
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#define M 1000;
int main()
{
int a = 10;
int b = 0;
if (a > 10)
b = M;
else
b = -M;
return 0;
}
- #define定义宏的时候,不要加;
- 能带括号的时候,尽量带括号
7.7 字符的特殊使用
7.7.1 #
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#define PRINT(X, FORMAT) printf("the value of "#X" is "FORMAT"\n", X);
int main()
{
int a = 10;
PRINT(a, "%d");
//printf("the value of ""a"" is %d\n", a);
float f = 5.5f;
PRINT(f, "%f");
//printf("the value of "f" is ""%f""\n", f);
return 0;
}
- # : 把一个宏参数变成对应的字符串。
7.7.2 ##
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#define CAT(X,Y,Z) X##Y##Z
int main()
{
int class101101 = 100;
printf("%d\n", CAT(class, 101, 101));
//printf("%d\n", class101101);
return 0;
}
- ##: 可以把位于它两边的符号合成一个符号
7.8 带副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
x+1;//不带副作用
x++;//带有副作用
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#define MAX(X,Y) ((X)>(Y)?(X):(Y))
int Max(int x, int y)
{
return x > y ? x : y;
}
int main()
{
int a = 5;
int b = 8;
int m = MAX(a++, b++);
//int m = ((a++) > (b++) ? (a++) : (b++));
printf("a=%d b=%d\n", a, b);
printf("m=%d\n", m);
return 0;
}
- 在宏定义中: 不要使用自增或自减运算符,
7.9 宏和函数对比
宏和函数的一个对比
属性 | #define定义宏 | 函数 |
代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长 | 函数代码只出现于一个地方;每 次使用这个函数时,都调用那个 地方的同一份代码 |
执行速度 | 更快 | 存在函数的调用和返回的额外开 销,所以相对慢一些 |
操作符优级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号。 | 函数参数只在函数调用的时候求 值一次,它的结果值传递给函 数。表达式的求值结果更容易预 测。 |
带有副作用的参数 | 参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果。 | 函数参数只在传参的时候求值一 次,结果更容易控制。 |
参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的, 它就可以使用于任何参数类型。 | 函数的参数是与类型有关的,如 果参数的类型不同,就需要不同 的函数,即使他们执行的任务是 不同的。 |
调试 | 宏是不方便调试的 | 函数是可以逐语句调试的 |
递归 | 宏是不能递归的 | 函数是可以递归的 |
宏的特殊用法
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#define MALLOC(num, type) (type*)malloc(num*sizeof(type))
int main()
{
//malloc(10*sizeof(int));
int*p = MALLOC(10, int);
//(int*)malloc(10 * sizeof(int));
return 0;
}
- 宏的参数可以出现类型,但是函数做不到
命名约定
- 宏名全部大写 函数名不要全部大写
7.10 #undef
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#define M 100
int main()
{
int a = M;
#undef M
printf("%d\n", M);
return 0;
}
- #undef: 移除一个宏定义
7.11 命令行定义
- 命令行中定义符号: gcc test.c -D M=10
7.12 条件编译
7.12.1 #if #elif #else #endif
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
#if 1==2
printf("hehe\n");
#elif 2==3
printf("haha\n");
#else
printf("heihei\n");
#endif
return 0;
}
- 真假判断
7.12.2 #ifdef #endif #if #endif
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#define TEST 0
#define HEHE 1
int main()
{
//如果TEST定义了,下面参与编译
//1
#ifdef TEST
printf("test1\n");
#endif
//2
#if defined(TEST)
printf("test2\n");
#endif
return 0;
}
- 符号定义判断
7.13 条件编译解决文件重复包含
解决方案一: #ifndef #define #endif
#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif //__TESH_H__
解决方案二: #pragma once
#pragma once
//头文件的内容
--------------------------------------------------------------------------------------------------------------------------------