【C++ Primer】第 12 章 动态内存

Spring-_-Bear 的 CSDN 博客导航

十二、动态内存

全局对象在程序启动时分配,在程序结束时销毁。对于局部自动对象,当我们进入其定义所在的程序段时被创建,在离开块时销毁。局部 static 对象在第一次使用前分配,在程序结束时销毁。

除了自动和 static 对象外,C++ 还支持动态分配对象。动态分配的对象的生存期与它们在哪里创建是无关的,只有当显式地被释放时,这些对象才会销毁。

动态对象的正确释放被证明是编程中极其容器出错的地方。为了更安全地使用动态对象,标准库定义了两个智能指针类型来管理动态分配的对象。当一个对象应该被释放时,指向它的智能指针可以确保自动地释放它。

静态内存用来保存局部 static 对象、类 static 数据成员以及任何定义在任何函数之外的变量。栈内存用来保存定义在函数内的非 static 对象。分配在静态或栈内存中的对象由编译器自动创建和销毁。对于栈对象,仅在其定义的程序块运行时才存在;static 对象在使用之前分配,在程序结束时销毁。

除了静态内存和栈内存,每个程序还拥有一个内存池。这部分内存被称作自由空间或堆。程序用堆来存储动态分配的对象,即那些在程序运行时分配的对象。动态对象的生存期由程序来控制,也就是说,当动态对象不再使用时,我们的代码必须显式地销毁它们。

12.1 动态内存与智能指针

C++ 中,动态内存的管理是通过一对运算符来完成的:

  • new:在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化
  • delete:接受一个动态对象的指针,销毁该对象,并释放与之关联的内存

动态内存的使用很容易出问题,因为确保在正确的时间释放内存是极其困难的。有时我们会忘记释放内存,在这种情况下就会产生内存泄漏;有时在尚有指针引用内存的情况下我们释放了它,在这种情况下就会产生非法内存的指针。

为了更容易(同时也更安全)地使用动态内存,C++11 新标准库提供了两个智能指针类型来管理动态对象。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。新标准库提供的这两种指针的区别在于管理底层指针的方式:

  • shared_ptr:允许多个指针指向同一个对象
  • unique_ptr:独占所指向的对象
  • weak_ptr:伴随类,它是一种弱引用,指向 shared_ptr 所管理的对象

这三种类型都定义在 memory 头文件中。

12.1.1 shared_ptr 类

类似 vector,智能指针也是模板:

#include <iostream>
#include <memory>
#include <string>
#include <list>

