C语言数据存储
类型的基本归类
有无符号的意义:生活中有写数据是没有符号之分的,将最高位(即符号位)利用可以扩大范围
整形 | 包含类型 | 占用空间 | 备注 |
---|---|---|---|
char | unsigned char | 1Byte | 字符的本质是ASCII值,是整形 |
signed char | 1Byte | 不同于直接创建 int a时,a为signed | |
char | 1Byte | char 到底是signed 还是 unsigned 标准是未定义的,取决于编译器(VS下为signed) | |
short | unsigned short (int) | 2Bytes | |
(signed) short (int) | 2Bytes | ||
int | unsigned int | 4Bytes | |
(signed) int | 4Bytes | ||
long | unsigned long (int) | 4/8Bytes | C语言在规定类型大小时,只规定了 sizeof(long) >= sizeof(int) |
(signed) long (int) | 4/8Bytes | long 在x86为4,x64为8 | |
long long | unsigned long long (int) | 8Bytes | long long 为C99中引入 |
(signed) long long (int) | 8Bytes |
浮点型 | 占用空间 | 备注 |
---|---|---|
float | 4Bytes | float 的精度低,存储范围小,double 精度高,范围大 |
double | 8Bytes | |
long double | 不定 | 长度可能会应编译器和平台的不同而产生差异,可能和 double 一样长,可能更长(8/12/16Bytes) |
long double 在 C99 标准中被引入,用以表示更高精度的浮点数
构造类型 | |
---|---|
结构体类型 | struct |
枚举类型 | enum |
联合类型 | union |
数组类型 | 各类数组 |
以及不同类型所对应的指针类型、空类型(void)
空类型常用于函数的返回类型(表示无返回)、函数的参数(无需参数)等
类型的意义
- 不同的类型对应所开辟的内存空间大小不同(内存大小决定使用范围)
- 程序看待对应内存空间的视角(程序以何种规则读取)
以下程序用于验证整形和浮点型的存取规则不同
#include <stdio.h>
int main()
{
int n = 9;//开辟4bytes
float* pFloat = (float*)&n;//把n的地址放到pfloat里面,实际还是指向那整形的4bytes
printf("n的值为:%d\n", n);//9
printf("*pFloat的值为:%f\n", *pFloat);//0.000000
//这里按照浮点的规则取出数据,结果错误,说明整形和浮点的存储方式不同
*pFloat = 9.0;
printf("num的值为:%d\n", n);//1,091,567,616
printf("*pFloat的值为:%f\n", *pFloat);//9.000000
return 0;
}
数据在内存中的存储
整形在内存中的存储
整形表示的范围在limits.h中定义(如果不用到上限和下限的话不用引)
程序中整形的二进制表示方式有三种,分别为原码、反码和补码
- 原码即为整数的二进制表示,对于有符号类型,最高位为符号位,0表示正数,1表示负数
- 正数的三码相同
- 负数的反码为原码按位取反(最高位不变),补码为反码加1
以7和-7为例(int)
整形 | 表示方式 | 数据 |
---|---|---|
7 | 原码 | 00000000 00000000 00000000 00000111 |
反码 | 00000000 00000000 00000000 00000111 | |
补码 | 00000000 00000000 00000000 00000111 | |
-7 | 原码 | 10000000 00000000 00000000 00000111 |
反码 | 11111111 11111111 11111111 11111000 | |
补码 | 11111111 11111111 11111111 11111001 |
在计算机系统中,数值一律用补码来表示和存储。通过补码,可以将负数视为正数,将加法和减法统一处理(CPU只有加法器)此外,补码与原码相互转换(补码的补码即为原码),其运算过程是相同的,不需要额外的硬件电路
int main()
{
int a = 20;
//原、反、补:00000000 00000000 00000000 00010100
// 十六进制 00 00 00 14 (0x 00 00 00 14)
int b = -10;
//最高位为符号位
//原:10000000 00000000 00000000 00001010
// 0x 80 00 00 0a
//反:11111111 11111111 11111111 11110101
// 0x ff ff ff f5
//补:11111111 11111111 11111111 11110110
// 0x ff ff ff f6
//对补码取反:
// 10000000 00000000 00000000 00001001
// +1:
// 10000000 00000000 00000000 00001010 得到原码
//20-10 = 20 + (-10)
int c = a + b;
//00000000 00000000 00000000 00010100
//11111111 11111111 11111111 11110110
//相加得到一个补码:
//(1)00000000 00000000 00000000 00001010 最高位被丢掉
//0x 00 00 00 0a (10)
return 0;
}
大小端
大小端即计算机系统中两种不同的数据存储方式
大端存储模式 将数据的低位保存在内存中的高位地址,将数据的高位保存在低位地址
小端存储模式 将数据的低位保存在内存中的低位地址,将数据的高位保存在高位地址
由于数据长度、寄存器大小不同,且计算机的存储是以字节为单位的,这就导致了不同长短的数据和寄存器(如16位、32位)之间存在数据存放的顺序问题(即哪些数据放在高地址,哪些放在低地址),大小端存储模式就是为了解决这类问题
我们常用的X86(x64) 结构是小端模式
这里以上面这段简单的代码为例,以下分别位a,b在内存中的存储
实际上,数据存储的规则可以是多种的,只要保证存入和取出的方式相同即可
浮点型同样有大小端,但对一个字节的数据,没有顺序可谈
//判断当前环境是大端还是小端
#include <stdio.h>
int check()
{
int i = 1;
return (*(char*)&i);
//将地址强制转为char*,即只读取最低位的数据
//当为小端,得到1;当为大端,得到0
}
int main()
{
int ret = check();
if (ret == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
整形提升和截断
截断发生在数据存储时,整形提升发生在使用时
有关整形提升和截断的内容,详见另一篇博客
#include <stdio.h>
int main()
{
char a = -1;
//char 在VS下是有符号的
// -1 是整形,为32位,要存入char,要发生截断
//原:10000000 00000000 00000000 00000001
//反:11111111 11111111 11111111 11111110
//补:11111111 11111111 11111111 11111111
//发生截断
// a -- 11111111
// %d 打印有符号整形,发生整形提升,对于有符号数,高位补1
// 11111111 11111111 11111111 11111111 -- 补码
// 10000000 00000000 00000000 00000000 -- 取反
// 10000000 00000000 00000000 00000001 -- +1 得到原码
// b的打印同理
// 截断和大小端无关,相当于是从内存中拿出数据后再发生截断
signed char b = -1;
unsigned char c = -1;
//原:10000000 00000000 00000000 00000000
//反:11111111 11111111 11111111 11111110
//补:11111111 11111111 11111111 11111111
// c -- 11111111
//无符号数高位补0
// 00000000 00000000 00000000 11111111 --- 直接就是原码
printf("a=%d,b=%d,c=%d", a, b, c);
//VS环境下打印 a=-1,b=-1,c=255
return 0;
}
浮点型在内存中的存储
根据国际标准IEEE(电气和电子工程协会) 754 (即IEEE二进制浮点数算术标准),任意一个二进制浮点数V可以表示成下面的形式:
(-1)^S * M * 2^E
(-1)^s — 符号位,当s=0,V为正数;当s=1,V为负数
M — 有效数字,大于等于1,小于2
2^E — 指数位
6.0的二进制表示就是 110.0
s=0,M=1.1,E=2
即为1.1×2^2
6.5就是110.1
s=0, M=1.101,E=2
即为1.101×2^2
浮点型的存储规则
float - 4bytes - 32 bit
最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M
double - 8bytes - 64bit
最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M
根据十进制转换二进制的计算规则(小数部分×2取整,得0为止)可知,当一个数的小数部分在多次乘法取整后仍不为0时,可能会将有效数字所占长度全部使用,后面的数据将无法存储。即浮点数存在丢失精度的情况
IEEE 754规定,在计算机内部保存M时,默认的第一位总是1,因此可以被舍去,只保存后面的小数部分(即保留24/53位有效数字),由此可以提高精度
E为一个无符号整数(unsigned int),当8为8位时,其范围为0~255,而科学计数法中的指数可以存在负数,因此IEEE 754规定,存入内存时E的真实值必须再加上一个中间数,即加上127/1023
E的不同情况
-
E不全为0或不全为1
此时,E的真实值计算即为(E-中间值)
-
E全为0
E+127 = 0,这时,浮点数的指数E等于1-127(或者1-1023)即为真实值
有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于
0的很小的数字
-
E全为1
E + 127 = 255
这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s)即 1.xxxxxx * 2^128
运用
利用这些规则,分析上面的代码
#include <stdio.h>
int main()
{
int n = 9;
//00000000 00000000 00000000 00001001
//0 00000000 00000000000000000001001
//s=0 E=00000000
float* pFloat = (float*)&n;
printf("n的值为:%d\n", n);//9
printf("*pFloat的值为:%f\n", *pFloat);//0.000000
//0 00000000 0000000000 0000000001 001
//s=0 E=00000000
//一个接近0的很小的数
*pFloat = 9.0;
//9.0 --- 1001.0
//s=0 E=3 --- 00000011
// 3 + 127 = 130 --- 1000 0010
//0 10000010 0010000000 0000000000 000
printf("num的值为:%d\n", n);//1,091,567,616
//01000001 00010000 00000000 00000000
printf("*pFloat的值为:%f\n", *pFloat);//9.000000
return 0;
}