本节书摘来自异步社区出版社《从缺陷中学习c/c++》一书中的第6章,第6.16节,作者: 刘新浙 , 刘玲 , 王超 , 李敬娜 , ,更多章节内容可以访问云栖社区“异步社区”公众号查看。
从缺陷中学习c/c++
代码示例
现象&后果
代码中定义了一个结构体,包括一个字符成员flag和整型成员i。在main函数中想通过指针方式将结构体整型成员i赋值为0x01020304,但打印输出显示i的实际值为0x01,赋值错误。
bug分析
上面程序的问题出在指针赋值处,即int pi = (int )(&foo.flag+1)。程序员误以为结构体字符成员flag地址加1就是整型成员i的地址,然后给该地址赋值,期望变量i会得到相应的赋值。但赋值结果并非所期望的。导致这个问题的根源是内存字节对齐。
内存字节对齐是指,为了保证cpu对内存的访问效率,各种类型数据需要按照一定的规则在内存存放,而不是完全字节挨字节的顺序存放。每种数据类型的默认对齐长度依赖于编译器具体实现,不同编译器可能有所不同。大多数情况下,基本数据类型的对齐长度就是自己数据类型所占空间大小(sizeof值)。例如,char型占一个字节,那么对齐长度就是一个字节;int型占4个字节,对齐长度就是4个字节,double型占8个字节,对齐长度就是8个字节。
对于结构体数据类型,默认的字节对齐一般需满足3个准则。
(1)结构体变量的首地址能够被其最宽数据类型成员的大小整除。
(2)结构体每个成员相对结构体首地址的偏移量都是该成员本身大小的整数倍,如有需要会在成员之间填充字节。
(3)结构体变量所占总空间的大小必定是最宽数据类型大小的整数倍。如有需要会在最后一个成员末尾填充若干字节,使得结构体所占空间大小是最宽数据类型大小的整数倍。
在结构体foo里,整型成员i占用4个字节,是占用空间最多的成员,所以foo必须驻留在4的整数倍内存地址。字符成员flag的起始地址即为foo的起始地址,flag占用1个字节。整型成员i的起始地址因为必须是4的整数倍,所以不能直接存放于flag+1的位置(flag已占用1个字节,flag+1地址不再是4的整数倍),而是存放于flag+4的位置。因此,flag后面的有3个字节浪费掉了。这样foo一共需要占用8个字节的内存空间,而不是5个字节(char型和int型的sizeof和)。
程序中,给flag+1地址处赋值为一个4字节整数0x01020304,因为有3个字节并未影响到变量i,所以赋值结果为0x01。
正确代码
不使用flag地址加1给变量i赋值,直接使用i的地址赋值。
编程建议
字节对齐的细节与具体编译器实现有关,不同的平台可能有所不同。一些编译器允许程序员在代码中通过预处理指令#pragma pack(n)或类型属性attribute((packed))来改变默认的内存对齐条件。
本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。