【C语言】理解数据在内存中的存储(详解)


🚀 作者简介:一名在后端领域学习,并渴望能够学有所成的追梦人。
🐌 个人主页:蜗牛牛啊
🔥 系列专栏:🛹初出茅庐C语言、🛴数据结构
📕 学习格言:博观而约取,厚积而薄发
🌹 欢迎进来的小伙伴,如果小伙伴们在学习的过程中,发现有需要纠正的地方,烦请指正,希望能够与诸君一同成长! 🌹


一、数据类型介绍

1.整型

数据类型所占存储空间的大小
char1个字节
short2个字节
int4个字节
long>=sizeof(int)
long long8个字节

char之所以被归为整型是因为在存储的时候存放的是ASCII码值。
char类型又可以分为unsigned charsigned char,单独用char定义的时候不确定是无符号还是有符号,所以根据需求直接写signed char或者是unsigned char
short可以分为unsigned shortsigned short,定义short的时候默认为signed short
int可以分为unsigned intsigned int,定义int的时候默认为signed int
long可以分为unsigned longsigned long,定义long的时候默认为signed long

2.浮点型

数据类型所占存储空间的大小
float4个字节
double8个字节

3.构造类型(自定义类型)

数组类型、结构体类型、枚举类型和联合体类型都是构造类型。
数组类型定义时,数组中存放的数据类型不同,数组就不同,例如 char c[5]int a[5];即便他们的数据类型相同,但是大小不同,数组也不相同,例如char c[5]char cc[6]
结构体类型、枚举类型和联合体类型在内存中时如何存储的,在文章 自定义类型 中有详细的讲解。

4.指针类型

指针类型在文章 从此不再恐惧——C语言指针详解 中有详细的讲解。

5.空类型

void表示空类型,void通常应用于函数的返回类型,函数的参数、指针类型中。

void应用于函数的返回类型中,表示这个函数无返回类型

void test1()//void应用于函数的返回类型中,表示test这个函数无返回类型
{}

void应用于函数的参数中时,明确表明这个函数没有参数。

void test2(void)
{}

void在表示指针类型时,要想使用void*类型的指针变量,要先进行强制类型转换。

#include <stdio.h>
int main()
{
	void* p = NULL;
	int a = 10;
	p = &a;//放的时候可以放,但是要用的时候要进行强制类型转换
	//一般void*类型的指针变量用来临时存放,用的时候强制类型转换去用
	//p++;因为p是void*类型的,++时候不知道向后移动几个字节
	//*p = 20;//不能直接对void*类型进行解引用,要先强制类型转换成int*
	*(int*)p = 20;
	printf("%d\n", a);
	return 0;
}

二、整型在内存中的存储

1.原码、反码和补码

计算机中的整数有三种二进制表示方法,分别是原码、反码和补码。
三种表示方法均由符号位(首位)和数值位(除首位外)两部分组成,符号位都是用‘0’和‘1’表示,‘0’表示正,‘1’表示负。
fuhao
正数的原码、反码和补码相同
负数的原码、反码和补码之间的转换规则如下:
原码:直接将数值按照正负数的形式翻译成二进制就可以得到原码。
反码:原码的符号位不变,其他依次按位取反就可以得到反码。
补码:反码+1得到补码。
例1:

int a = 20;
int类型的字节大小是4个字节(Byte),1个字节(Byte)=8个比特位(bit),比特位(bit)由二进制数字0和1表示,4Byte=32bit,所以20要用32个比特位来表示。

00000000000000000000000000010100 ---- 20的原码
正数的原码、反码和补码相同,所以20的反码、补码与原码相同
00000000000000000000000000010100 ---- 20的反码
00000000000000000000000000010100 ---- 20的补码
00 00 00 14 ---- 十六进制表示20的补码
在VS中,采用的是小端字节序存储。
f

int b = -10;
10000000000000000000000000001010 ---- -10的原码
11111111111111111111111111110101 ---- -10的反码
11111111111111111111111111110110 ---- -10的补码
ff ff ff f6 ---- 十六进制表示-10的补码
在VS中,采用的是小端字节序存储。
2