int main() {
    // 默认初始化的指针中保存着一个空指针
    std::shared_ptr<std::string> ssp = std::make_shared<std::string>(); // 可以指向 string
    std::shared_ptr<std::list<int>> lsp;                                // 可以指向 int 的 list

    // 解引用一个指针返回它指向的对象
    if (ssp && ssp->empty()) {
        *ssp = "Hi!";
    }

    // 现在 ssp 已经指向了一个 string 对象,可以安全地输出
    if (ssp) {
        std::cout << *ssp << std::endl; // Hi!
    } else {
        std::cout << "ssp is empty" << std::endl;
    }

    return 0;
}
两种智能指针都支持的操作说明
shared_ptr<T> sp空智能指针,可以指向类型为 T 的对象
unique_ptr<T> up同上
pp 用作一个条件判断,若 p 指向一个对象则为 true
*p解引用 p,获取它指向的对象
p->mem等价于 (*p).mem
p.get()返回 p 中保存的指针。若智能指针释放了其对象,返回的指针所指向的对象也就无效了
swap(p, q)交换 pq 中的指针
p.swap(q)同上
shared_ptr 独有的操作说明
make_shared<T> (args)返回一个 shared_ptr,指向一个动态分配的类型为 T 的对象。使用 args 初始化此对象
shared_ptr<T> p(q)pshared_ptr 智能指针 q 的拷贝;此操作为递增 q 中的计数器。q 中的指针必须能转换为 T*
p = qpq 都是 shared_ptr,所保存的指针必须能相互转换。此操作会递减 p 的引用计数,递增 q 的引用计数;若 p 的引用计数变为 0,则将其关联的原内存释放
p.unique()p.use_count() == 1,返回 true
p.use_count()返回与 p 共享对象的智能指针数量;可能很慢,主要用于调试
  1. make_shared 函数:最安全的分配和使用动态内存的方法是调用一个名为 make_shared 的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的 shared_ptr。与智能指针一样,make_shared 也定义在头文件 memory 中:

    // 指向一个值为 42 的 int 的共享智能指针
    std::shared_ptr<int> p1 = std::make_shared<int>(42);
    // 指向一个值为 "9999999999" 的 string 的共享智能指针
    std::shared_ptr<std::string> p2 = std::make_shared<std::string>(10, '9');
    // 指向一个值初始化的 int 的共享智能指针
    std::shared_ptr<int> p3 = std::make_shared<int>();
    // 使用 auto 更为方便
    auto p4 = std::make_shared<std::vector<int>>();
    

    类似顺序容器的 emplace 成员, make_shared 用其参数来构造给定类型的对象。

  2. shared_ptr 的拷贝和赋值:当进行拷贝或赋值操作时,每个 shared_ptr 都会记录有多少个其他 shared_ptr 指向相同的对象。我们可以认为每个 shared_ptr 都有一个关联的计数器,通常称其为引用计数。无论何时当我们拷贝一个 shared_ptr 时,计数器都会递增。当我们给 shared_ptr 赋予一个新值或是 shared_ptr 被销毁时,计数器就会递减。一旦一个 shared_ptr 的计数器变为 0,它就会自动释放自己所管理的对象:

    // r 指向的 int 只有一个引用者
    auto r = std::make_shared<int>(42);
    /*
     * 给 r 赋值,令它指向另一个地址
     * 递增 q 指向的对象的引用计数
     * 递减 r 原来指向的对象的引用计数
     * r 原来指向的对象已经没有引用者,会自动释放
     */
    auto q = std::make_shared<int>(1024);
    r = q;
    

    到底是用一个计数器还是其他数据结构来记录有多少指针共享对象,完全由标准库的具体实现来决定。关键是智能指针类能记录有多少个 shared_ptr 指向相同的对象,并能在恰当的时候自动释放对象。

  3. shared_ptr 自动销毁所管理的对象:当指向一个对象的最后一个 shared_ptr 被销毁时,shared_ptr 会自动销毁此对象。它是通过另一个特殊的成员函数析构函数完成销毁工作的。类似于构造函数,每个类都有一个析构函数。就像构造函数控制初始化一样,析构函数控制此类型的对象销毁时做什么操作。

    析构函数一般用来释放对象所分配的资源。shared_ptr 的析构函数会递减它所指向的对象的引用计数。如果引用计数变为 0shared_ptr 的析构函数就会销毁对象,并释放它占用的内存。

  4. shared_ptr 还会自动释放相关联的内存:当动态对象不再被使用时,shared_ptr 类会自动释放动态对象,这一特性使得动态内存的使用变得非常容易。由于在最后一个 shared_ptr 销毁前内存都不会释放,保证 shared_ptr 在无用之后不再保留就非常重要了。如果你将 shared_ptr 存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用 erase 删除不再需要的那些元素。

  5. 使用了动态生存期的资源的类:程序使用动态内存出于以下三种原因之一:

    • 程序不知道自己需要使用多少对象
    • 程序不知道所需对象的准确类型
    • 程序需要在多个对象间共享数据

    容器类是出于第一种原因而使用动态内存的典型例子。我们将在第 15 章看到出于第二种原因而使用动态内存的例子。在本节中,我们将定义一个类,它使用动态内存是为了让多个对象能共享相同的底层数据。

    我们希望定义一个名为 Blob 的类,保存一组元素。与容器不同,我们希望 Blob 对象的不同拷贝之间共享相同的元素。即,当我们拷贝一个 Blob 是,原 Blob 对象及其拷贝应该引用相同的底层元素。

  6. 定义 StrBlob 类:不能在一个 Blob 对象内直接保存 vector,因为一个对象的成员在对象销毁时也会被销毁。

    class StrBlob {
    public:
        typedef std::vector<std::string>::size_type size_type;
    
        StrBlob();
    
        StrBlob(std::initializer_list<std::string> il);
    
        size_type size() const {
            return data->size();
        }
    
        bool empty() const {
            return data->empty();
        }
    
        void push_back(const std::string &s) {
            data->push_back(s);
        }
    
        void pop_bak();
    
        std::string &front();
    
        std::string &back();
    
    private:
        std::shared_ptr<std::vector<std::string>> data;
    
        void check(size_type i, const std::string &msg) const;
    };
    
  7. StrBlob 构造函数:

    StrBlob::StrBlob() : data(std::make_shared<std::vector<std::string>>()) {}
    
    StrBlob::StrBlob(std::initializer_list<std::string> il) : data(std::make_shared<std::vector<std::string>>(il)) {}
    
  8. 元素访问成员函数:

    void StrBlob::check(StrBlob::size_type i, const std::string &msg) const {
        if (i >= data->size()) {
            throw std::out_of_range(msg);
        }
    }
    
    std::string &StrBlob::front() {
        check(0, "front on empty StrBlob");
        return data->front();
    }
    
    std::string &StrBlob::back() {
        check(0, "back on empty StrBlob");
        return data->back();
    }
    
    void StrBlob::pop_back() {
        check(0, "pop_back on emtpy StrBlob");
        data->pop_back();
    }
    
  9. StrBlob 的拷贝、赋值和销毁:StrBlob 使用默认版本的拷贝、赋值和销毁成员函数来对此类型的对象进行这些操作。默认情况下,这些操作拷贝、赋值和销毁类的数据成员。对于 StrBlob 构造函数分配的 vector,当最后一个指向它的 StrBlob 对象被销毁时,它会随之被自动销毁。

12.1.2 直接管理内存

