目录
一、概论
1.1 C语言中基本的数据类型
1.2 类型的基本归类
二、整形在内存中的存储
2.1 原码、反码、补码
2.2 存储补码和大小端存储
三、计算各基本数据类型的范围计算原理
3.1 有符号类型的整形范围
3.2 无符号类型的整形范围
3.3 例题
C语言提供了非常多的数据类型,我们可以用sizeof来计算它们在内存中所占的字节数,我们今天想要深入了解它们存储的底层原理。
char - 字符型 所占字节为1
short - 短整型 所占字节为2
int - 整形 所占字节为4
long - 长整型 所占字节>=4,通常为4或8
long long - 更长的整形 所占字节为8
float - 单精度浮点型 所占字节为4
double - 双精度浮点型 所占字节为8
类型的意义:
1. 使用这个类型开辟内存空间的大小(大小决定了使用范围)。
2. 如何看待内存空间的视角。
简单来说,我们要搞明白不同数据类型是怎么存进去的和怎么拿出来的。
整形家族:
char
unsigned char
signed char
short
unsigned short [int]
signed short [int]
int
unsigned int
signed int
long
unsigned long [int]
signed long [int]
浮点数家族:
float
double
构造类型:
> 数组类型
> 结构体类型 struct
> 枚举类型 enum
> 联合类型 union
指针类型:
int *pi;
char *pc;
float* pf;
void* pv;
空类型:
void 表示空类型(无类型)
通常应用于函数的返回类型、函数的参数、指针类型。
1)原码、反码、补码就是整数的二进制序列;
2)正整数的原码、反码和补码完全一致;
3)负整数的原码、反码和补码不一样,需要相互转换。
4)内存中存储的是补码,拿出来用的是原码
下面我们分别定义正整数a=10,负整数b=-10,由于是int类型,它们都占4个字节,用32个二进制位表示。
a的二进制序列,也是它的原码、反码、补码:
b的原码、反码和补码:
结论:
1)正整数的原码、反码和补码是一致的;
2)负整数的原码转化把符号位以外的二进制位按位取反得到反码,反码+1得到补码。
数据在内存中存储,以十六进制位表达:
内存中存储的是变量的补码,我们来验证一下,把a和b的补码转化为十六进制位:
注:a是正数,a的原码反码补码一致。
注:b是负数,要先写出-10的原码,然后转化为反码,反码再转化为补码。
结果表明内存中存储的是补码,但是是反过来存储的,这就涉及到大小端字节序的概念
大端存储:数据的低位保存在内存的高地址中,高位保存在内存的低地址中。
小端存储:数据的低位保存在内存的低地址中,高位保存在内存的高地址中。
对于b来说,
因此a和b的大端字节序和小端字节序存储分别是:
结论:在当前电脑的机器环境下,数据在内存中是以小端字节序存储的。
一道判断大小端存储的面试题:
请简述大小端字节序的概念,并设计一个程序判断当前机器的字节序。
在上面解释了大小端的概念,那如何判断当前机器的字节序呢?我们想要得到当前变量在内存中存储的十六进制序列,为了这个序列尽可能的简单,我们就定义一个整形变量让它的值为1,那么整形1的十六进制表达是0x00000001
如果是小端字节序存储,那么就是 (低地址) 01 00 00 00(高地址)
如果是大端字节序存储,那么就是 (低地址) 00 00 00 01(高地址)
我们只想访问其中1个字节,得到整形1的高位(或者低位),解引用看一看到底是1还是0,那么就能判断到底是大端存储还是小端存储
原本的变量是int*类型的,*解引用后得到的是4个字节,那么我们将其强制类型转化为char*类型,*解引用后就只得到1个字节
int main()
{
int a = 1;
char* p = (char*)&a;
if (*p == 1)
{
printf("小端字节序存储\n");
}
else
{
printf("大端字节序存储\n");
}
return 0;
}
参考来源:https://www.cnblogs.com/luofay/p/6070613.html
注:1个字节的类型的二进制序列有8位,2个字节的有16位,4个字节的有32位,8个字节的有64位,由于变量在内存中存储的是补码,补码的二进制序列和对应类型所占的字节数量有关,因此各基本数据类型的范围和它们所占字节的大小有关。
我们以char类型举例:
char类型由于只占1个字节的内存,所以它的二进制序列只有8位,所以我们容易列举出char类型二进制序列的所有可能,即从00000000~11111111。
我们一般默认char就是signed char,就是有符号的char,因此它的二进制序列的第一位要看成符号位,那么01111111和1000000就应该是char类型范围的两个边界,如图所示:
其实,在这255组可能的序列中,我们除了10000000这个序列无法正常计算出它的原码,其他254组二进制序列我们都可以用正数原反补相同、负数要相互转化的原理来计算出它们的原码以及对应的十进制数,所以规定10000000这个序列的原码对应的十进制数为-128。
因此有符号char类型的范围就是-128~127。
剩下short类型、int类型、long类型和long long类型的范围计算原理是一致的,只不过由于它们所占字节数不同,就要用不同长度的二进制序列表示,二进制序列一长,2的所占权重可能就增大,所以范围就相应地比char类型要大的多了。
int类型的范围是-2^31 ~ (2^31-1),short类型的范围是-2^15 ~ 2^15-1
我们容易观察表知,无符号类型的范围就是0~a,a是对应有符号类型的范围的长度。
比如char的范围是-128~127,unsigned char的范围就是0~255;int的范围是-2^31 ~ (2^31-1),unsigned int的范围就是0~2^32-1;short的范围是-2^15 ~ 2^15-1,unsigned short的范围是0~2^16-1等等。
我们以unsigned char类型举例,与char进行对比:
由于是无符号类型,所以第一位就不需要看成符号位,因此在内存中存储的补码和拿出来用的时候的原码是一致的。
解题步骤:
1)先写出变量值的原码;
2)根据是正数还是负数,写出反码和补码;
3)根据变量的类型,判断数据是否完全存储进这个变量,不完全存储则发生截断;
4)观察这个变量是有符号类型还是无符号类型,对应地发生整形提升:有符号位高位补符号位,无符号位高位补0。
5)观察是以什么方式打印出来的(以什么方式使用)以及变量本身是有符号还是无符号,根据补码计算出原码。
例题一
int main()
{
char a = -1;
signed char b = -1;
unsigned char c = -1;
printf("%d\n%d\n%d\n", a, b, c);
return 0;
}
解:a和b是一致的,在vs环境下char就是signed char,因此a和b打印出来是一样的。
c是无符号类型,所以在整形提升和最后的原码计算方面和a、b不一致。
例题二
int main()
{
char a = -128;
printf("%u", a);
return 0;
}
解:
例题三
int main()
{
char a = 128;
printf("%u", a);
return 0;
}
解:
例题四
int main()
{
int i = -20;
unsigned int j = 10;
printf("%d", i + j);
return 0;
}
解:由于此时i和j都是int类型,可以完全存储数据,所以不需要发生截断
例题五
int main()
{
unsigned int i = 9;
for (i = 9; i >= 0; i--)
{
printf("%u\n", i);
Sleep(1000);
}
return 0;
}
解:
这是一个死循环,说明循环不仅仅是正常从9执行到0。
i是无符号整形,说明i恒为正数,前9到0是正常的打印,后面的负数有可能转化为正数,所以我们考虑负数转为无符号整数打印出来的是怎样的数。
而且此时也不需要发生截断。