1.前言
从今天开始,我们正式进行C语言的进阶学习。今天要分享的内容的是数据的存储。
2.数据类型介绍
2.1疑问和意义
在C语言中,我们已经了解其基本内置类型,包括char、short、int、float、double、long、long long,但是你知道为什么会有这些类型吗?为什么不设置一种统一的类型?
也就是说这些数据类型有什么意义?1. 使用这个类型开辟内存空间的大小(大小决定了使用范围)。这样就可以做到不浪费内存空间。2. 如何看待内存空间的视角。用int向内存申请4个字节的空间,我们认为其存放的是整形,用float向内存申请4个字节的空间,我们认为其存放的是单精度浮点型。
2.2基本归类
- 整形家族
char
unsigned char
signed char
short
unsigned short [int]
signed short [int]
int
unsigned int
signed int
long
unsigned long [int]
signed long [int]
(1)每种类型都包括signed和unsigned。其中short、int和long都是有符号的,而char是signed还是unsigned取决于编译器,我们常用的编译器都是char = signed char。
(2)char不是存放字符吗?为什么属于整形家族?
char在内存中存储的是ASCII码值,ASCII是整形,所以字符属于整形家族。
- 浮点型家族
float
double
浮点型家族都是signed类型的。
- 构造类型
数组类型
结构体类型struct
枚举类型enum
联合类型union
构造类型是自定义类型,如数组类型。int arr[10]的类型是int [10],其类型随着元素个数和类型发生变化,所以是自定义类型。其他三种构造类型在后面会讲到,现在暂时不讲。
- 指针类型
int*pi
char*pc
float*pf
- 空类型
void
void表示空类型(无类型),通常用于函数的返回类型、函数参数、指针类型。
3.整形在内存中的存储
我们都知道整形在内存中存储的是其二进制的补码,但具体是如何存储的我们还不清楚。在前面的学习中,我们已经了解了整形的原码、反码和补码,今天我们先温习下。
3.1原码、反码、补码
- 这是整形的3种表现形式。这3种表现形式都有符号位和数值位。符号位决定其正负,0表示正数,1表示负数;数组位则决定数值的大小。
正数的原码、反码和补码相同。而负数的原、反、补之间需要相互转化。
- 原码
直接将数值按照正负数的形式翻译成二进制就可以得到原码。 - 反码
将原码的符号位不变,其他位依次按位取反就可以得到反码。 - 补码
反码+1就得到补码。
- 例子
int a = 20;//原码00000000000000000000000000010100
//反码00000000000000000000000000010100
//补码00000000000000000000000000010100
//整数的原、反、补相同
int b = -10;//原码10000000000000000000000000001010
//反码11111111111111111111111111110101
//补码11111111111111111111111111110110
在编译器中,我们可以查看变量的内存,从而了解其存储的方式。
变量a的补码的十六进制正好是00000014,变量b的补码的十六进制正好是FFFFFFF6。但是在内存中存放的顺序好像是倒着存放的,但好像也不完全倒着存放,这在后面会讲到。我们只需知道整形在内存中以二进制补码的形式存储。
3.2为什么存放的是补码?
- 原因:在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
- 例子
int a = 1;
int b = -1;
int c = a+b;
printf("%d",c);
CPU只有加法器,所以1-1会被转换成1+(-1),假设使用原码计算
1的原码:00000000000000000000000000000001
-1的原码:10000000000000000000000000000001
相加的结果:10000000000000000000000000000010
结果为-2,所以用原码计算存在问题,我们尝试用补码计算
1的补码:00000000000000000000000000000001
-1的补码:11111111111111111111111111111111
相加的结果00000000000000000000000000000000
结果为0,使用补码计算得到的还是补码,如果要得到这个数,我们就需要进行原、反、补的转换。但在本例中,最高位的1被截断,变为0,0表示正数,正数的原、反、补相同,所以无需进行转换。
3.3大小端介绍
前面我们提到数据在内存中好像是倒着存储的?是不是只有这种存储方式?现在就来解决这些问题。
- 大小端是大端字节序存储和小端字节序存储的简称,是以字节为单位讨论数据内存的存储顺序。
- 大小端字节序存储
int a = 0x11223344;//利用十六进制方便我们查看数据的内存
//一个字节等于两个十六进制位
//且查看内存时以十六进制的形式显示
//将数据转换成十六进制时左边的字节称为高位字节序,
//右边的字节称为低位字节序,例如44是地位字节序,11是高位字节序
以下是数据的三种存储方式
例如我使用的编译器数据的存储是按照小端字节序存储
- 练习
设计一个小程序来判断当前机器是大端还是小端
int main()
{
int a = 1;
char*pc = (char*)&a;
if(*pc==1)
{
printf("小端");
}
else
{
printf("大端");
}
return 0 ;
}
关键
只访问第一个字节,如果是1就是小端,如果是0就是大端。
3.4练习
int main()
{
char a = -1;
signed char b = -1;
unsigned char c = -1;
printf("%d %d %d",a,b,c);
return 0 ;
}
答案是-1 -1 255
思路
将整形的-1存放到char类型中需要进行截断,用%d打印有符号的整形,需要先进行整形提升,再转换成原码进行打印。
a和b的原理类似,但c不同,不同在整形提升这个环节,c是unsigned char类型,高位不是符号位,高位补0,00000000000000000000000011111111,正数的原、反、补相同,所以直接进行打印,无需转换成原码。
int main()
{
char a = -128;
printf("%u",a);//%u表示打印无符号的整形
return 0;
}
答案4294967168
思路
int main()
{
char a = 128;
printf("%u",a);
return 0 ;
}
答案4294967168
思路
int main()
{
int i = -20;
unsigned int j = 10;
printf("%d",i+j);
return 0 ;
}
答案-10
关键
%d打印有符号的int,i的补码加上j的补码后,需要转换成原码,但如果用%u打印无符号的int,无需进行转换,直接打印。
int main()
{
unsigned in i ;
for(i = 9;i>=0;i--)
{
printf("%u",i);
}
return 0;
}
答案是死循环
关键
i的类型是unsigned int,不可能<0,当i=0,i–不可能变成-1,而是变成一个非常大的数字。
int main()
{
char a[1000];
int i;
for(i = 0;i<1000;i++)
{
a[i] = -1-i;
}
printf("%d",strlen(a));
return 0 ;
}
答案255
关键
数组a中内容有-1,-2,-3,……,-127,-128,127,126,……2,1,0…………等等。char是signed char,其取值范围为-128~127,所以最小值是-128,最大值是127。当遇到0时,就可以算出数组长度,因为strlen求出0之前的数组长度。
int main()
{
unsigned char i = 0;
for(i = 0;i<=255;i++)
{
printf("%d",i);
}
return 0 ;
}
答案是死循环
关键
i<=255恒成立,当i=255时,i++后i变成0,重新循环。
4.浮点型在内存中的存储
根据国际标准IEEE(电气和电子工程协会) 754,
- 任意一个二进制的浮点数V在内存中可以表示为
(-1) ^S * M * 2 ^E
(-1)^S表示符号位,当S=0,V为正数;当S=1,V为负数。
M表示有效数字,大于等于1,小于2。
2^E表示指数位。
例如V=5.5,转换成二进制101.1,转换成科学记数法1.011 *2 ^2,再次转换成 (-1) ^ 0 * 1.011 * 2 ^2。S = 0,M = 1.011,E = 2。
-
任意浮点数只有S,M,E不同,所以只需存储S,M,E即可。
-
对于32位的浮点数,最高的一位为符号S,接着8位是指数E,剩下的23位是有效数字M
;对于64位的浮点数,最高的一位为符号S,接着11位是指数E,剩下的52位是有效数字M。
很多浮点数无法精确保存,float后只能存放23位小数,double只能存放52位小数。 -
(1).S直接放进去第一位。
(2).M总是>=1和<2,所以1可以直接省略,将来拿出来使用时再加上1,若将1也存放进去,就少了一位的精度,比如M = 1.01,只存放01,拿出来使用时再加上1。
(3).E的情况有些复杂,E是一个无符号的整数,但是通过科学计数法E可以是负数,所以在存放进去时要加入一个中间值,8位的要加上127,11位的要加上1023。
当E不全为0或不全为1时,E的真实值就是8位或者11位上的二进制转换的整数减去127或者1023,M的真实值就加上1;当E全为0,浮点数的指数E等于1-127(或者1-1023)即为真实值,M的真实值不再加上1,直接表示为0.xxxx。这样做是为了表示±0,以及接近于
0的很小的数字;当E全为1,有效数字M全为0 ,表示±无穷大(正负取决于符号位s)。 -
例子
float f = 5.5f;
用科学计数法表示(-1) ^ 0 * 1.011 * 2 ^ 2。其中S = 0,M = 1.011,E = 2。用二进制表示为0 10000001 01100000000000000000000。注意M要减去1,E要加上127,M后面不够的补0。
float f = 9.0f;
用科学计数法表示为(-1) ^ 0 * 1.001 * 2 ^ 3。其中S = 0,M = 1.001,E = 3.用二进制表示为 0 10000010 00100000000000000000000。
- 注意
int main()
{
int n = 9;
float * pf = (float *)&n;//以整形视角将数据存放在内存中
//0 00000000 00000000000000000001001
printf("%d",n);
printf("%f",*pf);//V = (-1)^0*0.……1001*2^(-126)
*pf = 9.0;//以浮点数的视角将数据存放到内存中
//0 10000010 00100000000000000000000
printf("%d",*pf);
printf("%f",*pf);
return 0 ;
}
答案9 0.000000 1091567616 9.000000
以整数形式放进去,只能以整数形式拿出来,以浮点数形式放进去,只能以浮点数形式拿出来。