new与delete时究竟发生了什么?

204 阅读4分钟

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

这是一个典型的浅拷贝的例子。造成浅拷贝的原因是,指针ab指向了同一块内存空间。其中,由new动态分配出的一块内存空间就在上,而指针ab本身则存在于上。

栈空间

在大多数编程语言中,函数的调用与本地变量的都需要通过栈空间来实现,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,满足先进后出的性质。具体地,在大部分计算机体系架构中,栈是由高地址向低地址增长。栈所占用的空间由栈顶指针表示(即栈空间的为栈底到栈顶指针指向的位置)。

当函数调用另一函数时,先将原函数的待执行的下一行汇编代码的地址压入栈中,然后再根据汇编指令中提供的新函数的首地址进行跳转,并进入该新函数。同时,栈指针向上(低地址方向)移动分配出本地变量所需的内存空间,然后再执行该函数中的代码。当新函数被执行完毕后,栈顶指针向下(高地址方向)移动释放出给新函数分配的空间,接着根据先于新函数入栈的汇编代码的地址,继续执行原函数的代码。

在栈空间的分配和释放通过栈顶指针的移动实现,严格遵循先进后出的性质,不会产生内存碎片

看上去只要使用栈空间,就能够完成程序的执行,为什么还需要堆空间呢?

  1. 栈空间是连续的,系统需要一次性申请占用大块的连续内存空间,这会导致内存资源被浪费。
  2. 将大量不必要的数据堆积在栈中,会导致栈空间过于臃肿,影响栈空间中代码的执行效率。
  3. 部分对象的大小在编译时不能确定(如容器等),栈无法预留出合适的空间。
  4. 当需要对象的引用,而非对象的复制作为返回值时。

因此,栈空间主要负责执行,而堆空间主要负责存储。

堆空间

当使用动态内存分配时,就用到了堆空间(堆空间与数据结构中的堆没有关系)。我们通过动态内存分配,在堆空间中给数据申请大块的内存空间。在C++中(不考虑GC机制),堆的操作主要涉及两种:

  1. 让内存管理器分配一块内存。对应new
  2. 让内存管理器释放一块内存。对应delete

new与delete,内存的分配与释放

在C++中,new的底层通过调用malloc实现,delete的底层通过调用free实现。此外,值得注意的是,相比于delete操作符,new操作符是有可能抛出异常的。

new在执行过程中,会先分配内存(分配内存失败时会抛出异常),然后创建一个指针指向构造的对象,并将该指针返回。若能成功返回该指针,new的操作结束,否则将释放分配好的内存并向外抛出构造函数异常。

delete则会先判断指针是否为空指针,若指针非空,则会调用析构函数并释放对应的内存。因此,delete空指针是一个合法的操作(什么都不做)。

同时为了满足用户对申请和释放内存时的特殊需求,C++也提供了newdelete的重载功能,但二者扔需要满足newdelete原先的特性,即:

  • new不应返回空指针,而应该使用std::bad_alloc表示内存分配失败。
  • delete不应抛出异常。

参考资料