《C++ Primer》笔记 第12章 动态内存

shared_ptr和unique_ptr都支持的操作 解释
shared_ptr sp或unique_ptr up 空智能指针,可以指向类型为T的对象
p 将p用作一个条件判断,若p指向一个对象,则为true
*p 解引用p,获得它指向的对象
p->mem 等价于(*p).mem
p.get() 返回p中保存的指针。要小心使用,若智能指针释放了其对象,返回的指针所指向的对象也就消失了
swap(p, q)或p.swap(q) 交换p和q中的指针
shared_ptr独有的操作 解释
make_shared(args) 返回一个shared_ptr,指向一个动态分配的类型为T的对象。使用args初始化此对象
shared_ptrp(q) p是shared_ptr q的拷贝;此操作会递增q中的计数器。q中的指针必须能转换为T*
p = q p和q都是shared_ptr,所保存的指针必须能相互转换。此操作会递减p的引用计数,递增q的引用计数;若p的引用计数变为0,则将其管理的原内存释放
p.unique() 若p.use_count()为1,返回true;否则返回false
p.use_count() 返回与p共享对象的智能指针数量;可能很慢,主要用于调试
  1. shared_ptr允许多个指针指向同一个对象;unique_ptr则“独占”所指向的对象。标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在memory头文件中。

  2. make_shared的标准库函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。与智能指针一样,make_shared也定义在头文件memory中。

    // 指向一个值为42的int的shared_ptr
    shared_ptr<int> p3 = make_shared<int>(42);
    // p4指向一个值为“9999999999”的string
    shared_ptr<string> p4 = make_shared<string>(10, '9');
    // p5指向一个值初始化的int,即,值为0
    shared_ptr<int> p5 = make_shared<int>();
    

    我们通常用auto定义一个对象来保存make_shared的结果,这种方式较为简单。

    // p6指向一个动态分配的空vector<string>
    auto p6 = make_shared<vector<string>>();
    
  3. shared_ptr自动销毁所管理的对象,shared_ptr还会自动释放相关联的内存。如果你将shared_ptr存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用erase删除不再需要的那些元素。

  4. 程序使用动态内存出于以下三种原因之一:

    • 程序不知道自己需要使用多少对象
    • 程序不知道所需对象的准确类型
    • 程序需要在多个对象间共享数据
  5. 示例:定义StrBlob类

    /* 
    *  功能:我们希望Blob对象的不同拷贝之间共享相同的元素。
    *  即,当我们拷贝一个Blob时,原Blob对象及其拷贝应该引用相同的底层元素。
    */
    
    #include <iostream>
    #include <vector>
    #include <string>
    #include <memory>
    
    using namespace std;
    
    class StrBlob
    {
    public:
        typedef vector<string>::size_type size_type;
        // 构造函数
        StrBlob() : data(make_shared<vector<string>>()) {}
        StrBlob(initializer_list<string> il) : data(make_shared<vector<string>>(il)) {}
        size_type size() const { return data->size(); }
        bool empty() const { return data->empty(); }
        // 添加和删除元素
        void push_back(const string &t) { data->push_back(t); }
        void pop_back()
        {
            check(0, "pop_back on empty StrBlob");
            data->pop_back();
        }
        // 元素访问:非const和const版本
        string &front()
        {
            // 如果vector为空,check会抛出一个异常
            check(0, "front on empty StrBlob");
            return data->front();
        }
        string &back()
        {
            check(0, "back on empty StrBlob");
            return data->back();
        }
        const string &front() const
        {
            check(0, "const front on empty StrBlob");
            return data->front();
        }
        const string &back() const
        {
            check(0, "const back on empty StrBlob");
            return data->back();
        }
    
    private:
        shared_ptr<vector<string>> data;
        // 如果data[i]不合法,抛出一个异常
        void check(size_type i, const string msg) const
        {
            if (i >= data->size()) 
                throw out_of_range(msg);
        }
    };
    
  6. 默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化。

  7. 我们可以使用传统的构造方式(使用圆括号),也可以使用列表初始化(使用花括号):

    int *pi = new int(1024); // pi指向的对象的值为1024
    string *ps = new string(10, '9'); // *ps为“9999999999”
    // vector有10个元素,值依次从0到9
    
  8. 也可以对动态分配的对象进行值初始化,只需在类型名之后跟一对空括号即可:

    string *ps1 = new string; // 默认初始化为空string
    string *ps = new string(); // 值初始化为空string
    int *pi1 = new int; // 默认初始化;*pi1的值未定义
    int *pi2 = new int(); // 值初始化为0;*pi2为0
    
  9. 如果我们提供了一个括号包围的初始化器,就可以使用auto从此初始化器来推断我们想要分配的对象的类型。但是,由于编译器要用初始化器的类型来推断要分配的类型,只有当括号中仅有单一初始化器时才可以使用auto:

    auto p1 = new auto(obj); // p指向一个与obj类型相同的对象
                             // 该对象用obj进行初始化
    auto p2 = new auto{a, b, c}; // 错误:括号中只能有单个初始化器
    
  10. 用new分配const对象是合法的:

    // 分配并初始化一个const int
    const int *pci = new const int(1024);
    // 分配并默认初始化一个const的空string
     const string *pcs = new const string;
    
  11. 类似其他任何const对象,一个动态分配的const对象必须进行初始化。对于一个定义了默认构造函数的类类型,其const动态对象可以隐式初始化,而其它类型的对象就必须显式初始化。由于分配的对象是const的,new返回的指针是一个指向const的指针。

  12. 默认情况下,如果new不能分配所要求的内存空间,它会抛出一个类型为bad_alloc的异常。

  13. 定位new表达式允许我们向new传递额外的参数。在此例中,我们传递给它一个由标准库定义的名为nothrow的对象。如果将nothrow传递给new,我们的意图是告诉它不能抛出异常。如果这种形式的new不能分配所需内存,它会返回一个空指针。bad_allocnothrow都定义在头文件new中。

    // 如果分配失败,new返回一个空指针
    int *p1 = new int; // 如果分配失败,new抛出std::bad_alloc
    int *p2 = new (nothrow) int; // 如果分配失败,new返回一个空指针
    
  14. 传递给delete的指针必须指向动态分配的内存,或者是一个空指针。

  15. 虽然一个const对象的值不能被改变,但它本身是可以被销毁的:

    const int *pci = new const int(1024);
    delete pci; // 正确:释放一个const对象
    
  16. 由内置指针(而不是智能指针)管理的动态内存在被显式释放前一直都会存在。

  17. 空悬指针:指向一块曾经保存数据对象但现在已经无效的内存的指针。

  18. 避免空悬指针问题:在指针即将要离开其作用域之前释放掉它所关联的内存。如果我们需要保留指针,可以在delete之后将nullptr赋予指针,这样就清楚地指出指针不指向任何对象。这只是提供了有限的保护

  19. 可以用new返回的指针来初始化智能指针:

    shared_ptr<double> p1; // shared_ptr可以指向一个double
    shared_ptr<int> p2(new int(42)); // p2指向一个值为42的int
    
  20. 接受指针参数的智能指针构造函数是explicit的。因此,我们不能将一个内置指针隐式转化为一个智能指针,必须使用直接初始化形式来初始化一个智能指针:

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

    一个返回shared_ptr的函数不能在其返回语句中隐式转换一个普通指针:

    shared_ptr<int> clone(int p)
    {
    	return new int(p); // 错误:隐式转换为shared_ptr<int>
    }
    

    我们必须将shared_ptr显式绑定到一个想要返回的指针上:

    shared_ptr<int> clone(int p)
    {
    	// 正确:显式地用int*创建shared_ptr<int>
    	return shared_ptr<int>(new int(p));
    }
    
  21. 默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象。我们可以将智能指针绑定到一个指向其他类型的资源的指针上,但是为了这样做,必须提供自己的操作来替代delete。

