文章目录
- 前言
- 一、整数在内存中的存储
- 二、大小端字节序和字节序判断
- 什么是大小端?
- 为什么会有大小端?
- 练习
- 练习1
- 练习2
- 练习3
- 练习4
- 练习5
- 练习6
- 练习7
- 总结
前言
本篇是修炼内功的文章
首先,你先明白一个事实,数据在内存中是以二进制的形式存储的
然后正文开始!
一、整数在内存中的存储
在讲解操作符的时候,我们就讲过了整数的2进制表示方法有三种:原码、反码和补码
对于有符号数来说,三种表示方法均有符号位和数值位两部分,符号位都是用 0 表示 ‘正’ ,用 ‘1’ 表示负,而数值位最高位的一位是被当作符号位,剩余的都是数值位
正整数的原、反、补码都相同
负整数的三种表示方法各不相同
原码:直接将数值按照正负数的形式翻译成二进制得到的就是原码
反码:将原码的符号位不变,其他位依次按位取反就可以得到反码
补码:反码 + 1就得到补码
并且,补码 -> 取反 -> + 1得到的就是补码
对于整型来说,数据存放内存中其实存放的是补码
为什么呢?我们再来回顾一下:
使用补码,可以将符号位和数值域统一处理;
同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需额外的硬件电路。
二、大小端字节序和字节序判断
什么是大小端?
申请一个变量:int a = 0x11223344; // 占用4个字节
请问,这样在内存中有几种可能?答案是三种:
我们也思考,数据的存储,是未来将来正确的取出,而第三种方式随便放就显得很奇葩,很难记下来,不符合我们常用的习惯,假如你是C语言设计者,也不会选择这一种方式
而第一种叫做大端字节序存储,第二种叫做小端字节序存储
两者的区别都是在存储超过一个字节的数据显现的,其实说到底就是存储顺序的差别,按照不同的存储顺序,我们分为大端字节序和小端字节序存储
大端存储模式:是指数据的低位字节内容保存在内存的高地址处,而数据的高位字节内容,保存在内存的低地址处
小端存储模式:是指数据的低位字节内容保存在内存的低地址处,而数据的高位字节内容,保存在内存的高地址处
为什么会有大小端?
这是因为在计算机系统中,我们是以字节为单位,每个地址单元都对应着一个字节,一个字节为8 bit 位,但是在C语言中除了8 bit 的 char 之外,还有16 bit 的 short 型,32 bit 的 long 型(要看具体的编译器),另外,对于位数大于8的处理器,例如有些存储器或宽度大于一个字节,那么必然存在着一个如何将多个字节安放的问题。
例如:一个16 bit的short型,在内存中的地址为0x0010,x的值为0x1122,那么0x11为高字节,0x22为低字节。对于大端模式,就将0x11放在低地址中,即0x0010中,0x22放在高地址中,即0x0011中。小端模式则相反。我们使用的X86结构是小端模式,而KEIL C51则为大端模式。很多ARM、DSP都是小端模式。有些ARM处理器还可以由用户选择大端模式也是小端模式。
练习
练习1
请编写一个小程序来判断你的机器环境是什么字节序
有趣的是,这是一道面试题
方法也不难,我们想一个 int 有四个字节,假如来个取址符&,就指向低地址的那个字节,这时候强转char*指针再解引用,就得到了那个低地址的数据,若为小端,数据低位在低地址,即拿到了0x01,就是1;若为大端,数据低位在高地址,即拿到了0x00,就是0
代码如下:
#include <stdio.h>
#include <string.h>
int check_sys()
{
int test = 0x00000001;
return *((char*)&test); // 拿到低字节
}
int main()
{
int ret = check_sys();
if (check_sys()) printf("是小端\n");
else printf("是大端\n");
return 0;
}
练习2
#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;
}
输出结果是 a = -1, b = -1, c = 255
我们来分析一下:
1.对于第一个有符号char,原码是10000000000000000000000000000001,反码是11111111111111111111111111111110,补码是11111111111111111111111111111111,然而我们现在要把补码放到比特里面,只能放八个比特位,也就是发生了截断,变成了11111111,同样其实b,c放得也是这样,现在对a来说,整型提升补的都是1,接着继续取反加1,这样就得到了-1的原码并打印
2.而对于c,整型提升成00000000000000000000000011111111,直接打印出255
%d是以十进制的形式打印有符号的整数
练习3
#include <stdio.h>
int main()
{
char a = -128;
printf("%u\n", a);
return 0;
}
%u是十进制的形式打印无符号的整数
我们可以先看看有符号的char变量在内存中存储的所有可能性
一旦看到10000000,直接会被解读为-128,所以有符号char的取值范围是 -128 ~ 127
其实你观察一下11111111,再加个1,直接变成00000000,所以符号char是循环存储的,请记一下
对于无符号char也是一样的
我们回到该题,-128在内存中的存储是
10000000000000000000000010000000 原码
11111111111111111111111101111111 反码
11111111111111111111111110000000 补码
所以截断后,a存储的是10000000
打印的时候,发生整型提升,提升的时候是按照自己的类型char提升
11111111111111111111111110000000,这时候,又以无符号的整数打印
就是一个很大的数了 -> 4,294,967,168
所以说,有符号的数尽量用%d打印,无符号的用%u来打印
练习4
#include <stdio.h>
int main()
{
char a = 128;
printf("%u\n", a);
return 0;
}
同上,分析后得出还是11111111111111111111111110000000,即4,294,967,168
练习5
#include <stdio.h>
int main()
{
char a[1000];
int i;
for (i = 0; i < 1000; i++)
{
a[i] = -1 - i;
}
printf("%d", strlen(a));
return 0;
}
理论上会有 -1 -2 … -1000,可是我们之前说了,char取值范围是个循环,范围在-128-127
到0的时候,strlen读取到就会停止,最后会发现0前面有255个数字,所以答案是255
练习6
#include <stdio.h>
unsigned char i = 0;
int main()
{
for (i = 0; i <= 255; i++)
{
printf("hello world\n");
}
return 0;
}
还是循环范围,unsigned char类型的 i 最大就是255,i循环到255后再自加,直接变成0,继续死循环
练习7
// x86环境,小端字节序
#include <stdio.h>
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;
}
第一个应该很好想,主要是第二个,直接把地址强制转换为整数再加1,再强制转化为 int* 指针,相当于跳过了一个字节,剩余的思考以图形式形象展现
总结
写到这里的时候,已经是晚上了,本来还想写浮点数在内存中的存储的,但是我困了,睡觉!
学到这里,你应该发现已经开始跟之前的知识串联起来了,如果你有这种感觉,很棒,继续加油!