C++学习笔记:结构体内存对齐
1.什么是结构体内存对齐
来看这样一种现象:
#include<stdio.h>
#include<stdlib.h>
typedef struct student{
//定义结构体类型
char a;
int age;
}stu;//stu是新的类型名 而不是一个变量
int main(int argc,char**argv)
{
printf("sizeof char = %d.\nsizeof int = %d.\nsizeof stu = %d.\n",
sizeof(char),sizeof(int),sizeof(stu));
return 0;
}
输出:
sizeof char = 1.
sizeof int = 4.
sizeof stu = 8.
为什么char
是1个字节,int
是4个字节,但是结构体却占了8个字节?
结构体中元素的访问本质上是用指针结合这个元素在整个结构体中的偏移量和这个元素的类型来进行访问的。
但是实际上结构体的元素的偏移量还要复杂,因为结构体要考虑元素的对齐访问,所以每个元素时间占的字节数和自己本身的类型所占的字节数不一定完全一样。比如char
实际占字节数可能是1,也可以是2,也可能是3,也可以能4。
一般来说,当我们用.
的方式来访问结构体元素时是不用考虑结构体的元素对齐的,因为编译器会帮我们处理这个细节。但是因为C/C++
语言本身是很底层的语言,当做嵌入式开发经常需要从内存角度,以指针方式来处理结构体及其中的元素,因此还是需要掌握结构体对齐规则。
2.结构体为何要对齐访问
结构体中元素对齐访问主要原因是为了配合硬件,也就是说硬件本身有物理上的限制,如果对齐排布和访问会提高效率,否则会大大降低效率。
例如对于一个64位的机器,在一个存取周期Tm内可以取出一行64位数据,假设有这样一批数据,它们的大小分别为8位,16位,32位,64位,来看下面几种在主存中数据存放的方法:
1.不浪费存储空间的方法:
首先存8位的数据,然后存16位的数据,当接下来存64位的数据时,一行只剩40位空间了,所以分成前40位和后24位存到两行里,这样的好处是不浪费存储空间,坏处就是当需要取出那个64位数据的时候,需要进行两次访存才可全部取出来,因为一次只能取出一行数据,牺牲了时间换取了空间。
2.从存储字起始位置开始放
来了一个8位数据就存放在第一个存储字里,剩下的56位就浪费了,来了一个16位的数据就放在第二哥存储字里,剩下的48位就浪费了。。。这样就可以保证所有的数据都可以在一个Tm内取出来,这样处理的缺点很明显,太浪费空间了,牺牲空间换取了时间。
3.边界对齐的数据存放方法
边界对齐的数据存放方法是上面2种方法的折中,存的下的时候就存,当某个数据一行内存不下的时候就另起一行。
内存本身是一个物理器件(DDR内存芯片,SoC上的DDR控制器),本身有一定的局限性:对于32位的机器来说,如果内存每次访问时按照4字节对齐访问,那么平均下来效率是最高的;如果不对齐访问效率要低很多。还有很多别的因素和原因导致需要对齐访问,比如Cache的一些缓存特性,还有其他硬件(如MMU、LCD显示器)的一些内存依赖特性,所以会要求内存对齐访问。
3.结构体对齐的规则和运算
编译器本身可以设置内存对齐的规则,有以下的规则:
- 1.当编译器设置为4字节对齐时,结构体整体本身必须安置在4字节对齐处,结构体对齐后的大小必须4的倍数(如果编译器设置为8字节对齐,则这里的4都要改成8)。
- 2.结构体中每个元素本身都必须对其存放,而每个元素本身都有自己的对齐规则。
编译器考虑结构体存放时,以满足以上2点要求的最少内存需要的排布来算。
#include<stdio.h>
#include<stdlib.h>
typedef struct student{
//定义结构体类型
char a;
int age;
}stu;//stu是新的类型名 而不是一个变量
int main(int argc,char**argv)
{
printf("sizeof char = %d.\nsizeof int = %d.\nsizeof stu = %d.\n",
sizeof(char),sizeof(int),sizeof(stu));
return 0;
}
4.#pragma pack(n)对齐指令
#pragma
是用来设置编译器的对齐方式的。在32位机器中,编译器的默认对齐方式是4,但是有时候我们不希望对齐方式是4而希望是别的,比如希望1字节对齐,也可能希望是8,甚至可能希望128字节对齐。
使用方法是以#prgama pack(n)
开头,以#pragma pack()
结尾定义一个区间,则这个区间内的时n字节对齐。举个栗子:
#include<stdio.h>
#pragma pack(1)
//#pragma pack(4)
typedef struct student{
//定义结构体类型
char b;
int a;
short c;
}stu;//stu是新的类型名 而不是一个变量
#pragma pack()
int main(int argc,char**argv)
{
printf("sizeof char = %d.\nsizeof int = %d.\nsizeof short = %d.\nsizeof stu = %d.\n",
sizeof(char),sizeof(int),sizeof(short),sizeof(stu));
return 0;
}
n=1时输出:
sizeof char = 1.
sizeof int = 4.
sizeof short = 2.
sizeof stu = 7.
n=4时输出:
sizeof char = 1.
sizeof int = 4.
sizeof short = 2.
sizeof stu = 12.
#prgma pack
的方式在很多C
环境下都是支持的,gcc
也支持但是不建议使用。
5.attribute()对齐指令
__attribute__((packed))
使用时直接放在要进行内存对齐的类型定义的后面,然后它起作用的范围只有加了这个东西的这一个类型。packed
的作用就是取消对齐访问,结果同于#pragma pack(1)
。举个栗子:
#include<stdio.h>
struct student{
//定义结构体类型
char b;
int a;
short c;
}__attribute__((packed));
int main(int argc,char**argv)
{
printf("sizeof char = %d.\nsizeof int = %d.\nsizeof short = %d.\nsizeof struct student = %d.\n",
sizeof(char),sizeof(int),sizeof(short),sizeof(struct student));
return 0;
}
输出:
sizeof char = 1.
sizeof int = 4.
sizeof short = 2.
sizeof struct student = 7.
__attribute__((aligned(n)))
使用时也是直接放在要进行内存对齐的类型定义的后面,它起作用的范围只有加了这个东西的这一个类型。它的作用是让整个结构体变量整体进行n字节对齐(注意是结构体变量整体n字节对齐,而不是结构体内各元素也要n字节对齐)
#include<stdio.h>
typedef struct student{
//定义结构体类型
char b;
int a;
short c;
}stu __attribute__((aligned(4)));
int main(int argc,char**argv)
{
printf("sizeof char = %d.\nsizeof int = %d.\nsizeof short = %d.\nsizeof struct student = %d.\n",
sizeof(char),sizeof(int),sizeof(short),sizeof(stu));
return 0;
}
输出:
sizeof char = 1.
sizeof int = 4.
sizeof short = 2.
sizeof struct student = 12.
注意:
__attribute__((aligned(n)))
是整体对齐,#prgama pack(n)
是内部元素和整体都对齐。往大去对齐2种方式都可以用,但是往小去对齐时,前者失效,后者有用。
6.C++11中内存对齐新增关键字
6.1 alignof
alignof()
用来测一个类型的对齐方式,,编译的时候要加-std=c++11
:
#include<iostream>
using namespace std;
typedef struct student{
char a;
int b;
short c;
}stu;
int main(int argc,char**argv)
{
cout<<alignof(stu)<<endl;
return 0;
}
输出:
4
6.2 alignas
alignas
一般在类型定义时放在名称前,它起到的作用和__attribute__((aligned(n)))
效果一样:
#include<iostream>
using namespace std;
typedef struct student{
char a;
int b;
short c;
}stu alignas(8);
int main(int argc,char**argv)
{
cout<<alignof(stu)<<endl;
return 0;
}
输出:
8