定义和改变shared_ptr的其他方法 解释
shared_ptr p(q) p管理内置指针q所指向的对象;q必须指向new分配的内存,且能够转换为T*类型
shared_ptr p(u) p从unique_ptr u那里接管了对象的所有权;将u置为空
shared_ptr p(q, d) p接管了内置指针q所指向的对象的所有权。q必须能转换为T*类型。p将使用可调用对象d来代替delete
shared_ptr p(p2, d) p是shared_ptr p2的拷贝,唯一的区别是p将用可调用对象d来代替delete
p.reset()或p.reset(q)或p.reset(q, d) 若p是唯一指向其对象的shared_ptrreset会释放此对象。若传递了可选的参数内置指针q,会令p指向q,否则会将p置为空。若还传递了参数d,将会调用d而不是delete来释放q
  1. 不要混合使用普通指针和智能指针。shared_ptr可以协调对象的析构,但这仅限于其自身的拷贝(也是shared_ptr)之间。

    // 在函数被调用时ptr被创建并初始化
    void process(shared_ptr<int> ptr)
    {
        // 使用ptr
    } // ptr离开作用域,被销毁
    
    int main()
    {
        shared_ptr<int> p(new int(42)); // 引用计数为1
        process(p); // 拷贝p会递增它的引用计数;在process中引用计数值为2
        int i = *p; // 正确:引用计数值为1
        
        int *x(new int(1024)); // 危险:x是一个普通指针,不是一个智能指针
        process(x); // 错误:不能将int*转换为一个shared_ptr<int>
        process(shared_ptr<int>(x)); // 合法的,但内存会被释放!
        int j = *x; // 未定义的:x是一个空悬指针!
        
        return 0;
    }
    

    使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时会被销毁。

  2. get用来将指针的访问权限传递给代码,你只有在确定代码不会delete指针的情况下,才能使用get。特别是,永远不要用get初始化另一个智能指针或者为另一个智能指针赋值。

  3. 函数退出有两种可能,正常处理结束或者发生了异常,无论哪种情况,局部对象都会被销毁。所以,如果使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放:

    void f()
    {
    	shared_ptr<int> sp(new int(42)); // 分配一个新对象
    	// 这段代码抛出一个异常,且在f中未被捕获
    } // 在函数结束时shared_ptr自动释放内存
    
  4. 如果使用内置指针管理内存,且在new之后在对应的delete之前发生了异常,则内存不会被释放。

  5. 智能指针可以提供对动态分配的内存安全而又方便的管理,但这建立在正确使用的前提下。为了正确使用智能指针,我们必须坚持一些基本规范:

    • 不使用相同的内置指针值初始化(或reset)多个智能指针。
    • 不delete get()返回的指针。
    • 不使用get()初始化或reset另一个智能指针。
    • 如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了。
    • 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。
  6. 一个unique_ptr“拥有”它所指向的对象。与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。

  7. 与shared_ptr不同,没有类似make_shared的标准库函数返回一个unique_ptr。当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上。类似shared_ptr,初始化unique_ptr必须采用直接初始化形式:

    unique_ptr<double> p1; // 可以指向一个double的unique_ptr
    unique_ptr<int> p2(new int(42)); // p2指向一个值为42的int
    

    由于一个unique_ptr拥有它所指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作:

    unique_ptr<string> p1(new string("Stogosaurus"));
    unique_ptr<string> p2(p1); // 错误:unique_ptr不支持拷贝
    unique_ptr<string> p3;
    p3 = p2; // 错误:unique_ptr不支持赋值
    
