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