目录
1. 整数在内存中的存储
在讲解操作符的时候,我们就讲过了下⾯的内容:
整数的2进制表⽰⽅法有三种,即 原码、反码和补码;
有符号的整数,三种表⽰⽅法均有符号位和数值位两部分,符号位都是⽤0表⽰“正”,⽤1表⽰“负”,最⾼位的⼀位是被当做符号位,剩余的都是数值位。
正整数的原、反、补码都相同。
负整数的三种表⽰⽅法各不相同。
原码:直接将数值按照正负数的形式翻译成⼆进制得到的就是原码。
反码:将原码的符号位不变,其他位依次按位取反就可以得到反码。
补码:反码+1就得到补码。
对于整形来说:数据存放内存中其实存放的是二进制的补码。为什么呢?
在计算机系统中,数值⼀律⽤补码来表⽰和存储。
原因在于,使⽤补码,可以将符号位和数值域统⼀处理;同时,加法和减法也可以统⼀处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是
相同的,不需要额外的硬件电路。
2. ⼤⼩端字节序和字节序判断
当我们了解了整数在内存中存储后,我们调试看⼀个细节:
#include <stdio.h>
int main()
{
int a = 0x11223344;
return 0;
}
调试的时候,我们可以看到在a中的 0x11223344 这个数字是按照字节为单位,倒着存储的。这是为
什么呢?
注:
1、整数在内存中 存储的是二进制的补码
2、在调试窗口中观察内存的时候,为了方便展示,显示的是16进制的值
3、存储的顺序是倒过来的
2.1 什么是⼤⼩端?
其实超过⼀个字节的数据在内存中存储的时候,就有存储顺序的问题,按照不同的存储顺序,我们分为⼤端字节序存储和⼩端字节序存储
拿上面的0x11223344举例:
在讲指针的时候,我们就说过,每个内存单元的⼤⼩取1个字节。
存储一个整型数据我们需要用到4个字节,那么,这4个字节存储的时候就需要拆成4个字节来存储,每个字节占一个内存单元,就出现了顺序的问题。
在此处,小编列出了4种,其实这4种都可以(如何存进去就如何拿出来,结果正确便可),但是由于右边两种的存放方式比较别扭,所以就去除掉了右边两种乱的排列方式,留下正着放与倒着放的两种方式,而它们分别有个名字----大端字节序存储、小端字节序存储
⼤端(字节序存储)模式:
是指数据的低位字节内容保存在内存的⾼地址处,⽽数据的⾼位字节内容,保存在内存的低地址处。
⼩端(字节序存储)模式:
是指数据的低位字节内容保存在内存的低地址处,⽽数据的⾼位字节内容,保存在内存的⾼地址处。
2.2 为什么有⼤⼩端?
为什么会有⼤⼩端模式之分呢?
这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着⼀个字节,⼀个字节为8bit 位,但是在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处理器还可以由硬件来选择是⼤端模式还是⼩端模式。
2.3 练习
2.3.1 练习1
请简述⼤端字节序和⼩端字节序的概念,设计⼀个⼩程序来判断当前机器的字节序。
//代码一
#include<stdio.h>
int main()
{
int n = 1;
if (*(char*)&n == 1)
printf("小端\n");
else
printf("大端\n");
return 0;
}
//代码二
#include<stdio.h>
int check(int n)
{
int n = 1;
return *(char*)&n;
}
int main()
{
int ret = check();
if (ret)
printf("小端\n");
else
printf("大端\n");
return 0;
}
此处强制类型是做不到的
unsigned int a= 0x1234;
unsigned char b = *(unsigned char *)&a;
在32位大端模式处理器上变量b等于什么?
将&a强转为unsigned char*类型后,访问一个低地址字节,得到的就是0,所以变量b等于0x00
2.3.2 练习2
#include <stdio.h>
int main()
{
//char到底是有符号的char还是无符号的char,取决于编译器,在VS中,是signed char
//-1
//10000000 00000000 00000000 00000001 --- 原码
//11111111 11111111 11111111 11111110 --- 反码
//11111111 11111111 11111111 11111111 --- 补码
//由于a是char类型,占1字节,即8个bit位,所以发生了截断
char a = -1;
//11111111 --- a(有符号数)
//通过%d打印的是有符号整数,而a、b、b都是个字符,需要进行整型提升
//1、有符号整数提升是按照变量的数据类型的符号位来提升的,直接补符号位
//2、⽆符号整数提升,⾼位补0
//11111111 11111111 11111111 11111111
signed char b = -1;//(有符号数)
//11111111 --- b
//进行整型提升
//11111111 11111111 11111111 11111111
unsigned char c = -1;//(无符号苏)
//11111111 --- c
//由于c是unsigned char类型,所以高位补0
//00000000 00000000 00000000 11111111
//提升完后,由于%d打印,认为内存中的是有符号整数,在通过%d打印时需转换为原码打印,
//a、b都需计算为原码才能打印,而c符号位是0,代表正数,正数的原反补码相同,不需转换
printf("a=%d,b=%d,c=%d", a, b, c);
return 0;
}
运行结果如下:
2.3.3 练习3
#include <stdio.h>
int main()
{
//10000000 00000000 00000000 100000000
//11111111 11111111 11111111 011111111
//11111111 11111111 11111111 100000000
//a是char类型,发生截断
//10000000 -- a
//整型提升
//11111111 11111111 11111111 10000000
char a = -128;
printf("%u\n", a);
//%u的形式打印,是认为a中存放的是无符号数
return 0;
}
#include <stdio.h>
int main()
{
char a = 128;
//128
//00000000 00000000 00000000 10000000 -- 原码(正数原反补码相同)
//截断
//10000000 -- a 代表
//进行整型提升,char类型,所以高位代表符号位
//11111111 11111111 11111111 10000000
//以%u形式打印,认为内存中的是补码,且其是无符号数(即正数),而正数原反补码相同
printf("%u\n", a);
return 0;
}
3个练习小总结
总的来说,将某个值赋予某个低于整型的类型的变量时,会将其先转换为补码,再通过类型进行截断,拿上面几个代码举例,上面各个代码中将-1、-128、128均赋予char类型变量或unsigned char类型变量或signed char类型变量(char是signed char还是unsigned char,取决于编译器,小编所用的是VS,编译器将char默认为signed char),会先将-1,-128,128均转换为补码(正整数的原、反、补码都相同;负整数的三种表⽰⽅法各不相同。所以正数不需进行转换)
11111111 11111111 11111111 11111111 --- (-1的补码)
11111111 11111111 11111111 10000000 --- (-128的补码)
00000000 00000000 00000000 10000000 --- (128的补码)
进行截断(根据类型截取):由于char类型只占一个字节,即8个bit位,所以截取8个比特位的数值。
11111111 --- (-1截断后的结果)
10000000 --- (-128截断后的结果)
10000000 --- (128截断后的结果)
截取完成后,由于%d打印的是十进制整数、%u都是打印的是无符号整数(unsigned int),所以需要进行整型提升,整型提升在操作符(2)中有讲解过,这里列出整型提升的规则:
1、有符号整数提升是按照变量的数据类型的符号位来提升的,直接补符号位
2、⽆符号整数提升,⾼位补0
这里拿VS编译器为例:
在上面代码中,-1被分别赋予了char、signed char、unsigned char三种类型的变量,-128被赋予了char类型
的变量,128被赋予了char类型的变量。
在VS编译器中,char默认为signed char,所以,char与signed char都认为其变量为有符号数,提升是按照
其变量数据类型的符号位来提升的,而unsigned char代表的是无符号数,高位直接补0
因此,对于变量的值是-1且类型是char与signed char的变量,整型提升结果是:
11111111 11111111 11111111 11111111 --- 补码
通过%d形式打印,认为是有符号整数,打印出来需要转换为原码;转换规则:符号位不变,数值位取反加一,
结果如下:
10000000 00000000 00000000 00000001 --- -1的原码
所以,被赋予-1的char与signed char类型的变量通过%d打印的结果为-1
对于被赋予了-1的unsigned char类型的变量,整型提升的结果是:
00000000 00000000 00000000 11111111 --- 255的补码(正数的原反补码相同)
对于被赋予了-128的char类型的变量,由于在VS编译器中char默认为signed char,所以认为被赋予-128的变
量是有符号数,按符号位来提升,整形提升结果如下:
11111111 11111111 11111111 10000000
通过%u形式打印,打印的是无符号整数,认为变量内存中的是补码,且其是无符号数(即正数),而正数原反
补码相同
所以直接将11111111 11111111 11111111 10000000转换为十进制形式输出,结果为4294967168
对于被赋予了128的char类型的变量,由于在VS编译器中char默认为signed char,所以认为被赋予128的变
量是有符号数,按符号位来提升,整形提升结果如下:
11111111 11111111 11111111 10000000
通过%u形式打印,打印的是无符号整数,认为变量内存中的是补码,且其是无符号数(即正数且高位不再是符号
位),而正数原反补码相同
所以直接将11111111 11111111 11111111 10000000转换为十进制形式输出,结果为4294967168
即在通过%d或%u打印前,整型提升结果仍然存放在内存中,若为%d形式打印,则将高位当作符号位来打印,
为%u形式打印,则全为数值位,不存在符号位
换句话说,本来上面的unsigned c=-1,本来认为c是无符号数,但耐不住有人想用%d打印,吃饱了撑的
所以,小编建议以后大家用的时候,有符号数就用有符号的占位符打印,无符号的用无符号的占位符打印
2.3.4 练习4
#include <stdio.h>
int main()
{
char a[1000];
int i;
for (i = 0; i < 1000; i++)
{
a[i] = -1 - i;
}
//-1 -2 -3 -4 -5 .... -127 -128 -129 -130
printf("%d", strlen(a));//求得是字符串的长度,统计的是\0(ASCII码值是0)之前的字符个数
return 0;
}
上述代码中,char类型的最小值是-128,如-129、-130等是不属于该范围的,那它能存放-129、-130么----应该是不可以的,那么,-129、130存进去是什么呢?
由上图我们可以知道,-128之后会到127,继续循环下去,
-1 -2 -3 -4 -5 -6 ... -127 -128 127 126 125 ... 4 3 2 1 0 -1 -2 ...
直到i=1000停止,而上述又讲过'\0'的ASCII码值为0,在0之前有255个数,
所以strlen(a)结果为255
#include<stdio.h>
int main()
{
unsigned char a = 200;
unsigned char b = 100;
unsigned char c = 0;
c = a + b;
printf(“%d %d”, a+b,c);
return 0;
}
说明:printf在传入参数的时候如果是整形会默认传入四字节,所以a+b的结果是用一个四字节的整数接收的,不会越界。而c已经在c = a + b这一步中丢弃了最高位的1,所以只能是300-256得到的44了。
※由于printf是可变参数的函数,所以后面参数的类型是未知的,所以甭管你传入的是什么类型,printf只会根据类型的不同将用两种不同的长度存储。其中8字节的只有long long、float和double(注意float会处理成double再传入),其他类型都是4字节。所以虽然a + b的类型是char,实际接收时还是用一个四字节整数接收的。另外,读取时,%lld、%llx等整型方式和%f、%lf等浮点型方式读8字节,其他读4字节。
2.3.5 练习5
#include <stdio.h>
unsigned char i = 0;
int main()
{
for (i = 0; i <= 255; i++)
{
printf("hello world\n");
}
return 0;
}
上述代码运行会陷入死循环,为什么呢?
通过 2.3.4 练习4中的例图我们可以推出,其实unsigned char在遇到超出其数据范围的数(0~255)也会进行循环,例如256就是0了,所以最终该代码会陷入一个死循环
#include <stdio.h>
int main()
{
unsigned int i;
for(i = 9; i >= 0; i--)
{
printf("%u\n",i);
}
return 0;
}
有了前两个代码的铺垫,不难想出,这个也会陷入死循环,当i=-1时,由于i是unsigned int类型(0~4 294 967 295),所以不存在负数,就会变成了4 294 967 295
由以上几个代码我们知道,整型数据(基本整型、短整型、长整型、双长整型、字符型等)其实都遵循一个循环过程,对超出其范围的数据会变成另一个数(注意:当你在测试是否遵循时,记得用对正确的占位符)
2.3.6 练习6
#include <stdio.h>
//X86环境 小端字节序
int main()
{
int a[4] = { 1, 2, 3, 4 };
int* ptr1 = (int*)(&a + 1);
int* ptr2 = (int*)((int)a + 1);
printf("%x,%x", ptr1[-1], *ptr2);
return 0;
}
通过上述小端字节序与大端字节序的学习,我们知道:
⼩端(字节序存储)模式:是指数据的低位字节内容保存在内存的低地址处,⽽数据的⾼位字节内容,保存在内存的⾼地址处。
所以,数组a中的元素在内存中的存储如下:
&a,这里的a代表数组名(取出的是一个数组指针,类型 — int(*)[4]),&a+1跳过一个数组,由于此时的类型是int( * )[4],所以应该强转为int *,所以最终ptr1指向图中位置,ptr1[-1]等价于 *(prt1-1),所以最终结果为4
a代表数组首元素的地址,强转为int后加+,就是普通的整型加1,所以此时ptr2指向图中位置,类型是int *,所以进行 *ptr2时,访问的是四个字节,且由于是在小端字节序模式下,所以取出结果十六进制形式为0x02000000
运行结果如下:
3. 浮点数在内存中的存储
常⻅的浮点数:3.14159、1E10等,浮点数家族包括: float、double、long double 类型。
浮点数表⽰的范围: float.h 中定义
在讲解前,让我们先来看一段代码
#include <stdio.h>
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是一个整型数据,取出来以整数形式打印,没半点毛病,对吧!
此时,将n的地址赋予一个float类型的指针,站在浮点型指针的角度来看,若对浮点型指针pFloat解引用,此时就要向后访问一个浮点型的空间,即4个字节,而其认为这4个字节中存储的是float类型的数据,最终取出的结果如图
由此可知,当我们以整数形式存进去,以浮点数形式取出,会造成结果与预期不同,所以,整数与浮点数在内存中的存储方式存在着差异
那么,浮点数是怎么存储的呢?
让我们了解完再回来看吧!
3.2浮点数的存储
上⾯的代码中, num 和 *pFloat 在内存中明明是同⼀个数,为什么浮点数和整数的解读结果会差别这么⼤?要理解这个结果,⼀定要搞懂浮点数在计算机内部的表⽰⽅法。
根据国际标准IEEE(电⽓和电⼦⼯程协会) 754,任意⼀个⼆进制浮点数V可以表⽰成下⾯的形式:
如何理解呢?
下面将以十进制形式的5.5为例:
5.5 --- 十进制的浮点数表示形式
这时候有人会问:能不能把5.5写成二进制形式呢?
答案是可以的。
那么如何转换呢? --- 小数点前后分别进行转换
我们知道小数点前面的5可以写成101,那么,小数点后面的5也是101么,不是的
对一个二进制数进行解读:
所以,5.5写成二进制应该是101.1,用科学计数法表示就是1.011*
2
2
2^{2}
22
由于该数是二进制,所以底数是2;如果是十进制的数话,如:123.45,用科学计数法表示就是1.2345*
1
0
2
10^{2}
102
IEEE(电⽓和电⼦⼯程协会) 754表示,任意⼀个⼆进制浮点数V可以表⽰成下⾯的形式:
所以1.011* 2 2 2^{2} 22又可以这样表示:V= ( − 1 ) 0 (-1)^{0} (−1)0 * 1.011 * 2 2 2^{2} 22;其中,S ---- 0,M ---- 1.011,E ---- 2
由于其中的(-1)与底数2都是固定的,所以浮点数的存储,其实存储的就是S、M、E相关的值
IEEE 754规定:
对于32位的浮点数(float),最⾼的1位存储符号位S,接着的8位存储指数E,剩下的23位存储有效数字M
对于64位的浮点数(double),最⾼的1位存储符号位S,接着的11位存储指数E,剩下的52位存储有效数字M
3.2.1 浮点数存的过程
IEEE 754 对有效数字M和指数E,还有⼀些特别规定。
前⾯说过, 1≤M<2 ,也就是说,M可以写成 1.xxxxxx 的形式,其中 xxxxxx 表⽰⼩数部分。
比如:0.5(十进制)
二进制:0.1 —>1.0 * 2 − 1 2^{-1} 2−1
IEEE 754 规定:
在计算机内部保存M时,默认这个数的第⼀位总是1,因此可以被舍去,只保存后⾯的xxxxxx部分。⽐如保存1.01的时候,只保存01,等到读取的时候,再把第⼀位的1加上去。这样做的⽬的,是节省1位有效数字。以32位浮点数为例,留给M只有23位,将第⼀位的1舍去以后,等于可以保存24位有效数字。
⾄于指数E,情况就⽐较复杂
⾸先,E为⼀个⽆符号整数(unsigned int)(IEEE 754规定)
这意味着。如果E是8位,它的取值范围为0~255;如果E为11位,它的取值范围为 0 ~2047。但是我们知道,科学计数法中的E是可以出现负数的(比如上面提到的十进制0.5,其二级制为0.1,科学计数法表示为1.0 * 2 − 1 2^{-1} 2−1,其中E为 -1),所以IEEE 754规定,存入内存时E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。比如: 2 10 2^{10} 210的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001。
在这里给出一个代码帮助理解:
//大家自己在举例的时候,不要举特殊数字
//3.14
//11.0010101000010101010101010100000010000010101010010101010
//如果小数点后的位太多,就可能导致浮点数在内存中无法精确的保存
int main()
{
//5.5
float f = 5.5f;
//5.5
//101.1
//1.011*2^2
//(-1)^0 * 1.011 * 2^2
//S=0
//M=1.011
//E=2
//2+127=129
//0100 0000 1011 00000000000000000000
//40 B0 00 00
//
return 0;
}
下面的图可以帮助了解! |
3.2.2 浮点数取的过程
指数E从内存中取出还可以再分成三种情况:
E不全为0或不全为1
这时,浮点数就采⽤下⾯的规则表⽰,即指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上第⼀位的1。
⽐如:0.5 的⼆进制形式为0.1,由于规定正数部分必须为1,即将⼩数点右移1位,则为1.0 *
2
−
1
2^{-1}
2−1,其阶码为-1+127(中间值)=126,表⽰为01111110,⽽尾数1.0去掉整数部分为0,补⻬0到23位,即00000000000000000000000,
则其⼆进制表⽰形式为:
0 01111110 00000000000000000000000
E全为0
这时,浮点数的指数E等于1-127(或者1-1023)即为真实值,有效数字M不再加上第⼀位的1,⽽是还原为0.xxxxxx的⼩数。这样做是为了表⽰±0,以及接近于0的很⼩的数字。
0 00000000 00100000000000000000000
E全为1
这时,如果有效数字M全为0,表⽰±⽆穷⼤(正负取决于符号位s);
0 11111111 00010000000000000000000
好了,关于浮点数的表⽰规则,就说到这⾥。
接下来,让我们回到一开始的代码题吧!
#include <stdio.h>
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;
}
注:此时针对的是32位浮点数
好了,到这里对数据在内存中的存储就结束了,希望大家都有所收获!