一、C++程序内存分配
C/C++程序编译时内存分为5大存储区
- 栈区,由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。通俗来讲就是函 数中的变量参数等等,即{}中的内容。
- 堆区,一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方 式倒是类似于链表。通俗讲就是动态内存分配,
- 全局区(静态区),—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域(RW), 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域(ZI)。 - 程序结束后有系统释放 。
- 文字常量区,文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放 (RO)
- 程序代码区,存放函数体的二进制代码。
下面是一个例子:
int a = 0; //全局区
int main(){
int b; //栈
static int c = 0; //全局区
char *p1 = new char[10]; //10个字节区域在堆区
char *p2; //栈
char *p3 = "123"; //其中,“123\0”常量区,p3在栈区
return 0;
}
二、内存对齐
1、什么是内存对齐?
计算机中的内存是按字节(byte)划分的,从理论上讲对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型的数据按照一定的规则在内存空间上排序,而不是顺序的一个接一个的排放,这就是内存对齐。内存对齐应注意下面两点:
- 内存对齐是为了让内存存取更有效率而采用的一种编译阶段优化内存存取的手段。
- 内存对齐是指首地址对齐,而不是说每个变量大小对齐。对不同类型的数据,首先计算出它的自身对齐值N,由它的对齐值获取它在内存中的存储起始地址,“存储起始地址 % N == 0”。
2、内存对齐相关的概念
2.1、基础数据类型自身对齐值
对于char类型其自身对齐值为1;对于short类型为2;对于int、float、double类型,其自身对齐值为4(单位/byte)
2.2、struct或class的自身对齐值
其成员中自身对齐值最大的那个值。如果某个成员是struct或class,找到这个成员的最大对齐值,然后与获取的类自身对齐值进行比较,较大的值就是最终的自身对齐值。例如:
struct Data{
char c;
int a;
double d;
};
class People{
short s;
int a;
Data d;
};
struct Data里面的数据类型有char、int、double,那么struct Data应该从8的整数倍开始存储,因此struct Data的自身对齐值为8。class People里面有一个成员是struct Data,class People里面包含的基本数据类型成员的最大自身对齐值为4(int),小于struct Data,因此class People的自身对齐值为8。
2.3、指定对齐值
#pragma pack (value)时的指定对齐值value。
2.4、基本数据类型、结构体和类的有效对齐值
自身对齐值和指定对齐值中小的那个值。例如:下面的程序输出的结果是6
#include <iostream>
using namespace std;
#pragma pack(4)
struct T{
char a;
short b;
char c;
};
#pragma pack()
int main()
{
cout << sizeof(T) << endl;
return 0;
}
3、对齐值的作用
3.1、根据对齐值可以讨论具体数据结构的成员和其自身的对齐方式
有效对齐值N是最终用来决定数据存放地址方式的值。有效对齐值N,就是表示对齐在N上,也就是说该数据的"存储起始地址 % N == 0"。
3.2、计算数据的存储起始位置
例如:对于struct A,它的对齐值是8,则struct A在内存中的"存储起始地址 % 8 == 0"。struct B中包含一个类型为struct A的成员,如何计算类型为struct B的成员存储位置?
struct A{
int i;
double d;
};
struct B{
short s; //存储位置:[0] - [1]
A a; //存储位置:[8] - [23]
}; //内存大小:sizeof(B) = (2 + 6) + 16 = 24;
short为两个字节,存储short s的位置为[0] - [1],由于struct A的对齐值为8,因此,成员A a的存储起始位置为8的倍数,成员A a的存储起始位置为8,存储位置为[8] - [23]。
3.3,完成struct或class的收尾工作
例如:对于struct D,它的对齐值为8,则整个struct D的大小应为8的整数倍
struct D{
double b; //存储位置:[0] - [7]
int i; //存储位置:[8] - [11]
short s; //存储位置:[12] - [13]
}; //内存大小:sizeof(D) = 8 + 4 + 2 + 2(补全的2个字节) = 16;
4、内存对齐的三个原则
4.1、数据成员对齐规则
class、struct、union的数据成员,第一个数据成员放在offset为0的地方,之后的数据成员的存储起始位置都是放在该数据成员大小的整数倍位置。如在32bit的机器上,int的大小为4,因此int存储的位置都是4的整数倍的位置开始存储。
4.2、结构体作为数据成员的对齐规则
在一个struct中包含另一个struct,内部struct应该以它的最大数据成员大小的整数倍开始存储。如 struct A 中包含 struct B,struct B 中包含数据成员 char、 int、double,则 struct B 应该以sizeof(double) = 8的整数倍为起始地址。
4.3、收尾工作的对齐规则
整个struct的大小,应该为最大数据成员大小的整数倍。
5、为什么要进行内存对齐?
5.1、平台原因(移植原因)
不是所有的硬件平台都能访问任意地址上的任意数据的,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
5.2、性能原因
CPU把内存当成是一块一块的,块的大小可以是2,4,8,16字节大小,因此CPU在读取内存时是一块一块进行读取的。块大小成为memory access granularity(内存读取粒度)。经过内存对齐后,CPU的内存访问速度大大提升。例如,CPU要读取一个int型4字节大小的数据到寄存器中,分两种情况讨论:
- 数据从0字节开始:该数据是从0字节开始时,CPU只需读取内存一次即可把这4字节的数据完全读取到寄存器中。
- 数据从1字节开始:当该数据是从1字节开始时,问题变的有些复杂,此时该int型数据不是位于内存读取边界上,这就是一类内存未对齐的数据。此时CPU先访问一次内存,读取[0] - [3]字节的数据进寄存器,并再次读取[4] - [7]字节的数据进寄存器,接着把0字节和5、6、7字节的数据剔除,最后合并1、2、3、4字节的数据进寄存器。对一个内存未对齐的数据进行了这么多额外的操作,大大降低了CPU性能。