第十二章 动态内存

  • 对象的生命周期:
    • 全局对象在程序启动时分配,结束时销毁。
    • 局部对象在进入程序块时创建,离开块时销毁。
    • 局部static对象在第一次使用前分配,在程序结束时销毁。
    • 动态分配对象:只能显式地被释放。
  • 对象的内存位置:
    • 静态内存用来保存局部static对象、类static对象、定义在任何函数之外的变量。
    • 栈内存用来保存定义在函数内的非static对象;由操作系统自动分配和释放,内存空间比较小
    • 堆内存,又称自由空间,用来存储动态分配的对象;手动申请和释放,内存空间比较大

12.1 动态内存与智能指针

12.1.0 动态内存

  • 使用new动态分配内存,返回的是一个指向该对象的指针
    • 动态分配的对象是默认初始化的,也可以使用值初始化、直接初始化(圆括号中有初始值)、列表初始化方式来进行初始化
      • 对于类而言,值初始化与默认初始化没有区别
      • 对于内置类型而言,
        • 值初始化有一个确定的初始值:int *p = new int(); // 此时所指对象值为0
        • 默认初始化的初始值未定:int *p = new int;
        • 直接初始化:auto *p = new int(2);
    • 可以使用new分配const对象,返回指向const类型的指针,但是动态分配的const对象必须初始化
      • const int* pc = new const int(1);
    • new失败会抛出bad_alloc异常
      • 使用定位new可以阻止抛出异常,定位new允许将new传递额外参数
      • 如果传递nothrow给new,则new在分配失败之后会返回空指针:int* p = new(nothrow) int;
  • 使用delete销毁对象,并释放内存
    • delete后的指针称为空悬指针(dangling pointer),应该在delete之后将指针值置空。
  • 使用new/delete,要么容易忘记释放内存引起内存泄露,要么释放内存后再使用引起use after free
  • 智能指针:定义在头文件memory
    • shared_ptr:允许多个指针指向同一个对象,共享内存
    • unique_ptr:独占所指向的对象
    • weak_ptr:是一种弱引用,指向shared_ptr所管理的对象

12.1.1 shared_ptr

声明和初始化

1
2
3
4
5
6
7
shared_ptr<T> sp1; // 指向类型T的空智能指针
shared_ptr<T> sp2(q); // 参数q为T*类型的内置指针, sp2接管对象的所有权
shared_ptr<T> sp3(q, d); // sp3使用删除器d代替默认删除器delete(删除器d必须接受一个T*类型的参数)
shared_ptr<T> sp4(sp); // 参数sp为shared_ptr<T>,等价于sp4 = sp;
shared_ptr<T> sp5 = sp4; // sp4引用计数递减,sp5引用计数递增
shared_ptr<T> sp6 = make_shared<T>(args); // 使用参数args初始化类型为T的对象
shared_ptr<T> sp7(u); // 参数u为unique_ptr<T>, sp7接管对象的所有权,并将u置为空
  • 语法相关:
    • 使用内置指针q进行初始化必须使用直接初始化形式(如sp2),不能使用拷贝初始化,因为调用了explicit的转换构造函数
    • 默认内置指针q必须指向动态内存(因为智能指针默认使用delete释放对象),如果将智能指针绑定到指向其他类型资源的指针上,需要使用自定义的删除器代替delete
  • 推荐使用sp6的初始化方式make_shared,即不要混合使用智能指针和内置指针
    • 不推荐sp2方式进行初始化,因为同一个内置指针q不能绑定到多个独立创建的shared_ptr,否则析构时多次delete
    • 如果使用q创建shared_ptr后(比如sp2),不要再使用q,因为q无法知道对象何时被shared_ptr释放,随时可能变成空悬指针
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    void add(shared_ptr<int> p) {}
    int* p1 = new int(1);
    shared_ptr<int> sp1(p1);
    // shared_ptr<int> sp(p1); // 报错,p1绑定到独立的两个shared_ptr
    
    int *p2 = new int(2);
    add(shared_ptr<int>(p2)); // 函数调用完后,智能指针引用计数为0,p2所指向内存被释放,此时p2成为悬空指针,危险!
    
    sp3 = make_shared<int>(3); // 推荐
    add(sp3); 
    

其他操作