unique_ptr操作 解释
unique_ptr u1或unique_ptr<T, D> u2 空unique_ptr,可以指向类型为T的对象。u1会使用delete来释放它的指针;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.reset(q)或u.reset(nullptr) 释放u指向的对象。如果提供了内置指针q,令u指向这个对象;否则将u置为空
  1. 虽然我们不能拷贝或赋值unique_ptr,但可以通过调用release或reset将指针的所有权从一个(非const)unique_ptr转移给另一个unique:

    // 将所有权从p1(指向string Stogosaurus)转移给p2
    unique_ptr<string> p2(p1.release()); // release将p1置为空
    unique_ptr<string> p3(new string("Trex"));
    // 将所有权从p3转移给p2
    p2.reset(p3.release()); // reset释放了p2原来指向的内存
    
  2. 调用release会切断unique_ptr和它原来管理的对象间的联系。release返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。但是,如果我们不用另一个智能指针来保存release返回的指针,我们的程序就要负责资源的释放:

    p2.release(); // 错误:p2不会释放内存,而且我们丢失了指针
    auto p = p2.release(); // 正确,但我们必须记得delete(p)
    
  3. 不能拷贝unique_ptr的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr。最常见的例子是从函数返回一个unique_ptr:

    unique_ptr<int> clone(int p)
    {
    	// 正确:从int*创建一个unique_ptr<int>
    	return unique_ptr<int>(new int(p));
    }
    

    还可以返回一个局部对象的拷贝

    unique_ptr<int> clone(int p)
    {
    	unique_ptr<int> ret(new int(p));
    	// ...
    	return ret;
    }
    
  4. 重载一个unique_ptr中的删除器会影响到unique_ptr类型以及如何构造(或reset)该类型的对象。与重载关联容器的比较操作类似,我们必须在尖括号中unique_ptr指向类型之后提供删除器类型。在创建或reset一个这种unique_ptr类型的对象时,必须提供一个指定类型的可调用对象(删除器)

    // p指向一个类型为objT的对象,并使用一个类型为delT的对象释放objT对象
    // 它会调用一个名为fcn的delT类型对象
    unique_ptr<objT, delT> p(new objT, fcn);
    
    void f(destination &d /* 其他需要的参数 */)
    {
    	connection c = connect(&d); // 打开连接
    	// 当p被销毁时,连接将会关闭
    	unique_ptr<connection, decltype(end_connection)*> p(&c, end_connection);
    	// 使用连接
    	// 当f退出时(即使是由于异常而退出),connection会被正确关闭
    }
    
  5. weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。

