目 录
数据在内存中是以二进制的形式进行存储的,也是以二进制的形式进行各种算术运算,而不同类型的数据转化为二进制存储在内存中的方式有所不同,(比如正数和负数的二进制数该怎么表示,整型和浮点型的二进制数表示有什么不同。)了解不同的数据在内存中的存储方式,才能更清楚数据之间的具体运算过程。
1 数据类型划分
首先,我们可以先了解数据的类型都有哪些?按大方向划分,可以将数据的类型分为整型、浮点型、构造类型、指针类型以及空类型。而各个类型又可做如下细分:
整型
char:
char
signed char
unsigned char
short:
signed short
unsigned short
int:
signed int
unsigned int
long:
signed long
unsigned long
long long:
signed long
unsigned long
这里将字符型char又细分为三种是因为若char前没有具体注明是signed(有符号数)还是unsigned(无符号数),则其具体表示的是哪一种类型的数据取决于编译器的实现,即在不同的编译器下char表示的数据有所不同(在VS的编译器下char表示的是有符号数)。而其余整型若无具体注明是signed(有符号数)还是unsigned(无符号数),均表示为有符号数。
浮点型
float—单精度浮点型
double—双精度浮点型
构造类型
数组类型—如:int arr[10];
结构体类型—struct
枚举类型—enum
联合类型—union
数组根据定义的数据类型以及数组长度的不同而有所差异,也因此划分为构造类型。
指针类型
char* p;
short* p;
int* p;
long* p;
long long* p;
float* p;
double* p;
空类型
void* p;
根据指向的数据类型的不同有不同的指针类型;空类型指针没有规定具体指向的数据类型,可以接收存储不同类型数据的地址,但其不能通过*p解引用的方式对该地址中的内容进行访问,也不能进行++p等运算操作,因为不明确数据类型就无法知道该访问多少字节的数据,也无法知道指针p的步长是几个字节,前进一步后所指向的地址是哪个。可以认为空类型指针起到一个临时保管地址的作用,当有需要时再使用别的明确指向的数据类型的指针取出。
2 数据在内存中的存储
了解了数据的类型划分后,接下来主要讨论整型和浮点型两大类型在内存中的存储方式。
2.1 整型在内存中的存储
整数在内存中的二进制表示
①计算机中,整数有三种二进制表示方法,即原码、反码、补码,而整数在内存中是以二进制补码的形式进行存储的。整数可分为正数和负数,而二者的求得补码的方式又有所不同:
>规定取最高一位比特位作为符号位(如int型则取第32位),0表示正数;1表示负数;
>正数的最高位为0,其原码、反码、补码均相同;
如(int型)1的原码:0000 0000 0000 0000 0000 0000 0000 0001
反码:0000 0000 0000 0000 0000 0000 0000 0001
补码:0000 0000 0000 0000 0000 0000 0000 0001
>负数的最高位为1,其反码为通过将原码除符号位以外的其他比特位进行取反得到,其补码通过在反码的基础上加1得到;
如(int型)-1的原码:1000 0000 0000 0000 0000 0000 0000 0001
反码:1111 1111 1111 1111 1111 1111 1111 1110
补码:1111 1111 1111 1111 1111 1111 1111 1111
而若想从一个负数的补码得到其原码,其方法与从原码求得补码的方法一致。
正数1存储在内存中表示为:0000 0000 0000 0000 0000 0000 0000 0001
负数-1存储在内存中表示为:1111 1111 1111 1111 1111 1111 1111 1111
②计算机中也是以补码的形式进行运算的,且CPU中没有减法器,只有加法器,如2 - 1的运算在计算机中就会转换为2 + (-1)进行。
这里以一道题目来加深对整型在内存中的存储的理解:求以下代码的输出结果。
分析:i为int型,大小为4个字节,a[i]为char型,大小为1个字节,a[i]的取值范围为-128~127。strlen函数用于计算字符串长度,遇到'\0'(ASCII码值为0)则停止,那么要知道strlen(a)的值,关键在于a[i]何时为0,观察代码:进入循环,随着i的增加,a[i]逐渐减小,直到i = 127时,a[i]为-128,取到最小值,而当i继续增加为128时,这时我们来看a[i] = -1 - i;的具体运算过程:
以int整型计算:-1-i = -129,-129在内存中的存储情况如下:
-129原码:1000 0000 0000 0000 0000 0000 1000 0001
-129反码:1111 1111 1111 1111 1111 1111 0111 1110
-129补码:1111 1111 1111 1111 1111 1111 0111 1111
将int型的值(-1 - i = -129)赋值给char型变量a[i]:
此时将一个int型大小的数据存入char型大小的变量的内存中会发生截断:只有低位一个字节的数据存入到了a[i]变量的内存空间中,即a[i]变量的内存空间存入的数据为-129的低8比特位:0111 1111
以char型读取该数据,最高位为0,认为其为正数,则原、反、补码均相同为:0111 1111,即 a[i] = 127
可以发现,随着i从0开始增加,a[i]在其取值范围形成的循环内从-1开始进行取值(-1 > -128 > 127 > 0 > -1),如下图所示,可以计算当i = 255时,a[i] = '\0',因此strlen(a) = 255。
以下运行验证:
字节序
①计算机在内存中进行数据存储和访问都是以字节为单位的,那么当存储超过一个字节长度的数据时(这里以int型为例),该数表示为二进制补码后各字节的数据又是以什么顺序存放在开辟好的4字节空间中?以下通过举例来观察:
如下图所示,定义一个int型变量a,并给其初始化为正数6,运行程序,在调试->窗口->内存中输入&a,查看变量a地址中的存储情况,已知6的补码表现为十六进制时为:0x00000006,其中00 00 00 06长度各为1个字节。可以看到,低位数据06存储在了低地址0x000000ABC39FF544空间中,高位数据00存储在了高地址0x000000ABC39FF547空间中,对于我们的阅读习惯来说应该是倒序的,这即是一种字节序。
② 那是否所有超过一个字节的数据在内存中都是以这个顺序存放的呢?答案是不一定,对于不同的处理器,所采用的字节序有所不同,通常将字节序分为两种:
大端字节序:指数据的低位保存在内存的高地址中,数据的高位保存在内存低地址中;
小端字节序:指数据的低位保存在内存的低地址中,数据的高位保存在内存高地址中;
如上例子中机器的字节序即为小端字节序。我们常用的X86结构是小端字节序,Keil C51是大端字节序。
这里以一道题目来加深对字节序的理解:设计一个小程序来判断当前机器的字节序?
分析:当存储一个int类型的数据在内存中时,这里假设为0x00000001,如过数据按照小端字节序进行存储,那01这一个字节的低位内容将存储在内存中的低地址处;而如果按照大端字节序存储,那00这一个字节的高位内容将存储在内存中的低地址处。如此,若能获取数据的存储地址,通过对低地址处的这一个字节的存储内容进行访问,根据获取到的不同内容就可以判断当前机器的字节序了。
以下是具体的代码实现:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int check_sys() {
int num = 1;
return *(char*)# //获取num的地址,只访问低地址处的一个字节的内容
}
int main() {
int ret = check_sys();
if (ret == 1) {
printf("小端字节序!");
}
else {
printf("大端字节序!");
}
return 0;
}
2.2 浮点型在内存中的存储
根据国际标准IEEE(电气和电子工程协会) 745,任何一个二进制浮点数都可以表示为以下形式:
— (-1)^S * M * 2^E— (-1)^S表示符号位,当S=0,为正数;当S=1,为负数。— M表示有效数字,大于等于1,小于2。— 2^E表示指数位。
以下举例:
十进制的6.5,表示成二进制为:110.1,相当于1.101*2^2,则按照以上规定:S = 0;M = 1.101; E = 2;即:110.1 = (-1)^0 * 1.101 * 2^2;
这里值得注意的一点是:0.5 = 1 * 2^(-1);此时的0.5是能用二进制精确表示的,若是0.3用二进制表示后只能无限逼近,即0.3 = 2^(-3) + 2^(-4) + ...,但内存空间是有限的,所以有的小数用二进制表示后是无法取得精确值的。
IEEE 745规定:
对于32位的单精度浮点数,最高的1位是符号位S,接着的8位是指数E,剩下的23位为有效数字M:
S(1bit) E(8bit) M(23bit)
[ ] [ ][ ][ ][ ][ ][ ][ ][ ] [ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]
对于 64 位双精度的浮点数,最高的 1 位是符号位S,接着的 11 位是指数 E ,剩下的 52 位为有效数字 M:S(1bit) E(11bit) M(52bit)
[ ] [ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ] [ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]...
在计算机内部保存 M 时,默认这个数的第一位总是 1 ,因此可以被舍去,只保存1.xxxxx后面的 xxxxxx部分,如此就相当于M可有24位有效数字。
指数E为无符号数, 如果E 为 8 位,则其取值范围为 0~255 ;如果 E 为 11 位,则其取值范围为 0~2047。IEEE 754 规定:存入内存时 E 的真实值必须再加上一个中间数,对于 8 位的 E ,这个中间数是 127 ;对于 11 位的 E ,这个中间数是1023 。如此,E的真实值就可以取得一定范围内的负数。比如, 2^10 的 E 是 10 ,所以保存成 32 位浮点数时,E必须保存成 10+127=137 ,即 10001001。将指数E从内存中取出可分为三种情况:E不全为0或不全为1:此时E的真实值为计算值-127(或1023),有效数字M真实表示为取出的二进制数前面加1后的值;E全为0:规定此时E的真实值为(1 - 127)或(1 - 1023),有效数字M前面不再加1,还原为0.xxxxxx的小数,以此表示正负0,以及接近于0的很小数字;E全为1: 这时,如果有效数字M全为0,表示正负无穷大(正负取决于符号位s);
3 数据运算时的注意事项总结
①确定数据类型(整型还是浮点型,数据类型的大小,有符号还是无符号等)
②注意使用补码进行运算
③注意运算过程中的整型提升和算数转换
④注意机器的字节序
以上为个人学习理解,如有错误,希望大家帮忙指正,也欢迎大家给予建议和讨论,谢谢!