C++内存管理:基本知识与new-delete的使用


Dynamic Memory之前的问题

这篇文章中已经总结了C++中的变量和各种初始化方式,了解这些内容对于变量的内存使用是必要的。到目前为止,就scope和lifetime来说,有三种类型的变量:

  • Global objects are allocated at program start-up and destroyed when the program ends.
  • Local, automatic objects are created and destroyed when the block in which they are defined is entered and exited.
  • Local static objects are allocated before their first use and are destroyed when the program ends.

虽然,它们在scope和lifetime上均不同,但是有一个共同点,即都是由程序隐式控制整个lifetime,即内存的分配和释放不用程序员操心。

但是,有时候,内存的管理非常复杂,根据该文

  • you cannot determine the maximum amount of memory to use at compile time;
  • you want to allocate a very large object;
  • you want to build data structures (containers) without a fixed upper size;

另外一个目的,根据C++ Primer的说法:

One common reason to use dynamic memory is to allow multiple objects to share the same state.

这时候,Dynamic Memory能够解决问题。

Dynamically allocated objects have a lifetime that is independent of where they are created; they exist until they are explicitly freed.

但是,在Dynamic Memory中的一个核心的问题是——在正确的时间,申请和释放正确的内存

变量在内存中的分配

在C++中,变量在内存中的分配区域,多种资料使用的术语不尽相同,虽然查了很多资料,但是无法统一。虽然存在差异,但是基本的原则大体相同,如下图所示:

一个程序的memory layout

基本可以包括:

  • stack
    • used for nonstatic objects defined inside functions.
    • 也就是之前提到的automatic objects;
  • heap
    • objects which are dynamically allocated at run time
  • static data section
    • bss segment: uninitialized data
      • stand for block starting symbol
      • including
        • uninitialized global objects
        • uninitialized local static objects
      • these uninitialized objects will be initialized by zero/nullptr.
    • data segment: initialized data
      • including
        • initialized global objects
        • initialized local static objects
        • constants: declared by const;
        • external variables: declared by extern keyword
  • code/text segments
    • store program executable code.
    • read-only with fixed size

new & delete的使用

C++中,最开始使用new & delete 两个操作符来实现内存的申请和释放:

  • new, which allocates, and optionally initializes, an object in dynamic memory and returns a pointer to that object.
  • delete, which takes a pointer to a dynamic object, destroys that object, and frees the associated memory.

new

使用new在heap中分配的内存是没有名字的,因为不能通过变量名来访问,但是new返回了指向该内存地址的一个指针,可以通过指针来访问这个内存中的object。

// pi points to int object without initialization
int *pi = new int

初始化方式

  • default initialization

    虽然分配了内存,但是如果没有显式初始化的话,这些object使用default initialization

    • objects of built-in or compound type have undefined value;
    • objects of class type are initialized by their default constructor
    // initialized to empty string
    string * ps = new string; 
    // pi points to an uninitialized int
    int *pi = new int;
    
  • direct initialization

    // object to which pi points has value 1024 
    int * pi = new int(1024);
    // *ps is "9999999999"
    string *ps = new string(10,9); 
    
  • list initialization

    // vector with ten elements with values from 0 to 9 
    vector<int> * pv = new vector<int>{0,1,2,3,4,5,6,7,8,9};
    
  • value initialization(following the type name with a pair of empty parentheses)

    • For class types, the object is initialized by the default constructor.
    • For built-in types, they are initialized by well-defined values. (一般为0)
    // value initialized to the empty string
    string * ps = new string();
    // value initialized to 0; *pi2 is 0
    int *pi = new int();
    

考虑到不同的初始化方式的影响,我们最好显式地对动态分配的内存进行初始化。

auto的使用限制

使用auto可以进行类型推导,但是必须从initilizer中推导,当使用direct initialization时,只能提供一个initializer。

// ok
auto p1 = new auto(1);
// error: initialization of new-expression for type ‘auto’ requires exactly one element
auto p2 = new auto{1, 2, 3};
// error: initialization of new-expression for type ‘auto’ requires exactly one element
auto p2 = new auto(1, 2, 3);

处理const

使用newconst对象分配内存是可行的:

// allocate and initialize a const int 
const int * pci = new const int(1024);
// allocate a default-initialized const empty string 
const string * pcs = new const string;

