1整型的存储方式
这里的整型指的是int,long,short,char以及相应的signed和unsinged类型。对于这些数据的存储,涉及的是原码,反码,补码还有整型提升(integer promotion)。
1.1三个码
通常我们cout或者printf(“%d”)时,我们看到的整型数字是由对应的原码组成的,而该码的具体内容由该数字的二进制形式表示。如-10的原码 = 10000000 00000000 00000000 00001010。
反码,就是原码的取反,但是如果原码的首位是符号位,则不改变符号位的值,于是-10的反码就是11111111 11111111 11111111 11110101。
补码就是在反码的基础上+1,即11111111 11111111 11111111 11110110。补码取反后(符号位不变)+1=原码。补码就是-10这个数字在内存中保存的样子(如果在VS环境下查看的话,是补码的16进制表示,且是小端字节序)。而CPU在运算时,用的就是补码。
实际上,原码反码补码是一个只针对负数的,有符号的一个概念,对于一个正数来说,原码就是他的补码。更深一点,由于CPU中只有加法器,不能执行1-1这样的操作,只能执行1+(-1)的操作,而补码的概念就类似与将负数转化成+(-1)的操作。我们考虑如下的情况:
请用原码补码计算10-10
10 = 00000000 00000000 00000000 00001010
因为10是正数,所以两个码一致。
-10 = 100000000 00000000 00000000 00001010
根据上文推导,-10的补码=11111111 11111111 11111111 11110110,两个补码相加得到:1 00000000 00000000 00000000 00000000。相加得到的东西似乎不等0,但实际上由于int是只有4个字节大小,即32个bits大小,所以需要截断最后相加的数据,从后往前数32然后丢弃剩下的就是最后的值,即00000000 00000000 00000000 00000000=0,于是CPU完成了这样的计算。
1.2整型提升
整型提升发生在当该数据类型不满足4个字节长度时,即当数据类型是char,short以及对应的unsigned, signed,然后又要求他们要像int这样处理。
我们结合以下代码来探讨。
#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;
}
//ps:char在大部分编译器下是signed char,int 则全是signed int
对于-1的补码=11111111 11111111 11111111 11111111,由于a,b和c是char类型数据(1字节),所以他们都只能存储a=b=c=11111111,但是我们在print的时候又将他们看做int,此时将会发生整型提升,提升的规则如下:
- 如果在提升前的数据类型是有符号的,则在前面补1。如,a和b,在打印时,内存将他们视为11111111 11111111 11111111 11111111,这是补码,翻译成原码就变成了-1。
- 如果在提升前的数据类型是没有符号的,则在前面补0。如c,内存将其视为00000000 00000000 00000000 11111111,因为是无符号的,所以原码跟补码一样,打印出来就是255。
1.3练习题
输出什么?
int main()
{
char a = -128;
cout<<(unsigned int)a<<endl;
cout<<(int) a<<endl;
return 0;
}
输出什么
int main()
{
char a =128;
cout<<(unsigned int)a<<end;
}
输出什么?
int main()
{
int i = -20;
unsigned j = 10;
cout<<i+j;
return 0;
}
2浮点型的存储方式
一下内容如果直接理解有困难,可以访问这个网站
https://www.h-schmidt.net/FloatConverter/IEEE754.html
你可以通过这个网站变看边做。
C/C++对于浮点型的存储遵循IEEE(电子和电子工程协会)754,任意一个2进制浮点数V可以表示成下面的形式:
- (-1)^s*M*2^E
- (-1)^s表示符号位,当s=0,V为正;s=1,V为负。
- M为有效数字,[1,2)
- 2^E表示指数位
- 即只需要存储s,M,E三个数
此外,IEEE 754对有效数字M和指数E,还有一些规定。由于M总是以1.xxxx,1开头,所以只存储xxxx部分,所以实际上M处可以保存24为有效数字(float);至于E的存储规则细节不谈,结论是8bit要+中间值127,11bit+中间值1023。(注意这个结论只是为了方便使用,而且在精度要求不高的前提下是可行的,E的存储细节是区分三种不同浮点数的关键,你可以查看《CSAPP》了解)
依据这种规则:
float f = 5.5f->101.1->1.011*2^2;
则 s=0,M=011,E=2+127=129(E_r=2);
则内存中的存储为:0100 0000 1000 0000 0000 0000 0000 0011对应的16进制储存 40 b0 00 00,因为是小端字节序(等下会讲),所以实际上看到的是00 00 b0 40。
对于float来说,他占32bits,即4字节,他是如何存储s,M,E这三个数呢?
而对于double,他占64个bits,存储方式如下所示:
说了这么多不如直接上手感受一下,
#include<iostream>
using namespace std;
int main()
{
int n=9;
//将整数9以浮点型存入内存中
float* pFloat = (float*)&n;
cout<<(int)n<<endl;
cout<<(float)*pFloat<<endl;
*pFloat = 9.0;
cout<<(int)n<<endl;
cout<<(float)*pFloat<<endl;
system("pause");
return 0;
}
对于int n = 9, 来说,int是一个4字节的数据,跟float字节数一样,他的补码(就是他的原码) = 0 00000000 00000000000000000001001,注意到我这次不再是每8个空一下,我这里的空法是跟float的存储方式的图是保持一致的,当将9视为一个浮点数进行打印时,第一个0代表s,00000000为E,对应的实际E为-127,00000000000000000001001是M,则实际上他打印的是一个:(-1)^0*1.00000000000000000001001*2^(0-127)这个二进制对应的一个数,这个数在可以展示的精度下直接变成0.0000…。
而对于一个*pFloat=9.0来说,他的二进制表示为1001.0 = 1.001*2^3,此时,s=0,E=3+127=1000 0010,M=001, 0 1000 0010 00000000000000000000001 = *pFloat,要求他以整数形式输出(因为是正数,所以不用原码补码的转换),则输出的就是1,090,519,041。
2.1习题
这部分没习题,因为我搞不来小数点后面的二进制。
3大小端字节序
编译器在安排内存的存储时,将数据倒着存(小端字节序)。比如,int a = 0x11223344,大端字节序为11223344,把数据的低位字节序的内容放在高地址处,高位字节序内容放低地址处;小端字节序为44332211。这个你们可以在VS这个IDE中查看,而对于不能查看的,请完成下面的一个练习。
设计一个小程序判断当前机器的字节序。
#include <iostream>
using namespace std;
int main()
{
int a = 1;
char *pa = (char *)&a;
if (*pa == 1)
{
cout << "s" << endl;
}
else
{
cout << "l" << endl;
}
system("pause");
return 0;
}
4 结构体的存储
结构体的存储涉及到offset,偏移量以及对齐的概念。
struct A
{
char a;
int b;
int c;
};
存储规则如下:
- 对于第一个数据成员,他永远放在偏移量为0的地方
- 对于其他数据成员,他会在偏移量为他的对齐数处存放。
- 对齐数 = Min{sizeof(该数据成员类型),编译器默认的对齐数(VS是8)}
- 存放完所有数据后,结构体的大小为数据成员中最大的对齐数的最小整数倍(当然是大于里面所有数据所占内存大小)
这是另一个例子,在这个例子中我们假设运行再32位机子上,则
struct foo{
int a;// at 0-3
char b;// at 4
short c;// at 6-7
char *d;// at 8 - 11
char e;//12
};
12 - 0 + 1 = 13 ,不是其中最大的对齐数(4,int)的整数倍,所以额外的14-15这两个空间对齐进来。