C/C++数据对齐详细解析_C 语言

Data Alignment

关于数据对齐问题,现在多多少少有了一些接触,简单地说下自己的看法。

1、对齐的背景

大端和小端的问题有必要在这里介绍一下,计算机里面每个地址单元对应着一个字节,一个字节为8bit,对于位数大于8位的处理器来说,寄存器的宽度是大于一个字节的,例如16bit的short型变量x,在内存中的地址是0x0010,x的值为0x1122,0x11为高字节,0x22为低字节,常用的X86结构是小端模式,很多ARM,DSP都是小端模式,而KEIL C51则为大端模式。内存空间是按照byte进行划分的,理论上对任何类型的变量的访问可以从任何地址开始,但实际上访问特定变量的时候经常在特定的内存地址访问,这就需要各类型的数据按照一定的规则在空间上排列,而不是顺序排列,这就是对齐。

2、对齐的原因

不同硬件平台对存储空间的处理是有很大不同的,一些平台对某些特定类型的数据只能从某些特定地址开始存取。其他平台可能没有这种情况, 但是最常见的是如果不按照适合其平台的要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为 32位)如果存放在偶地址开始的地方,那么一个读周期就可以读出,而如果存放在奇地址开始的地方,就可能会需要2个读周期,并对两次读出的结果的高低 字节进行拼凑才能得到该int数据。显然在读取效率上下降很多。这也是空间和时间的博弈。

3、对齐的实现

通常我们在写代码的时候是不需要考虑对齐的影响的,都是依赖编译器来为我们选择适合的对齐策略,我们也可以通过传递给编译器预编译指令来指定数据对齐的方法。

以struct数据结构的sizeof方法为例,环境是Mac OS X 64位内核,结构体的定义如下:

struct A {
int a;
char b;
short c;
};

struct B {
char b;
int a;
short c;
};

#pragma pack(2)
struct C {
char b;
int a;
short c;
};
#pragma pack()

#pragma pack(1)
struct D {
char b;
int a;
short c;
};
#pragma pack()

int main(int argc, char** argv)
{
printf("size of struct A : %lu \n", sizeof(struct A));
printf("size of struct B : %lu \n", sizeof(struct B));
printf("size of struct C : %lu \n", sizeof(struct C));
printf("size of struct D : %lu \n", sizeof(struct D));
return 0;
}

输出:
size of struct A : 8
size of struct B : 12
size of struct C : 8
size of struct D : 7

结构体中包含了4字节长度的int一个,1字节长度的char一个以及2字节长度的short一个。加起来所用到的内存空间为7个字节,但实际使用sizeof时发现,结构体之间占用的内存是不一样的。

关于对齐有几个需要说明的:
(1)数据类型自身的对齐值:基本数据类型的自身对齐值,char类型为1,short类型为2,int,float,double为4;

(2)指定对齐值:#pragma pack(value)时的指定对齐值value;

(3)结构体或者类的自身对齐值:其成员中自身对齐值最大的那个值;

(4)数据成员、结构体和类的有效对齐值:自身对齐值或指定对齐值中较小值。

对于一个具体的数据结构的成员和其自身的对齐方式,有效对齐值N将最终决定数据存放地址的方式的值,对齐在N上就意味着数据“存放的起始地址%N=0”,

下面来针对上面的例子进行分析:
struct B {
char b;
int a;
short c;
};

假设B从地址空间0x0000开始,默认的对齐值是4(这里有个问题想请教大家,我的是64位的内核,但是测试我的默认对齐方式为4),第一个成员变量b的自身对齐值为1,比默认值小所以有效对齐值为1,存放地址0x0000%1=0,第二个成员变量a,自身对齐值为4,存放的起始地址为0x0004到0x0007这个4个连续的字节空间中,0x0004%4=0,第三个变量c,自身对齐值为2,存放的起始地址为0x0008到0x0009,地址同样符合要求。结构体B的自身对齐值为变量中的最大对齐值(b)4,(10+2)%4=0,所以0x000A到0x000B也是被结构体B占用。
内存中的示意图:
b - - -
a a a a
c c

#pragma pack(2)
struct C {
char b;
int a;
short c;
};
#pragma pack()

第一个变量b的自身对齐值为1,指定对齐值为2,有效对齐值为1,b存放在0x0000,a的自身对齐值为4,大于指定对齐值,所有有效的对齐值为2,a占有的字节为0x0002、0x0003、0x0004、0x0005四个连续字节中,c的自身对齐值为2,所以有效对齐值也是2,顺序存放在0x0006、0x0007。结构体C的自身对齐值为4,所以有效对齐值为2,8%2=0。
内存中的示意图:
b -
a a
a a
c c

其实想到内存中的示意图一切都会简单很多。

时间: 2024-09-24 20:51:16

C/C++数据对齐详细解析_C 语言的相关文章

C++中的friend函数详细解析_C 语言

