题目也许大家都见过。也是一道经典的C面试题。
题目如下
typedef struct bitstruct{
int b1:5;
int :2;
int b2:2;
}bitstruct;
void main(){
bitstruct b;
memcpy(&b,"EMC EXAMINATION",sizeof(b));
printf("%d,%d\n", b.b1, b.b2);
}
答案是:5,-2.
这道题考到了“位域”,“Little Endian”和“内存补齐”(内存补齐只要知道sizeof(b)等于4就行,不是这题的重点)
首先是知识的普及:(如果都懂就跳过吧)
1.位域的概念和特点
C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“位域”(bit field)。(1)位段成员的类型必须指定为unsigned或int类型;(2)若某一位段要从另一个字开始存放,用:0长度为0的空位段,作用就是 使下一个位段从下一个存储单位(视不同编译系统而异)开始存放;(3)一个位段必须存储在同一存储单元中,不能跨两个单元;(4)可以定义无名字段例 如":2";(5)位段的长度不能大于存储单元的长度,也不能定义位段数组;(6)位段可以用整形格式符输出;(7)位段可以在数值表达式中引用,它会被 系统自动地转换成整形数。[1][3]
2.Little-endian systems的内存布局特点
先问一个问题:Endian这个词是什么意思?
“endian”这个词出自《格列佛游记》。小人国的内战就源于吃鸡蛋时是究竟从大头(Big-Endian)敲开还是从小头(Little-Endian)敲开,由此曾发生过六次叛乱,其中一个皇帝送了命,另一个丢了王位。
我们一般将endian翻译成“字节序”,将big endian和little endian称作“大尾”和“小尾”。这也在当今的CPU派别中一样存在。Motorola的PowerPC系列CPU采用的Big-endian, Intel的X86系列CPU采用的是Little-endian。Little-endian的特点是高高低低,即高位地址存放最高有效字节,低位地址 存放最低有效字节;而Big-endian正好相反。下面用图的方式说明起来更直观,例如0x12345678(特别注意,这是单个数,不是字符串,如果 是字符串就不一定这样了。之前没有特别注意这点,害的我多花了冤枉时间)。
Big Endian
低地址 高地址
----------------------------------------->
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 12 | 34 | 56 | 78 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Little Endian
低地址 高地址
----------------------------------------->
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 78 | 56 | 34 | 12 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
这里补充很重要的一点就是小尾不是以“字节”为单位的,而是以“位”。但是平时为什么我们不用考虑到“位”的层次呢?
因为小尾的机器按照小尾的存储方式存储数据后,再按小尾的方式来读取和构建数据,对于我们来说是透明的,无需转换。
比如:
0x45,即0100 0101,这里是一个字节。在内存中的存储存储方式为:
低地址 高地址
----------------------------------------->
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 1 0 1 0 | 0 0 1 0 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
正常情况下,C的内置数据类型最少读取1个字节(如char)。读取顺序是从低位开始读的。那么读取出来的值也应该是1010 0010.但是在构建这个char值时会逆转成 0100 0101.
这样读取出来的就还是 0x45 对应的字符为“E”。
但是特殊情况下,有可能不是读取一个字节。比如这题,只读取5位。那么读取出来的数应该是10100.构建int型也需要逆转,就成了00101.
好了现在来看题目:
首先,32位机一般以四字节对齐(内存补齐),sizeof(b)应该为4,从b的首地址后四个字节的内容应该为EMC和一个空格。
其次,对于这个结构来说只有第一个字节的八位和第二个字节的第一位有意义,也就是说只有E的八位二进制,和M的二进制的首位有意义。按照前面位段知识普及中提到的“(3)一个位段必须存储在同一存储单元中,不能跨两个单元;”为什么不是第一个字节的八位和第二字节的前两位呢?答案很简单,因为编译器不支持。
E的ASCII为0x45,即0100 0101,M的ASCII为0x4D,即0100 1101。
在内存中的存储方式为:
低地址 高地址
----------------------------------------->
E M
1010 0010 1011 0010
其中b.b1对应1-5位即 10100 。取出来之后转换成int要逆序成00101,最高位符号位为0,代表是正数,值就是5。
其中b.b2对应8-9位即 01。取出来之后转换成int要逆序成10, 因为最高位为1所以是负数, 即10为实际数的补码,按补码转原码运算规则(符号位其余各位取反,然后再整个数加1)0取反得1再加1得2因符号位为1,所以是负数,即为-2。