【C语言】整型数据在内存中的存储原理 (附有例题和解析)

前言

代码语言:C语言
开发环境:Visual Studio 2022
通过这篇文章,你将了解到:

  1. 整型家族成员;
  2. 整型在内存中的存储方式;
  3. 大小端字节序存储。

1. 数据类型

1.1. 常见的数据类型

Alt

1.1.1. 基本数据类型的分类

以下是基本数据类型的归类:
Alt
对上图的解释:

  1. signed是有符号,unsigned是无符号,平常写的int类型就是signed int类型,以此类推。signed通常可以省略不写
  2. [int]指的是该int可以省略不写。比如unsigned short int类型,可以省略int,等价于unsigned short类型。
  3. char类型是字符类型,字符存储的时候存的是ASCII码值,ASCII码值是整数,所以char类型归类到整型家族。
  4. 整型家族的各个成员只是存储数据的取值范围不同,但都是整数。既然是整数就可以表示正整数,0和负整数,有unsigned修饰的数据类型就不能表示负整数。

1.1.2. 基本数据类型所占存储空间的大小

Alt
对上图对的解释:

1.在不同的编译器下长整型的存储空间是不同的,C语言只规定:long类型所占空间大小 >= int类型所占空间大小( sizeof(long) >= sizeof(int) )。
2. C语言还明确规定long long类型是8个字节,float类型是4个字节,double类型是8个字节。

1.2. 为什么要有不同的数据类型?

  1. 不同类型占不同大小的空间,根据具体的数据合理分配空间
  2. 不同的数据类型决定了开辟内存空间的大小。内存空间的大小不同,存储数据的取值范围也就不同
  3. 数据类型不同,看待内存空间的视角就不同。

2. 整型数据在内存中的存储

创建一个变量,就要给这个变量开辟一个空间,这个空间的大小由数据类型决定。创建好一个变量后,下一步就是存储数据。10怎么存储?-10又怎么存储?下面就是讲整型家族的数据是如何存储的。

2.1. 计算机的数据存储

在计算机中,任何数据都是以二进制的形式存储

2.1.1. 常见的进制

最常见的十进制数字:由0,1,2,3,4,5,6,7,8,9组成。逢10进1

二进制数字:由0,1组成。逢2进1

16进制数字:由0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e, f 组成。逢16进1
16进制数字如果用10,11,12,13,14,15表示会与前面的0到5的数字产生歧义,所以从10到15,分别用a,b,c,d,e,f表示。

八进制数字:由0,1,2,3,4,5,6,7组成。逢8进1

为了防止把16进制的1,二进制的1,八进制的1和十进制的1混淆,一个16进制数字会在前面加一个前缀0x。一个八进制数字会在前面加一个前缀0。
C语言中没有直接表示二进制的前缀。

例子:
0x1234中的0x除了表示1234是16进制数字没有任何其他意思。
01234中的0是八进制的标志。
1234没有任何前缀就是十进制的1234。1234的二进制是010011010010。具体怎么换算看下一节。

2.1.2. 进制的换算方法

通过两个例子举一反三

  1. 任意进制转十进制
    Alt
    Alt

  2. 十进制转任意进制(短除法)
    Alt
    Alt

  3. 使用进制换算计算器计算,这里用Windows自带的计算器介绍。
    打开Windows计算器,点击左上角的打开导航,选中计算器的程序员。
    Alt
    HEX是16进制
    DEC是十进制
    OCT是八进制
    BIN是二进制
    点击DEC输入418,就可以看到各个进制的表示形式
    在这里插入图片描述

2.2. 原码,反码,补码

原码:十进制数据的二进制表示形式。为了分清正负数,把最高位(最左边)规定为符号位(0为正,1为负),其他位为数值位。

以一个字节大小(8个比特位)为例:00010101是21的原码,10010101是-21的原码
Alt
现在想让 21 和 -21 的原码进行相加,是否会等于0呢?
Alt
0的二进制应该是00000000,可是上面计算的结果显然不是。试试 21 加 1:
Alt
正数相加没问题,再看看负数相加,比如 -21和-1相加:

Alt
-21+(-1)本应该是-22,可结果得到的是22。由这3个例子可以知道:

原码是负数时不能计算出正确的结果。

而反码的存在就是为了能解决负数计算的问题。

反码:正数的反码是原码本身,负数的反码是符号位不变,其余位取反

举个例子(以一个字节的大小为例):

Alt
因为正数的反码和原码一样,也能进行正数的计算。接着我们试试用反码进行负数的计算:
Alt
貌似可以计算,但还有特殊情况:
Alt
为什么小了1?因为0多算了一次。0有+0和-0,+0的反码是原码本身:00000000,-0的原码是10000000,所以-0的反码是11111111。因为0算了两次,所以少了个1。也体现出反码计算的缺陷:负数计算的结果跨越0,会有1的偏差。 而补码的出现就是为了解决跨0计算的问题。

