目录
前言
1. 数据类型详细介绍
2. 整形在内存中的存储:原码、反码、补码
3. 大小端字节序介绍及判断
4. 浮点型在内存中的存储解析
前言
大家好,今天这篇文章来深入剖析一下数据在内存中的存储,包括整型和浮点型,中间会穿插一些相关内容的例题来帮助理解、巩固。
1. 数据类型详细介绍
首先,C语言中的基本数据类型包括:(C99标准也加入了bool类型)
注意C语言中并没有字符串类型。
那么数据类型有什么意义呢?首先,数据类型决定了使用这个类型时我们需要开辟的内存空间大小,如上面的大小(字节)分别为1、2、4、4、8、4、8。其次,数据类型也决定了我们访问该块内存空间时是以什么视角访问的。(如指针的使用和数据的打印等等)
下面我们把这些基本类型进行再分类
整型家族:
一般编译器下,我们写的char short int long 默认为signed char short int long,也就是带符号的整型
浮点数家族:
构造类型:
指针类型:
以及空类型(void),也算老相识了,在函数返回类型,函数参数,指针类型里经常用到。
2. 整形在内存中的存储:原码、反码、补码
在计算机中整数一般有三种表示方法,即原码、反码、补码,这三种方法均包含符号位和数值位两部分,其中最高位为符号位,用0表示+,1表示-
原码:直接按照二进制将十进制转换即可(包含符号)
反码:原码的符号位不变,其他位按位取反
补码:反码+1
正数的原码反码补码相同,负数的则需要按上述进行运算。
对于整型来说数据的存储其实是用的补码,是因为CPU中只有加法器,使用补码可以将符号位和数值域统一进行处理,加减法运算统一处理。此外,原码、补码可以互相转换,运算过程相同,不需要额外的硬件电路。
这里我们给出一个负数的例子,看一下它在内存中是怎么存储的
#define _CRT_SECURE_NO_WARNINGS 1 #includeint main() { int a = -10; return 0; }
内存窗口中数据以16进制显示
首先a是一个int类型,那就开辟4个字节,32个比特位
-10的原码:10000000 00000000 00000000 00001010
-10的反码:111111111 111111111 111111111 111110101
-10的补码:111111111 111111111 111111111 111110110
16进制: ff ff ff f6
数据看来是一样的,验证了数据存储的是补码,但是为什么数据的存储顺序好像是反着的呢,下面来解决这个问题。
3. 大小端字节序介绍及判断
首先,什么是大小端字节序?
大端(存储)模式,是指数据的低位(低字节)保存在内存的高地址中,而数据的高位(高字节),保存在内存的低地 址中; 小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地 址中。 比如 int a=0x11223344;0x44为低字节,0x11为高字节 那么上面-10的存储是不是就解决了呢,因为VS编译器采用的是小端字节序,因此会出现上图结果 接下来,为什么会有大小端字节序? 这是因为在计算机系统中,我们是以字节为单位的,每个地址单 元都对应着一个字节,一个字节为8 bit。但是在C语言中除了8 bit的char之外,还有16 bit的 short型,32 bit的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位 或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。 例如:一个 16bit 的 short 型 x ,在内存中的地址为 0x0010 , x 的值为 0x1122 ,那么0x11 为高字节, 0x22 为低字节。对于大端模式,就将 0x11 放在低地址中,即0x0010 中,0x22 放在高地址中,即 0x0011 中。小端模式,刚好相反。我们常用的 X86 结构是小端模式,而 KEIL C51 则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。 那如何判断我们的电脑是大端还是小端呢?#includeint check_sys() { int i = 1; return (*(char *)&i); } int main() { int ret = check_sys(); if(ret == 1) { printf("小端n"); } else { printf("大端n"); } return 0; }
首先,假定一个变量i是正数1,原码(补码)为00000000 00000000 00000000 00000001
那么,如果我们只需要访问第一个字节的数据,如果是小端存储,那访问到的就是00000001 ,十进制也就是1,如果是大端存储,那访问到的就是00000000,也就是0,而因为i是一个整型,用整型指针访问时一次会访问4个字节,不管是大端还是小端都会返回1。因此返回的时候强制类型转换把i的地址存到char* 类型中再解地址访问,就可以访问一个字节。
理解了大小端字节序,我们来看几道题目来巩固理解,会先后给出代码和输出结果。分别判断下面的代码输出什么(需要首先知道计算表达式和存储数据的时候用数据的补码,printf打印的时候用原码)
1.
#includeint main() { char a= -1; signed char b=-1; unsigned char c=-1; printf("a=%d,b=%d,c=%d",a,b,c); return 0; }
首先,上面已经说过,大多数情况下默认char就是signed char 因此a,b的定义是一样的,-1的补码为11111111 11111111 11111111 11111111,存储到char类型中发生截断,因此a,b中存储的数据为11111111,再打印a和b时打印的是%d形式又要发生整型提升(1字节到4字节),补最高符号位到32位,又变成了 11111111 11111111 11111111 11111111,注意这个二进制序列还是补码,打印的时候要还原成原码,补码-1再除符号位按位取反,变成10000000 00000000 00000000 00000001 也就是-1的原码,因此打印-1,那为什么打印c确实255呢 ?因为c在定义的时候类型是无符号char类型,因此-1的补码11111111 11111111 11111111 11111111截断后11111111就会被当成一个正数的补码来存储到c中,打印时发生整型提升,因为无符号,因此整型提升时补0, 00000000 00000000 00000000 11111111,而正数的原码和补码相同,因此这也是c中数据的原码,即十进制255,因此打印后就是255
2.
#includeint main() { char a = -128; printf("%un",a); return 0; }
-128的补码是11111111 11111111 11111111 10000000,截断后存储到c中的就是10000000
打印时因为a是个有符号char,因此发生整型提升补最高位符号位1,11111111 11111111 11111111 10000000,以%u形式打印,认为内存中存储的是无符号数,那么原码又等于补码,因此就打印11111111 11111111 11111111 10000000对应的十进制数,也就是如图结果,可以用计算器进行验证
3.
#includeint main() { char a = 128; printf("%un",a); return 0; }
结果和上面一样,这里只是把a变成了+128,128的补码也就是原码为10000000 00000000 00000000 10000000,发生截断,存储在a中的就是10000000,接下来发生和上一题同样的步骤,因此结果也是一样的,因为底层逻辑a中存储的数据是一样的,a数据的类型也都是有符号char
4.
#includeint main() { int i = -20; unsigned int j = 10; printf("%dn", i + j); return 0; }
-20的补码:11111111 11111111 11111111 11011000
10的补码:00000000 00000000 00000000 00001010
相加后补码 11111111 11111111 11111111 11110110
转换为原码 10000000 00000000 00000000 00001010 也就是-10
5.
#includeint main() { unsigned int i; for (i = 9; i >= 0; i--) { printf("%un", i); } return 0; }
首先,死循环是肯定的,因为i是无符号整型,肯定会一直大于等于0进入循环,当i=0时,进入循环,打印0,然后i--变成-1,-1的补码是32位全1,以无符号形式打印就会把-1的补码当成一个无符号数的补码,而无符号数的原码和补码相同,因此打印32位全1对应的十进制
也就是上图的4294967295,此后类推,就会逐步打印上述结果
6.
#includeint main() { char a[1000]; int i; for (i = 0; i < 1000; i++) { a[i] = -1 - i; } printf("%d", strlen(a)); return 0; }
首先我们知道strlen计算字符串长度遇到停止,而数组a是char类型的,也就是遇到一个字节8个比特位全为0的时候就停止计算长度,-1的补码,老生常谈了,32位全1,i是一个整型,对应的数据的二进制补码也是32位的,而数组a中的元素是char类型的,存储时发生截断,留下-1-i对应的低八位的补码,也就是我们要找的数字i就变成了补码低八位全为1,能和-1的补码相减后变成0的i,i还是个正数,补码原码相同,也就是要找i对应的二进制的低八位为全1的时候,那不就是00000000 00000000 00000000 11111111对应的i么,也就是255,因此本题答案就是字符串长度为255。
到这里这个题已经可以做出来了,但是我们再来深入剖析一下更深层的东西,来看一下有符号、无符号的char类型的十进制取值范围,下面的二进制序列都是补码,
无符号的char取值范围是0-255
有符号的char取值范围是-128~127,其中10000000对应的就是-128,那上面的题第一次出现低八位为全0的时候是不是就是127+128=255,也是一种思路。
7.
#includeunsigned char i = 0; int main() { for(i = 0;i<=255;i++) { printf("hello worldn"); } return 0; }
结果应该是死循环,因为上面已经讨论过了,无符号char的取值范围是0-255,循环条件恒成立,因此无限循环
嗨呀累的一批就这几道题写了我好久啊
4. 浮点型在内存中的存储解析浮点数的存储规则:
根据国际标准IEEE(电气和电子工程协会) 754,任意一个二进制浮点数V可以表示成下面的形式:
(-1)^S * M * 2^E (-1)^s表示符号位,当s=0,V为正数;当s=1,V为负数。 M表示有效数字,大于等于1,小于2。 2^E表示指数位。 举例来说: 十进制的5.0,写成二进制是 101.0 ,相当于 1.01×2^2 。 那么,按照上面V的格式,可以得出s=0,M=1.01,E=2。 十进制的-5.5,写成二进制是 -101.1 ,相当于 -1.011×2^3 。那么,s=1,M=1.011,E=3。 IEEE 754 规定: 对于 32 位的浮点数,最高的 1 位是符号位 s ,接着的 8 位是指数 E ,剩下的 23 位为有效数字M。对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。
int main() { int n = 9; float *pFloat = (float *)&n; printf("n的值为:%dn",n); printf("*pFloat的值为:%fn",*pFloat); *pFloat = 9.0; printf("num的值为:%dn",n); printf("*pFloat的值为:%fn",*pFloat); return 0; }
首先n的值是9是肯定没有问题的,当把n的地址存储到pFloat中时,9的原码补码相同,但因为pFloat是float类型,因此存储时:
E为全0,因此打印的时候就是0.000000(默认保留6位小数)
再来看第三个打印结果,9.0转换为二进制是1001.0,(-1)^0*1.001*2^3,存储的时候S就是0,E就是3+127=30,M就是1.001,因此二进制序列为0 10000010 001 0000 0000 0000 0000 0000
这个二进制序列还原成十进制就是1091567616
结束
至此本文所有内容结束,从底层剖析了数据是如何存储的,其中浮点数的存储稍有拓展,好好掌握冲冲冲
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)