《从缺陷中学习C/C++》——6.16 结构体成员内存对齐问题

简介: 本节书摘来自异步社区出版社《从缺陷中学习C/C++》一书中的第6章,第6.16节,作者: 刘新浙 , 刘玲 , 王超 , 李敬娜 , ,更多章节内容可以访问云栖社区“异步社区”公众号查看。 6.16 结构体成员内存对齐问题 从缺陷中学习C/C++ 代码示例 struct{   char flag;   int i; } foo; int main() {  foo.

本节书摘来自异步社区出版社《从缺陷中学习C/C++》一书中的第6章,第6.16节,作者: 刘新浙 , 刘玲 , 王超 , 李敬娜 , ,更多章节内容可以访问云栖社区“异步社区”公众号查看。

6.16 结构体成员内存对齐问题

从缺陷中学习C/C++
代码示例

struct{
  char flag;
  int i;
} foo;
int main()
{
 foo.flag = 'T';
 int *pi = (int *)(&foo.flag + 1);
 *pi = 0x01020304;
 printf("flag=%c, i=%x\n", foo.flag, foo.i);
 return 0;
}

现象&后果
代码中定义了一个结构体,包括一个字符成员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的地址赋值。

struct{
  char flag;
  int i;
} foo;
int main()
{
 foo.flag = 'T';
 int *pi = &foo.i;
 *pi = 0x01020304;
 printf("flag=%c, i=%x\n", foo.flag, foo.i);
 return 0;
}

编程建议
字节对齐的细节与具体编译器实现有关,不同的平台可能有所不同。一些编译器允许程序员在代码中通过预处理指令#pragma pack(n)或类型属性attribute((packed))来改变默认的内存对齐条件。

本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。

相关文章
|
4月前
|
安全 C语言 C++
比较C++的内存分配与管理方式new/delete与C语言中的malloc/realloc/calloc/free。
在实用性方面,C++的内存管理方式提供了面向对象的特性,它是处理构造和析构、需要类型安全和异常处理的首选方案。而C语言的内存管理函数适用于简单的内存分配,例如分配原始内存块或复杂性较低的数据结构,没有构造和析构的要求。当从C迁移到C++,或在C++中使用C代码时,了解两种内存管理方式的差异非常重要。
178 26
|
9月前
|
存储 程序员 编译器
玩转C++内存管理:从新手到高手的必备指南
C++中的内存管理是编写高效、可靠程序的关键所在。C++不仅继承了C语言的内存管理方式,还增加了面向对象的内存分配机制,使得内存管理既有灵活性,也更加复杂。学习内存管理不仅有助于提升程序效率,还有助于理解计算机的工作原理和资源分配策略。
|
5月前
|
C语言 C++
c与c++的内存管理
再比如还有这样的分组: 这种分组是最正确的给出内存四个分区名字:栈区、堆区、全局区(俗话也叫静态变量区)、代码区(也叫代码段)(代码段又分很多种,比如常量区)当然也会看到别的定义如:两者都正确,记那个都选,我选择的是第一个。再比如还有这样的分组: 这种分组是最正确的答案分别是 C C C A A A A A D A B。
98 1
|
8月前
|
存储 Linux C语言
C++/C的内存管理
本文主要讲解C++/C中的程序区域划分与内存管理方式。首先介绍程序区域,包括栈(存储局部变量等,向下增长)、堆(动态内存分配,向上分配)、数据段(存储静态和全局变量)及代码段(存放可执行代码)。接着探讨C++内存管理,new/delete操作符相比C语言的malloc/free更强大,支持对象构造与析构。还深入解析了new/delete的实现原理、定位new表达式以及二者与malloc/free的区别。最后附上一句鸡汤激励大家行动缓解焦虑。
|
8月前
|
安全 C++
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
452 6
|
10月前
|
C++ 开发者
C++学习之继承
通过继承,C++可以实现代码重用、扩展类的功能并支持多态性。理解继承的类型、重写与重载、多重继承及其相关问题,对于掌握C++面向对象编程至关重要。希望本文能为您的C++学习和开发提供实用的指导。
157 16
|
9月前
|
安全 C语言 C++
彻底摘明白 C++ 的动态内存分配原理
大家好,我是V哥。C++的动态内存分配允许程序在运行时请求和释放内存,主要通过`new`/`delete`(用于对象)及`malloc`/`calloc`/`realloc`/`free`(继承自C语言)实现。`new`分配并初始化对象内存,`delete`释放并调用析构函数;而`malloc`等函数仅处理裸内存,不涉及构造与析构。掌握这些可有效管理内存,避免泄漏和悬空指针问题。智能指针如`std::unique_ptr`和`std::shared_ptr`能自动管理内存,确保异常安全。关注威哥爱编程,了解更多全栈开发技巧。 先赞再看后评论,腰缠万贯财进门。
435 0
|
10月前
|
存储 程序员 编译器
什么是内存泄漏?C++中如何检测和解决?
大家好,我是V哥。内存泄露是编程中的常见问题,可能导致程序崩溃。特别是在金三银四跳槽季,面试官常问此问题。本文将探讨内存泄露的定义、危害、检测方法及解决策略,帮助你掌握这一关键知识点。通过学习如何正确管理内存、使用智能指针和RAII原则,避免内存泄露,提升代码健壮性。同时,了解常见的内存泄露场景,如忘记释放内存、异常处理不当等,确保在面试中不被秒杀。最后,预祝大家新的一年工作顺利,涨薪多多!关注威哥爱编程,一起成为更好的程序员。
466 0
|
9月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
5月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
149 0