进阶C语言
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;//0x00000001char* 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';//errarr[0] = 'w';printf("%c\\n", *ps);//hprintf("%s\\n", ps);//hello bitprintf("%s\\n", arr);//wello bitreturn 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");elseprintf("str1 and str2 are not same\\n");if (str3 == str4)printf("str3 and str4 are same\\n");elseprintf("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;//OKint (*pf)(int, int) = Add;//Add === pfint ret = 0;ret = (*pf)(3, 5);//1printf("%d\\n", ret);ret = pf(3, 5);//2printf("%d\\n", ret);ret = Add(3, 5);//3printf("%d\\n", ret);//int ret = * pf(3, 5);//errreturn 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>bdo {menu();int (*pfArr[5])(int, int) = { NULL, Add, Sub, Mul, Div };int x = 0;int y = 0;int ret = 0;printf("请选择:>");scanf("%d", &input);//2if (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>bdo {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,-4return 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);//POINTprintf("%s\\n", *-- * ++cpp + 3);//ERprintf("%s\\n", *cpp[-2] + 3);//STprintf("%s\\n", cpp[-1][-1] + 1);//EWreturn 0;
}
- char*c[],charcp[],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