当为const对象分配内存时,必须要初始化。

  • 对于基本类型,必须显式初始化,否则报错。

    // ok, *pci = 0
    // pointer to const int
    const int *pci = new const int();
    // error: uninitialized const in ‘new’ of ‘const int’
    const int *pci = new const int;
    
  • 对于class type,则情况有点复杂。

    • 类有成员变量,无默认构造函数;

      class A {
      public:
          int i;
      };
      // ok
      A *a = new A;
      // ok
      A *a = new A();
      // error: uninitialized const in ‘new’ of ‘const class A’
      const A *a = new const A;
      // ok
      const A *a = new const A();
      
    • 类有成员变量,有默认构造函数;

      class A {
      public:
          int i;
          A() = default;
      };
      // ok
      A *a = new A;
      // ok
      A *a = new A();
      // error: uninitialized const in ‘new’ of ‘const class A’
      const A *a = new const A;
      // ok
      const A *a = new const A();
      
    • 类无成员变量,无默认构造函数;

      class A {
      };
      // ok
      A *a = new A;
      // ok
      A *a = new A();
      // ok
      const A *a = new const A;
      // ok
      const A *a = new const A();
      
    • 类无成员变量,有默认构造函数;

      class A {
      public:
          A() = default;
      };
      // ok
      A *a = new A;
      // ok
      A *a = new A();
      // ok
      const A *a = new const A;
      // ok
      const A *a = new const A();
      

    总结来看:

    • 对于使用new来动态分配const对象的内存时,如果有成员变量时,必须显式初始化,不能隐式初始化;
    • 对于没有成员变量时,可以使用隐式初始化;

异常处理

当内存分配失败时,使用new会抛出异常,如下:

// if allocation fails, new throws std::bad_alloc
int *p1 = new int; 

一般在分配大内存时,有可能会失败。 如果我们不希望new抛出异常,可以使用nothrow关键字。

// if allocation fails, new returns a null pointer
int *p2 = new (nothrow) int;

这种使用方式称为placement new

bad_allocnothrow均定义在<new>header中。

delete

与new对应的,delete完成2件事情:

  • It destroys the object to which its given pointer points,
  • and it frees the corresponding memory.

特点

  • 传入的pointer value可以是new分配的,也可以是nullptr;

    也就是说,delete一个空指针也是可以的

    // p must point to a dynamically allocated object or be null
    delete p;
    
  • 删除不是new分配的内存时,其行为是undefined的。

    int i = 10;
    int *pi1 = &i;
    int *pi2 = nullptr;
    // error: i is not a pointer
    // error: type ‘int’ argument given to ‘delete’, expected pointer
    delete i;
    // undefined: pi1 refers to a object on stack, no errors occurred from the compiler.
    delete pi1; 
    // ok: it is always ok to delete a null pointer
    delete pi2;
    
  • 对同一块内存释放两次,其行为也是undefined的。

    double *pd = new double(33);
    double *pd2 = pd;
    // ok 
    delete pd;
    // undefined: the memory pointed to by pd2 was already freed
    delete pd2;
    

    在clang++编译器下,对于释放两次内存的这种操作,运行时出现错误如下:

    释放两次内存运行时出现的错误

  • 释放const对象对应的资源,其方式与一般的object并无二致。

    const int * pci = new const int(1024);
    // ok: deletes a const object
    delete pci;
    
  • 通过new创建的动态内存,在没有显式释放前会一直存在;

    这意味着block scope对其不起作用,即便函数返回了,其中创建的动态内存也不会释放,对应的object也不会有任何变化。这与smart pointers是不同的。

    这样的好处是,这块内存以及对应的变量会一直存在,不用担心由系统收回。但是也存在弊端,当我们忘记显式释放这块内存时,会导致内存泄露,尤其是当在函数中返回时,我们无法获取到对应的指针了,就无法释放对应的内存。

    void func(){
        int *p = new int(10);
        return;
    }
    

    此时,其中p指向的内存,在函数返回后仍然存在,但是无法获取到指针p,也就无法释放了。

  • 在delete指向对应内存的指针后,将该指针置nullptr是一种推荐的方式。

    因为即便删除了对应的内存,但是在一些系统中,对应的指针仍然持有这块已经被删除的内存的地址,这时候该指针成为dangling pointer。为了避免再次使用该指针,显式地将其赋值为nullptr,起到提醒的作用。

    但是对于指向相同内存空间的不同指针,将其中一个置为nullptr,不会影响另外的指针。

    int * p(new int(42));
    // p and q point to the same memory
    auto q = p;
    // invalidates both p and q
    delete p;
    // indicates that p is no longer bound to an object
    p = nullptr;
    // q is still a dangling pointer .....
    

参考资料

  1. C++ Primer - 12.1.2 Managing Memory Directly
  2. https://en.cppreference.com/w/cpp/language/new
  3. https://courses.engr.illinois.edu/cs225/fa2022/resources/stack-heap/
  4. https://sbme-tutorials.github.io/2018/data-structures/notes/2_week2a.html
  5. https://www.javatpoint.com/memory-layout-in-c
  6. https://developerinsider.co/memory-layout-representation-of-c-program/
  7. https://lovemesomecoding.com/data-structure-algorithm/data-structure-algorithm-memory
  8. https://www.scaler.com/topics/c/memory-layout-in-c/
  9. https://hackthedeveloper.com/memory-layout-c-program/
  10. https://en.wikipedia.org/wiki/Data_segment

文章作者: alex Li
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 alex Li !
  目录