C语言数据的存储
一、数据类型简介
在开始介绍之前我们先来对数据进行一个分类
1. C语言的数据类型总览
char //字符数据类型
short //短整型
int //整形
long //长整型
long long //更长的整形
float //单精度浮点数
double //双精度浮点数
struct Type //结构体类型
union Type //联合体类型
enum Type //枚举类型
c语言本身没有字符串类型,字符串的存储是用char类型的数组来存储的
2. 整型家族
顾名思义,整型是用来存放整数的,也就是像0 1 2 -1 -99这类数字
在C语言中的整型数据类型如下
char //字符数据类型
short //短整型
int //整形
long //长整型
long long //更长的整形
Q : 欸,char不是用来存放字符的吗,怎么就成了整形啦?
A : 其实在内存中只能存放0和1,所以在表示字符时,内存中真正存放的也是二进制位的0和1,也可以被解读成-128~127的数字, 所以我们也可以把字符看作是一个整数
补:ASCII(美国信息交换标准代码)制定了一张表,用来确定0~127分别表示什么字符,例如字符 ‘A’ 的ASCII码值就是65,所以我们让计算机把65当作字符来看待时,计算机就会认为是字符 ‘A’
3. 浮点型家族
浮点型是用来存放小数的,也就是像 0.123 这类数字
在C语言中的整型数据类型如下
float //单精度浮点数
double //双精度浮点数
float占用4个字节空间
double占用8个字节空间
所以float的范围比double的范围小
但是float数值范围比long long的范围还要大的多
具体原因下面会有详细的解释
4. 指针类型
用上述类型可以创建各种类型的指针
如下代码 : 指针的简单应用
void test(int* num) {
*num += 1;
}
int main() {
int num = 1;
test(&num);
return 0;
}
略微分析一下代码:
- 在main函数中开辟了一个int类型的空间给num
- 然后调用了test函数,并把num的地址给这个函数
- 那么test函数就可以通过这个地址找到原来的num,并对num做修改
很简单, 对吧, 我们加深一点难度
void test(int** p) {
*p = (int*)malloc(sizeof(int));
}
int main() {
int num = 1;
int* p = #
printf("%p\n", p);
test(&p);
printf("%p\n", p);
return 0;
}
分析一下代码:
- 在main函数中开辟了一个int类型的空间给num,开辟了一块int*的空间给p指针,然后p指针指向了num的地址
- 然后打印当前p指向的地址,再调用了test函数,并把p的地址给这个函数
- test函数内调用了malloc函数,再次分配了一个int大小的空间,并交由p指针保管
- 再次打印p的地址, 两次打印的地址已经不同
- 这次的代码时为了修改一个指针变量,所以取出了指针变量的地址,要存放这个地址必然用到了二级指针
从这两个小代码片段就可以很好的说明指针的用处 : 可以在函数体内改变不属于这个函数的变量的值, 只需要把这个变量的地址取出,交给函数即可。
当这个变量是指针类型的时候也不要怕, 清楚我们要干什么,我们要修改一个不属于这个函数的指针,所以用二级指针来指向这个指针。
二、整形在内存的存储
1.原码反码补码
二进制的由来
计算机由电驱动,所以无论在计算还是存储的时候都只有两种状态:
高电平(对地有电压差),低电平(电势为0,即接地)
故而衍生出二进制:可以用1表示高电平状态,用0表示低电平状态
根据这样的性质,我们可以把任意一个整数转换成对应的二进制
例如:10(十进制) —> 1010(二进制)
二进制1010的每一位的位权从低位到高位分别为 2^0 … 2^1 … 2^2 … 2^n
1010(二进制转换十进制):1*2^3 + 0*2^2 + 1*2^1 + 0*2^0 = 10(十进制)
Q:但是问题就来了:负数怎么表示?
A:额外用最高的一位来表示正负
于是就有这样的规定:
- 最高位为1,则表示这个数是负数
- 最高位为0,则表示这个数为正数
所以上述的10的二进制真正应该这样表示:01010
最高位的0说明这个数是正的,余下的位表示这个数的数值
Q:-1怎么表示呢?
A:根据上述规定,最高位为1,表明这个数是负的,它的值是1
所以十进制的-1:11(二进制)
源码,反码,补码的由来
既然每一个整数都可以用二进制来表示,我们来看一看他们的运算:
例如 5 + ( -1 )
5的二进制:0101
-1的二进制:11
从低位开始相加,相加为2则产生进位,向前进一
0101
+ 11
--------
1000
我们直接把这个值读出来:最高位为1,这个是负数,后面都为0,则这个数表示十进制数是?
负0!
0难道还有正负之分?并且结果也不对啊,这就出问题了,怎么解决?
于是聪明的科学家想到了补码来解决这个问题:
- 把两个数的位数补齐到相同的位数,写出原码
- 正数的原码和补码一致,不需要更改
- 负数的话除去最高位的数字以外,每一位都取反(反码)
- 把得到的值+1 即得到负数的补码
还是上述5 + ( -1 )的例子:
5 -> 0101 (原码,反码,补码都是一样的)
-1 -> 1001 (原码) 与5的二进制位数一致,4位,以4位写出原码
-1 -> 1110 除去最高位按位取反
-1 -> 1111 加一得到补码
补码进行运算:
0101
+ 1111
--------
10100
从低位开始截取原来的二进制位数(当前的例子是4位):得到0100(二进制) -> 4(十进制)
为了保证数据计算的准确性:计算机在内部存储或运算都是采用的补码
2.C语言的整数存储
以下是C语言的整形:
char //字符数据类型
short //短整型
int //整形
long //长整型
long long //更长的整形
所占字节数:(每个字节是8个bit,也就是8个二进制位)
char(1) < short(2) < int(4) <= long(4/8) <= long long(8)
以char为例:char占一个字节的空间
即可以表示 0000 0000 ~ 1111 1111的数字
如果我们把这些当成有符号的数看待:
0000 0000 -> 0
0000 0001 -> 1
....
0111 1111 -> 127
1000 0000 -> -128(1000 0000 是补码哦,-1然后除符号位按位取反,得到1111 1111才是原码)
1000 0001 -> -127
....
1111 1111 -> -1
如果我们把这些当成无符号的数看待:(即最高位不是符号位了,直接翻译成十进制即可)
0000 0000 -> 0
0000 0001 -> 1
....
0111 1111 -> 127
1000 0000 -> 128
1000 0001 -> 129
....
1111 1111 -> 255
综上:当一个char的空间被认为是有符号的数,表示的范围是-128~127。
当一个char的空间被认为是无符号的数,表示的范围是0~255。
short int等类型的推导类似,不在重复
接下来来看一段有趣的代码:
#include <stdio.h>
int main()
{
unsigned char i;
for (i = 0; i <= 255; i++)
printf("%d", i);
return 0;
}
分析一下代码:
- 用unsigned char定义了变量 i
- i <= 255会进入循环,然后 i 自增
- 程序会死循环
原因:unsigned char类型的变量的范围是0~255,判断条件恒为真
留下给读者的思考:为什么 i 到 255 后在自增会为 0 ?
3.C语言类型提升以及数据的截取
类型提升:
- 整形类型在不超过int时,会自动提升到int类型进行计算,缺少的位数按照数据的类型进行补齐,数据类型为有符号的,则补符号位,无符号则补0
例子:
unsigned char i = 10;
二进制序列:0000 1010
补齐到int型: 0000 0000 0000 0000 0000 0000 0000 0000 1010
(往前的位数补0,原因:这是无符号的数)
char b = 10;
二进制序列:0000 1010
补齐到int型: 0000 0000 0000 0000 0000 0000 0000 0000 1010
(往前的位数补0,原因:这是有符号的数,补齐符号位0)
char c = -10;
二进制序列:1111 0111
补齐到int型: 1111 1111 1111 1111 1111 1111 1111 01111
(往前的位数补1,原因:这是有符号的数,补齐符号位1)
类型的截取:
- 数据类型小于int时,提升至int计算,结果再截取成相应的低位数据
例子:
char c= -10;
二进制序列:1111 0111
补齐到int型: 1111 1111 1111 1111 1111 1111 1111 01111
... 参与运算
... 参与运算
假设最后的结果为: 1111 1111 1111 1111 1111 1111 0000 0000
截取最后的8位(char是8位)数据
存放到变量c中: 0000 0000
这也就不难解释上述提出的问题:unsigned char类型在为255时,再次自增为什么会得到0
4.大小端存储
为了方便的管理内存,内存都有编号,一个编号代表8个比特的空间,即一个字节。
但是在C语言中除了8 bit的char之外,还有16 bit的short型,32 bit的int型
对于超过一个字节的数据的存放方式就有多种了:
大端存储:
- 大端存储,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址处
小段存储:
- 小端存储,是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地址处
举个例子:
同一个数值 0x12345678 (十六进制)
大端存储模式下,存放到内存:
真实存放:12 34 56 78
地 址:低 -----> 高
小端存储模式下,存放到内存:
真实存放:78 56 34 12
地 址:低 -----> 高
小段存储:
- 地址的低位,存放着数值的低位,所以在小段存储时,数据的低位78,存放在了内存的前面,所以给我们的感觉是倒着存的
例题:设计一个函数,返回当前环境是否为小端存储
思考:
- 数据的低位存放在内存的低位
- 指针的类型决定了加减的步长,也决定了拿取数据的多少
- 用int型存数据,用char型的指针取
- 取出低地址的值就可以判断是否为小段存储了
函数实现:
int check_sys()
{
int i = 1;
return (*(char *)&i);
}
分析:
a变量存放数值1(十进制)---> 0x00000001(十六进制)
小端存储模式下,存放到内存:
真实存放:01 00 00 00
地 址:低 -----> 高
用char*类型的指针取值,取出的是第一个字节的数据
于是得到的值是1
大端存储模式下,存放到内存:
真实存放:00 00 00 01
用char*类型的指针取值,取出的是第一个字节的数据
于是得到的值是0
三、浮点数的存储
1. 浮点数和整数存放的差异
讲解之前,我们来举个例子看看浮点型和整型存储的不同:
float f = 9.0f;
int x = *(int*)&f;
printf("%d\n", x);
输出的结果是:1092616192
分析:
- 定义了一个浮点型变量f,里面存储了9.0这个浮点数
- 取出f的地址,把他转换成int*的指针,然后解引用,把数值放到x中
- 用 %d的方式 打印x的值
Q:按照我们常理来说,9.0转换成整形,丢掉小数不就好了吗,咋会出现一个这么大的值呢?
A:这就很好的说明了浮点数和整数的存放方式存在很大的差异
2. 浮点数存储标准—IEEE754标准
浮点数的表示方法
- 小数法: 520.5 ------ 平常书写小数的方法
- 科学计数法:5.205 X 10 ^ 2 ------ 用数值和指数来描述一个数
既然浮点数数分为数值部分和指数部分,是不是可以在内存中用不同的比特位来记录不同的信息从而表示出浮点数呢?
IEEE标准(以4字节float举例)
- 第一个比特位用来记录这个是正的还是负的
- 接下来8个比特位用来记录指数的大小
- 剩下的23个比特位用来记录数值的大小
于是我们可以把一个数转换成二进制后,再转换成二进制的指数,按照上述方法便可存入float中
以上述的 520.5 举例
- 将数字转化成二进制数 --------- 1000001000 . 1
- 将二进制数规范化 --------------- 1 . 0000010001 X 2 ^ 9
- 按照刚刚的划分,把对应的数值存入即可
- 第一个比特位为记录正数还是负数: 0
- 接下来八个比特位:将9加上127 -> 136 存入 1000 1000 (下方解释)
- 剩余的比特位记录数值:0000010001 只需要记录小数点后的数值,因为在二进制中的底数始终为1,可以省略
即得到 520.5 在内存中真正的存储:
0(符号位) 1000 1000(指数位) 00000100010000000000000(数值位)
如果用int类型来看待内存里的值,读出来的结果为 1140989952
程序验证
int main()
{
float a = 520.5f;
int pint = *(int*)&a;
printf("%d\n", pint);
return 0;
}
Q : 为什么在存储指数的时候要加上127?
A : 因为指数也有正负! 例如0.5 可以表示成 2 ^ -1。在存储指数的时候,加上127,如果得到的结果小于127,则存储的是负数,反之则为正数,这样可以少用一个专门的比特位来记录指数的正负数信息。
double类型存储和float相似,所占用字节大小不同而已:
-
第一个比特位用来记录这个是正的还是负的
-
接下来11个比特位用来记录指数的大小
-
剩下的52个比特位用来记录数值的大小
Q : 为什么float数值范围比long long的范围还要大的多?
A : 指数的存储方式决定了float表示的范围可以到127位有效小数,以 1 X 2 ^ 127为例,这个数实际约等于1.7×10^38,这个数是170亿亿亿亿,比long long表示的范围还会大很多很多