C语言数据在内存中的存储详解
目录
- 文章摘要
- 一、C语言的数据类型
- 数据类型基本分为:
- 二、隐式类型转换
- 1.什么是隐式类型转换
- 2.整型提升
- 3.类型转换
- 三、机器大小端
- 1.什么是大小端
- 2.大小端在截断的应用
- 3.判断当前机器的字节序是大端还是小端
- 四、整型在内存中的存储
- 1.原码、反码、补码
- 2.举例实践整型数据在内存的存储
- 总结
文章摘要
本文通过内存底层原理,帮你透彻了解数据存储进内存与从内存中读取的区别以及不同数据类型下数据计算、赋值的变化情况
要透彻理解这些,必须知道隐式类型转换以及机器大小端的概念,本文会对此做简单介绍(这两个概念对C语言数据的深度理解非常重要)
一、C语言的数据类型
数据类型基本分为:
1.整性
char //字符本质上是整型,只是char类型值截断开辟一个字节 unsigned char signed char short //2字节 unsigned short [int] signed short [int] int //4字节 unsigned int signed int long //4字节 unsigned long [int] signed long [int]
2.浮点型
float double
3.构造类型
数组类型 结构体类型 struct 枚举类型 enum 联合类型 union
4.指针类型
int* pi; char* pc; float* pf; void* pv; ...
5.空类型
void
二、隐式类型转换
1.什么是隐式类型转换
在C语言中,隐式类型转换是编译器自发的行为,它往往是从小到大的转换,在数据类型上表现是少字节数据类型,转换成多字节数据类型,保证数据的完整性;(面向对象语言也有该概念,并且对于类也会有隐式类型转换)一般来说,隐式类型转换大体分为两种:整性提升和类型转换
2.整型提升
1.定义: C的整型算术运算总是至少以缺省整型类型的精度来进行的。为了获得这个精度,表达式中的 字符(char类型1字节) 和 短整型(short int类型2字节) 操作数在使用之前被转换为 普通整型(int类型4字节) ,这种转换称为整型提升。
通俗来说:无论数据类型是否为 char 、short int 、…,其在读取到CPU进行计算时,都会先通过整性提升到32位计算,而结算结果的读取位数取决于读取的数据类型,若为char类型,则截断取8位(bit).
【这里注意:通常CPU在计算时,用的数据是源码已翻译后的补码来计算】
2.整性提升是按照变量的数据类型(指自身类型,而不是数值类型)的符号位来提升
//eg1.负数的整性提升 char a = -1; //char类型默认为有符号类型 //其二进制源码为:1 000 0001 // 补码为:1 111 1111 //整性提升时,由于8bit的char类型数据中符号位为1; //故提升为32位后 11111111 11111111 11111111 11111111;(补码) //eg2.正数的整性提升 char a = 1; //其二进制源码为:0 000 0001 // 补码=源码为:0 000 0001 //整性提升时,由于8bit的char类型数据中符号位为0; //故提升为32位后 00000000 00000000 00000000 00000001;(补码)
3.截断的具体体现:
//eg3. char c = -129; printf("%d",c);
结果为:127
原因是:-129源码为:1000 0000 0000 0000 0000 0000 1000 0001
在内存中的补码为:1111 1111 1111 1111 1111 1111 0111 1111
而字符变量c 只截断8bit位 即c变量保存的是:0111 1111(补码)
输出d%位整型,且符号位为0
整型提升为0000 0000 0000 0000 0000 0000 0111 1111(补码)
转为源码即为127
【注意这里的截断原则与机器大小端有关,且截断是在内存上对补码进行操作】
3.类型转换
1.概念:操作符两边的操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行;而这种转换就是类型转换(编译器自发)
2.从下至上,自动转换
long double double float unsigned long int long int unsigned int int
3.【注意】这种类型转换只是建立在运算操作符之间,不然会出现不合理问题
eg4. float f = 3.14; int num = f;//隐式转换,会有精度丢失
赋值情况下导致在高位的float类型转为低位的int类型,导致精度丢失
三、机器大小端
1.什么是大小端
小端(存储)模式,是指数据的底位(低权值)保存在内存的底地址中,而数据的高位(高权值),保存在内存高地址中;
【大多数机器都采用小端模式】
大端(存储)模式,是指数据的底位(低权值)保存在内存高的地址中,而数据的高位,保存在内存低地址中;
2.大小端在截断的应用
上文的eg3.中出现了截断,即字符c截断整型数值-129
//eg3. char c = -129;
我们将代码中的整型a变量在内存的地址储存数据显示出来,从内存地址可以看出,序列从高到低递增
a:补码为 1111 1111 1111 1111 1111 1111 0111 1111
转为16进制后即为 ff ff ff 7f;(权值左边最高,右边最低)
再将字符变量c内存的地址储存数据显示出来,可以看出,由于char类型只有一字节,会优先从四字节a中截断地址最低的一字节
由图看出它截断了低地址里的数据7f,而7f也是低权值。
故,在vs2013中,采用的是小端原则
3.判断当前机器的字节序是大端还是小端
#include<stdio.h> #include<Windows.h> #pragma warning(disable:4996) int check_sys() { int i = 1; return (*(char*)&i);//注意,发生数据类型转换 } int main() { int ret = check_sys(); if (ret) { printf("小端\n"); } else { printf("大端\n"); } system("pause"); return 0; }
【注意】(*(char*)&i);
这里是对指针的解引用时,从内存所取的字节大小由其指向的数据类型决定。 说白了就是 i 的地址从int *被强转为char *,再解引用时,其指向的数据类型从int变为char,因此显示的数据会发生截断;
由上面的截断方式我们可以知道,1在内存是以32位存储的,按一字节来说,其高权值位为0、低权值位为1.故可以通过return传参的1或0判断大小端。
四、整型在内存中的存储
1.原码、反码、补码
一个数在计算机中的二进制表示形式, 叫做这个数的机器数。机器数是带符号的,在计算机用一个数的最高位存放符号, 正数为0, 负数为1.
比如,十进制中的数 +3 ,计算机字长为8位,转换成二进制就是00000011;
如果是 -3 ,就是 10000011
在C语言中,整型在计算机的储存情况是按原反补的规则储存,即对于整型来说,数据存放在内存中其实是补码。
计算机采用这种规则可以使数据运算时的+ - * / 运算都通过加法解决,这样设计的计算机只需设计出加法模块,大大节省成本。
具体规则如下:
1.正数
正数的原、反、补码都相同,与原码一样
2.负数
原码:该数的机器数,最高位为符号位
反码:原码除符号位不变,其余位按位取反
补码:反码+1
2.举例实践整型数据在内存的存储
//例1.尝试判断输出结果是什么 int main() { char a = -1; signed char b = -1; unsigned char c = -1; printf("a=%d,b=%d,c=%d\n", a, b, c); system("pause"); return 0; }
结果:
例1解析:
-1在内存的补码:1111 1111 1111 1111 1111 1111 1111 1111
char a 、signed char b 、unsigned char c 存放时发生截断,其在内存的补码均为:1111 1111
但是三位在以%d(整型)输出时,会发生整型提升,由原来的8位整型提升到32位,而整型提升时高位补0还是补1需看数据自身类型(有符号类型补符号位,无符号类型直接补0)
char a 与 signed char b 均属于有符号型,且符号位为1,补24位1
内存数值为:1111 1111 1111 1111 1111 1111 1111 1111;输出%d时反向推回原码,答案即为 -1
unsigned char c 属于无符号型,补24位0
内存数值为:0000 0000 0000 0000 0000 0000 1111 1111;输出%d时反向推回原码,答案即为 255
//例2.尝试判断输出结果是什么 int main() { char a = 128; char b = -128; printf("a=%u,b=%u\n", a,b); system("pause"); return 0; }
结果:
例2解析:
128在内存的补码:0000 0000 0000 0000 0000 0000 1000 0000
-128的内存补码: 1111 1111 1111 1111 1111 1111 1000 0000
char a 、char b 存放时发生截断,其在内存的补码均为:1000 0000
%u(无符号整型)输出时,会发生整型提升,由原来的8位整型提升到32位
char a 与 char b 均属于有符号型,且符号位为1,补24位1
内存数值均为:1111 1111 1111 1111 1111 1111 1000 0000;输出%u时反码直接当原码,
答案即为 :
//例3.尝试判断输出结果是什么 int main() { int i = -20; unsigned int j = 10; printf("i+j = %d\n", i + j); system("pause"); return 0; }
结果:
例3解析:
-20在内存的补码:1111 1111 1111 1111 1111 1111 1110 1100
10在内存的补码:0000 0000 0000 0000 0000 0000 0000 1010
int i 与 unsigned int j 都是四字节类型变量故存储时不会发生截断,
但 ·i + j = 表达式会发生类型转换,int 会自动转换为 unsigned int 类型计算
CPU中将两变量补码进行相加得到:1111 1111 1111 1111 1111 1111 1111 0110
计算结果以%d(整型)输出,反向推回原码:1000 0000 0000 0000 0000 0000 0000 1010
答案即为 -10
//例4.尝试判断输出结果是什么 int main() { unsigned int i; for (i = 9; i >= 0; i--) { printf("%u\n", i); } system("pause"); return 0; }
结果:
例4解析:
由于 i 变量时 unsinged int 类型,因此其无符号位,
且 ·i >= 0
表达式会发生类型转换,int 0 会自动转换为 unsigned int 类型计算
故其比较结果永远为真,因为无符号类型第32bit位(符号位)永远为0
for 循环条件永远满族,答案即为死循环
//例5.尝试判断输出结果是什么 int main() { char a[1000]; int i; for (i = 0; i <1000; i++) { a[i] = -1 - i; } printf("%d\n", strlen(a)); system("pause"); return 0; }
结果:
例5解析:
char a[1000]数组的每一位元素都是1字节的 char 类型,有字符位,故其保存的数值范围:[-128,127];
-1-i 范围从 -1到 -1000,但在循环体 a[i] = -1 - i
中每次赋值都会发生截断,由下图可知,char类型保存的数值依次递减时,-1 继续减到 -128 ,128 减一位到 127,127 继续减到0,0再减一位到 -1,继续下一轮循环;
而该题的输出时数组字符长度,strlen遇 ‘\0'(等价于数值0),而在初始化后的char a[1000]数组中,数值第一次出现0在a[255];
故答案为255
//例6.尝试判断输出结果是什么 int main() { unsigned char i = 0; for (i = 0; i <= 255; i++) { printf("%d ", i); Sleep(30); } system("pause"); return 0; }
结果:0-255无限循环
例6解析:
由例5解析的图可知:unsigned char 类型的变量 i 的取值范围:[-128,127],永远小于255;
而%d输出时,无符号类型直接整型提升补24位0:
0000 0000 (0)转为 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000(补),补码转原码:符号位为0,原码与补码一样,值为0;
1000 0000(-128)转为0000 0000 0000 0000 0000 0000 0000 0000 1000 0000(补),补码转原码:1000 0000 0000 0000 0000 0000 1000 0000,值为128;
0111 1111(127)转为0000 0000 0000 0000 0000 0000 0000 0111 1111(补),补码转原码:符号位为0,原码与补码一样,值为127
由此可知,无符号字符类型变量整型提升后再%d输出没有负数
故答案为:0-255循环
总结
本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注我们的更多内容!