C++ 语言定义了两个运算符来分配和释放动态内存。运算符 new 分配内存,delete 释放 new 分配的内存。相对于智能指针,使用这两个运算符管理内存非常容易出错。 而且,自己直接管理内存的类与使用智能指针的类不同,它们不能依赖类对象拷贝、赋值和销毁操作的任何默认定义。因此,使用智能指针的程序更容器编写和调试。

  1. 使用 new 动态分配和初始化对象:默认情况下,动态分配的对象是默认初始化的(参见 2.2.1 节),这意味着内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化:

    int *pi = new int;                  // pi 指向一个动态分配的、未初始化的 int
    std::string *ps = new std::string;  // ps 指向一个空 string
    

    我们可以使用直接初始化方式(参见 3.2.1 节)来初始化一个动态分配的对象。我们可以使用传统的构造方式(使用圆括号),在 C++11 新标准下,也可以使用列表初始化(使用花括号):

    int *pi = new int(1024);
    std::string *ps = new std::string(10, '9');
    std::vector<int> *pv = new std::vector<int>{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    

    也可以对动态分配的对象进行值初始化(参见 3.3.1 节),只需在类型名之后跟一对空括号即可:

    std::string *ps1 = new std::string;         // 默认初始化为空 string
    std::string *ps2 = new std::string();       // 值初始化为空 string
    int *pi1 = new int;                         // 默认初始化,*pi1 的值未定义
    int *pi2 = new int();                       // 值初始化为 0,*pi2 = 0
    

    对于定义了自己的构造函数的类类型来说,要求值初始化是没有意义的;不管采用什么形式,对象都会通过默认构造函数来初始化。但对于内置类型,这两种形式的差别就很大了;值初始化的内置类型有着良好定义的值,而默认初始化的对象的值则是未定义的。类似的,对于类中那些依赖于编译器合成的默认构造函数的内置类型成员,如果它们未在类内被初始化,那么它们的值也是未定义的(参见 7.1.4 节)。

    如果我们提供了一个括号包围的初始化器,就可以使用 auto 从此初始化器来推断我们想要分配的对象的类型。但是,由于编译器要用初始化器的类型来推断要分配的类型,只有当括号中仅有单一初始化器时才可以使用 auto

    auto p1 = new auto(1);          // 正确:p 指向一个 int 类型的对象
    auto p2 = new auto{1, 2, 3};	// 错误:括号中只能有单个初始化器
    
  2. 动态分配的 const 对象:类似其他任何 const 对象,一个动态分配的 const 对象必须进行初始化。对于一个定义了默认构造函数的类类型,其 const 动态对象可以隐式初始化,而其他类型的对象就必须显式初始化。由于分配的对象是 const 的,new 返回的指针是一个指向 const 的指针:

    // 分配并初始化一个 const int
    const int *pci = new const int(1024);
    // 分配并默认初始化一个 const 的空 string
    const std::string *psc = new const std::string;
    
  3. 内存耗尽:默认情况下,如果 new 不能分配所要求的内存空间,它会抛出一个类型为 bad_alloc 的异常。我们可以改变使用 new 的方式来阻止它抛出异常:

    int *p1 = new int;                  // 如果分配失败,new 抛出 std::bac_alloc 异常
    int *p2 = new(std::nothrow) int;    // 如果分配失败,new 返回一个空指针
    

    我们称这种形式的 new 为定位 new。定位 new 表达式允许我们向 new 传递额外的参数。bad_allocnothrow 都定义在头文件 new 中。

  4. 释放动态内存:delete 表达式接受一个指针,指向我们想要释放的对象。其执行两个操作:销毁给定的指针指向的对象、释放对应的内存:

    int *p1 = new int;
    delete p1;
    
  5. 指针值和 delete:我们传递给 delete 的指针必须指向动态分配的内存,或者是一个空指针。释放一块并非 new 分配的内存,或者将相同的指针值释放多次,其行为是未定义的:

    int i, *pi1 = &i, *pi2 = nullptr;
    double *pd = new double(33), *pd2 = pd;
    
    delete i;       // 错误:i 不是一个指针
    delete pi1;     // 未定义:pi1 指向一个局部变量
    delete pd;      // 正确
    delete pd2;     // 未定义:pd2 指向的内存已经被释放了
    delete pi2;     // 正确:释放一个空指针总是没有错误的
    
    const int *pci = new const int(1024);
    delete pci;     // 正确:释放一个 const 对象
    
  6. 动态对象的生存期直到被释放时为止:对于一个由内置指针关联的动态对象,直到被显式释放之前它都是存在的。返回指向动态内存的指针(而不是智能指针)的函数给其调用者增加了一个额外负担:调用者必须记得释放内存。

    与类类型不同,内置类型的对象被销毁时什么也不会发生。特别是,当一个指针离开其作用域时,它所指向的对象什么也不会发生。如果这个指针指向的是动态内存,那么内存将不会被自动释放。

    动态内存的管理非常容易出错,使用 newdelete 管理动态内存的三个常见问题:忘记 delete 内存、使用已经释放掉的对象和同一块内存释放两次。

  7. delete 之后重置指针值:当我们 delete 一个指针后,指针值就变为无效了。虽然指针已经无效,但在很多机器上指针仍然保存着(已经释放了的)动态内存的地址。在 delete 之后,指针就变成了空悬指针,即,指向一块曾经保存数据对象但现在已经无效的内存的指针。

    未初始化指针的所有缺点空悬指针也都有。有一种方法可以避免空悬指针的问题:在指针即将离开其作用域之前释放掉它关联的内存。这样,在指针关联的内存被释放掉之后,就没有机会继续使用指针了。如果我们需要保留指针,可以在 delete 之后将 nullptr 赋予指针,这样就清楚地指出指针不指向任何对象。

    // 创建一个动态分配的 int 指针
    int *ptr = new int(10);
    // 使用指针
    std::cout << "Value of ptr: " << *ptr << std::endl;
    // 删除指针并释放内存
    delete ptr;
    // 将指针重置为 nullptr,避免空悬指针
    ptr = nullptr;
    // 现在 ptr 是一个空指针,不指向任何对象
    // 如果我们尝试解引用 ptr,它将不会导致未定义行为
    // 因为编译器通常会在解引用 nullptr 时抛出异常
    if (ptr == nullptr) {
        std::cout << "ptr is now a nullptr and does not point to any object." << std::endl;
    } else {
        std::cerr << "ptr is not a nullptr" << std::endl;
    }
    
  8. 这只是提供了有限的保护:动态内存的一个基本问题是可能有多个指针指向相同的内存。在 delete 内存之后重置指针的方法只对这个指针有效,对其他任何指向(已释放的)内存的指针是没有作用的。

12.1.3 shared_ptr 和 new 结合使用

如果不初始化一个智能指针,它就会被初始化为一个空指针。可以用 new 返回的指针来初始化智能指针:

// 空智能指针,可以指向一个 double 对象
std::shared_ptr<double> p1;
// 指向一个值为 42 的 int 对象
std::shared_ptr<int> p2(new int(42));

接受指针参数的智能指针构造函数是 explicit 的(参见 7.5.4 节)。因此,不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化方式:

std::shared_ptr<int> p1 = new int(1024); 	// 编译错误:必须使用直接初始化形式
std::shared_ptr<int> p2(new int(1024)); 	// 正确

默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用 delete 释放它所关联的对象。我们可以将智能指针绑定到一个指向其他类型的资源的指针上,但是为了这样做,我们必须提供自己的操作来替代 delete

定义和改变 shared_ptr 的其他方法说明
shared_ptr<T> p(q)p 管理内置指针 q 所指向的对象;q 必须指向 new 分配的内存,且能够转换为 T* 类型
shared_ptr<T> p(u)punique_ptr u 那里接管了对象的所有权;将 u 置为空
shared_ptr<T> p(q, d)p 接管了内置指针 q 所指向的对象的所有权。q 必须能转换为 T* 类型。p 将使用可调用对象 d 来代替 delete
shared_ptr<T> p(p2, d)pshared_ptr p2 的拷贝,唯一的区别是 p 将用可调用对象 d 来代替 delete
p.reset()p 是唯一指向其对象的 shared_ptrreset 会释放此对象,并将 p 置为空
p.reset(q)同上。但会令 p 指向内置指针 q
p.reset(q, d)同上。但调用 d 而不是 delete 来释放 q
  1. 不要混合使用普通指针和智能指针:shared_ptr 可以协调对象的析构,但这仅限于其自身的拷贝之间。这也是为什么推荐使用 make_shared 而不是 new 的原因。这样,我们就能在分配对象的同时就将 shared_ptr 与之绑定,从而避免了无意中将同一块内存绑定到多个独立创建的 shared_ptr 上:

    #include <memory>
    
    using namespace std;
    
    void process(std::shared_ptr<int> ptr) {
    }
    
    int main() {
        std::shared_ptr<int> p(new int(42)); 	// 引用计数为 1
        process(p); 							// 拷贝 p 会递增它的引用计数,在 process 中引用计数值为 2
        int i = *p; 							// 正确:引用计数值为 1
    
        int *x(new int(1024));
        process(std::shared_ptr<int>(x)); 		// 合法:但 x 的内存在 process 执行完后会被智能指针释放
        int j = *x;	 							// 未定义的行为:x 是一个空悬指针
    }
    

    当将一个 shared_ptr 绑定到一个普通指针时,我们就将内存的管理责任交给了这个智能指针。一旦这样做了,我们就不应该再使用内置指针来访问 shared_ptr 所指向的内存了。

  2. 也不要使用 get 初始化另一个智能指针或为指针指针赋值:使用 get() 返回的指针的代码不能 delete 此指针

    std::shared_ptr<int> p(new int(42));
    int *q = p.get();
    {
        std::shared_ptr<int> r(q); 	// 两个独立的 shared_ptr 指向相同的内存
    }
    int foo = *p; 					// 未定义:p 指向的内存已经被 r 释放了
    
  3. 其他 shared_ptr 操作:

    // 将一个新的指针赋予一个 shared_ptr
    std::shared_ptr<int> ptr = std::make_shared<int>(1024);
    ptr.reset(new int(42));
    

    reset 成员经常和 unique 一起使用,用来控制多个 shared_ptr 共享的对象。在改变底层对象之前,我们检查自己是否是当前对象仅有的用户。如果不是,在改变之前要制作一份新的拷贝:

    std::shared_ptr<int> ptr = std::make_shared<int>();
    if (!ptr.unique()) {
        ptr.reset(new int(*ptr));
    }
    *ptr += 1024;
    

12.1.4 智能指针和异常

如果使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放。与之相对的,当发生异常时,我们直接管理的内存是不会自动释放的。

  1. 智能指针和哑类:包括所有标准库类在内的很多 C++ 类都定义了析构函数,负责清理对象使用的资源。但是,不是所有的类都是这样良好定义的。特别是那些为 CC++ 两种语言设计的类,通常都要求用户显式地释放所使用的任何资源。

    那些分配了资源,而又没有定义析构函数来释放这些资源的类,可能会遇到与使用动态内存相同的错误——程序员非常容易忘记释放资源。类似的,如果在资源分配和释放之间发生了异常,程序也会发生资源泄漏。

    与管理动态内存类似,我们通常可以使用智能指针技术来管理不具有良好定义的析构函数的类。

  2. 使用我们自己的释放操作:

    struct destination {
    };
    
    struct connection {
    };
    
    connection connect(destination *);
    
    void disconnect(connection*);
    
    void f(destination &d) {
        connection c = connect(&d);
        // 当 f 退出(即使是异常退出),connection 会被正确关闭
        std::shared_ptr<connection> p(&c, disconnect);
    }
    
  3. 智能指针陷阱:智能指针可以提供对动态分配的内存安全而又方便的管理,但这建立在正确使用的前提下。为了正确使用智能指针,我们必须坚持遵守一些基本规范:

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

12.1.5 unique_str

一个 unique_ptr 拥有它所指向的对象。与共享智能指针不同,某个时刻只能有一个 unique_ptr 指向一个给定对象。当 unique_ptr 被销毁时,它所指向的对象也被销毁。

当我们定义一个 unique_ptr 时,需要将其绑定到一个 new 返回的指针上,并且必须采用直接初始化形式:

std::unique_ptr<double> p1;             // 可以指向一个 double 的 unique_ptr
std::unique_ptr<int> p2(new int(42));   // p2 指向一个值为 42 的 int 对象
unique_ptr 操作说明
unique_ptr<T> u1unique_ptr,可以指向类型为 T 的对象。调用 delete 释放它的指针
unique_ptr<T, D> u2同上。但使用一个类型为 D 的可调用对象来释放它的指针
unique_ptr<T, D> u(d)unique_ptr,指向类型为 T 的对象,用类型 D 的对象 d 代替 delete
u = nullptr释放 u 指向的对象,将 u 置为空
u.release()u 放弃对指针的控制权,返回指针,并将 u 置为空
u.reset()释放 u 指向的对象,将 u 置为空
u.reset(q)同上。但令 u 指向内置指针 q
u.reset(nullptr)同上。但将 u 置为空

由于一个 unique_ptr 拥有它所指向的对象,因此 unique_ptr 不支持普通的拷贝或赋值。但可以通过调用 releasereset 将指针所有权从一个(非 constunique_ptr 转移给另一个 unique_ptr

std::unique_ptr<int> p1(new int(42));
// 将所有权从 p1 转移给 p2,p1 被置为空
std::unique_ptr<int> p2(p1.release());
std::unique_ptr<int> p3(new int(1024));
// 将所有权从 p3 转移给 p2,p3 被置为空
p2.reset(p3.release());
  1. 传递 unique_ptr 参数和返回 unique_ptr:不能拷贝 unique_ptr 的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的 unique_ptr。最常见的例子是从函数返回一个 unique_ptr

    std::unique_ptr<int> clone(int p) {
        return std::unique_ptr<int>{new int(p)};
    }
    

    编译器明显知道要返回的对象将要被销毁。在此情况下,编译器执行一种特殊的拷贝。

    向后兼容:标准库的较早版本包含了一个名为 auto_ptr 的类,它具有 unique_ptr 的部分特性,但不是全部。特别是,我们不能再容器中保存 auto_ptr,也不能从函数中返回 auto_ptr。虽然 auto_ptr 仍是标准库的一部分,但编写程序时应该使用 unique_ptr

  2. unique_ptr 传递删除器:重载一个 unique_ptr 中的删除器会影响到 unique_ptr 类型以及如何构造(或 reset)该类型的对象。在创建或 reset 一个这种 unique_ptr 类型的对象时,必须提供一个指定的删除器:

    struct destination {
    };
    
    struct connection {
    };
    
    connection connect(destination *);
    
    void disconnect(connection *);
    
    void f(destination &d) {
        connection c = connect(&d);
        // 当 f 退出(即使是异常退出),connection 会被正确关闭
        std::unique_ptr<connection, decltype(disconnect) *> p(&c, disconnect);
    }
    

12.1.6 weak_ptr

weak_ptr 是一种不控制所指向对象生存期的智能指针,它指向一个 shared_ptr 管理的对象。将一个 weak_ptr 绑定到一个 shared_ptr 不会改变 shared_ptr 的引用计数。一旦最后一个指向对象的 shared_ptr 被销毁,对象就会被释放。即使有 weak_ptr 指向对象,对象也还是会释放。

weak_ptr说明
weak_ptr<T> wweak_ptr 可以指向类型为 T 的对象
weak_ptr<T> w(sp)shared_ptr sp 指向相同对象的 weak_ptrT 必须能转换为 sp 指向的类型
w = pp 可以是一个 shared_ptr 或一个 weak_ptr。赋值后 wp 共享对象
w.reset()w 置为空
w.use_count()w 共享对象的 shared_ptr 的数量
w.expired()return w.use_count() == 0;
w.lock()如果 expiredtrue,返回一个空 shared_ptr;否则返回一个指向 w 的对象 shared_ptr

由于是弱共享,创建 wp 不会改变 sp 的引用计数。wp 指向的对象可能被释放掉。由于对象可能不存在,因此我们不能使用 weak_ptr 直接访问对象,而必须调用 lock() 返回的 sp 进行对象的访问操作:

std::shared_ptr<int> sp = std::make_shared<int>(1);
std::weak_ptr<int> wp(sp);
if (std::shared_ptr<int> np = wp.lock()) {
    // 在 if 中,np 与 sp 共享对象
}

weak_ptrC++ 中是一种智能指针,用于解决 shared_ptr 可能导致的循环引用问题。以下是 weak_ptr 的一些典型应用场景:

  1. 解决循环引用: 当两个或多个 shared_ptr 相互指向对方时,即使它们都不再被其他指针所使用,它们指向的对象也不会被销毁,因为每个对象的引用计数都不会降到零。使用 weak_ptr 可以打破这种循环引用,使得对象可以在不再需要时被正确地销毁。
  2. 缓存机制: 在实现缓存时,可以使用 weak_ptr 来引用缓存中的对象。这样,当对象不再被其他 shared_ptr 所引用时,它可以从缓存中自动移除,而不会导致内存泄漏。
  3. 观察者模式: 在观察者模式中,一个对象(被观察者)需要通知多个观察者对象状态的改变。如果观察者对象通过 shared_ptr 来持有被观察者的引用,可能会造成循环引用。使用 weak_ptr 可以避免这个问题。
  4. 避免父对象在子对象之前被销毁: 在树形结构或者类似的层级结构中,子对象可能需要访问父对象,但又不希望阻止父对象的销毁。使用 weak_ptr 可以在子对象中安全地引用父对象。
  5. 实现懒加载: 当需要延迟对象的创建直到真正需要时,可以使用 weak_ptr。只有当需要使用对象时,才通过 weak_ptr 创建或提升为 shared_ptr
  6. 避免不必要的对象复制: 当你只想观察一个对象的存在而不需要保持其活跃状态时,使用 weak_ptr 可以避免不必要的复制和增加引用计数。

下面是一个简单的示例,展示了 weak_ptr 如何用于解决循环引用问题:

#include <iostream>
#include <memory>

class B;

class A {
public:
    std::shared_ptr<B> bptr;
    ~A() { std::cout << "A deleted" << std::endl; }
};

class B {
public:
    std::weak_ptr<A> aptr; // 使用 weak_ptr 避免循环引用
    ~B() { std::cout << "B deleted" << std::endl; }
};

int main() {
    std::shared_ptr<A> ap = std::make_shared<A>();
    std::shared_ptr<B> bp = std::make_shared<B>();

    ap->bptr = bp;
    bp->aptr = ap; // B 只持有 A 的弱引用,不会增加 A 的引用计数

    return 0;
}

12.2 动态数组

newdelete 运算符一次分配或释放一个对象,但某些应用需要一次为很多对象分配内存的功能,例如 vectorstring。为了支持这种需求,C++ 语言和标准库提供了两种一次分配一个对象数组的方法:

  • 定义了另一种 new 表达式语法,可以分配并初始化一个对象数组
  • 标准库包含一个名为 allocator 的类,允许我们将分配和初始化分离。使用 allocator 通常会提供更好的性能和更灵活的内存管理能力

大多数应用应该使用标准库容器而不是动态分配的数组。使用容器更为简单、更不容易出现内存管理错误并且可能有更好的性能。使用容器的类可以使用默认版本的拷贝、赋值和析构操作。分配动态数组的类则必须定义自己版本的操作,在拷贝、复制以及销毁对象时管理所关联的内存。

12.2.1 new 和数组

为了让 new 分配一个对象数组,我们要在类型名之后跟一对方括号,在其中指明要分配的对象的数目:

typedef int arrT[42];

int main() {
    // 分配动态数组
    int *ptrIntArr = new int[10];
    // 通过数组类型的类型别名分配动态数组
    int *ptrIntArr2 = new arrT;
}
  1. 分配一个数组会得到一个元素类型的指针:当用 new 分配一个数组时,我们并未得到一个数组类型的对象,而是得到一个数组元素类型的指针。即使我们使用类型别名定义了一个数组类型,new 也不会分配一个数组类型的对象。new 返回的是一个元素类型的指针。

    由于分配的内存并不是一个数组类型,因此不能对动态数组调用 beginend。出于相同的原因,也不能用范围 for 语句来处理所谓的动态数组中的元素。

  2. 初始化动态分配对象的数组:默认情况下,new 分配的对象,不管是单个分配的还是数组中的,都是默认初始化的。可以对数组中的元素进行值初始化,方法是在大小之后跟一对空括号:

    int *pia = new int[10];                         // 10 个未初始化的 int
    int *pia2 = new int[10]();                      // 10 个值初始化为 0 的 int
    std::string *psa = new std::string[10];         // 10 个空 string
    std::string *psa2 = new std::string[10]();      // 10 个空 string
    

    C++11 新标准下,我们还可以提供一个元素初始化器的花括号列表:

    • 提供元素不足时剩余元素执行值初始化
    • 如果初始化器数目大于元素数目,则 new 表达式执行失败,不会分配任何内存。抛出一个 bad_array_new_length 异常(定义在 new 头文件中)
    int *pia = new int[10]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    std::string *psa = new std::string[10]{"a", "spring", "bear"};
    

    虽然上述代码用空括号对数组元素进行值初始化,但不能在括号中给出初始化器,这意味着不能用 auto 分配数组。

  3. 动态分配一个空数组是合法的:虽然我们不能创建一个大小为 0 的静态数组对象,但当 n 等于 0 时,调用 new[0] 是合法的:

    char arr[0];                // 错误:不能定义长度为 0 的数组
    char *cp = new char[0];     // 正确:但 cp 不能解引用
    

    当我们用 new 分配一个大小为 0 的数组时,new 返回一个合法的非空指针。此指针保证与 new 返回的其他任何指针都不相同。对于零长度的数组来说,此指针就像尾后指针一样,我们可以像使用尾后迭代器一样使用这个指针。可以向此指针加上或减去 0,也可以从此指针减去自身从而得到 0

  4. 释放动态数组:为了释放动态数组,我们使用一种特殊形式的 delete,在指针前加上一个空方括号对:

    int *p = new int(1024);
    int *pa = new int[10];
    
    delete p;       // p 必须指向一个动态分配的对象或为空
    delete []pa;    // pa 必须指向一个动态分配的数组或为空
    

    第二条语句销毁 pa 指向的数组中的元素,并释放对应的内存。数组中的元素按逆序销毁。

    当我们释放一个指向数组的指针时,空方括号对是必需的:它指示编译器此指针指向一个对象数组的第一个元素。如果我们在 delete 一个指向数组的指针时忽略了方括号(或者在 delete 一个指向单一对象的指针时使用了方括号),其行为是未定义的。在使用一个类型别名来定义一个数组类型时,new 表达式中可以不用 [],但在释放一个数组指针时必须使用方括号:

    typedef int arrT[42];
    int *p = new arrT;
    delete []p;
    
  5. 智能指针和动态数组:标准库提供了一个可以管理 new 分配的数组的 unique_ptr 版本。为了用一个 unique_ptr 管理动态数组,我们必须在对象类型后面跟一对方括号:

    // up 指向一个包含 10 个未初始化 int 的数组
    std::unique_ptr<int[]> up(new int[10]);
    // 自动用 delete [] 销毁其指针
    up.release();
    

    当一个 unique_ptr 指向一个数组时,我们不能使用点和箭头成员运算符。毕竟 unique_ptr 指向的是一个数组而不是单个对象,因此这些运算符是毫无意义的。另一方面,我们可以使用下标运算符来访问数组中的元素:

    指向数组的 unique_ptr说明
    unique_ptr<T[]> uu 可以指向一个动态分配的数组,数组元素类型为 T
    unique_ptr<T[]> u(p)u 指向内置指针 p 所指向的动态分配的数组。p 必须能转换为类型 T*
    u[i]返回 u 拥有的数组中位置 i 处的对象

    unique_ptr 不同,shared_ptr 不直接管理动态数组。如果希望使用 shared_ptr 管理一个动态数组,必须提供自己定义的删除器:

    std::shared_ptr<int> sp(new int(10), [](int *p) {
        delete []p;
    });
    // 使用提供的 lambda 释放数组
    sp.reset();
    

    shared_ptr 不直接支持动态数组管理这一特性影响我们如何访问数组中的元素。shared_ptr 未定义下标运算符,而且智能指针类型不支持指针算术运算。因此,为了访问数组中的元素,必须使用 get 获取一个内置指针,然后用它来访问数组元素:

    for (int i = 0; i < 10; i++) {
        *(sp.get() + i) = i;
    }
    

12.2.2 allocator 类

new 存在一些灵活性上的局限,其中一方面表现在它将内存分配和对象构造组合在了一起。类似的,delete 将对象析构和内存释放组合在了一起。我们分配单个对象时,通常希望将内存分配和对象初始化组合在一起。因为在这种情况下,我们几乎肯定知道对象应有什么值。

当分配一大块内存时,我们通常计划在这块内存上按需构造对象。在此情况下,我们希望将内存分配和对象构造分离。这意味着我们可以分配大块内存,但只在真正需要时才真正执行对象创建操作。

一般情况下,将内存分配和对象构造组合在一起可能会导致不必要的浪费。例如如下代码中 len 不一定等于 n,并且,对于那些确实使用到的对象,我们也在初始化之后立即赋予了它们新值。每个使用到的元素都被赋值了两次:第一次是在默认初始化时,随后是在赋值时:

int n = 10;
std::string *const p = new std::string[n];
std::string s;
std::string *q = p;
while (std::cin >> s && q != p + n) {
    *q++ = s;
}
const std::size_t len = q - p;

delete[] p;

更重要的是,那些没有默认构造函数的类就不能动态分配数组了。

  1. allocator 类:标准库 allocator 类定义在头文件 memory 中,它帮助我们将内存分配和对象构造分离开来。它提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。当一个 allocator 对象分配内存时,它会根据给定的对象类型来确定恰当的内存大小和对齐位置:

    // 可以分配 string 的 allocator 对象
    std::allocator<std::string> stringAllocator;
    // 分配 10 个未初始化的 string
    auto const p = stringAllocator.allocate(10);
    
    allocator 类及其算法说明
    allocator<T> a定义了一个名为 aallocator 对象,它可以为类型为 T 的对象分配内存
    a.allocate(n)分配一段原始的、未构造的内存,保存 n 个类型为 T 的对象
    a.deallocate(p, n)释放从 T* 指针 p 中地址开始的内存,这块内存保存了 n 个类型为 T 的对象;p 必须是一个先前由 allocate 返回的指针,且 n 必须是 p 创建时所要求的大小。在调用 deallocate 之前,必须对每个在这块内存中创建的对象调用 destroy
    a.construct(p, args)p 必须是一个类型为 T* 的指针,指向一块原始内存:args 被传递给类型为 T 的构造函数,用来在 p 指向的内存中构造一个对象
    a.destroy(p)pT* 类型的指针,此算法对 p 指向的对象执行析构函数
  2. allocator 分配未构造的内存:在 C++11 新标准中,construct 成员函数接受一个指针和零个或多个额外参数,在给定位置构造一个元素。额外参数用来初始化构造的对象:

    std::allocator<std::string> alloc;
    auto p = alloc.allocate(10);
    auto q = p;
    alloc.construct(q++);               // *q 为空字符串
    alloc.construct(q++, 3, 'c');       // *q = "ccc"
    alloc.construct(q++, "hi");         // *q = "hi"
    

    为了使用 allocate 返回的内存,我们必须用 construct 构造对象。使用未构造的内存,其行为是未定义的。

    当我们用完对象后,必须对每个构造的元素调用 destroy 来销毁它们。函数 destroy 接受一个指针,对指向的对象执行析构函数:

    while (q != p) {
        alloc.destroy(--q);
    }
    

    一旦元素被销毁后,就可以重新使用这部分内存来保存其他 string,也可以将其归还给系统。释放内存通过调用 deallocate 来完成:

    alloc.deallocate(p, 10);
    
  3. 拷贝和填充未初始化内存的算法:标准库还为 allocator 类定义了两个伴随算法,可以在未初始化内存中创建对象,它们都定义在头文件 memory 中。两个伴随算法在给定目的位置创建元素,而不是由系统分配内存给它们:

    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 必须指向足够大的未构造的原始内存,足够容纳给定数量的对象
    std::vector<int> vi = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    std::allocator<int> alloc;
    // 分配比 vi 中元素所占用空间大一倍的动态内存
    auto p = alloc.allocate(vi.size() * 2);
    // 通过拷贝 vi 中的元素来构造从 p 开始的元素
    auto q = std::uninitialized_copy(vi.begin(), vi.end(), p);
    // 将剩余元素初始化填充为 42
    std::uninitialized_fill_n(q, vi.size(), 42);
    

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

我们的程序允许用户在一个给定文件中查询单词。查询结果是单词在文件中出现的次数及其所在行的列表。如果一个单词在一行中出现多次,此行值列出一次。行会按照升序输出。

element occurs 112 times
	(line 36) A set element contains only a key;
	(line 158) operator creates a new element
	(line 160) Regardless of whether the element
	(line 168) When we fetch an element from a map, we
	(line 214) If the element is not found, find returns
#include <iostream>
#include <map>
#include <vector>
#include <memory>
#include <set>
#include <sstream>
#include <fstream>

using namespace std;
using line_no = vector<string>::size_type;

class QueryResult;

class TextQuery {
public:
    TextQuery(ifstream &);

    QueryResult query(const string &) const;

private:
    // 输入文件
    shared_ptr<vector<string> > file;
    // 每个单词到它所在行号的集合的映射
    map<string, shared_ptr<set<line_no> > > word_map;
};

// 读取输入文件并建立单词到行号的映射
TextQuery::TextQuery(ifstream &is) : file(new vector<string>) {
    string text;
    while (getline(is, text)) {
        file->push_back(text);
        size_t n = file->size() - 1;
        // 将文本分解为单词
        istringstream line(text);
        string word;
        while (line >> word) {
            // 如果单词不在 word_map 中,以之为下标在 word_map 中添加一项
            auto &lines = word_map[word];
            if (!lines) {
                lines.reset(new set<line_no>);
            }
            lines->insert(n);
        }
    }
}

class QueryResult {
    friend ostream &print(ostream &, const QueryResult &);

public:
    QueryResult(string s, shared_ptr<set<line_no> > p, shared_ptr<vector<string> > f) : sought(s), lines(p), file(f) {
    }

private:
    // 查询单词
    string sought;
    // 出现的行号
    shared_ptr<set<line_no> > lines;
    // 输入文件
    shared_ptr<vector<string> > file;
};

QueryResult TextQuery::query(const string &sought) const {
    static shared_ptr<set<line_no> > nodata(new set<line_no>());
    auto loc = word_map.find(sought);
    if (loc == word_map.end()) {
        return QueryResult(sought, nodata, file);
    }

    return QueryResult(sought, loc->second, file);
}

ostream &print(ostream &os, const QueryResult &q) {
    os << q.sought << " occurs " << q.lines->size() << " "
            << (q.lines->size() > 1 ? "times" : "time") << endl;
    for (auto num: *q.lines) {
        os << "\t(line " << num + 1 << ") "
                << *(q.file->begin() + num) << endl;
    }
    return os;
}

int main() {
    ifstream ifs("/text.txt");
    TextQuery q(ifs);
    auto query_result = q.query("hello");
    print(std::cout, query_result);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

春天熊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值