大端/小端,高字节/低字节,高地址/低地址,移位运算
其实大端小端的概念比较好理解的,大端:数据的高字节存放在内存的低地址中。
数组的声明方式是从左往右,地址逐渐增大。
int8_t a[] = { 1, 2, 3 }; for (int i = 0; i < 3; i++) printf("a[%d]: %p\n", i, &a[i]);
a[0]: 0x7ffce52cf290 a[1]: 0x7ffce52cf294 a[2]: 0x7ffce52cf298
int8_t是<stdint.h>定义的跨平台数据类型,代表8位(1个字节)。这里a[0]地址比a[1]地址小,a[0]就是低地址,a[1]就是高地址。
现代人的阅读习惯都是从左向右,大端就是先看到的一边(低地址)是“大”的(高字节),那么什么是高字节呢?
用十进制来举例,小时候学数的表示,有个位、十位、百位、千位……比如一个十进制的数:1234。个位是4,十位是3,那么低位就是4,高位是3。
计算机存储的是二进制数,8位构成一个字节,作为基本单位。比如两个字节的数:0x1122,化成十进制即17*256+34。低位就是34(0x22),高位就是0x11。
在书写数字时,我们则习惯把高位的写在前面(左边),这和地址的书写顺序(低地址地写在左边)刚好相反。
于是,对于0x1122,假如计算机存储的顺序是:0x11 0x22,那么就是大端,符合书写习惯,高位的0x11在左边(低地址)。
但是不是所有计算机都会按照我们习惯上这样这么存储,假如计算机存储的顺序是:0x22 0x11,那么就是小端。
判断的方式很简单
int16_t x = 0x1122; int8_t* p = (int8_t*)&x; if (p[0] == 0x11 && p[1] == 0x22) // 大端
int8_t a[] = { 0x11, 0x22 }; int16_t* p = (int16_t*)&a[0]; if (*p == 0x1122) // 大端
C的指针指向的是一个数据类型的起始字段,比如这里用1字节的指针指向2字节的数,假设其占据的内存范围为[p, p+2),那么2字节数的起始地址是p,&x得到的也是p,指针之间的强制转换不会改变指针的值(比如之前输出的&a[0]、&a[1]、&a[2]的结果就是指针的值,也就是地址的值),只会告诉编译器,如果要用*p读取这个数,我们只想要这个数类型对应大小的字节。
可能说得比较绕,具体表现就是:*(&x)得到的是[p, p+2)的部分,*((int8_t)&x)得到的是[p, p+1)的部分。
在网络传输中,如果没有一个固定的存储方式,那么不同存储方式的主机互相通信时就会表达失败。比如A想告诉B:“我爱你”。结果B听到的是“我爱你”,但是理解的却是“你爱我”,这样就导致了误解。这就是区分出大端和小端的意义,在网络传输中会都转换成小端或大端的一种。
好了,大端小端的问题说完了,现在说下移位运算。我之前用下面这种做法判断系统是大端还是小端
int16_t x = 0x1122; if ((x >> 8) == 0x11) // 大端
但是这是种错误的做法!
移位运算是独立于地址存储方式的,因此x >> 8得到的是2字节数x的高字节,右移代表向低字节移动,低字节的0x22没有更低的位置了,于是消失,高字节的0x11移动到了0x22的位置,因此x >> 8必然得到0x0011。