“内存对界”原理——数据是如何对齐的

1. 内存对界原理

在C语言中,结构是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是一些复合数据类型(如数组、结构、联合等)的数据单元。在结构中,编译器为结构的每个成员按其自然对界(alignment)条件分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。

例如:

struct struct1

{
char x1; // 对界条件为1,不填充,偏移地址为0
short x2; // 对界条件为2,前面填充一个字节,偏移地址为2
double x3; // 对界条件为4,不填充,偏移地址为4 
char x4; // 对界条件为1,不填充,偏移地址为8
short x5; // 对界条件为2,前面填充一个字节,偏移地址为10
};

struct1 st1; cout << &st1 << endl; // 输出:0012FF54
void* p = &st1.x1;
cout << p << endl;// 输出:0012FF54
cout << &st1.x2 << endl;// 输出:0012FF56
cout << &st1.x3 << endl;// 输出:0012FF5C
p = &st1.x4;
cout << p << endl;// 输出:0012FF64
cout << &st1.x5 << endl;// 输出:0012FF66
cout << sizeof(struct1) << endl;// 输出:12


其内存分布如右图所示:

 

 

x1为结构体的第一个成员,其地址和整个结构的地址相同,因而其偏移地址为0。

x2为short类型,其大小为2,因而其自然对界也为2,所以其偏移地址必须为2的整数倍,所以编译器在x2和x1之间添充了1个空字节。这样,x2的偏移地址为2。

x3为double类型,其大小为8,因而其自然对界也为8,所以其偏移地址必须是8的整数倍,所以编译器在x3和x2之间添充了4个空字节。这样,x3的偏移地址为8。

同理,可以算出x4的偏移地址为16,紧跟着x3存储。x5的偏移地址为18。

最后,由于x3要求8字节对界,是该结构所有成员中要求的最大对界单元,因而整个结构struct1的自然对界条件为8字节(注意不是该结构的总大小),所以该结构的大小必须满足8的整数倍,这就要求在x5之后还要再添充4个字节,使整个结构体的大小为24字节。

通过上面的分析可得到如下结论:

对于基本数据类型(如int、long、float等。注意,还包括指针),其自然对界条件为该类型所占用存储空间的大小,指针点用4个字节的空间;

对于复合数据类型(如数组、结构、联合等),其自然对界条件为该数据类型的所有成员中要求的最大对界单元。

有了上面的分析,就不难明白下面的例子,包含了同样数据类型的结构体,其占用内存空间的差距咋就这么大呢!

struct struct1

{
char c1;
char c2;
double d;
};

struct struct2
{
char c1;
double d;
char c2;
};

cout << sizeof(struct1) << endl; // 输出:16
cout << sizeof(struct2) << endl; // 输出:24

想到了什么?原来优化结构体内变量类型的存储顺序,可以减少对内存的占用!

 

2. 更改C编译器的缺省字节对齐方式

在缺省情况下,C编译器为每一个变量或是数据单元按其自然对界条件分配空间。一般地,可以通过下面的方法来改变缺省的对界条件:

1>使用伪指令#pragma pack (n),C编译器将按照n个字节对齐。

2>使用伪指令#pragma pack (),取消自定义字节对齐方式。

另外,还有如下的一种方式:

__attribute((aligned (n))),让所作用的结构成员对齐在n字节自然边界上。如果结构中有成员的长度大于n,则按照最大成员的长度来对齐。

__attribute__ ((packed)),取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。

以上的n = 1, 2, 4, 8, 16... 第一种方式较为常见。

3. 加深理解

为了加深对内存对齐的理解,再看下面的例子:处理结构体时要小心!!!

#pragma pack(8)
struct s0
{
short s;
};
struct s1
{
short a;
long b;
};
struct s2
{
char c;
s1 d;
long long e;
};
#pragma pack()


cout << sizeof(s0) << endl; // 输出:2
cout << sizeof(s1) << endl; // 输出:8
cout << sizeof(s2) << endl; // 输出:24

问:sizeof(s2) = ?  s2的c后面空了几个字节接着是d?

分析:成员对齐有一个重要的条件,即每个成员分别对齐,每个成员按自己的方式对齐。

也就是说上面虽然指定了按8字节对齐,但并不是所有的成员都是以8字节对齐。其对齐的规则是,每个成员按其类型的对齐参数(通常是这个类型的大小)和指定对齐参数(这里是8字节)中较小的一个对齐。并且结构的长度必须为所用过的所有对齐参数的整数倍。不够就补空字节。

