> 文章列表 > 进阶C语言

进阶C语言

进阶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)();// 请问该代码什么意思

  1. void(*)() - 函数指针类型
  2. (void(*)())0 - 对0进行强制类型转换,被解释为一个函数地址
  3. *(void(*)())0 - 对0地址进行解引用操作
  4. (*(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 题一 

考查的是:指针类型决定了指针的运算 

  1.  p+0x1中p为结构体指针变量,这个结构体的大小为20,0x1实际上就是1,p+1会跳过一个结构体的大小,指向的是数组后面空间的地址0x100000+20=0x100014,结果为0x100014
  2. (unsigned long)p + 0x1中将p强制类型转换为unsigned long,它加1就是加1,0x100000+1=0x100001
  3. (unsigned int*)p+0x1中将p强制类型转换为unsigned long*,p变成了无符号整形指针,它加一就是加一个int,0x100000+4=0x100004

2.5.2 题二  

  1.   ptr1是一个整形指针,指向的是数组后面空间的地址,&a取出的是数组的地址
  2. ptr2是一个整形指针,(int)a + 1中a表示首元素的地址,再将其强制类型转换问int,它加一就是加一(地址加1)相当于向后偏移了一个字节,在内存中一个字节给一个地址,如:0x0012ff44-->int+1-->0x0012ff45
    在小端机器下,
  3. *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;
}
  1.    char*c[],charcp[],char*cpp这三者之间的指向关系如下:
  2. ++cpp表示先cpp+1,再解引用,指向c+2的地址,再解引用,指向P的地址,结果为POINT
  3. *-- * ++cpp + 3表示先cpp+1,由于上面的运算cpp变成了cpp+1,所以这里的cpp变成了cpp+2,再解引用,指向c+1的地址,再减1,指向c的地址,再解引用,指向E的地址,再加3,指向第四个E的地址,结果为ER
  4. *cpp[-2] + 3等价于*(*(cpp-2))+3表示为cpp-2,由于上面的运算cpp变成了cpp+2,所以这里的cpp变成了cpp,再解引用,指向c+3地址,再解引用,指向F的地址,再加3,指向S的地址,结果为ST
  5. cpp[-1][-1] + 1等价于*(*(cpp-1)-1)+1,由于上面的运算cpp变成了cpp+2,所以这里的cpp变成了cpp+1,再解引用,指向c+2的地址,再减1,指向c+1的地址,再解引用,指向N的地址,再加一指向E的地址,结果为EW