今天想给大家分享一下几个简单数据类型在内存中的存储方式以及规则,附有一些例题,都很有趣,那么咱们开始吧!
整数二进制有三种表示方式,分别是:原码,反码,补码
对于整型来说:计算机中存储的形式是补码
原码是十进制的直接二进制表示形式
就整型来说,还可分为有符号整型(+/-,0表示正数,1表示负数)和无符号整型,下面先来讨论有符号整型的转换。
为了便于大家的理解这里举个例子:
(这里为了方便采用八个比特位模拟展示)
十进制整数 6
二进制原码:00000101
二进制反码:00000101
二进制补码:00000101
十进制整数 -6
二进制原码:10000101
二进制反码:11111010
二进制补码:11111011
为什么能用原码存储数据,还要引入反码和补码呢?
这是因为,再计算机数字计算的过程中,实现二进制减法的时候是一个相对加法复杂很多的过程。引入补码便可以通过简单的加法运算来实现有符号整数的加减法,而不再需要单独处理符号位了。这里有两点性质:补码加上其原码等于零,补码的补码等于其原码。
在刚刚讲解过整数的存储后,其他类型数据的存储也就好理解了。
了解了整数在内存中存储的形式,其实还有必要了解一下内存存储数据的不同字节顺序,这里分为大端序字节存储和小端序字节存储
大端字节序存储:高位字节存储在低地址
小端字节序存储:高位字节存储在高地址
我们可以先存储一个数据,来在编译器中观察一下其存储顺序
#include
int main()
{
int a = 0x11223344;
return 0;
}
在初始化a过后,我们打开存储a的地址内存,可以发现,11 22 33 44似乎是从右向左存储的,这代表着11这样的高位字节存储在了高位地址,是典型的小端字节序存储。下图在再进行进一步解释,大小端序。
总的来说,大小端是存储数据的不同方式,它们之间也没有什么优劣之分,只要能达到存储和拿取数据的目的,哪一种方式都可以。
说了这么多,其实让我联想到了一个大厂的笔试题,考的是通过写一份代码,来判断运行机器是大端序还是小端序。 我把这道题的答案放到下面供大家来参考
#include
int main()
{
int n = 1;
if (*(char*)&n == 1)
printf("小端\n");
else
printf("大端\n");
return 0;
}
这道题的原理其实很好理解
当为小端序时,指向的是0x01,表示的数字也就是1;当为大端序时 ,指向的是0x00,表示的数字是0。这样,就能很好的把大小端区分开了。
整型提升是指将较小的整数类型转换为较大的整数类型的过程。举个例子,如果有一个int类型的变量和一个char类型的变量进行加法运算时,char类型的变量会被提升为int类型再进行运算。这是因为int类型大于char类型,这样提升能有效避免精度的丢失。
整型提升的规则:
- 有符号整数提升按照数据类型的符号位来提升,提升时补符号位
- 无符号整数提升,高位直接补0
下面举两个整型提升的例子
//有符号整型提升
//负数整型提升
char a = -1;
内存中存储:11111111(8位)
整型提升后:11111111111111111111111111111111(32位)
//正数整型提升
char a = 1;
内存中存储:00000001(8位)
整型提升后:00000000000000000000000000000001(32位)
//无符号整型提升
unsigned char a = -1;
内存中存储:11111111(8位)
整型提升后:00000000000000000000000011111111(32位)
unsigned char a = 1;
内存中存储:00000001(8位)
整型提升后:00000000000000000000000000000001(32位)
现在我们来看一道例题
#include
int main()
{
char a = -1;
signed char b = -1;
unsigned char c = -1;
printf("%d %d %d\n", a, b, c);
return 0;
}
问:求以上程序的输出结果?
先看a再打印出来之前经历的转换
a
原码:10000001
反码:11111110
补码:11111111
打印时整型提升(补符号位,这时符号位为1):
11111111111111111111111111111111(此时这个二进制再计算机内存中,为补码)
打印原码:10000000000000000000000000000001
打印结果为:-1
由于signed char == char,所以b打印的结果也为-1
再来看c
c
原码:10000001
反码:11111110
补码:11111111(-1会先被转化成补码赋给无符号的c)
打印时整型提升(无符号位提升,直接补0):
00000000000000000000000011111111(此时这个二进制再计算机内存中,为补码)
打印原码:00000000000000000000000011111111(无符号数原码和补码相等)
打印结果为:255
讲到这里,刚刚的题也给大家解释清楚了,现在想再补充一个内容,以char举例,竟然char的范围是-128~127/0~255,那么用二进制码表示的位置是怎样的呢?这里展示一个图解,能极大的帮助大家理解这个问题。
以上两张图,外圈是存储再计算机中的补码表示形式,内圈是对应类型原码所表示的十进制数值,此图用以表示有符号和无符号对应的数字存储形式,其实不但应用于char类型,同样也适用于int,long,longlong等类型。
这里还要注明一点,char类型中10000000中按照常理来说表示的应该是-0才对,但是再计算机编译环境中的规定,表示的是-128,不只是char,int,long等其他类型也是如此。所以,再有符号类型中,负数最小值的绝对值一般比正数的最大值大1。
下面再来看例题二,一下就简单多了
#include
int main()
{
char a = -128;
printf("%d\n", a);
return 0;
}
问:以上代码打印结果?
对于-128,计算机中放入的对应值在上图中已经显而易见,10000000,而%u则表示打印无符号的整型,在-128的整型提升中,10000000,变成了11111111 11111111 11111111 10000000(有符号数整型提升补符号位),在打印无符号数的时候结果为4294967168。
#include
int main()
{
char a = 128;
printf("%d\n", a);
return 0;
}
问:以上代码的打印结果?
可以注意到128已经超出了char能存储的范围,但是这个程序硬是将这样一个值放入其中的化,那么128会转化成补码10000000,放入a,那a的补码10000000解读过来又是什么呢?是-128,所以这是和例题二便重合了,打印结果依然是 4294967168。
#include
#include
int main()
{
char a[1000];
for (int i = 0; i < 1000; i++) {
a[i] = -1 - i;
}
printf("%d\n", strlen(a));
return 0;
}
问:以上代码的打印结果?
我们之前已经介绍过了char类型数据在内存中的存储,题目代码中,是由-1逐渐存到-1000,那在内存中便是-1,-2,-3。。。-127,-128,127,126,125。。。3,2,1,0,-1,-2。。。
以以上这种方式存储在内存中,并且存储的数字是一个完美的闭环,在打印字符串长度的过程中,我们知道strlen计算的截止标志是'\0',那么strlen最后计算的便是-1,-2,-3。。。-127,-128,127,126,125。。。3,2,1这之间数字的个数了,程序最终运行打印得255。
如果不了解strlen和字符串函数,我的上一篇博客有详细讲到大家可以参考(链接如下):
字符串函数-C语言-CSDN博客
#include
unsigned char i = 0;
int main()
{
for (int i = 0; i <= 255; i++) {
printf("Hello world!");
}
return 0;
}
问:以上代码输出结果?
由于unsigned char类型最大值也无法超过255,所以此程序会循环打印Hello world!
#include
int main()
{
int a[4] = { 1,2,3,4 };
int* ptr1 = (int*)(&a + 1);
int* ptr2 = (int*)((int)a + 1);
printf("%x,%x\n", ptr1[-1], *ptr2);
return 0;
}
问:以上代码输出结果?
&a取出整个数组的地址,加一个1跳过一个数组指向末尾,在打印ptr1[-1]时相当于*(ptr1 - 1),所以前一个打印的值是4
第二个指针在初始化的时候转成了整型,加一再解引用相当于往后移了一个字节,最后转成int*类型指针最后打印结果为0x02000000
再以一道题引入
#include
int main()
{
int n = 9;
float* pFloat = (float*)&n;
printf("n的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);
*pFloat = 9.0;
printf("num的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);
return 0;
}
问:以上代码输出结果?
先把答案给大家,看看大家是否能理解
n的值为:9
*pFloat的值为:0.000000
num的值为:1091567616
*pFloat的值为:9.000000
相信第二行和第三行的结果会与本来所想的结果不同吧
具体原因还是要来了解一下浮点数再内存中的存储
浮点数在内存中的存储遵循IEEE754标准,是目前最广泛使用的浮点数表示方法
任意一个二进制浮点数V可以表示成下面的形式:
V=(-1)^S*M*2^E
其中:(-1)^S表示符号位,S=0时V为正数;S=1时V为负数
M表示有效数字,M大于等于1,小于2
2 ^E表示指数位
单精度浮点数,也就是float:有一个符号位,8个指数位以及23位尾数位
双精度浮点数,也就是double:有一个符号位,11位指数位以及52位尾数位
拿5.0来举例:
十进制的5.0,写成二进制为101.0,相当于1.01*2^2。
可以得到S=0,M=1.01,E=2。在计算机保存M时,默认第一位总是1,因此可以被舍去,只保留后面的部分。在保存1.01的时候,在计算机中保存的数为01,在读取时,在加上之前省略的1,变回1.01,再将剩下的用0填充。因此M在储存1.01时,便为01000000000000000000000(23位)
再来看E,E作为一个无符号整数,再E为8位时,取值范围是0~255;如果为11位,则取值为0~2047,但是在实际运用的过程中,也常常会碰到指数为负的情况,这怎么办呢?据此,IEEE754规定,存入内存时在指数位E之间加一个中间数,对于八位的E,这个中间数为127;而对于11位的E,这个数为1023,指数位存入的数据便是(八位:127+E)(十一位:1023+E)所以就可以据此来表示指数位的正负了。
最终5.0在计算机中存储的数据为:
0 10000001 01000000000000000000000
在读取浮点数的过程中,根据数值E的不同,可以分以下三类情况
这样的指数值代表了有效的浮点数。在解析中,指数值会见去中间量,得到最终的指数值。
对语32位float类型浮点数,中间值为127,如刚才存入的E:10000001,最终指数值为(129-128=2) 对于64位的double也是同理。
这种情况下,浮点数表示出来的是一种极其小的数值,浮点数小于等于10^-127/10^-1023,这时,返回浮点数时M便不在第一位补1,而是直接还原为0.000.....的小数
这时表示浮点数是一个无穷大的数,符号位决定了是正无穷还是负无穷
最后,在来看看之前的那道题
#include
int main()
{
int n = 9;
float* pFloat = (float*)&n;
printf("n的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);
*pFloat = 9.0;
printf("num的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);
return 0;
}
9以整型形式存储的过程中,得到的二进制为:
0000 0000 0000 0000 0000 0000 0000 1001
将此数以浮点数形式读出,会发现E全为0,是上方提到的情况,所以小数就为0.000000
而浮点数9.0,为二进制1001.0,转换成存入计算机中的形式1.001*2^3,S=0,E=127+3=130,M为00100000000000000000000,所以二进制表示式为
0 10000010 00100000000000000000000
将此数转化成整型打印出来便是 1091567616了。
一篇博客写下来真的耗时耗力,本来想着上午快快肝完,这一写就是将近一整天┭┮﹏┭┮,如果感觉本篇博客对你有帮助的话,还请留个小赞,放个收藏,点个关注再走啊!如果本博客有任何错误和不完善还请各位大佬再评论区指正,那就先到这里了,下篇博客再见。比心---♥