文章

六:C++内存模型、管理

六:C++内存模型、管理

前文

C++的优势是在运行时能有效的分配和释放内存,本章节说明C++的内存怎么分配,有哪些相关名词,相关函数。

很多资料的都说明了C++程序运行时会将内存分为几个部分,如栈、堆、数据、代码区等,但不同资料间又有所不同, 本章节内容会综合相关资料。

正文

内存模型

参考资料1参考资料2

查看反汇编代码一般程序存储分层了好几段,但综合网络上资料,还是将其大致分成四个部分(未初始化bss segment和已初始化data segment放在一起)讲解:

  • 代码区(一般还会包含一些常量,字符串字面量等)。
  • 数据区(未初始化block started by symbol和已初始化,静态区,全局区)。
  • 栈(调用栈,Call Stack)。
  • 堆(Heap)。

对于不同的区域,编译器定的生命周期是不一样的。

主要针对函数(包括main函数)内的数据(不包括static extern修饰的变量),由编译器自动分配释放(主动分配内存是在堆中进行),存放局部变量等,当调用函数的时候,函数体的数据被压入栈,而结束时,需要返回的数据出栈,且符合先进后出原则(这也就是为什么函数能递归),到函数体结束时释放内存。

1
2
3
4
5
func () {
	int a;
	int b;
	b += param;
}

如上函数,对应的在栈(设栈stack,从左往右即为从上至下)的为, 首先a被压入栈,stack{a},接着b被压入栈stack{b, a},接着是stack{param, b, a}param被调用完出栈,此时为stack{b, a}

由编程人员自己分配,如果不进行释放操作,则程序结束系统释放,使用堆是为了更好的管理内存,如分配数组,往往一次性定义数组大小并不会全部使用完,浪费了内存,体现在代码中为使用new/delete关键字。这里需要注意内存泄漏。

1
2
3
4
int a;
cin >> a;
int b[a];
cout << sizeof(b);

上述代码在一些编译器是能编译通过的,但不意味着是正确的,因为a是在运行时确定的,编译器无法知道该预留多少空间给b, 正确的是在堆中进行分配并且使用完释放。

1
2
3
4
int a;
cin >> a;
int *b = new int[a];
// delete[] b;

代码区

代码区存放二进制代码和常量,如字符串字面量,需要程序运行完释放,且这个区域一般是只读的,也可写则意味着可以修改程序。

数据区

数据区的变量运行在程序的开始和结束,体现在代码中,即为不包含在函数和类中的变量。存放全局变量包括static等,把static和内存联系起来即static不改变访问修饰权限的原因。 其中又分为未初始化区(或初始化为0的数据)和已初始化区。

1
2
3
4
func() {
	static int a = 0;
	int b = 0;
}

如上代码,如果函数内的参数被static修饰,则其在程序开始时便被初始化为0,而b则需要func被调用时才初始化。

类中被static修饰的变量也一样,此时,变量归属于这个类,而不是具体某个对象:

1
2
3
4
Class {
	static int a;
	int b;
}

调用b可以用Class的实例调用,如cls1.b,但cls1.a不行,这是因为a属于这个类,需要Class::a来访问a(这里把Class换成结构体Struct也是如此)。

其他说明

堆和栈是相向的,设堆heap,栈stack, 他们在内存中是heap---> ... <---stack,这里从左往右对应从上至下,中间...是未使用区域,这样可以更好的分配内存。

查看数据区bss和data的分配

1
2
3
4
5
int a;
int main() {
	static int b[3] = {1, 2, 3};
	static char c;
}

将上述代码存入test.cpp,并gcc -c汇编至test.o,使用objdump -h test.o命令查看简要信息可发现:

1
2
3
4
5
6
7
8
9
10
Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         0000005d  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         0000000c  0000000000000000  0000000000000000  000000a0  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000006  0000000000000000  0000000000000000  000000ac  2**2
                  ALLOC
  3 .init_array   00000008  0000000000000000  0000000000000000  000000b0  2**3
  ...

其中data区的大小是c,转换为十进制便是12,这是因为初始化的c数组占了12字节。这里bss段占6个字节,其中a3字节,c1字节,bss段本身1字节。

动态分配内存

首先说明动态是什么意思,动态的意思是说程序运行时动态加载、释放,相应的,静态指编译时就确定的,如全局变量,这个地址编译时就确定了,所以一般不说静态内存管理。

C中用malloc, calloc, reallocfree,C++中是newdelete(注意,这两个是操作符),new需配合使用delete

注意C++中仍保留了malloc,但new不只是分配了内存,它还创建了对象。

new delete 注意事项

语法,单个数据:

1
2
3
4
数据类型 *指针变量 = new 数据类型;
delete 指针变量;
// int *p = new int;
// delete p;

对于单个数据来说,即对相应的地址分配内存,如在指针变量上连续分配数据类型的大小(注意这里不需要sizeof了),同样对指针变量进行释放。

注意,分配如果失败需要检查p是否为空:

1
2
3
4
5
int * foo;
foo = new (nothrow) int [5];
if (foo == nullptr) {
  // error assigning memory. Take measures.
}

这里nullptr是C++11引入的,用于表示空指针,NULL虽然也可以,但实际上NULL被定义为整数0,可能会导致类型错误

如上述例子,nothrow即告诉编译器分配失败不要抛出异常,其次为如果是空指针的逻辑。

注意

不要删除同一个地址多次,这和使用未知内存的风险是一样的,如int a = new int, int* b = &a, 如果此时delete b,则不需要释放a,因为a/b是同一个地址,再次释放很可能会该地址已经被重新使用了。

语法,数组:

1
2
3
4
数据类型 *指针变量 = new 数据类型[大小];
delete[] 指针变量;
// int *p = new int[10];
// delete[] 指针变量;

同样的,这里连续分配了10个int大小的空间,这里也可以用*p; *(p+1)来访问元素,删除时则需告知编译器是数组。

new的优势

  • new操作符能被重载
  • 内存不足会抛出异常
  • 注意new创建后返回的数据类型不再需要类型准换
  • 不需要使用sizeof
  • 可以初始化对象
本文由作者按照 CC BY 4.0 进行授权

热门标签