操作解释
p.get()返回p中保存的指针。如果智能指针释放了对象,则get返回一个悬空指针。
p.use_count()返回与p共享对象的智能指针的数量
p.unique()当前对象是否被p独占(或者当p.use_count()==1时返回true
p.reset()如果p时唯一指向其对象的shared_ptr,则释放此对象
p.reset(q)p指向内置指针q
p.reset(q, d)p指向内置指针q,并调用删除器d(而非默认删除器)来释放q
  • 智能指针不支持指针算数运算
  • get函数
    • 智能指针的get函数返回一个内置指针,指向智能指针管理的对象,主要用于向不能使用智能指针的代码传递内置指针。
    • 使用get返回的指针不能用来delete
    • 不要使用get函数初始化另一个智能指针或为智能指针赋值(因为析构时多次delete)
    • 只有在确定代码不会delete指针的情况下,才使用get
  • unique函数通常用与reset一起使用,检查shared_ptr是否独占当前对象,如果不是需要使用reset指向新的元素或拷贝。例子

使用建议

  • 不使用相同的内置指针初始化或reset多个智能指针
  • delete get()返回的指针。
  • 不使用get()初始化或reset另一个智能指针
  • 如果你使用get()返回的指针,记得当最后一个对应的智能指针销毁后,你的指针就无效了。
  • 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除函数。

12.1.2 unique_ptr

声明和初始化

1
2
3
4
5
unique_ptr<T> u1; 
unique_ptr<T> u1(q); // q为类型T*的内置指针
uniuqe_ptr<T, D> u2; // 定义一个unique_ptr,指向类型T,有一个类型为D的删除器
unique_ptr<T, D> u3(q, d); 
unique_ptr<T> u4 = make_uniuqe<T>(args); // C++14
  • unique_ptr将删除器类型放在尖括号中,因为删除器类型也是unique_ptr类型的一部分
  • 同一时刻只能有一个unique_ptr指向一个给定的对象。当unique_ptr被销毁时,它指向的对象也被销毁
    1
    2
    3
    
    int p = new int(1);
    uniuqe_ptr<int> u(p); // 正确,但是u销毁之后p成为空悬指针
    uniuqe_ptr<int> up(new int(1)); // 推荐写法
    
  • unique_ptr必须使用内置指针进行直接初始化(圆括号初始化),不支持拷贝或赋值操作(unique的含义,而且其拷贝构造函数是删除的)
    • 例外:可以拷贝或赋值一个即将被销毁的unique_ptr(移动构造、移动赋值)

其他操作

操作解释
u.get()返回u中保存的指针。如果智能指针释放了对象,则get返回一个悬空指针。
u.release()u放弃对指针的控制权(但不会释放指向对象的内存),返回内置指针,并将u置空。
u.reset()释放u指向的对象
u.reset(q)u指向内置指针q指向的对象,u原来指向的对象被释放
  • release返回的指针通常用来初始化另一个智能指针(reset)或给智能指针赋值
    1
    2
    3
    4
    
    unique_ptr<int> p1(new int(1)), p2(new int(2)), p3(new int(3));
    p1.release(); // p1放弃了对象的控制权,对象的内存没有释放,而且对象的指针丢失
    int* p = p2.release(); // 使用p保存对象的指针,但是后续需要使用delete(p)释放内存
    unique_ptr<int> u(p3.release()); // u接管p3
    

12.1.3 weak_ptr

声明和初始化

1
2
3
weak_ptr<T> w1;
weak_ptr<T> w2(sp); // sp是shared_ptr<T>类型, w2指向一个由shared_ptr管理的对象,但是不改变shared_ptr的引用计数
weak_ptr<T> w3 = p; // p可以是shared_ptr或weak_ptr
  • weak_ptr是一种不控制所指向对象生存期的智能指针。

其他操作

操作解释
w.reset()w置为空。
w.use_count()w共享对象的shared_ptr的数量。
w.expired()w.use_count()为0,返回true,否则返回false
w.lock()如果expiredtrue,则返回一个空shared_ptr;否则返回一个指向w的对象的shared_ptr
  • weak_ptr不能直接访问对象。因为如果shared_ptr被销毁,即使有weak_ptr指向对象,对象仍然可能被释放
  • 使用weak_ptr访问对象时,必须先调用lock函数,以检查指向的对象是否仍然存在
    1
    2
    3
    
    shared_ptr<int> sp = make_shared<int>(1);
    weak_ptr<int> wp(sp);
    if (shared_ptr<int> p = wp.lock()) cout<<*p<<endl;
    

12.2 动态数组

C++中提供了两种动态数组的分配方式:

  • new动态数组,将内存分配和对象构造结合在一起,对应的delete将对象析构和内存释放结合在一起
  • 使用allocator类,可以实现内存分配与对象构造的分离,管理内存更灵活

12.2.1 动态数组

new和动态数组

  • new一个动态数组,返回指向第一个对象的指针(返回的指针不是数组类型,而是数组元素类型)
    • 由于new分配的内存不是数组类型(比如int[10]),因此不能对动态数组调用begin和end,也不能使用range-for遍历元素
  • new的数组可以进行值初始化、列表初始化
    1
    2
    3
    4
    
      const int sz = 10;
      int *p1 = new int[sz]; // 没有初始化
      int *p2 = new int[sz](); // 值初始化为0
      int *p3 = new int[sz]{1,2,3,4,5}; // 列表初始化  
    
  • 因为值初始化时不能提供参数,所以没有默认构造函数的类是无法动态分配数组的。
  • 动态分配一个空数组是合法的,此时返回一个合法的非空指针,类似于尾后指针
  • 使用delete []释放动态分配的数组,使用delete释放动态分配的对象

unique_ptr和动态数组

可以使用unique_ptr管理new分配的数组

1
2
3
unique_ptr<int[]> p(new int[10]()); // p指向一个包含10个元素的int数组,数组元素使用值初始化
p[1] = 1; // 指向数组的unique_ptr不支持成员访问运算符(点和箭头),支持下标访问
p.release(); // 自动用delete[]销毁其指针

shared_ptr和动态数组

  • shared_ptr不支持直接管理动态数组,如果想用shared_ptr管理动态数组,必须提供自定义的删除器(否则使用delete释放动态数组,报错)
  • shared_ptr未定义下标运算符,智能指针也不支持指针算数运算。可以通过get函数获取内置指针再进行访问
1
2
3
shared_ptr<int> p(new int[10](), [](int *p){delete []p;}
for(int i=0; i<10; ++i)
    cout<<*(p.get()+i)<<" ";

12.2.2 allocator类

  • 标准库allocator类定义在头文件memory中,帮助我们将内存分配和对象构造分离开。
  • 分配的是原始的、未构造的内存,程序需要再内存中构造对象。(直接使用未构造的内存是未定义的行为)
  • 对象使用完之后,需要对每个构造的元素调用destroy进行销毁
标准库allocator类及其方法
操作解释
allocator<T> a定义了一个名为aallocator对象,它可以为类型为T的对象分配内存
a.allocate(n)分配一段原始的、未构造的内存,保存n个类型为T的对象,返回一个指向类型T的指针
a.deallocate(p, n)释放从T*指针p中地址开始的内存,这块内存保存了n个类型为T的对象;p必须是一个先前由allocate返回的指针。且n必须是p创建时所要求的大小。在调用deallocate之前,用户必须对每个在这块内存中创建的对象调用destroy
a.construct(p, args)p必须是一个类型是T*的指针,指向一块原始内存;args被传递给类型为T的构造函数,用来在p指向的内存中构造一个对象。使用时需要p++移动指针
a.destroy(p)pT*类型的指针,此算法对p指向的对象执行析构函数。
  • construct和destroy一次只能构造或销毁一个对象,使用中可能需要使用指针对每个元素进行遍历
allocator伴随算法
操作解释
uninitialized_copy(b, e, b2)从【迭代器be给定的输入范围】中拷贝元素到【迭代器b2指定的未构造的原始内存】中。b2指向的内存必须足够大,能够容纳输入序列中元素的拷贝。
uninitialized_copy_n(b, n, b2)从迭代器b指向的元素开始,拷贝n个元素到【b2开始的未构造内存】中。
uninitialized_fill(b, e, t)在【迭代器be指向的原始内存范围】中创建对象,对象的值均为t的拷贝。
uninitialized_fill_n(b, n, t)从【迭代器b指向的原始内存地址】开始创建n个对象。b必须指向足够大的未构造的原始内存,能够容纳给定数量的对象。
  • 进行拷贝和填充未初始化内存
  • 返回最后一个构造元素的尾后位置

12.3 使用标准库:文本查询程序

参考