对于整型来说,数据存放在内存中其实存放的是补码。整型表达式计算时,使用的是内存中的补码计算的,打印的时候我们看到的是原码转换成的十进制数字。
正数的原码、反码和补码相同,负数的原码和补码的相互转换方式如下:
(1)原码到补码-----原码的符号位不变、数值位取反后+1得到补码。
(2)补码到原码-----补码的符号位不变、数值位取反后+1得到原码。从补码到原码另一种方式就是补码-1后,取反(符号位除外)得到原码。可以用-10的原码和补码进行验证。

10000000000000000000000000001010 ---- -10的原码
11111111111111111111111111110110 ---- -10的补码

扩展:为什么内存中存放的是补码呢?在计算机系统中,数值一律用补码来表示和存储,原因在于使用补码可以将符号位和数值位统一进行处理;同时,加法和减法也可以统一处理(cpu只有加法器),此外,补码和原码相互转换,其运算过程是相同的不需要额外的硬件电路。通过下面例子可能会对此有更好的理解:

int c = 1 - 1;
1-1可以转换为1-(-1)
00000000000000000000000000000001 ---- 1的原码
10000000000000000000000000000001 ----- -1的原码
假设原码相加得到:
10000000000000000000000000000010 ----- -2
取出他们的补码:
00000000000000000000000000000001 ---- 1的补码
11111111111111111111111111111111 ----- -1的补码
补码相加后得到:
100000000000000000000000000000000 ---- 总共33位,内存中存不下,要把最前面的1丢掉
丢掉后得到:
00000000000000000000000000000000 ---- 0

2.大小端字节序存储

在内存中数据都是以字节为单位存储的,当1个数据的大小超过一个字节的时候,要存储到内存中就会有顺序的问题,而在上述中我们已经知道VS中是小端字节序存储。
大端字节序存储:是指数据的低位保存在内存的高地址中,而数据的高位保存在内存的低地址中。
小端字节序存储:是指数据的低位保存在内存的低地址中,而数据的高位保存在内存的高地址中。

我们先了解一下数值的高位和低位,在十进制数字123中,1的权重是102 , 2的权重是101,3的权重是100,我们把权重大便是高位,权重小的便是低位。
权重
练习1:

请简述大端字节序存储和小端字节序存储的概念,并设计一个程序来判断当前机器的字节序。
解析:
大端字节序存储:是指数据的低位保存在内存的高地址中,而数据的高位保存在内存的低地址中。
小端字节序存储:是指数据的低位保存在内存的低地址中,而数据的高位保存在内存的高地址中。

#include <stdio.h>
int main()
{
	int a = 1;
	char* p = (char*)&a;//拿出a的地址,强制类型转换成char*,取地址的时候只取一个字节判断存储方式
	if (*(&a) == 1)
	{
		printf("小端字节序");
	}
	else
	{
		printf("大端字节序");
	}
	return 0;
}

3.练习题

练习题1

#include <stdio.h>
int main()
{
	char a = -1;
	signed char b = -1;
	unsigned char c = -1;
	printf("a=%d,b=%d,c=%d\n", a, b, c);
	return 0;
}

解析:
输出结果为:a=-1,b=-1,c=255。
-1是整型,存放到内存中的补码是32位bit。
10000000000000000000000000000001 ----- -1的原码
11111111111111111111111111111111 ----- -1的补码
a是char类型的,在内存中只占1个字节,即8bit,截断后得到a的补码
11111111 ------ a的补码
但是最后输出的时候是以%d的形式输出的,所以要进行整型提升,将a的补码整型提升后与-1的补码相同。
整型提升:有符号整型提升的时候高位补符号位,符号位为0,高位补0;符号位为1,高位补1。
无符号整型提升的时候,高位补0。

a进行整型提升后a的补码和-1的补码相同,转换成原码输出是a=-1,signed char b = -1char a = -1相同,故不做赘述。
unsigned char c = -1中c是无符号整型,在进行整型提升之后得到c的补码为
00000000000000000000000011111111 ----- c的补码
因为c是无符号整型,所以c的补码就是c的原码
00000000000000000000000011111111 ----- c的原码
转换成十进制数字之后就是255。

