六:C++内存模型、管理
前文
C++的优势是在运行时能有效的分配和释放内存,本章节说明C++的内存怎么分配,有哪些相关名词,相关函数。
很多资料的都说明了C++程序运行时会将内存分为几个部分,如栈、堆、数据、代码区等,但不同资料间又有所不同, 本章节内容会综合相关资料。
正文
内存模型
查看反汇编代码一般程序存储分层了好几段,但综合网络上资料,还是将其大致分成四个部分(未初始化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
个字节,其中a
占3
字节,c
占1
字节,bss
段本身1
字节。
动态分配内存
首先说明动态是什么意思,动态的意思是说程序运行时动态加载、释放,相应的,静态指编译时就确定的,如全局变量,这个地址编译时就确定了,所以一般不说静态内存管理。
C中用malloc, calloc, realloc
和free
,C++中是new
和delete
(注意,这两个是操作符),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
了 - 可以初始化对象