weak_ptr 解释
weak_ptr w 空weak_ptr可以指向类型为T的对象
weak_ptr w(sp) 与shared_ptr sp指向相同对象的weak_ptr。T必须能转换为sp指向的类型
w = p p可以是一个shared_ptr或一个weak_ptr。赋值后w与p共享对象
w.reset() 将w置为空
w.use_count() 与w共享对象的shared_ptr的数量
w.expired() 若w.use_count()为0,返回true,否则返回false
w.lock() 如果expired为true,返回一个空shared_ptr;否则返回一个指向w的对象的shared_ptr
  1. 由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock。

    auto p = make_shared<int>(42);
    weak_ptr<int> wp(p); // wp弱共享p;p的引用计数未改变
    
    if (shared_ptr<int> np = wp.lock()) // 如果np不为空则条件成立
    {
    	// 在if中,np与p共享对象
    	// ...
    }
    

    只有当lock调用返回true时我们才会进入if语句体。在if中,使用np访问共享对象是安全的。

  2. 示例:StrBlobPtr

    /*
    *  功能:为StrBlob新增一个伴随指针类
    */
    #include <iostream>
    #include <vector>
    #include <string>
    #include <initializer_list>
    #include <memory>
    #include <stdexcept>
    using namespace std;
    
    // 提前声明,StrBlob中的友类声明所需
    class StrBlobPtr;
    
    class StrBlob
    {
        friend class StrBlobPtr;
    public:
        typedef vector<string>::size_type size_type;
        StrBlob();
        StrBlob(initializer_list<string> il);
        StrBlob(vector<string> *p);
        size_type size() const { return data->size(); }
        bool empty() const { return data->empty(); }
        // 添加和删除元素
        void push_back(const string &t) {data->push_back(t);}
        void pop_back();
        // 元素访问
        string& front();
        const string& front() const;
        string& back();
        const string& back() const ;
    
        // 提供给StrBlobPtr的接口
        StrBlobPtr begin();  // 定义StrBlobPtr后才能定义这两个函数
        StrBlobPtr end();
        // const版本
        StrBlobPtr begin() const;
        StrBlobPtr end() const;
    private:
        shared_ptr<std::vector<std::string>> data;
        // 如果data[i]不合法,抛出一个异常
        void check(size_type i, const std::string &msg) const;
    };
    
    inline StrBlob::StrBlob(): data(make_shared<vector<string>>()) { }
    inline StrBlob::StrBlob(initializer_list<string> il) : data(make_shared<vector<string>>(il)) { }
    inline StrBlob::StrBlob(vector<string> *p): data(p) { }
    
    inline void StrBlob::check(size_type i, const string &msg) const
    {
        if (i >= data->size())
        {
            throw out_of_range(msg);
        }
    }
    
    inline string& StrBlob::front()
    {
        // 如果vector为空,check会抛出一个异常
        check(0, "front on empty StrBlob");
        return data->front();
    }
    
    // const版本front
    inline const string& StrBlob::front() const
    {
        check(0, "front on empty StrBlob");
        return data->front();
    }
    
    inline string& StrBlob::back()
    {
        check(0, "back on empty StrBlob");
        return data->back();
    }
    
    // const版本back
    inline const string& StrBlob::back() const
    {
        check(0, "back on empty StrBlob");
        return data->back();
    }
    
    inline void StrBlob::pop_back()
    {
        check(0, "pop_back on empty StrBlob");
        data->pop_back();
    }
    
    // 当试图访问一个不存在的元素时,StrBlobPtr抛出一个异常
    class StrBlobPtr
    {
        friend bool eq(const StrBlobPtr&, const StrBlobPtr&);
    public:
        StrBlobPtr(): curr(0) { }
        StrBlobPtr(StrBlob &a, size_t sz = 0): wptr(a.data), curr(sz) { }
        StrBlobPtr(const StrBlob &a, size_t sz = 0): wptr(a.data), curr(sz) { }
    
        string& deref() const;
        string& deref(int off) const;
        StrBlobPtr& incr();     // 前缀递增
        StrBlobPtr& decr();     // 前缀递减
    private:
        // 若检查成功,check返回一个指向vector的shared_ptr
        shared_ptr<vector<string>> check(size_t, const string&) const;
    
        // 保存一个weak_ptr,意味着底层vector可能会被销毁
        weak_ptr<vector<string>> wptr;
        size_t curr;    // 在数组中的当前位置
    };
    
    inline shared_ptr<vector<string>> StrBlobPtr::check(size_t i, const string &msg) const
    {
        auto ret = wptr.lock(); // vector还存在吗?
        if (!ret)
        {
           throw runtime_error("unbound StrBlobPtr");
        }
        if (i >= ret->size())
        {
           throw out_of_range(msg);
        }
        return ret; // 否则,返回指向vector的shared_ptr
    }
    
    inline string& StrBlobPtr::deref() const
    {
        auto p = check(curr, "dereference past end");
        return (*p)[curr]; // (*p)是对象所指向的vector
    }
    
    inline string& StrBlobPtr::deref(int off) const
    {
        auto p = check(curr + off, "dereference past end");
        return (*p)[curr + off]; // (*p)是对象所指向的vector
    }
    
    // 前缀递增:返回递增后的对象的引用
    inline StrBlobPtr& StrBlobPtr::incr()
    {
        // 如果curr已经指向容器的尾后位置,就不能递增它
        check(curr, "increment past end of StrBlobPtr");
        ++curr; // 推进当前位置
        return *this;
    }
    
    // 前缀递减:返回递减后的对象的引用
    inline StrBlobPtr& StrBlobPtr::decr()
    {
        // 如果curr已经为0,递减它就会产生一个非法下标
        --curr;     // 递减当前位置
        check(-1, "decrement past begin of StrBlobPtr");
        return *this;
    }
    
    // StrBlob的begin和end成员的定义
    inline StrBlobPtr StrBlob::begin()
    {
        return StrBlobPtr(*this);
    }
    
    inline StrBlobPtr StrBlob::end()
    {
      	auto ret = StrBlobPtr(*this, data->size());
        return ret;
    }
    
    // const版本
    inline StrBlobPtr StrBlob::begin() const
    {
    	  return StrBlobPtr(*this);
    }
    
    inline StrBlobPtr StrBlob::end() const
    {
        auto ret = StrBlobPtr(*this, data->size());
        return ret;
    }
    
    // StrBlobPtr的比较操作
    inline bool eq(const StrBlobPtr &lhs, const StrBlobPtr &rhs)
    {
        auto l = lhs.wptr.lock(), r = rhs.wptr.lock();
        // 若底层的vector是同一个
        if (l == r)
        {
        	  // 则两个指针都是空,或者指向相同元素时,它们相等
        	  return (!r || lhs.curr == rhs.curr);
        }
        else
        {
        	  return false; // 若指向不同vector,则不可能相等
        }
    }
    
    inline bool neq(const StrBlobPtr &lhs, const StrBlobPtr &rhs)
    {
    	  return !eq(lhs, rhs);
    }
    
  3. 为了让new分配一个对象数组,我们要在类型名之后跟一对方括号,在其中指明要分配的对象的数目。在下例中,new分配要求数量的对象并(假定分配成功后)返回指向第一个对象的指针:

    // 调用get_size确定分配多少个int
    int *pia = new int[get_size()]; // pia指向第一个int
    

    方括号中的大小必须是整型,但不必是常量。

  4. 分配一个数组会得到一个元素类型的指针,而非一个数组类型的对象。由于分配的内存并不是一个数组类型,因此不能对动态数组调用begin或end。也不能用范围for语句来处理(所谓的)动态数组中的元素。要记住我们所说的动态数组并不是数组类型,这是很重要的。

  5. 默认情况下,new分配的对象,不管是单个分配的还是数组中的,都是默认初始化的。可以对数组中的元素进行值初始化,方法是在大小之后跟一对空括号。我们还可以提供一个元素初始化器的花括号列表。

    int *pia = new int[10]; // 10个未初始化的int
    int *pia2 = new int[10](); // 10个值初始化为0的int
    string *psa = new string[10]; // 10个空string
    string *psa2 = new string[10](); // 10个空string
    
    // 10个int分别用列表中对应的初始化器初始化
    int *pia3 = new int[10]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    // 10个string,前4个用给定的初始化器初始化,剩余的进行值初始化
    string *ps3 = new string[10]{"a", "an", "the", string(3,'x')};
    
  6. 如果初始化器数目小于元素数目,剩余元素将进行值初始化。如果初始化器数目大于元素数目,则new表达式失败,不会分配任何内存。new会抛出一个类型为bad_array_new_length的异常。类似bad_alloc,此类型定义在头文件new中。

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

  8. 动态分配一个空数组是合法的。当我们用new分配一个大小为0的数组时,new返回一个合法的非空指针。此指针保证与new返回的其他任何指针都不相同。我们可以像使用尾后迭代器一样使用这个指针。但此指针不能解引用——毕竟它不指向任何元素。

    char arr[0]; // 错误:不能定义长度为0的数组
    char *cp = new char[0]; // 正确:但cp不能解引用
    
  9. 为了释放动态数组,我们使用一种特殊形式的delete——在指针前加上一个空方括号对:

    delete p; // p必须指向一个动态分配的对象或为空
    delete [] pa; // pa必须指向一个动态分配的数组或为空
    
  10. 数组中的元素按逆序销毁,即,最后一个元素首先被销毁,然后是倒数第二个,以此类推。

  11. 当我们使用一个类型别名来定义一个数组类型时,在new表达式中不使用[]。即使是这样,在释放一个数组指针时也必须使用方括号。

    typedef int arrT[42]; // arrT是42个int的数组的类型别名
    int *p = new arrT; // 分配一个42个int的数组;p指向第一个元素
    delete [] p; // 方括号是必需的,因为我们当初分配的是一个数组
    
  12. 如果我们在delete一个数组指针时忘记了方括号,或者在delete一个单一对象的指针时使用了方括号,编译器很可能不会给出警告。我们的程序可能在执行过程中在没有任何警告的情况下行为异常。

  13. 标准库提供了一个可以管理new分配的数组的unique_ptr版本。为了用一个unique_ptr管理动态数组,我们必须在对象类型后面跟一对空方括号:

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

    类型说明符中的方括号(<int[]>)指出up指向一个int数组而不是一个int。由于up指向一个数组,当up销毁它管理的指针时,会自动使用delete[]。