signed char和char的取值范围相同都是-128 ~ 127;unsigned char的取值范围是0 ~ 128,无符号数的最高位代表数值位。

signed char

-128的原码为110000000,转换成补码之后为110000000,存放到signed char中要发生截断,截断后为10000000。

练习题2

#include <stdio.h>
int main()
{
	char a = -128;
	printf("%u\n", a);//%u表示打印的是一个无符号整型,认为内存中存放的补码是一个无符号数
	return 0;
}

解析
输出结果为4294967168。
char类型的-128在内存中的补码是10000000,最后输出的是无符号整型,要进行整型提升,因为是有符号数,在进行整型提升的时候高位补1,整型提升后为
11111111111111111111111110000000 — 补码
%u打印的是无符号数,所以补码等于原码,打印出来的结果为4294967168。

练习题3

#include <stdio.h>
int main()
{
	char a = 128;
	printf("%u\n", a);//%u表示打印的是一个无符号整型,认为内存中存放的补码是一个无符号数
	return 0;
}

解析:
输出结果为4294967168。
128看做是int类型的话,原码是00000000000000000000000010000000,正数的原反补相同所以补码为
00000000000000000000000010000000 — 补码
但是a为char类型,截断后为10000000,最后输出的为无符号整型,按照符号位进行整型提升后补码为11111111111111111111111110000000,无符号数的原反补相同,所以原码也为11111111111111111111111110000000,最后输出结果为4294967168。

练习题4

#include <stdio.h>
int main()
{
	int i = -20;
	unsigned int j = 10;
	printf("%d", i + j);
	return 0;  
}

解析:
输出结果为-10。
10000000000000000000000000010100 — -20的原码
11111111111111111111111111101100 ----- -20的补码

00000000000000000000000000001010 — 10的原码
00000000000000000000000000001010 ---- 10的补码,因为10为正数所以原码和补码相同
在内存中是以补码进行计算的,所以i和j相加,是以补码计算。
11111111111111111111111111101100 ----- -20的补码
00000000000000000000000000001010 ---- 10的补码
计算之后得到
11111111111111111111111111110110 — 计算得到的补码
10000000000000000000000000001010 -----原码
将原码转换成十进制得到数字-10,所以最后结果为-10。

练习题5

#include <stdio.h>
#include <Windows.h>
int main()
{
	unsigned int i ;
	for (i = 9; i >= 0; i--)
	{
		printf("%u\n", i);
		Sleep(100);
	}
	return 0;
}

解析:
输出结果为无限循环。
unsigned int为无符号数,取值范围为0~4294967295;始终满足循环条件中i>=0,所以刚开始会从9开始输出,到0之后,i就会变成最大数开始输出,如此循环……

练习题6

#include <stdio.h>
int main()
{
	char a[1000];
	int i;
	for (i = 0; i < 1000; i++)
	{
		a[i] = -1 - i;
	}
	printf("%d", strlen(a));
	return 0;
}

解析:
输出结果为255。
char的取值范围为-128 ~ 127。strlen函数遇见\0停止,计算的是\0之前的字符个数。\0的ASCII值为0,所以strlen遇见0就会停止。i=0时,a[0]=-1;i=1时,a[i]=-1-1可以看成-1+(-1),-1与-1的补码相加得到的补码为10000000000000000000000000000010,转换成char类型的,要发生截断,截断后为10000010,转换成十进制为-2,a[1]=-2……当a[i]=0时,strlen函数停止,而a[i]为char类型的,取值范围为-128 ~ 127,所以最后输出结果为255。

练习题7

#include <stdio.h>
unsigned char i = 0;
int main()
{
	for (i = 0; i <= 255; i++)
	{
		printf("hello world\n");
	}
	return 0;
}

解析:
输出结果为无限循环hello world。
因为unsigned char的取值范围为0 ~ 255,相当于循环中i<=255恒成立。

三、浮点型在内存中的存储

