要了解数据是如何存储的,我们就得先知道C语言中的数据类型
基本数据类型,也就是C语言内置类型:
char -> 字符型
short -> 短整型
int -> 整型
long -> 长整型
long long -> 更长的整型
float -> 单精度浮点型
double -> 双精度浮点型
C语言中可用sizeof操作符来计算数据类型在内存中所占空间的大小,单位是字节
#include
int main()
{
printf("%zd\n", sizeof(char)); //char大小
printf("%zd\n", sizeof(short));//short的大小
printf("%zd\n", sizeof(int));//int的大小
printf("%zd\n", sizeof(long));//long的大小
printf("%zd\n", sizeof(long long));//long long 的大小
printf("%zd\n", sizeof(float));//float的大小
printf("%zd\n", sizeof(double));//double的大小
return 0;
}
1、类型决定了要开辟空间的大小
2、类型的大小决定了空间的使用范围
3、类型决定了如何看待内存空间的视角
char
unsignde char
signed char
short
unsigned short
signed short
int
unsigned int
signed int
long
unsigned long
signed long
long long
unsigned long long
signed long long
注:char类型也是整型家族的一员
char类型在内存中存储的时候,存的是其对应的ASICC码值
unsigned 表示 无符号数
signed 表示 有符号数
float 单精度浮点型
double 双精度浮点型
构造类型也叫自定义类型
是我们自己所创建的类型
数组类型
结构体类型 struct
联合类型 union
枚举类型 enum
指针类型分类:
整型指针:int*
字符指针:char*
浮点型指针:float*
空类型指针:void*
........
注:空类型的指针可以接收任意类型的指针数据
void 空类型(无类型)
用途:
函数的返回类型
函数的参数
指针类型
......
整型家族在内存中是以二进制的补码进行存储
那么要了解它的存储,
我们首先要了解清楚,原码、反码、补码的概念
为什么存的是补码?
因为使用补码,可以将符号位和数值域统一处理
同时,加法和减法也可以统一处理(CPU只有加法器)
此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路
原码 —> 反码 —> 补码 (原码转补码)
原码:将整数直接转化成二进制,这个二进制数就是它的原码
反码:将原码的符号位不变,其它位按位取反得到反码
补码:给反码 +1 得到补码
注:转化二进制数的最高位是符号位,其中 0 表示正数,1 表示负数
eg:
#include
int main()
{
int a = 10;
//定义一个整型变量,在内存中开辟4字节的空间
//变量名为a,这个空间里面存储10
//那么数字10是如何存储的呢?
// 整型家族在存储的时候存的是二进制的补码
// 先将数字化成二进制 得到原码
// 00000000 00000000 00000000 00001010 --》原码
// 再将原码按符号位不变,其它位按位取反得到反码(最高位是符号位,0表示正数,1表示负数)
// 01111111 11111111 11111111 11110101 --》反码
//再让反码+1得到补码
// 01111111 11111111 11111111 11110110 --》补码
//而在内存中存的就是二进制的补码
printf("%d\n",a);
return 0;
}
方法1: 倒着推回去
补码 —> 反码 —> 原码
先让补码 -1 得到反码,
再让反码符号位不变其它位按位取反得到原码
方法2: 重新按照原码转补码的步骤走一遍
先让补码符号位不变其它位按位取反 得到一个二进制序列
再让这个二进制序列 +1 得到原码
eg:
#include
int main()
{
int a = 10;
// 定义一个整型变量a
// a的原码:00000000 00000000 00000000 00001010
// a的反码:01111111 11111111 11111111 11110101
// a的补码:01111111 11111111 11111111 11110110
printf("%d\n", a);
//打印a
//因为在内存中存的是补码,而我们取出来的时候是要用原码,
//所以 要将补码 转回 成原码
// 方法1:倒着推回去
// a的补码:01111111 11111111 11111111 11110110
// 让补码 -1 得到反码
// a的反码:01111111 11111111 11111111 11110101
// 让反码符号位不变,其它位按位取反得到原码
// a的原码:00000000 00000000 00000000 00001010
// 最后将原码以 %d(十进制数)的形式打印出来
//方法2:重新按照原码转补码走一遍
// a的补码:01111111 11111111 11111111 11110110
// 让补码符号位不变,其它位按位取反 得到一个二进制序列
//二进制序列:00000000 00000000 00000000 00001001
//再让这个二进制序列 +1 得到原码
// a的原码:00000000 00000000 00000000 00001010
return 0;
}
每个机器的存储模式不同,
二进制的补码在存储时 有的机器是从前往后存储,而有的机器是从后王前存储
由于存储顺序不同,就产生了大小端的概念
当前机器是从后往前存储的
大端存储:数据的低权值位保存在内存的高地址处,数据的高权值位保存在内存的低地址处
小端存储:数据的低权值位保存在内存的低地址处,数据的高权值位保存在内存的高地址处
(小端口诀:小小小(低权值位,低地址,小端))
例题:判断当前机器是大端字节序存储,还是小端字节序存储
#include
int main()
{
int a = 1;
//原码:00000000 00000000 00000000 00000001
//反码:01111111 11111111 11111111 11111110
//补码:01111111 11111111 11111111 11111111
//显示的时候是用十六进制显示
//原码:00000000 00000000 00000000 00000001
// 十六进制显示:00 00 00 01
// 小端存储:10 00 00 00
// 大端存储:00 00 00 01
//将a的第一个字节的地址给b
// *b取到第一个字节的内容,若为1 是小端,若为0 是大端
char* b = &a;
if (*b == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
//第一题
#include
int main()
{
char a = -1;
//原码:10000001
//反码:11111110
//补码:11111111
signed char b = -1;
//有符号的char取出来的时候还是将补码转为原码
// 取出来的原码:10000001
unsigned char c = -1;
//无符号char 取出来的时候,认为补码就是原码
//取出来的原码:11111111
printf("a=%d b=%d c=%d\n", a, b, c);// -1 -1 255
return 0;
}
//第二题
#include
int main()
{
char a = -128;
// 原码:10000000
// 反码:01111111
// 补码:10000000
printf("%u\n", a);//非常大的一个数字
//在取的时候是用无符号整数
//认为补码就是原码
// 10000000 00000000 00000000 00000000
return 0;
}
//第三题
#include
int main()
{
char a = 128;
// 正数的原反补相同
// 补码: 10000000
printf("%u\n", a);//2^32
//在取得时候 进行整型提升
//10000000 00000000 00000000 00000000
return 0;
}
//第四题
#include
int main()
{
int i = -20;
//原码:10000000 00000000 00000000 00010100
//反码:11111111 11111111 11111111 11101011
//补码:11111111 11111111 11111111 11101100
unsigned int j = 10;
//原反补相同
//补码:00000000 00000000 00000000 00001010
//补码:11111111 11111111 11111111 11101100
// 11111111 11111111 11111111 11110110
//让他们的补码相加,在取的时候是%d 有符号的,所以结果是-10
// 10000000 00000000 00000000 00001001
// 10000000 00000000 00000000 00001010 -10
printf("%d\n", i + j);
return 0;
}
//第五题
#include
int main()
{
unsigned int i = 0;
for (i = 9; i >= 0; i--)
{
printf("%u ", i);
}
// 9 8 7 6 5 4 3 2 1 0
//当i--变成-1的时候-1的二进制序列为:10000001
//但因为i是无符号整数,所以在取的时候,会将符号位当做是数位计算进去
//在打印的时候会发生整型提升,所以会发生死循环!
// 10000001 0000000 00000000 00000000
return 0;
}
//第六题
#include
int main()
{
char a[1000];
int i = 0;
for (i = 0; i < 1000; i++)
{
a[i] = -1 - i;
}
// -1 ..... -127 128 ...... 0
// 127+128=255
//strlen函数遇到’'\0'才停止 而‘\0'的ASICC值是 0 所以遇到0就结束
printf("%d\n", strlen(a));
return 0;
}
//第七题
#include
unsigned char i = 0;
int main()
{
// 00000000
// 00000001
// .......
// 11111111
// ......
// 10000000
//死循环
for (i = 0; i <= 255; i++)
{
printf("hello world\n");
}
return 0;
}
浮点家族:float、double、long double 类型
浮点数存储的一个例子:
#include
int main()
{
int n = 9;
float* p = (float*)&n;
printf("%d\n", n);//9
printf("%f\n", *p);//0.000000
*p = 9.0;
printf("n的值为:%d\n", n);//1091567616
printf("*p的值为:%f\n", *p);//9.000000
return 0;
}
看完代码结果跟我们想的完全不一样,为什么不一样呢,是因为浮点数的存储,
以下让我们仔细了解一下浮点数到底是如何存储的
根据国际标准IEEE(电气和电子工程协会) 754,任意一个二进制浮点数V可以表示成下面的形式:
(-1)^S * M * 2^E
(-1)^S表示符号位,当S=0,V为正数;当S=1,V为负数。
M表示有效数字,大于等于1,小于2。
2^E表示指数位。
举个例子:
浮点数的 5.0,转成二进制:101.0,相当于:1.01*2^2
按照IEEE规定则可以看成:(-1)^0 * 1.01 * 2^2
按照公式:S=0,M=1.01,E=2
存储时如何放入内存的呢?
IEEE 754 规定:
1、32位的浮点数的存储:
最高的1位是符号位S,接着的8位是指数E,剩下的32位是有效数字M
2、 64位浮点数的存储
最高的1位是符号位S,接着的11位是指数E,剩下的52位是有效数字M
IEEE 754,对有效数字M,和指数E 有些特别的规定:
1、对于有效数字M:
因为 1≤M<2 所以M总是 1.xxxx... 的数,
因此1 可以被舍弃,我们在存储的时候只存储M的xxxx...部分,
等到读取的时候在xxx...前面把1加上去
好处:舍弃1这样可以存储24位有效数字
2、对于指数E:
首先E为一个无符号整数:
因为E是科学技术法的指数,所以会出现负数的情况,但是E是一个无符号整数,
所以IEEE 754 规定:E在存入内存时,其真实值必须加上一个中间数
对于8位的E 中间数是 127,对于11位的E 中间数是 1023
eg: 2^10 中 指数E为10,将它保存成32位浮点数,E=10+127=137
存入内存中:10001001
然而指数E从内存中取出又分为三种情况:
取E的三种情况:
1、E不全为0或不全为1:
此时取的时候 将E减去中间值(127或1023)得到真实值,
再将有效数字M前面加上第一位的1
eg:浮点数0.5 (1/2) 二进制为:0.1
转化为:(-1)*0 * 1.0 * 2^-1
在存储时 给8位E加上中间数127,舍弃M第一位的1,不够位的补0
S=0,E=-1+127=126,M=1.0
则存储起来的二进制为:0 01111110 00000000000000000000000
2、E为全0:
此时取的时候,指数E=1-127或者E=1-1023,即为真实值
有效数字M不再加上第一位的1,而是还原为 0.xxxx... 的小数
3、E为全1:
如果有效数字M全为0 ,表示无穷大 (±无穷大)(正负号取决于S)
注:由于浮点数跟整数的存取方式不同,用不同的方式取的时候结果也大不相同
此时我们再回过头来解释刚刚的例题
#include
int main()
{
//由于浮点数跟整数的存取方式不同,用不同的方式取的时候结果也大不相同
int n = 9;//n是整数,存在原反补问题
//原码:00000000 00000000 00000000 00001001
//正数的原码、反码、补码相同
//补码:00000000 00000000 00000000 00001001
// 内存中存的就是整数的补码
float* p = (float*)&n;
//将整型的内存空间强制转化成浮点型
printf("n = %d\n", n);//9
// 用%d读取的时候,打印的是有符号十进制整数
// 由于n是正数,原码、反码、补码都相同
// 所以用%d打印 结果就是9
printf("%*p = %f\n", *p);//0.000000
//用%f读取的时候,打印的是浮点数,
// 而这片空间里面存储的是:00000000 00000000 00000000 00001001
// 用浮点数的存储规则取出来
// 0 00000000 00000000000000000001001
// S E M
// 在取的时候,S为0表示正数,E全为0的情况 有效数字M不加第一位的1
//而在打印的时候%f只能精确到有效数字后六位
// 所以结果为:0.000000
*p = 9.0;
//在p指针指向的空间n里面放入浮点数字9.0
//9.0的二进制数:1001.0 -- 1.001*2^3
//由浮点数的存储规则可知:
//(-1)^0 * 1.001 * 2^3
//S=0,E=3,M=1.001
//在存储的时候让E加上中间值,让M舍弃第一位的1
//S=0,E=3+127=130,M=001
//存入到内存的二进制数,不够位数的补0
//0 10000010 00100000000000000000000
printf("n = %d\n", n);//1091567616
//用%d读取的时候是以整数的形式读取内存中存入的二进制数
//0 10000010 00100000000000000000000
//将内存中的二进制数读取为整数结果是:1091567616
printf("n = %f\n", *p);//9.000000
/*用%f读取的时候读取的是浮点数
而内存中本身就存入的是浮点数
打印的结果就是它本身:9.000000*/
return 0;
}