前言:我们都知道,C里面有很多的内置类型,如char, short, int, long, long long, float, double类型等,类型的意义是使用这个数据类型开辟内存空间的大小,以及如何看待内存的视角(类型定了,不管放上什么样的数据,都当做类型名数据看待)。
目录
1.类型的基本归类
char
unsigned char
signed char 是不是相当于char取决于编译器,c语法没有规定
short
unsigned short(int) //无符号短整型,int可省略
signed short 相当于short
...
还有很多自定义类型
数组类型
结构体类型 struct
枚举类型 enum
联合类型 union
还有void(空类型)等等
各种类型在计算机中是如何存储的呢?
2.原码,反码和补码
计算机中有三种表示方法,原码,反码和补码
三种方法都由符号位和数值位组合表示,其中,0表示正数,1表示负数。
特别的,对于正数,其原码,反码,和补码都相同
对于负数,其反码等于原码的符号位不变,数值位按位取反,其补码等于反码的符号位不变,在数值位的最低位加1。
补充一点,原码到补码的运算可以是一致的,都可以取反加1来进行转换。
那么在计算机中数据是按那种形式来存储数据的呢?我们不妨来验证一下
对于上面的定义,,正数的原反补码无法区分,负数才能更好的区分存储类型,
#include <stdio.h>
int main()
{
int a=-10;
//10000000000000000000000000001010 a的原码(四个字节,32个bit)
//11111111111111111111111111110101 a的反码
//11111111111111111111111111110110 a的补码
//ff ff ff f6 a的补码翻译成的16进制数
return 0;
}
运行上面的代码,调试下看看
发现在a的地址中存储着上面的十六进制数
通过对比发现 与补码翻译成的16进制数相等,只是存储顺序上正好相反,这个涉及数据存储的大小端问题,我们后面在讨论。
那么为什么数据要以补码进行存储呢?其原因在于。电脑的cpu中只有加法器,如果使用其他的码制,举个例子,运算1-1,由于计算机中只存在加法器,所以只能让1+(-1)
对于补码运算:
00000000000000000000000000000001 1的补码
1111111111111111111111111111111111111 -1的补码
相加结果为
100000000000000000000000000000000 33个bit的结果,舍弃最高为,则结果为0
对于原码计算
00000000000000000000000000000001 1的原码
10000000000000000000000000000001 -1的原码
如果原码相加,结果有争议(首先,符号位能不能相加,其次,计算结果为+2或者-2不符合逻辑)
这就是计算机用补码存储数据的原因。
2.1 大小端介绍
前面的存储顺序是和我们写出来的顺序是相反的,现在我们来说一下这个问题:
#include <stdio.h>
int main()
{
int a = 0x112233;
return 0;
}
调出a的地址中的存储
可见 存储和我们写的正好相反
大端字节序存储:
把一个数据的低字节的数据存放在高地址处,把高字节的数据存放在低地址处
小端字节序存储:
把一个数据的低字节的数据存放在低地址处,把高字节的数据存放在高地址处
那如何查看我们不同的编译环境下使用的是大端还是小端存储呢,一种办法就是调试直接看地址上的值就好了,还有一种代码实现办法,对于上面的介绍来看,大小端存储的区别在于,高低地址处存放的是对应数据的高字节位还是低字节位,我们可以利用这个区别来实现;
假设对于int a=1;那么它在计算机中存储的二进制序列为00000000000000000000000000000001
写成16进制则为 00 00 00 01,我们只需要看在相同位置的地址下,存储的是1还是0即可判断
注:本次编译环境选择为vs2022的x64,环境不同可能导致结果不同
#include <stdio.h>
int f()
{
int a = 1;
//1.强制地址转换法
//return *(char*)&a;//(char*)强制将a的四个字节的地址转换为1个字节的地址,取得是低地址处的那个字节
//2.位运算法
return (a<<1)&2; //以大端存储为例 a<<1为 00000010 00000000 00000000 00000000 & 2(00000000 00000000 00000000 00000010)=0
}
int main()
{
if (f())
printf("机器采用小端存储\n");
else
printf("机器采用大端存储\n");
return 0;
}
3.整形提升与截断
例题引入:
#include <stdio.h>
int main()
{
char a = -1;
signed char b = -1;
unsigned char c = -1;
printf("a=%d b=%d c=%d\n", a, b, c);
return 0;
}
输出结果为:
整形提升:整型提升是C程序设计语言中的一项规定,通常在表达式计算时,计算机为了方便是进行32个bit位计算,那么对于不够32个bit位的类型就会进行整形提升,然后再执行表达式的运算。
截断:截断则与整形提升相反,就如字面意思,把一个占用过多比特位的数据给截断。通常发生在把更大类型的值赋予一个较小的类型时。例如把一个int类型的值赋给char类型时,由于int是4个字节,char是一个字节,那么为了把值放入char就会对int类型的值进行截断,把它截断取其中低地址的1个字节。
char a = -1;
//10000000 00000000 00000000 00000001 a的原码
//11111111 11111111 11111111 11111110 a的反码
//11111111 11111111 11111111 11111111 a的补码
//再看类型,char类型占用一个字节,舍弃高位,留低位字节即11111111,这就叫截断
如果对于a进行整形输出(%d),那就要用到整形提升,再看啊的类型为char 有正负之分所以有符号位,则提升时默认最高位为符号位,其他高位按符号位提升
则1 1111111就变为11111111 11111111 11111111 11111111 (这个数最低位的7个1是原来自己的,中间的24个1是根据符号位提升的),再求这个数的原码进行输出,结果即为-1;
signed char b = -1;
//对于打印结果a和b相同,说明我的编译器(VS022)认为signed char=char
unsigned char c = -1;
//前面几步相同,直到11111111(截断后的二进制的值)
//再看c的类型,无符号字符型,那么没有符号位,直接在前面补0
//结果为 00000000 00000000 00000000 11111111,十进制数为255
3.1拓展
char:
一个char类型为8个bit
其二进制表示为
00000000
00000001
...
01111111 (127)
10000000 这个没法求(C指定其为 -128),
如果表示-128,则其原码为110000000,反码为101111111,补码为110000000,char类型取最低的八位,则就会将最高位的符号位丢弃,成为10000000
10000001 反码:10000000 原码:11111111(-127)
...
111111110 反码:11111101 原码:10000010(-2)
111111111 反码:11111110 原码:10000001(-1)
其最高位为符号位,0表示正数,1表示负数,可见,从10000000开始就已经是负数了,可见有符号char类型的取值范围是 [-128,127],
unsigned char:
其二进制的表示形式同上,只是不在有最高位的符号位,全都看做数值位,那么其取值范围就是[0,2^8-1],也就是0-255;
对于其他数据类型,如short[-2^15,2^15-1], unsigned short[0,2^16-1],都可以计算其储存范围。
总之,对于整形数据存储来说,先将其32位二进制形式的补码读入数据,再按照需要的存储类型和解读类型将二进制的补码进行截断或者提升。
%d- 打印整形 ;%u -打印无符号整形
例1:
#include <stdio.h>
int main()
{
char a = -128;
//1.
//10000000 00000000 00000000 10000000 a的原码
//11111111 11111111 11111111 01111111 a的反码
//11111111 11111111 11111111 10000000 a的补码
//2.
//char类型,截断 10000000
//3.解读类型为整形->整形提升看类型
//先看a的类型为有符号char类型
//整形提升 11111111 11111111 11111111 10000000
//4.解读类型为无符号整形(符号位变为数值位)
printf("%u", a);
return 0;
}
输出结果为:
对应的是11111111 11111111 11111111 10000000(unsigned int)
例2(解析都在代码中啦):
#include<stdio.h>
#include <string.h>
/*
int main()
{
char a[1000];
//char类型的取值范围是-127-128
for (int i = 0; i < 1000; i++)
{
a[i] = -1 - i;
}
//理论序列应该为-1,-2,-3,-4,-5.......-999,-1000
实际上,char类型的变量到-128在减去1就会变成127,接着从127再减到0(注意ASCII码值为0的对应的字符为'\0',也就是字符串终止符),所以strlen(a)的大小为255(128+127)
printf("%d\n", strlen(a));
return 0;
}
*/
/*
unsigned char i = 0;
int main()
{
int cot = 0;
for (i = 0; i <= 255; i++)
{
printf("hello world!\n");
//当i到达256的时候,其二进制表示为0001 0000 0000,因为数据类型是char类型,所以只保留较低位的8个bit,所以最后结果是0,会死循环进行下去
}
return 0;
}
*/
4.金句省身
清醒的堕落才是可怕的,刷手机时间长了的你并不能获得真正的快乐,你起不来的早晨,有人能起来;你吃不了的苦,有人能吃。如果你真的不甘心,那么就请狠下心来努力,把手机抛得远远的,学上几个小时,哪有那么的天赋异禀,谁还不是一边崩溃一遍自愈?只有涅槃才能重生,心栖梦归处,不负韶华年...
5.双精度浮点类型(续)
我们先来看一个代码:
int main()
{
int n = 9;
float* p = (float*)&n;
printf("%d\n", n);
printf("%f\n", *p);
*p = 9.0;
printf("%d\n", n);
printf("%f\n", *p);
return 0;
}
猜猜这个代码的输出结果是啥?
想不到吧?别急,我们来慢慢说:
5.1 双精度浮点类型的存储方式
根据大名鼎鼎的IEEE(电气和电子工程协会)754规定,任意一个二进制浮点数V的表示形式如下:
(-1)^S*M*2^E
其中S表示该数是正数(S=0),负数S=1,
M表示二进制小数的科学表示法,M>=1&&M<2
E表示指数位
看个例子,比如数据3.5,二进制小数中小数点后面的第一位的权值为2^(-1),第二位为2^(-2),以此类推,所以3.5用二进制小数可表示为11.1,接着,我们都知道科学计数法是针对十进制而言的,那么二进制有没有科学计数法呢,答案是肯定的,和十进制数类似,其小数部分大于一小于二(二进制嘛),指数部分为2^E,E为进的位数,就如11.1就可以表示为1.11*2^1
1.11*2^1这个数和上面的形式是不是有点相似了,再加上(-1)^0*1.11*2^1,这个就是3.5的二进制浮点数表示方法了。
float其实是32位的浮点型
double类型是64位的浮点型
对于64位浮点数来说,最高的1位仍是符号位S,接着是11位的指数E,最后是52位的有效数字M
对于以上的部分构成来说,S显然无可厚非只占一位,所以不是0就是1,我们重点来看E和M的部分
对于M来说,前面我们知道M是大于等于1小于2的数,所以我们取得是有效数字的小数点后面的数字存入M中,这个有人可能会说,那0.5呢,怎么存?其实0.5用二进制浮点数可以表示为1.0*2^(-1),这样不就可以存了
对于E就更加复杂了,
首先,E是一个无符号整形
这就要求E必须是一个大于0的数,那么像上面提到的-1就没法存了,这里的IEEE754协会规定,对于8位的E,在它的实际值上面加上127再存入E中,对于11位的E,则加上1023再存入。
当E的值既有1也有0时,可以正常计算,但是当对于E为全0的时候,也就是其真是的指数为-127或者-1023(11bit),就不需要再算了,这个数会非常小,可以直接看做1-127或者1-1023为其指数的真实值,这时,有效数字的M的整数部分就不需要在加1了,直接看做零点几计算即可,同样的,当E为全1时,所表示的真实数字就会非常大,如果有效数字M为全0,就可看做无穷大了。
说完了这些,我们再回头来看开头的那道题,
9的二进制序列为00000000 00000000 00000000 00001001(原码)
对于浮点数指针来说,就会按上面说的方式拆分其二进制的补码为S,M和E,
0(S)0000000 0(E)0000000 00000000 00001001(M)
则结果为(-1)^0*2^(-126)*0.1001,结果很小,所以就显示0.000000了,
同样的,第二次是以浮点数的视角来存储浮点数字,(-1)^0*1.001*2^3,那么S=0,E=3+127=130(10000010),M=00100000000000000000000(23位后面补0),
其原码形式为01000001 00010000 00000000 00000000
当以%d形式打印时,看做补码,因为符号位为0,所以其原码等于补码,输出就会是很大的数
但是以浮点数形式输出就会是正确的结果。
好了,这些就是C语言的数据存储基础了,掌握这些知识点,应该就足以应付面试了,加油。计算机人......