为什么要使用友元函数 在实现类之间数据共享时,减少系统开销,提高效率.如果类A中的函数要访问类B中的成员(例如:智能指针类的实现),那么类A中该函数要是类B的友元函数.具体来说:为了使其他类的成员函数直接访问该类的私有变量.即:允许外面的类或函数去访问类的私有变量和保护变量,从而使两个类共享同一函数. 实际上具体大概有下面两种情况需要使用友元函数:(1)运算符重载的某些场合需要使用友元.(2)两个类要共享数据的时候. 使用友元函数的优缺点 优点:能够提高效率,表达简单.清晰. 缺点:友元函数破环

va_list(),va_start(),va_arg(),va_end() 详细解析_C 语言

(一)写一个简单的可变参数的C函数 下面我们来探讨如何写一个简单的可变参数的C函数.写可变参数的C函数要在程序中用到以下这些宏: 复制代码 代码如下: void va_start( va_list arg_ptr, prev_param );type va_arg( va_list arg_ptr, type );void va_end( va_list arg_ptr ); va在这里是variable-argument(可变参数)的意思.这些宏定义在stdarg.h中,所以用到可变参数的程序

全局变量与局部变量在内存中的区别详细解析_C 语言

一.预备知识-程序的内存分配 一个由c/C++编译的程序占用的内存分为以下几个部分 1.栈区(stack)- 由编译器自动分配释放,存放函数的参数值,局部变量的值等.其操作方式类似于数据结构中的栈. 2.堆区(heap) - 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 .注意它与数据结构中的堆是两回事,分配方式倒是类似于链表. 3.全局区(静态区)(static)-,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域(.data),未初始化的全局变量

C/C++中可变参数的用法详细解析_C 语言

可变参数即表示参数个数可以变化,可多可少,也表示参数的类型也可以变化,可以是int,double还可以是char*,类,结构体等等.可变参数是实现printf(),sprintf()等函数的关键之处,也可以用可变参数来对任意数量的数据进行求和,求平均值带来方便(不然就用数组或每种写个重载).在C#中有专门的关键字parame,但在C,C++并没有类似的语法,不过幸好提供这方面的处理函数,本文将重点介绍如何使用这些函数. 第一步 可变参数表示用三个点-来表示,查看printf()函数和scanf(

STL list链表的用法详细解析_C 语言

本文以List容器为例子,介绍了STL的基本内容,从容器到迭代器,再到普通函数,而且例子丰富,通俗易懂.不失为STL的入门文章,新手不容错过! 0 前言1 定义一个list2 使用list的成员函数push_back和push_front插入一个元素到list中3 list的成员函数empty()4 用for循环来处理list中的元素5 用STL的通用算法for_each来处理list中的元素6 用STL的通用算法count_if()来统计list中的元素个数7 使用count_if()的一个更

C++ new/delete相关知识点详细解析_C 语言

每个程序在执行时都占用一块可用的内存空间,用于存放动态分配的对象,此内存空间称为程序的自由存储区(free store)或堆(heap).C语言用一堆标准库函数malloc和free在自由存储区中分配存储空间,而C++则用new和delete表达式实现相同的功能. 一.new和delete创建和释放动态数组:数组类型的变量有三个重要的限制:数组长度固定,在编译时必须知道其长度,数组只在定义它的语句内存在.动态数组:长度固定,编译时不必知道其长度,通常是运行时确定:一直存在,直到程序显示释放它.

C语言中的内联函数(inline)与宏定义(#define)详细解析_C 语言

先简明扼要,说下关键:1.内联函数在可读性方面与函数是相同的,而在编译时是将函数直接嵌入调用程序的主体,省去了调用/返回指令,这样在运行时速度更快. 2.内联函数可以调试,而宏定义是不可以调试的.内联函数与宏本质上是两个不同的概念如果程序编写者对于既要求快速,又要求可读的情况下,则应该将函数冠以inline.下面详细介绍一下探讨一下内联函数与宏定义. 一.内联函数是什么?内联函数是代码被插入到调用者代码处的函数.如同 #define 宏(但并不等同,原因见下文),内联函数通过避免被调用的开销来提

C语言的指针类型详细解析_C 语言

指针存储了内存的地址,同时指针是有类型的,如int*,float*,那么,一个自然的猜想就是指针变量应该存储这两方面的信息:地址和指针类型,比如,就像下面的结构体: 复制代码 代码如下: struct pointer{    long address;    int type;} 举个例子:打印sizeof(int*),值为4,可见4字节是存储内存地址用的,反过来就说明指针并没有存储类型信息的地方,那么指针的类型信息存放在哪儿呢?下面剖析一段简单的代码. 复制代码 代码如下: // ma.cpp

C++初始化函数列表详细解析_C 语言

在以下三种情况下需要使用初始化成员列表: 一,需要初始化的数据成员是对象的情况: 二,需要初始化const修饰的类成员: 三,需要初始化引用成员数据: 原因:C++可以定义引用类型的成员变量,引用类型的成员变量必须在构造函数的初始化列表中进行初始化.对于类成员是const修饰,或是引用类型的情况,是不允许赋值操作的,(显然嘛,const就是防止被错误赋值的,引用类型必须定义赋值在一起),因此只能用初始化列表对齐进行初始化.成员类型是没有默认构造函数的类.若没有提供显示初始化式,则编译器隐式使用成