指向数组的unique_ptr 解释
—— 指向数组的unique_ptr不支持成员访问运算符(点和箭头运算符)
—— 其他unique_ptr操作不变
unique_ptr<T[]> u u可以指向一个动态分配的数组,数组元素类型为T
unique_ptr<T[]> u(p) u指向内置指针p所指向的动态分配的数组。p必须能转换为类型T*
u[i] 返回u拥有的数组中位值i处的对象。u必须指向一个数组
  1. unique_ptr不同,shared_ptr不直接支持管理动态数组。如果希望使用shared_ptr管理一个动态数组,必须提供自己定义的删除器:

    // 为了使用shared_ptr,必须提供一个删除器
    shared_ptr<int> sp(new int[10], [](int *p){ delete[] p; });
    sp.reset(); // 使用我们提供的lambda释放数组,它使用delete[]
    

    如果未提供删除器,这段代码将是未定义的。默认情况下,shared_ptr使用delete销毁它指向的对象。

  2. shared_ptr未定义下标运算符,而且智能指针类型不支持指针算术运算。因此,为了访问数组中的元素,必须用get获取一个内置指针,然后用它来访问数组元素。

    // shared_ptr未定义下标运算符,并且不支持指针的算术运算
    for (size_t i = 0; i != 10; ++i)
    	*(sp.get() + i) = i; // 使用get获取一个内置指针
    
  3. new有一些灵活性上的局限,其中一方面表现在它将内存分配和对象构造组合在了一起。类似的,delete将对象析构和内存释放组合在了一起。更重要的是,那些没有默认构造函数的类就不能动态分配数组了。

  4. 标准库allocator类定义在头文件memory中,它帮助我们将内存分配和对象构造分离开来。

