C语言初阶——数据在内存中的存储详解
- 整数在内存中的存储
正整数的原、反、补码都相同,负整数的三种表示方法各不相同。
原码:直接将数值按照正负数的形式翻译成⼆进制得到的就是原码。
反码:将原码的符号位不变,其他位依次按位取反就可以得到反码。
补码:反码+1就得到补码。
反码得到原码也是可以使用:取反,+1的操作。(符号位不变)
对于整数来说,内存中储存的是补码。
- 大端字节序存储与小端字节序存储
#include<stdio.h>
int main()
{
int a = 0x11223344;
return 0;
}
调试结果:0x0035F90C 44 33 22 11
我们在调试的过程中,发现数字 a 是以字节序倒着存储的,这样的存储方式我们称为小端字节序存储。
大端字节序存储:是指数据的低位字节内容保存在内存的高地址处,而数据的高位字节内容,保存在内存的低地址处。
例如:0x11 22 33 44
小端字节序存储:是指数据的低位字节内容保存在内存的低地址处,而数据的高位字节内容,保存在内存的高地址处。
例如:0x44 33 22 11
口诀:(数字的)大端在前为大端,小端在前为小端。
- 设计一个程序,判断当前机器的字节序:
void fun()
{
int a = 1;//该数字存储方式可能为00 00 00 01或01 00 00 00
char* p = (char*)&a;//通过指针获取该数字最左边的两个十六进制位的地址,解引用后即可判断是0/1
if (*p == 1) {
printf("小端字节序存储\n");
}
else {
printf("大端字节序存储\n");
}
}
int main()
{
fun();
return 0;
}
- 关于整数存储的经典问题
- 练习一:
注意:在VS2022环境中,char类型均为有符号类型
#include <stdio.h>
int main()
{
char a = -1;
signed char b = -1;
unsigned char c = -1;
printf("a = %d b = %d c = %d", a, b, c);
return 0;
}
输出结果:a = -1 b = -1 c = 255
分析过程:
#include <stdio.h>
int main()
{
char a = -1;
//先得到-1的补码:11111111111111111111111111111111
//由于a为char类型,会发生截断,即char类型中储存的内容为:11111111
//以%d形式打印,会出现以char类型的整型提升,得到11111111111111111111111111111111
//转化为原码为10000000000000000000000000000001,即输出结果为-1
signed char b = -1;
//由于在VS2022中,char类型就为signed char类型,所以输出结果相同
unsigned char c = -1;
//同理,储存的内容为11111111,以%d形式打印,会出现以unsigned char类型的整型提升
//得到的结果为00000000000000000000000011111111,由于是无符号类型,该值补码等于原码
//即输出结果为255
printf("a = %d b = %d c = %d", a, b, c);
return 0;
}
注意:在发生整型提升时,是按照赋值时的类型提升,而不是按照打印时的占位符。
- 练习二:
#include<stdio.h>
int main()
{
char a = -128;
char b = 128;
printf("a=%u b=%u", a, b);
return 0;
}
输出结果:a=4294967168 b=4294967168
分析过程:
#include<stdio.h>
int main()
{
char a = -128;
//a的补码为11111111111111111111111110000000
//char类型发生截断后,存储的是10000000,以char类型发生整型提升后
//可以得到11111111111111111111111110000000
//由于是无符号数,该补码就等于原码,输出结果为4294967168
char b = 128;
//b的补码为00000000000000000000000010000000
char类型发生截断后,存储的是10000000,以char类型发生整型提升后
///可以得到11111111111111111111111110000000,所以两数输出结果相同
printf("a=%u b=%u", a, b);
return 0;
}
- 练习三:
#include<stdio.h>
#include<string.h>
int main()
{
char str[1000] = { 0 };
for (int i = 0; i < 1000; i++) {
str[i] = -(1 + i);
}
printf("%zd", strlen(str));
return 0;
}
输出结果:255
题目解析:
#include<stdio.h>
#include<string.h>
int main()
{
char str[1000] = { 0 };
for (int i = 0; i < 1000; i++) {
str[i] = -(1 + i);
}
//字符数组str会被不断地从-1开始不断减少(括号中的为补码)
//-1(11111111) -2(11111110) ... -127(10000001) -128(10000000)
//如果再将其-1,其中-129(原码:100...0010000001)会产生截断得到127(01111111)
//再依次减少,最终得到0(00000000),然后-1(11111111) -2(11111110)...产生循环
//由于'\0'的ASCII码值为0,strlen()函数是统计字符'\0'之前的字符个数,即得到结果128+127=255
printf("%zd", strlen(str));
return 0;
}
- 练习四:
#include<stdio.h>
typedef unsigned char uchar;
typedef unsigned int uint;
int main()
{
for (uchar i = 0; i <= 255; i++) {
printf("这是第%u次循环\n", i);
}
for (uint j = 9; j >= 0; j--) {
printf("这是第%u次循环\n", j);
}
return 0;
}
注意:上述两个for-loop均会进入死循环,此代码并无法真正执行。
分析过程:
#include<stdio.h>
typedef unsigned char uchar;
typedef unsigned int uint;
int main()
{
for (uchar i = 0; i <= 255; i++) {
printf("这是第%u次循环\n", i);
}
//在i达到255之前,循环正常进行,当i达到255(11111111)时,再+1得到256(100000000)
//发生截断,以00000000存入uchar中,i的值变为0,继续循环
for (uint j = 9; j >= 0; j--) {
printf("这是第%u次循环\n", j);
}
//在j达到0前,循环正常进行,当j等于0(0000...00000)时,再-1得到-1(补码:111111....1111111)
//由于该数字是uint类型,该数直接以111111....1111111打印,得到结果4294967295
return 0;
}
- 练习五:
#include <stdio.h>
int main()
{
int a[4] = { 1, 2, 3, 4 };
int* ptr1 = (int*)(&a + 1);
int* ptr2 = (int*)((int)a + 1);
printf("%#x %#x", ptr1[-1], *ptr2);
return 0;
}
输出结果:0x4 0x2000000
#include <stdio.h>
//在x86与小端模式的环境下
int main()
{
int arr[4] = { 1, 2, 3, 4 };
int* ptr1 = (int*)(&arr + 1);//由于&arr的类型为int(*)[4]类型,需要强制转换为int*类型
int* ptr2 = (int*)((int)arr + 1);//将数组的地址强制转化为了int类型,操作后又转化为了int*类型
printf("%#x %#x", ptr1[-1], *ptr2);
//数组名为首元素的地址(假设为0x0012FF40),转化为int类型后+1得到0x0012FF41
//强制转化为int*类型后,该指针解引用后从0x0012FF41开始向后访问四个字节
//由于是小端模式存储,该数组在内存中按照地址存储的内容是:
//01 00 00 00 02 00 00 00 03 00 00 00 ......
//所以该指针解引用后访问的是00 00 00 02
//即得到十六进制数0x02000000
return 0;
}
- 浮点数在内存中的存储
- 根据国际标准IEEE754规定,任意⼀个二进制浮点数 V 可以表示成下面的形式:
V = ( − 1 ) s ∗ M ∗ 2 ∗ E V=(-1)^s*M*2*E V=(−1)s∗M∗2∗E
S 表示符号位,当 S=0 时,V为正数;当 S=1 时,V为负数。
M 表示有效数字,其中1<=M<2,E 表示指数位
例如:十进制的5.5,写成二进制是 101.5 ,相当于 1.015×2^2 。我们可以得出S=0,M=1.015,E=2。
十进制的-5.0,写成二进制是-101.0 ,相当于-1.01×2^2 。那么,S=1,M=1.01,E=2。
- 根据标准规定:
对于32位的浮点数(float类型),最高的1位存储符号位 S,之后的的8位存储指数 E,剩下的23位存储有效数字M
对于64位的浮点数(double类型),最高的1位存储符号位 S,之后的11位存储指数E,剩下的52位存储有效数字M
- 浮点数存储的过程
M 可以写成 1.xxxxxx 的形式,其中 xxxxxx 表示小数部分,规定在存储 M 时,将前面的1舍去,只保留小数部分。例如1.011在 M 中只保存011,读取时再将前面的1加上,这样就可以多存储一位的有效数字。
对与指数 E 来说,首先 E 是一个无符号数(unsigned int类型),因此存入内存时 E 的真实值必须再加上一个中间数,对于8位的 E(取值范围是0~255),这个中间数是127,对于11位的 E(取值范围是0~2047),这个中间数是1023。例如,浮点数1.01*2^10的 E 是 10,所以保存为float类型时,必须保存成10+127=137,即10001001。
-
浮点数读取的过程
E 不全为0或不全为1:
这时,浮点数就采用下面的规则表示,即指数 E 的计算值减去127(或1023),得到真实值,再将有效数字 M 前加上第一位的1。
例如二进制数字0.1,根据规定将小数点右移1位,得到1.0*2^(-1),其中 E 等于-1+127(中间值)=126,表示为01111110,而尾数1.0去掉整数部分为0,补齐0到23位 00000000000000000000000,则其二进制表示形式为:
0 01111110 00000000000000000000000
E 全为0:
这时,浮点数的指数E等于-126(或者-1022)即为真实值,有效数字M不再加上第⼀位的1,直接还原为0.xxxxxx的小数,这样做是为了表示±0,以及接近于0的很小的数字。(由于不表示1了,其指数为自动+1)
E 全为1:
这时,如果有效数字M全为0,通常用于表示无穷大。
-
练习:
#include<stdio.h>
int main()
{
int n = 9;
float* pFloat = (float*)&n;
printf("n的值为:%d\n",n);
printf("*pFloat的值为:%f\n",*pFloat);
*pFloat = 9.0;
printf("num的值为:%d\n",n);
printf("*pFloat的值为:%f\n",*pFloat);
return 0;
}
- 题目解析:
#include<stdio.h>
int main()
{
int n = 9;
float* pFloat = (float*)&n;//将&n强制类型转化为float*类型读取
printf("n的值为:%d\n",n);//以%d读取
printf("*pFloat的值为:%f\n",*pFloat);//以%f读取
*pFloat = 9.0;//解引用后,按照存入浮点数9.0
printf("num的值为:%d\n",n);//以%d读取
printf("*pFloat的值为:%f\n",*pFloat);//以%f读取
return 0;
}
//题目解析:
//首先,我们写出9的补码:00000000000000000000000000001001
//按照%d读取,可以得到9,按照%f打印,0 00000000 00000000000000000001001
//其中S=0,E为全零,E的真实值为-126,M的真实值为0.00000000000000000001001
//即V=(-1)^0*0.00000000000000000001001*2^(-126),为一个极小的数字,获得结果为0.000000
//当以float类型赋值为9.0时,存储的内容为0 10000010 00100000000000000000000
//以%d打印时,由于符号位为0,直接获得结果为1091567616,以%f打印就为9.000000
输出结果:
n的值为:9
*pFloat的值为:0.000000
num的值为:1091567616
*pFloat的值为:9.000000