整型的取值范围限定在limits.h中,浮点型的取值范围限定在float.h中。
浮点数存储规则:根据国际标准IEEE(电气电子工程师学会)754(就是浮点数在内存中的存储规则)任意一个二进制浮点数V可以表示成这样的形式:(-1)S * M * 2E
(-1)S表示符号位,当S=0,V为正数;当S=1时,V为负数。
M表示有效数字(1<=M<2)。
2E表示指数位。
如何用二进制表示浮点数中小数点后面的数字呢?
答:二进制中小数点后的权重分别为2-1、2-2、2-3、2-4……,浮点数5.5小数点后的数字这样计算1 * 2-1=0.5,所以用二进制表示5.5为101.1;浮点数9.125小数点后的数字0 * 2-1+0 * 2-2+1 * 2-3=0.125,所以用二进制表示为1001.001。但是有些数字无法精确表示,如数字3.3,小数点后为3,无法计算出准确的数字。

浮点数5.5用二进制可以表示为101.1,用科学计数法表示为1.011 * 22 ,转换成(-1)S * M * 2E形式后表达式为(-1)0 * 1.011 * 22
浮点数9.125用二进制可以表示为1001.001,用科学计数法表示为1.001001 * 23 ,转换成(-1)S * M * 2E形式后表达式为(-1)0 * 1.001001 * 23

单精度浮点数float,IEEE 754规定:对于32位的浮点数,最高的1位是符号位S,接着的8位是指数E,剩下的23位为有效数字M。
danjingshu

双精度浮点数double,IEEE 754规定:对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。
shaungIEEE 754对有效数字M和指数E,还有一些特别的规定。上面说过1<=M<2,也就是说,M可以写成1.xxx的形式,其中xxx表示小数部分,IEEE 754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxx部分。比如保存5.5的时候,只保存011,等到读取的时候再把第一位的1加上去。这样做的目的是节省一位有效数字。以32位浮点数为例,留给M只有23位,将第一位的1舍去以后,等于可以保存24位有效数字。
对于指数E,首先E是一个无符号整数(unsigned int),这意味着如果E为8位,它的取值范围为0 ~ 255;如果E为11位,它的取值范围为0 ~ 2047。但是我们知道科学计数法中的E是可以出现负数的,所以IEEE 754规定,存入内存时的E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。比如210 的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001。

float f = 5.5f,32位的浮点数 f 写成(-1)S * M * 2E形式后表达式为(-1)0 * 1.011 * 22。E=2,M=1.011,S=0;
保存到内存中,E+127=129,M只保存小数部分011,不够23位后面补0。
在内存中的存储形式为0 10000001 01100000000000000000000

指数E从内存中取出还可以分成三种情况:
(1)、E不全为1或不全为0
这时浮点数的指数E的计算值减去127(或者1023),得到真实值,再将有效数字M前加上第一位的1。
(2)、E全为0
这时浮点数的指数E等于1-127(或1-1023)即为真实值,有效数字M不再加上第一位的1,而是还原为0.xxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。
(3)、E全为1
在E全为1时,如果有效数字M全为0,表示±无穷大(±取决于符号位S)。

#include <stdio.h>
int main()
{
	int n = 9;
	float* pf = (float*)&n;
	printf("n = %d\n", n);
	printf("*pf = %f\n", *pf);
	*pf = 9.0;
	printf("n = %d\n", n);
	printf("*pf = %f\n", *pf);
	return 0;
}

解析:
输出结果为n = 9 , * pf = 0.000000 , n = 1091567616 ,* pf = 9.000000
9的原码为00000000000000000000000000001001
正数的原码和补码相同,9的补码为00000000000000000000000000001001
强制类型转换成float类型后从内存中取出时,首位是符号位S,后面的8位是E,最后面的是M。E全为0,无限接近于0,所以pf=0.000000。
对*pf赋值后可以用表达式(-1)0 * 1.011 * 22。E+127=129,10000001;M取小数部分,01100000000000000000000。所以最后为0 10000001 01100000000000000000000。按照整数的形式补码是01000000101100000000000000000000,正数的原码和补码相同所以最后的输出结果为n=1091567616 。

  • 29
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 27
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 27
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不 良

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值