s1中,成员a是2字节默认按2字节对齐,指定对齐参数为8,这两个值中取2,a按2字节对齐;成员b是4个字节,默认是按4字节对齐,这时就按4字节对齐,所以sizeof(s1)应该为8。

s2 中,c和s1中的a一样,按2字节对齐,而d 是个结构,它是8个字节,它按什么对齐呢?对于结构来说,它的默认对齐方式就是它的所有成员使用的对齐参数中最大的一个,s1的就是4。所以,成员d就是按4字节对齐。成员e是8个字节,它是默认按8字节对齐,和指定的一样,所以它应对到8字节的边界上,这时,已经使用了12个字节了,所以又添加了4个字节的空,从第16个字节开始放置成员e。这时,长度为24,已经可以被8(成员e按8字节对齐)整除。这样,一共使用了24个字节。

s1的内存布局:

a

 

b

1

1

x

x

1

1

1

1

s2的内存布局:

c

 

d

 

e

1

x

x

x

1

1

1

1

1

1

1

1

x

x

x

x

1

1

1

1

1

1

1

1

这里有三点很重要:

1. 每个成员分别按自己的方式对齐,并能最小化长度;

2. 复杂类型(如结构)的默认对齐方式是它最长的成员的对齐方式,这样在成员是复杂类型时,可以最小化长度;

3. 对齐后的长度必须是成员中最大的对齐参数的整数倍,这样在处理数组时可以保证每一项都边界对齐;

4. 数组的对齐方式

对于数组,对齐方式为元素的对齐方式。

比如:char a[3];这种,它的对齐方式和分别写3个char是一样的,也就是说它还是按1个字节对齐.如果写: typedef char Array3[3]; Array3这种类型的对齐方式还是按1个字节对齐,而不是按它的长度。

不论类型是什么,对齐的边界一定是1,2,4,8,16,32,64....中的一个。

struct s1
{
char a[8];
};

struct s2
{
double d;
};
struct s3

{
s1 s;
char a;
};

struct s4
{
s2 s;
char a;
};

cout << sizeof(s1) << endl; // 输出:8
cout << sizeof(s2) << endl; // 输出:8
cout << sizeof(s3) << endl; // 输出:9
cout << sizeof(s4) << endl; // 输出:16;

s1和s2大小虽然都是8,但是s1的对齐方式是1,s2的对齐方式是8(double),所以在s3和s4中才有这样的差异。所以,在自己定义结构体的时候,如果空间紧张的话,最好考虑对齐因素来排列结构体里的元素。

5. union的对齐方式

union的对齐方式:为成员中最大的对齐方式,长度为按照这个对齐方式调整最长成员得到的长度。

union u1

{
double a;
int b;
};
union u2
{
char a[13];
int b;
};

union u3
{
char a[13];
char b;
};
cout << sizeof(u1) << endl; // 输出:8
cout << sizeof(u2) << endl; // 输出:16(特别注意!)
cout << sizeof(u3) << endl; // 输出:13

都知道union的大小取决于它所有的成员中,占用空间最大的一个成员的大小。所以对于u1来说,大小就是最大的double类型成员a了,所以 sizeof(u1)=sizeof(double)=8。

但是对于u2和u3,最大的空间都是char[13]类型的数组,为什么u3的大小是13,而 u2是16呢?关键在于u2中的成员int b。由于int类型成员的存在,使u2的对齐方式变成4,也就是说,u2的大小必须在4的对界上,所以占用的空间变成了16(最接近13的对界)。

所以:复合数据类型,如union,struct,class的对齐方式为成员中对齐方式最大的成员的对齐方式。

对界是可以更改的,使用#pragmapack(x)宏可以改变编译器的对界方式,默认是8。C++固有类型的对界取编译器对界方式与自身大小中较小的一个。例如,指定编译器按2对界,int类型的大小是4,则int的对界为2和4中较小的2。在默认的对界方式下,因为几乎所有的数据类型都不大于默认的对界方式8(除了 longdouble),所以所有的固有类型的对界方式可以认为就是类型自身的大小。更改一下上面的程序:

#pragma pack(2)

union u2
{
char a[13];
int b;
};
union u3
{
char a[13];
char b;
};

#pragma pack(8)
cout << sizeof(u2) << endl; // 输出:14
cout << sizeof(u3) << endl; // 输出:13