标准库allocator类及其算法 解释
allocator a 定义了一个名为a的allocator对象,它可以为类型为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) p为T*类型的指针,此算法对p指向的对象执行析构函数
allocator<string> alloc; // 可以分配string的allocator对象
auto const p = alloc.allocate(n); // 分配n个未初始化的string

auto q = p; // q指向最后构造的元素之后的位置
alloc.construct(q++); // *q为空字符串
alloc.construct(q++, 10, 'c'); // *q为cccccccccc
alloc.construct(q++, "hi"); // *q为hi

cout << *p << endl; // 正确:使用string的输出运算符
cout << *q << endl; // 灾难:q指向未构造的内存!

while (q != p)
    alloc.destroy(--q); // 释放我们真正构造的string

alloc.deallocate(p, n); // 释放内存
  1. 为了使用allocate返回的内存,我们必须用construct构造对象。使用未构造的内存,其行为是未定义的。
  2. 我们只能对真正构造了的元素进行destroy操作。
allocator算法 解释
—— 这些函数在给定目的位置创建元素,而不是由系统分配内存给它们
uninitialized_copy(b, e, b2) 从迭代器b和e指出的输入范围中拷贝元素到迭代器b2指定的未构造的原始内存中。b2指向的内存必须足够大,能容纳输入序列中元素的拷贝
uninitialized_copy_n(b, n, b2) 从迭代器b指向的元素开始,拷贝n个元素到b2开始的内存中
uninitialized_fill(b, e, t) 在迭代器b和e指定的原始内存范围中创建对象,对象的值均为t的拷贝
uninitialized_fill_n(b, n, t) 从迭代器b指向的内存地址开始创建n个对象。b必须指向足够大的未构造的原始内存,能够容纳给定数量的对象
  1. 类似copyuninitialized_copy返回(递增后的)目的位置迭代器。因此,一次uninitialized_copy调用会返回一个指针,指向最后一个构造的元素之后的位置。

    // 分配比vi中元素所占用空间大一倍的动态内存
    auto p = alloc.allocate(vi.size() * 2);
    // 通过拷贝vi中的元素来构造从p开始的元素
    auto q = uninitialized_copy(vi.begin(), vi.end(), p);
    // 将剩余元素初始化为42
    uninitialized_fill_n(q, vi.size(), 42);
    
  2. 如果两个类概念上“共享”了数据,可以使用shared_ptr来反映数据结构中的这种共享关系。

  3. 当我们设计一个类时,在真正实现成员之前先编写程序使用这个类,是一种非常有用的方法。通过这种方法,可以看到类是否具有我们所需要的操作。

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

    • my_TextQuery.cpp

    #include "my_TextQuery.h"
    #include "make_plural.h"
    
    #include <cstddef>
    #include <memory>
    #include <sstream>
    #include <string>
    #include <vector>
    #include <map>
    #include <set>
    #include <iostream>
    #include <fstream>
    #include <cctype>
    #include <cstring>
    #include <utility>
    
    using std::size_t;
    using std::shared_ptr;
    using std::istringstream;
    using std::string;
    using std::getline;
    using std::vector;
    using std::map;
    using std::set;
    using std::cerr;
    using std::cout;
    using std::cin;
    using std::ostream;
    using std::endl;
    using std::ifstream;
    using std::ispunct;
    using std::tolower;
    using std::strlen;
    using std::pair;
    
    // read the input file and build the map of lines to line numbers
    TextQuery::TextQuery(ifstream &is): file(new vector<string>)
    {
        string text;
        while (getline(is, text)) {       // for each line in the file
    		file.push_back(text);        // remember this line of text
    		int n = file.size() - 1;     // the current line number
    		istringstream line(text);     // separate the line into words
    		string word;
    		while (line >> word) {        // for each word in that line
                word = cleanup_str(word);
                // if word isn't already in wm, subscripting adds a new entry
                auto &lines = wm[word]; // lines is a shared_ptr
                if (!lines) // that pointer is null the first time we see word
                    lines.reset(new set<line_no>); // allocate a new set
                lines->insert(n);      // insert this line number
    		}
    	}
    }
    
    // not covered in the book -- cleanup_str removes
    // punctuation and converts all text to lowercase so that
    // the queries operate in a case insensitive manner
    string TextQuery::cleanup_str(const string &word)
    {
        string ret;
        for (auto it = word.begin(); it != word.end(); ++it) {
            if (!ispunct(*it))
                ret += tolower(*it);
        }
        return ret;
    }
    
    QueryResult
    TextQuery::query(const string &sought) const
    {
    	// we'll return a pointer to this set if we don't find sought
    	static shared_ptr<set<line_no>> nodata(new set<line_no>);
    
        // use find and not a subscript to avoid adding words to wm!
        auto loc = wm.find(cleanup_str(sought));
    
    	if (loc == wm.end())
    		return QueryResult(sought, nodata, file);  // not found
    	else
    		return QueryResult(sought, loc->second, file);
    }
    
    ostream &print(ostream & os, const QueryResult &qr)
    {
        // if the word was found, print the count and all occurrences
        os << qr.sought << " occurs " << qr.lines->size() << " "
           << make_plural(qr.lines->size(), "time", "s") << endl;
    
        // print each line in which the word appeared
    	for (auto num : *qr.lines) // for every element in the set
    		// don't confound the user with text lines starting at 0
            os << "\t(line " << num + 1 << ") "
    		   << qr.file.begin().deref(num) << endl;
    
    	return os;
    }
    
    // debugging routine, not covered in the book
    void TextQuery::display_map()
    {
        auto iter = wm.cbegin(), iter_end = wm.cend();
    
        // for each word in the map
        for ( ; iter != iter_end; ++iter) {
            cout << "word: " << iter->first << " {";
    
            // fetch location vector as a const reference to avoid copying it
            auto text_locs = iter->second;
            auto loc_iter = text_locs->cbegin(),
                            loc_iter_end = text_locs->cend();
    
            // print all line numbers for this word
            while (loc_iter != loc_iter_end)
            {
                cout << *loc_iter;
    
                if (++loc_iter != loc_iter_end)
                     cout << ", ";
    
             }
    
             cout << "}\n";  // end list of output this word
        }
        cout << endl;  // finished printing entire map
    }
    
    • my_TextQuery.h

    #ifndef TEXTQUERY_H
    #define TEXTQUERY_H
    
    #include <memory>
    #include <string>
    #include <vector>
    #include <map>
    #include <set>
    #include <fstream>
    #include "my_QueryResult.h"
    
    /* this version of the query classes includes two
     * members not covered in the book:
     *   cleanup_str: which removes punctuation and
     *                converst all text to lowercase
     *   display_map: a debugging routine that will print the contents
     *                of the lookup mape
    */
    
    class QueryResult; // declaration needed for return type in the query function
    class TextQuery {
    public:
    	using line_no = std::vector<std::string>::size_type;
    	TextQuery(std::ifstream&);
        QueryResult query(const std::string&) const;
        void display_map();        // debugging aid: print the map
    private:
        StrBlob file; // input file
        // maps each word to the set of the lines in which that word appears
        std::map<std::string,
    	         std::shared_ptr<std::set<line_no>>> wm;
    
    	// canonicalizes text: removes punctuation and makes everything lower case
        static std::string cleanup_str(const std::string&);
    };
    
    #endif
    
    • make_plural.h

    #include <cstddef>
    using std::size_t;
    
    #include <string>
    using std::string;
    
    #include <iostream>
    using std::cout; using std::endl;
    
    #ifndef MAKE_PLURAL_H
    #define MAKE_PLURAL_H
    
    // return the plural version of word if ctr is greater than 1
    inline
    string make_plural(size_t ctr, const string &word, 
                                   const string &ending)
    {
    	return (ctr > 1) ? word + ending : word;
    }
    
    #endif
    
    • my_QueryResult.h

    #ifndef QUERYRESULT_H
    #define QUERYRESULT_H
    
    #include <memory>
    #include <string>
    #include <vector>
    #include <set>
    #include <iostream>
    #include "my_StrBlob.h"
    
    class QueryResult {
    friend std::ostream& print(std::ostream&, const QueryResult&);
    public:
    	typedef std::vector<std::string>::size_type line_no;
    	typedef std::set<line_no>::const_iterator line_it;
    	QueryResult(std::string s,
    	            std::shared_ptr<std::set<line_no>> p,
    	            StrBlob f):
    		sought(s), lines(p), file(f) { }
    	std::set<line_no>::size_type size() const  { return lines->size(); }
    	line_it begin() const { return lines->cbegin(); }
    	line_it end() const   { return lines->cend(); }
    	StrBlob get_file() { return file; }
    private:
    	std::string sought;  // word this query represents
    	std::shared_ptr<std::set<line_no>> lines; // lines it's on
    	StrBlob file;  //input file
    };
    
    std::ostream &print(std::ostream&, const QueryResult&);
    
    #endif
    
    • my_StrBlob.h

    #ifndef MY_STRBLOB_H
    #define MY_STRBLOB_H
    
    #include <vector>
    #include <string>
    #include <initializer_list>
    #include <memory>
    #include <stdexcept>
    using namespace std;
    
    // 提前声明,StrBlob中的友类声明所需
    class StrBlobPtr;
    
    class StrBlob
    {
        friend class StrBlobPtr;
    public:
        typedef vector<string>::size_type size_type;
        StrBlob();
        StrBlob(initializer_list<string> il);
        StrBlob(vector<string> *p);
        size_type size() const { return data->size(); }
        bool empty() const { return data->empty(); }
        // 添加和删除元素
        void push_back(const string &t) {data->push_back(t);}
        void pop_back();
        // 元素访问
        string& front();
        const string& front() const;
        string& back();
        const string& back() const ;
    
        // 提供给StrBlobPtr的接口
        StrBlobPtr begin();  // 定义StrBlobPtr后才能定义这两个函数
        StrBlobPtr end();
        // const版本
        StrBlobPtr begin() const;
        StrBlobPtr end() const;
    private:
        shared_ptr<std::vector<std::string>> data;
        // 如果data[i]不合法,抛出一个异常
        void check(size_type i, const std::string &msg) const;
    };
    
    inline StrBlob::StrBlob(): data(make_shared<vector<string>>()) { }
    inline StrBlob::StrBlob(initializer_list<string> il) : data(make_shared<vector<string>>(il)) { }
    inline StrBlob::StrBlob(vector<string> *p): data(p) { }
    
    inline void StrBlob::check(size_type i, const string &msg) const
    {
        if (i >= data->size())
        {
            throw out_of_range(msg);
        }
    }
    
    inline string& StrBlob::front()
    {
        // 如果vector为空,check会抛出一个异常
        check(0, "front on empty StrBlob");
        return data->front();
    }
    
    // const版本front
    inline const string& StrBlob::front() const
    {
        check(0, "front on empty StrBlob");
        return data->front();
    }
    
    inline string& StrBlob::back()
    {
        check(0, "back on empty StrBlob");
        return data->back();
    }
    
    // const版本back
    inline const string& StrBlob::back() const
    {
        check(0, "back on empty StrBlob");
        return data->back();
    }
    
    inline void StrBlob::pop_back()
    {
        check(0, "pop_back on empty StrBlob");
        data->pop_back();
    }
    
    // 当试图访问一个不存在的元素时,StrBlobPtr抛出一个异常
    class StrBlobPtr
    {
        friend bool eq(const StrBlobPtr&, const StrBlobPtr&);
    public:
        StrBlobPtr(): curr(0) { }
        StrBlobPtr(StrBlob &a, size_t sz = 0): wptr(a.data), curr(sz) { }
        StrBlobPtr(const StrBlob &a, size_t sz = 0): wptr(a.data), curr(sz) { }
    
        string& deref() const;
        string& deref(int off) const;
        StrBlobPtr& incr();     // 前缀递增
        StrBlobPtr& decr();     // 前缀递减
    private:
        // 若检查成功,check返回一个指向vector的shared_ptr
        shared_ptr<vector<string>> check(size_t, const string&) const;
    
        // 保存一个weak_ptr,意味着底层vector可能会被销毁
        weak_ptr<vector<string>> wptr;
        size_t curr;    // 在数组中的当前位置
    };
    
    inline shared_ptr<vector<string>> StrBlobPtr::check(size_t i, const string &msg) const
    {
        auto ret = wptr.lock(); // vector还存在吗?
        if (!ret)
        {
           throw runtime_error("unbound StrBlobPtr");
        }
        if (i >= ret->size())
        {
           throw out_of_range(msg);
        }
        return ret; // 否则,返回指向vector的shared_ptr
    }
    
    inline string& StrBlobPtr::deref() const
    {
        auto p = check(curr, "dereference past end");
        return (*p)[curr]; // (*p)是对象所指向的vector
    }
    
    inline string& StrBlobPtr::deref(int off) const
    {
        auto p = check(curr + off, "dereference past end");
        return (*p)[curr + off]; // (*p)是对象所指向的vector
    }
    
    // 前缀递增:返回递增后的对象的引用
    inline StrBlobPtr& StrBlobPtr::incr()
    {
        // 如果curr已经指向容器的尾后位置,就不能递增它
        check(curr, "increment past end of StrBlobPtr");
        ++curr; // 推进当前位置
        return *this;
    }
    
    // 前缀递减:返回递减后的对象的引用
    inline StrBlobPtr& StrBlobPtr::decr()
    {
        // 如果curr已经为0,递减它就会产生一个非法下标
        --curr;     // 递减当前位置
        check(-1, "decrement past begin of StrBlobPtr");
        return *this;
    }
    
    // StrBlob的begin和end成员的定义
    inline StrBlobPtr StrBlob::begin()
    {
        return StrBlobPtr(*this);
    }
    
    inline StrBlobPtr StrBlob::end()
    {
      	auto ret = StrBlobPtr(*this, data->size());
        return ret;
    }
    
    // const版本
    inline StrBlobPtr StrBlob::begin() const
    {
    	  return StrBlobPtr(*this);
    }
    
    inline StrBlobPtr StrBlob::end() const
    {
        auto ret = StrBlobPtr(*this, data->size());
        return ret;
    }
    
    // StrBlobPtr的比较操作
    inline bool eq(const StrBlobPtr &lhs, const StrBlobPtr &rhs)
    {
        auto l = lhs.wptr.lock(), r = rhs.wptr.lock();
        // 若底层的vector是同一个
        if (l == r)
        {
        	  // 则两个指针都是空,或者指向相同元素时,它们相等
        	  return (!r || lhs.curr == rhs.curr);
        }
        else
        {
        	  return false; // 若指向不同vector,则不可能相等
        }
    }
    
    inline bool neq(const StrBlobPtr &lhs, const StrBlobPtr &rhs)
    {
    	  return !eq(lhs, rhs);
    }
    
    #endif
    
上一篇:《C++ Primer》笔记 第15章 面向对象程序设计


下一篇:《C++primer学习笔记》——第6章函数