C++是如何管理资源的?
C++是目前主流编程语言中,唯一通过初始化的方式获取资源(RAII,Resource Acquisition Is Initialization)的编程语言。与其它流行编程语言相比(如Java,JavaScript等),C++不需要编程语言提供GC机制,也能有效的对内存进行管理(手动管理)。先来看一段简单的C++代码。
#include<stdio.h>
int main()
{
int * a = new int(10);
int * b = a;
*b = 11;
cout<<*a<<" "<<*b<<endl;
delete a;
return 0;
}
11 11
这是一个典型的浅拷贝的例子。造成浅拷贝的原因是,指针a
和b
指向了同一块内存空间。其中,由new
动态分配出的一块内存空间就在堆上,而指针a
和b
本身则存在于栈上。
栈空间
在大多数编程语言中,函数的调用与本地变量的都需要通过栈空间来实现,C++也不例外。在数据结构中,栈遵循“先进后出”的原则,栈空间也符合这一特性。事实上,函数的调用就是通过栈空间实现的。先执行的函数会先入栈,后执行的函数后入栈。
#include<stdio.h>
void bar() {
cout<<"bar done"<<endl;
}
void foo() {
bar();
cout<<"foo done"<<endl;
}
int main()
{
foo();
cout<<"main done"<<endl;
return 0;
}
bar done
foo done
main done
在这段代码中,函数执行时的入栈顺序为main foo bar
,函数执行完毕的出栈顺序为bar foo main
,满足先进后出的性质。具体地,在大部分计算机体系架构中,栈是由高地址向低地址增长。栈所占用的空间由栈顶指针表示(即栈空间的为栈底到栈顶指针指向的位置)。
当函数调用另一函数时,先将原函数的待执行的下一行汇编代码的地址压入栈中,然后再根据汇编指令中提供的新函数的首地址进行跳转,并进入该新函数。同时,栈指针向上(低地址方向)移动分配出本地变量所需的内存空间,然后再执行该函数中的代码。当新函数被执行完毕后,栈顶指针向下(高地址方向)移动释放出给新函数分配的空间,接着根据先于新函数入栈的汇编代码的地址,继续执行原函数的代码。
在栈空间的分配和释放通过栈顶指针的移动实现,严格遵循先进后出的性质,不会产生内存碎片。
看上去只要使用栈空间,就能够完成程序的执行,为什么还需要堆空间呢?
- 栈空间是连续的,系统需要一次性申请占用大块的连续内存空间,这会导致内存资源被浪费。
- 将大量不必要的数据堆积在栈中,会导致栈空间过于臃肿,影响栈空间中代码的执行效率。
- 部分对象的大小在编译时不能确定(如容器等),栈无法预留出合适的空间。
- 当需要对象的引用,而非对象的复制作为返回值时。
因此,栈空间主要负责执行,而堆空间主要负责存储。
堆空间
当使用动态内存分配时,就用到了堆空间(堆空间与数据结构中的堆没有关系)。我们通过动态内存分配,在堆空间中给数据申请大块的内存空间。在C++中(不考虑GC机制),堆的操作主要涉及两种:
- 让内存管理器分配一块内存。对应
new
。 - 让内存管理器释放一块内存。对应
delete
。
new与delete,内存的分配与释放
在C++中,new
的底层通过调用malloc
实现,delete
的底层通过调用free
实现。此外,值得注意的是,相比于delete
操作符,new
操作符是有可能抛出异常的。
new
在执行过程中,会先分配内存(分配内存失败时会抛出异常),然后创建一个指针指向构造的对象,并将该指针返回。若能成功返回该指针,new
的操作结束,否则将释放分配好的内存并向外抛出构造函数异常。
delete
则会先判断指针是否为空指针,若指针非空,则会调用析构函数并释放对应的内存。因此,delete
空指针是一个合法的操作(什么都不做)。
同时为了满足用户对申请和释放内存时的特殊需求,C++也提供了new
与delete
的重载功能,但二者扔需要满足new
与delete
原先的特性,即:
new
不应返回空指针,而应该使用std::bad_alloc
表示内存分配失败。delete
不应抛出异常。