由于手动更改对界方式为2,所以int的对界也变成了2,u2的对界取成员中最大的对界,也是2了,所以此时sizeof(u2)=14。

所以:C++固有类型的对界取编译器对界方式与自身大小中较小的一个。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
第 1 章:C++ 基础知识 此模块将向您介绍 C++,包括其历史、设计理念以及几个最重要的功能。此模块简要概述几个 C++ 功能,包括 C++ 程序的一般形式、一些基本控制语句和运算符。它不会介绍太多细节,而会重点介绍对所有 C++ 程序都通用的一般概念。 第 2 章:数据类型和运算符简介 编程语言的核心在于其数据类型和运算符。不出您所料,C++ 支持大量数据类型和运算符,使其适合的编程范围非常广泛。此模块对 C++ 基本数据类型及其最常用运算符进行探讨。我们还将进一步了解变量,并研究表达式。 第 3 章:程序控制语句 此模块讨论用于控制程序执行流的语句。有三种类别的程序控制语句:选择语句,包括 if 和 switch 语句;迭代语句,包括 for、while 和 do-while 循环;以及跳转语句,包括 break、continue、return 和 goto 语句。 第 4 章:数组、字符串和指针 此模块讨论数组、字符串和指针。数组是变量的集合,这些变量具有相同的类型,由一个公用名引用。数组为创建相关变量的列表提供了一种便利方法。C++ 语言不定义内置字符串数据类型。相反,字符串作为字符数组实现。指针是包含内存地址的对象。通常,指针用于访问另一个对象的值。 第 5 章:函数简介 此模块开始深度探讨函数。函数是 C++ 的构建基块,深入理解函数是成为成功 C++ 编程人员的基础。下面,您将了解如何创建函数。您还将了解传递参数、返回值、局部变量和全局变量、函数原型和递归。 第 6 章:进一步了解函数 此模块继续探讨函数。它讨论了 C++ 的三个最重要的函数相关主题:引用、函数重载和默认参数。 第 7 章:更多数据类型和运算符 此模块返回到数据类型和运算符的主题。除了您到目前为止已在使用的数据类型,C++ 还支持其他几种数据类型。其中一些数据类型由已知类型加上修饰符组成。其他数据类型包括 enumeration 和 typedef。C++ 还提供多个附加运算符,极大地扩展了 C++ 可以应用到的编程任务范围。 第 8 章:类和对象 类是 C++ 的基本封装单位。类用于创建对象。若要编写面向对象的程序,需要使用类。类和对象对于 C++ 非常重要,因此本书其余内容大部分都或多或少与它们相关。 第 9 章:进一步了解类 此模块继续探讨模块 8 中谈到的类。它涉及很多与类相关的主题,包括重载构造函数、传递对象到函数以及返回对象。它还介绍一种特殊类型的构造函数(称为复制构造函数),这种函数在需要对象副本时使用。接下来介绍友元函数,然后是结构和联合,以及 this 关键字。此模块最后介绍运算符重载,这是 C++ 中最吸引人的功能之一。 第 10 章:继承、虚函数和多态性 此模块讨论 C++ 中与面向对象编程直接相关的三个功能:继承、虚函数和多态性。继承是允许一个类继承另一个类特性的功能。虚函数是在继承的基础上构建的。虚函数支持多态性(面向对象编程的“一个接口,多种方法”原理)。 第 11 章:C++ I/O 系统 C++ I/O 系统非常大,无法在此讨论每个类、函数或功能,不过此模块将介绍最重要和最常用的部分。具体而言,它说明如何输入或输出所设计类的对象。它还介绍如何设置输出格式以及如何使用 I/O 操纵器。此模块最后讨论文件 I/O。 第 12 章:异常、模板和其他高级主题 最后一个模块将介绍几个重要的、高级 C++ 主题,包括异常处理、模板、动态分配和命名空间。另外还介绍运行时类型 ID 和转换运算符。完成此模块后,您将掌握这种语言的核心元素,能够开始编写实际程序。 掌握检查的答案 附录 A:预处理器 预处理器是编译器的一部分,在将源代码实际转换为对象代码之前,预处理器对程序执行各种文本操作。可以为预处理器提供文本操作命令。这些命令称为预处理器指令,它们实际上不是 C++ 的组成部分,但扩展了 C++ 编程环境的范围。
提供的源码资源涵盖了Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 适合毕业设计、课程设计作业。这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。 所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随时与博主沟通,第一时间进行解答!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值