补码:正数的原码,反码和补码都是相同的,负数的补码就是反码加1。
一张图明白负数的原码,反码和补码:
在这里插入图片描述
此时不管是+0还是-0都是00000000,就不影响计算了。

2.2.1. 原反补码存在的意义

实际上,整型数据在内存中都是以补码的形式存储的。当我们输入一个整数,计算机先把这个整数转换为二进制的形式,也就是原码,然后再转换为补码的形式进行运算。
为什么还要转换为补码再进行计算,不麻烦吗?
用补码可以直接计算正负整数,且CPU只有加法器,也就是只能进行加减法运算(减法可以加一个负数实现),并且原码转补码和补码转原码可以用取反加1实现转换,不需要再使用额外的机器进行换算。效率反而更高。
Alt
最后再回过头来看看10和-10的存储方式:
Alt
我们可以在开发环境下看到10和-10在内存中存储的位置:
Alt
本质上内存存放的是二进制,但为了方便显示,该环境(vs2022)显示的是16进制 (注意内存中的数据要倒着读,原因在大小端字节序中解释。),变量num和num2的空间大小都是4个字节(也就是32个比特位),因为1个16进制数字需要用4个二进制数字表示,8个16进制数字就要32个二进制数字表示,所以16进制的00 00 00 0a就是二进制的00000000 00000000 00000000 00001010,也就是10。

画图解释:
Alt
-10同理:
Alt
Alt

2.3. 整数类型存储的取值范围

列举部分整型家族的数据类型:
char:-128 到 127 (有符号)
unsigned char: 0 到 255 (无符号)

short:-32,768 到 32,767 (有符号)
unsigned short: 0到65535 (无符号)

int:-2,147,483,648 到 2,147,483,647 (有符号)
unsigned int: 0到4294967295 (无符号)

long:-9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 (有符号)
unsigned long: 0到18446744073709551615 (无符号)

怎么得出的取值范围?以char类型为例:
char类型的大小是1个字节(8个比特位
Alt
+1轮回:
Alt

3. 大小端字节序存储

内存里以1个字节为单位,如果需要存储一个大小为多个字节的整型数据时,就需要多个内存单元存储这个数据,那么要怎么存?顺着存还是倒着存?这是个问题。实际上,不同的机器存的顺序不同,这里需要了解大小端的概念。

3.1. 什么是大小端字节序?

1. 字节序:以字节为单位讨论存储顺序。char类型不讨论顺序,因为char类型的数据只占一个字节。
2. 大端字节序存储:把一个数据的低位字节的内容,存放在高地址处,把这个数据的高位字节的内容,存放在低地址处
3。 小端字节序存储:把一个数据的低位字节的内容,存放在低地址处,把这个数据的高位字节的内容,存放在高地址处

举例,在int类型的变量中存储一个数 0x11223344:
Alt

3.2. 测试当前机器是大端还是小端?

创建一个整型变量a,把1赋值给a,内存会为变量a开辟连续的4个字节的空间并且存储1。如果该机器是小端字节序存储,那么读取变量a的最低位的字节时读取到的应该是1,否则该机器是大端字节序存储。
Alt
为什么(int* )类型的地址a要强制转换为(char*)类型?
对(int* )类型的地址进行解引用,对该地址访问对象的大小是4个字节;
对(char* )类型的地址进行解引用,对该地址访问对象的大小是1个字节;
而我们只需要看变量a地址指向的第一个的字节是否是1即可,不访问后面3个字节的数据,所以把地址a的(int* )类型强制转换为(char*)类型。
Alt
代码实现如下:

#include<stdio.h>
//小端返回1,大端返回0
int check_system()
{
    
    
	int a = 1;
	return *(char*)&a;
}

int main()
{
    
    
	int ret = check_system();
	if(ret ==1)
		printf("小端");
	else
		printf("大端");
	return 0;
}

4. 例题和解析

例1:

#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;
}

例1解析:
Alt

例2:

#include<stdio.h>
int main()
{
    
    
	char a = -128;
	char b = 128;
	printf("%u,%u", a,b);
	return 0;
}

例2解析:
Alt
Alt

例3:

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

例3解析:
Alt
例4:

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

例4解析:
unsigned int类型的取值范围:0到4294967295,是不可能出现负数的,所以i>=0是恒成立的,出现死循环。
0减1会变成32个1组成的二进制序列,直接转换为二进制序列就是4294967295,然后一只减1,最后减到0又回到4294967295…

例5:

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

例5解析:
Alt
例6:

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

例6解析:
死循环,因为i<=255恒成立,unsigned char类型的变量只能存一个字节,255是一个字节能表示的最大的数字了,255加1因为无法存储第9位,前8位数都变成0…

猜你喜欢

转载自blog.csdn.net/weixin_73276255/article/details/131561806
今日推荐