《C++ Primer 5th》笔记(12 / 19):动态内存 痛定思痛。 2022-10-16 07:33 229阅读 0赞 ### 文章目录 ### * * 动态内存与智能指针 * * shared\_ptr类 * * make\_shared函数 * shared\_ptr的拷贝和赋值 * shared\_ptr自动销毁所管理的对象…… * .…shared\_ptr还会自动释放相关联的内存 * 使用了动态生存期的资源的类 * 定义StrBlob类 * StrBlob构造函数 * 元素访问成员函数 * StrBlob的拷贝、赋值和销毁 * 直接管理内存:new和delete * * 使用new动态分配和初始化对象 * 动态分配的const对象 * 内存耗尽 * 释放动态内存 * 指针值和delete * 动态对象的生存期直到被释放时为止 * 小心:动态内存的管理非常容易出错 * delete之后重置指针值…… * ……这只是提供了有限的保护 * shared\_ptr和new结合使用 * * 不要混合使用普通指针和智能指针... * ...也不要使用get初始化另一个智能指针或为智能指针赋值 * 其它shared\_ptr操作 * 智能指针和异常 * * 智能指针和哑类 * 使用我们自己的释放操作 * 注意:智能指针陷阱 * unique\_ptr * * 传递unque\_ptr参数和返回unique\_ptr * 向后兼容:auto\_ptr * 向unique\_ptr传递删除器 * weak\_ptr * * 核查指针类 * 指针操作 * 动态数组 * * new和数组 * * 分配一个数组会得到一个元素类型的指针 * 初始化动态分配对象的数组 * 动态分配一个空数组是合法的 * 释放动态数组 * 智能指针和动态数组 * allocator类 * * allocator类:将\*\*内存分配\*\*和\*\*对象构造\*\*分离 * allocator分配未构造的内存 * 拷贝和填充未初始化内存的算法 * 使用标准库:文本查询程序 * * 文本查询程序设计 * * 数据结构 * 在类之间共享数据 * 文本查询程序类的定义 * * TextQuery构造函数 * QueryResult类 * query函数 * 打印结果 到目前为止,我们编写的程序中所使用的对象都有着严格定义的生存期。 * 全局对象在程序启动时分配,在程序结束时销毁。 * 对于局部自动对象,当我们进入其定义所在的程序块时被创建,在离开块时销毁。 * 局部static对象在第一次使用前分配,在程序结束时销毁。 除了自动和static对象外,C++还支持动态分配对象。动态分配的对象的生存期与它们在哪里创建是无关的,只有当显式地被释放时,这些对象才会销毁。(MyNote:自己开发自己负责。) 动态对象的正确释放被证明是编程中极其容易出错的地方。为了更安全地使用动态对象,标准库定义了两个智能指针类型来管理动态分配的对象。当一个对象应该被释放时,指向它的智能指针可以确保自动地释放它。 -------------------- 我们的程序到目前为止只使用过静态内存或栈内存。 **静态内存**用来保存 1. 局部static对象(参见第6章) 2. 类static数据成员(参见第7章) 3. 定义在任何函数之外的变量。 **栈内存**用来保存定义在函数内的非static对象。 分配在静态或栈内存中的对象由编译器自动创建和销毁。 * 对于栈对象,仅在其定义的程序块运行时才存在 * static对象在使用之前分配,在程序结束时销毁。 除了静态内存和栈内存,每个程序还拥有一个内存池。这部分内存被称作**自由空间**(free store)或**堆**(heap)。程序用堆来存储**动态分配**(dynamically allocate)的对象——即,那些在程序运行时分配的对象。动态对象的生存期由程序来控制,也就是说,当动态对象不再使用时,我们的代码必须显式地销毁它们。 **WARNING**:虽然使用动态内存有时是必要的,但众所周知,正确地管理动态内存是非常棘手的。 ## 动态内存与智能指针 ## 在C++中,动态内存的管理是通过一对运算符来完成的: * new,在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化; * delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。 动态内存的使用很容易出问题,因为确保在正确的时间释放内存是极其困难的。 * 有时我们会忘记释放内存,在这种情况下就会产生内存泄漏; * 有时在尚有指针引用内存的情况下我们就释放了它,在这种情况下就会产生引用非法内存的指针。 为了更容易(同时也更安全)地使用动态内存,C++11新的标准库提供了两种**智能指针**(smart pointer)类型来管理动态对象。智能指针的行为类似常规指针,**重要的区别是它负责自动释放所指向的对象**。 新标准库提供的这两种智能指针的区别在于管理底层指针的方式: * shared\_ptr允许多个指针指向同一个对象; * unique\_ptr则“独占”所指向的对象。 标准库还定义了一个名为weak\_ptr的伴随类,它是一种弱引用,指向shared \_ptr所管理的对象。 这三种类型都定义在memory头文件中。 ### shared\_ptr类 ### 类似vector,**智能指针也是模板**。因此,当我们创建一个智能指针时,必须提供额外的信息——指针可以指向的类型。与vector一样,我们在尖括号内给出类型,之后是所定义的这种智能指针的名字: shared_ptr<string> p1; // shared_ptr,可以指向string shared_ptr<list<int>> p2;// shared_ptr,可以指向int的list 默认初始化的智能指针中保存着一个空指针。 智能指针的使用方式与普通指针类似。解引用一个智能指针返回它指向的对象。如果在一个条件判断中使用智能指针,效果就是检测它是否为空: //如果p1不为空,检查它是否指向一个空string if (p1 && p1->empty()) *p1 = "hi"; //如果pl指向一个空string,解引用p1,将一个新值赋予string 下表列出了shared\_ptr和 unique\_ptr都支持的操作。 <table> <thead> <tr> <th>操作</th> <th>说明</th> </tr> </thead> <tbody> <tr> <td>shared_ptr<T> sp<br>unique_ptr<T> up</td> <td>空智能指针,可以指向类型为T的对象</td> </tr> <tr> <td>p</td> <td>将p用作一个条件判断,若p指向一个对象,则为true</td> </tr> <tr> <td>*p</td> <td>解引用p,获得它指向的对象</td> </tr> <tr> <td>p->mem</td> <td>等价于(*p).mem</td> </tr> <tr> <td>p.get()</td> <td>返回p中保存的指针。要小心使用,若智能指针释放了其对象,返回的指针所指向的对象也就消失了</td> </tr> <tr> <td>swap(p, a)<br>p.swap(q)</td> <td>交换p和q中的指针</td> </tr> </tbody> </table> 下表列出只适用于shared\_ptr的操作。 <table> <thead> <tr> <th>操作</th> <th>说明</th> </tr> </thead> <tbody> <tr> <td>make_shared(args)</td> <td>返回一个shared_ptr,指向一个动态分配的类型为T的对象。使用args初始化此对象</td> </tr> <tr> <td>shared_ptrp(q)</td> <td>p是shared_ptr q的拷贝;此操作会递增q中的计数器。q中的指针必须能转换为T*</td> </tr> <tr> <td>p=q</td> <td>p和q都是shared_ptr,所保存的指针必须能相互转换。此操作会递减p的引用计数,递增q的引用计数;若p的引用计数变为0,则将其管理的原内存释放</td> </tr> <tr> <td>p.unique()</td> <td>若p.use_count()为1,返回true;否则返回false</td> </tr> <tr> <td>p.use_count()</td> <td>返回与p共享对象的智能指针数量;可能很慢,主要用于调试</td> </tr> </tbody> </table> #### make\_shared函数 #### **最安全的分配和使用动态内存的方法是调用一个名为make\_shared的标准库函数**。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared\_ptr。与智能指针一样,make\_shared也定义在头文件memory中。 当要用make\_shared时,必须指定想要创建的对象的类型。定义方式与模板类相同,在函数名之后跟一个尖括号,在其中给出类型: //指向一个值为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>(); 类似顺序容器的emplace成员,make\_shared用其参数来构造给定类型的对象。 例如, * 调用make\_shared<string>时传递的参数必须与string的某个构造函数相匹配, * 调用make\_shared<int>时传递的参数必须能用来初始化一个int, 依此类推。如果我们不传递任何参数,对象就会进行值初始化。 当然,我们通常用auto定义一个对象来保存make\_shared的结果,这种方式较为简单: // p6指向一个动态分配的空vector<string> auto p6 = make_shared<vector<string>>(); #### shared\_ptr的拷贝和赋值 #### 当进行拷贝或赋值操作时,每个shared\_ptr都会记录有多少个其他shared\_ptr指向相同的对象: auto p = make_shared<int>(42); // p指向的对象只有p一个引用者 auto q(p); // p和q指向相同对象,此对象有两个引用者 我们可以认为每个shared\_ptr都有一个关联的计数器,通常称其为**引用计数**(reference count)。 无论何时我们拷贝一个shared\_ptr,计数器都会递增。例如,当用一个shared\_ptr初始化另一个shared\_ptr,或将它作为参数传递给一个函数以及作为函数的返回值时,它所关联的计数器就会**递增**。 当我们给shared\_ptr赋予一个新值或是shared\_ptr被销毁(例如一个局部的shared\_ptr离开其作用域)时,计数器就会**递减**。 一旦一个shared\_ptr的计数器变为0,它就会自动释放自己所管理的对象: auto r = make_shared<int>(42); // r指向的int 只有一个引用者 r = q;//给r赋值,令它指向另一个地址 //递增q指向的对象的引用计数 //递减r原来指向的对象的引用计数 //r原来指向的对象已没有引用者,会自动释放 此例中我们分配了一个int,将其指针保存在r中。接下来,我们将一个新值赋予r。在此情况下,r是唯一指向此int的shared\_ptr,在把q赋给r的过程中,此int被自动释放。 **Note**:到底是用一个计数器还是其他数据结构来记录有多少指针共享对象,完全由标准库的具体实现来决定。**关键是智能指针类能记录有多少个 shared\_ptr指向相同的对象,并能在恰当的时候自动释放对象**。 #### shared\_ptr自动销毁所管理的对象…… #### 当指向一个对象的最后一个shared\_ptr被销毁时,shared\_ptr类会自动销毁此对象。它是通过另一个特殊的成员函数——**析构函数**(destructor)完成销毁工作的。类似于构造函数,每个类都有一个析构函数。就像构造函数控制初始化一样,析构函数控制此类型的对象销毁时做什么操作。 析构函数一般用来释放对象所分配的资源。例如, * string 的构造函数(以及其他string成员)会分配内存来保存构成string的字符。string 的析构函数就负责释放这些内存。 * 类似的,vector的若干操作都会分配内存来保存其元素。vector的析构函数就负责销毁这些元素,并释放它们所占用的内存。 shared\_ptr的析构函数会递减它所指向的对象的引用计数。如果引用计数变为0,shared\_ptr的析构函数就会销毁对象,并释放它占用的内存。 #### .…shared\_ptr还会自动释放相关联的内存 #### 当动态对象不再被使用时,shared\_ptr类会自动释放动态对象,这一特性使得动态内存的使用变得非常容易。 例如,我们可能有一个函数,它返回一个shared\_ptr,指向一个Foo类型的动态分配的对象,对象是通过一个类型为T的参数进行初始化的: //factory返回一个shared_ptr,指向一个动态分配的对象 shared_ptr<Foo> factory(T arg) { //恰当地处理arg // shared_ptr负责释放内存 return make_shared<Foo>(arg) ; } 由于factory返回一个shared\_ptr,所以我们可以确保它分配的对象会在恰当的时刻被释放。 (MyNote:工厂方法) 例如,下面的函数将factory返回的shared\_ptr保存在局部变量中: void use_factory(T arg) { shared_ptr<Foo> p = factory(arg); //使用p }//p离开了作用域,它指向的内存会被自动释放掉 由于p是use\_factory函数中的局部变量,在use\_factory结束时它将被销毁。当p被销毁时,将递减其引用计数并检查它是否为0。在此例中,p是唯一引用factory返回的内存的对象。由于p将要销毁,p指向的这个对象也会被销毁,所占用的内存会被释放。 但如果有其他shared\_ptr也指向这块内存,它就不会被释放掉: void use_factory(T arg) { shared_ptr<Foo> p = factory(arg); //使用p return p;//当我们返回p时,引用计数进行了递增操作 }// p离开了作用域,但它指向的内存不会被释放掉 在此版本中,use\_factory中的return语句向此函数的调用者返回一个p的拷贝。拷贝一个shared\_ptr会增加所管理对象的引用计数值。现在当p被销毁时,它所指向的内存还有其他使用者。对于一块内存,shared\_ptr类保证只要有任何shared\_ptr对象引用它,它就不会被释放掉。 由于在最后一个shared\_ptr销毁前内存都不会释放,保证shared\_ptr在无用之后不再保留就非常重要了。如果你忘记了销毁程序不再需要的shared\_ptr,程序仍会正确执行,但会浪费内存。 share\_ptr在无用之后仍然保留的一种可能情况是,**你将shared\_ptr存放在一个容器中,随后重排了容器,从而不再需要某些元素**。在这种情况下,你应该确保用erase删除那些不再需要的shared\_ptr元素。(MyNote:漏网之鱼。) **Note**:如果你将shared\_ptr存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用erase删除不再需要的那些元素。 #### 使用了动态生存期的资源的类 #### 程序使用动态内存出于以下三种原因之一: 1. 程序不知道自己需要使用多少对象 2. 程序不知道所需对象的准确类型 3. 程序需要在多个对象间共享数据 容器类是出于第一种原因而使用动态内存的典型例子,我们将在第15章看到出于第二种原因而使用动态内存的例子。在本节中,我们将定义一个类,**它使用动态内存是为了让多个对象能共享相同的底层数据**。 到目前为止,我们使用过的类中,分配的资源都与对应对象生存期一致。例如,每个vector“拥有”其自己的元素。当我们拷贝一个vector时,原vector和副本vector中的元素是相互分离的: vector<string> v1; //空vector { //新作用域 vector<string> v2 = { "a", "an", "the" }; v1 = v2; //从v2拷贝元素到v1中 )// v2被销毁,其中的元素也被销毁 // v1有三个元素,是原来v2中元素的拷贝 由一个vector分配的元素只有当这个vector存在时才存在。当一个vector被销毁时,这个vector中的元素也都被销毁。 但某些类分配的资源具有与原对象相独立的生存期。例如,假定我们希望定义一个名为Blob的类,保存一组元素。**与容器不同,我们希望Blob对象的不同拷贝之间共享相同的元素**。即,当我们拷贝一个Blob时,原Blob对象及其拷贝应该引用相同的底层元素。 一般而言,如果两个对象共享底层的数据,当某个对象被销毁时,我们不能单方面地销毁底层数据: Blob<string> b1; //空Blob { //新作用域 Blob<string> b2 = { "a", "an", "the" } ; b1 = b2; // b1和b2共享相同的元素 }// b2被销毁了,但b2中的元素不能销毁 // b1指向最初由b2创建的元素 在此例中,b1和 b2共享相同的元素。当b2离开作用域时,这些元素必须保留,因为b1仍然在使用它们。 **Note:使用动态内存的一个常见原因是允许多个对象共享相同的状态。** (MyNote:疑问,用引用不行吗?) #### 定义StrBlob类 #### 最终,我们会将Blob类实现为一个模板,但我们直到第16章才会学习模板的相关知识。因此,现在我们先定义一个管理string的类,此版本命名为StrBlob。 实现一个新的集合类型的最简单方法是使用某个标准库容器来管理元素。采用这种方法,我们可以借助标准库类型来管理元素所使用的内存空间。在本例中,我们将使用vector来保存元素。 但是,我们不能在一个Blob对象内直接保存vector,因为一个对象的成员在对象销毁时也会被销毁。例如,假定b1和 b2是两个Blob对象,共享相同的vector。如果此vector保存在其中一个Blob中——例如 b2中,那么当b2离开作用域时,此vector也将被销毁,也就是说其中的元素都将不复存在。为了保证vector中的元素继续存在,我们将vector保存在动态内存中。 **为了实现我们所希望的数据共享,我们为每个strBlob设置一个shared\_ptr来管理动态分配的 vector**。此shared\_ptr的成员将记录有多少个StrBlob共享相同的vector,并在vector的最后一个使用者被销毁时释放vector。 我们还需要确定这个类应该提供什么操作。当前,我们将实现一个vector操作的小的子集。我们会修改访问元素的操作(如front和 back):在我们的类中,如果用户试图访问不存在的元素,这些操作会抛出一个异常。 我们的类有一个默认构造函数和一个构造函数,接受单一的initializer\_list<string>类型参数。此构造函数可以接受一个初始化器的花括号列表。 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(); } // add and remove elements void push_back(const std::string &t) { data->push_back(t);} void pop_back(); // element access std::string& front(); std::string& back(); private: std::shared_ptr<std::vector<std::string>> data; // throws msg if data[i] isn't valid void check(size_type i, const std::string &msg) const; }; 在此类中,我们实现了size、empty和push\_back成员。这些成员通过指向底层vector的data成员来完成它们的工作。例如,对一个StrBlob对象调用size()会调用data->size(),依此类推。 #### StrBlob构造函数 #### 两个构造函数都使用初始化列表来初始化其data成员,令它指向一个动态分配的vector。默认构造函数分配一个空vector: StrBlob::StrBlob() : data (make_shared<vector<string>>()){ } StrBlob::StrBlob (initializer_list<string> il): data (make_shared<vector<string>>(il)){ } 接受一个initializer\_list的构造函数将其参数传递给对应的vector构造函数(参见第2章)。此构造函数通过拷贝列表中的值来初始化vector的元素。 #### 元素访问成员函数 #### pop\_back、front和 back操作访问vector中的元素。这些操作在试图访问元素之前必须检查元素是否存在。由于这些成员函数需要做相同的检查操作,我们**为StrBlob定义了一个名为check的private工具函数,它检查一个给定索引是否在合法范围内**。 除了索引,check 还接受一个string 参数,它会将此参数传递给异常处理程序,这个string描述了错误内容: void StrBlob::check(size_type i, const string &msg) const{ if (i >= data->size()) throw out_of_range(msg); } pop\_back和元素访问成员函数首先调用check。如果check成功,这些成员函数继续利用底层vector的操作来完成自己的工作: strings StrBlob::front(){ //如果vector为空,check会抛出一个异常 check (0,"front on empty StrBlob"); return data->front(); } string& StrBlob::back(){ check (0, "back on empty strBlob"); return data->back(); } void StrBlob::pop_back(){ check (0,"pop_back on empty StrBlob" ); data->pop_back (); } front和back应该对const进行重载(参见第7章)。 #### StrBlob的拷贝、赋值和销毁 #### 类似Sales\_data类,StrBlob使用默认版本的拷贝、赋值和销毁成员函数来对此类型的对象进行这些操作(参见第7章)。默认情况下,这些操作拷贝、赋值和销毁类的数据成员。我们的StrBlob类只有一个数据成员,它是shared\_ptr类型。因此,当我们拷贝、赋值或销毁一个StrBlob对象时,它的shared\_ptr成员会被拷贝、赋值或销毁。 如前所见,拷贝一个shared\_ptr会递增其引用计数;**将一个shared\_ptr赋予另一个shared\_ptr 会递增赋值号右侧shared\_ptr 的引用计数,而递减左侧shared\_ptr的引用计数**。如果一个shared\_ptr的引用计数变为0,它所指向的对象会被自动销毁。因此,对于由 StrBlob构造函数分配的vector,当最后一个指向它的StrBlob对象被销毁时,它会随之被自动销毁。 ### 直接管理内存:new和delete ### C++语言定义了两个运算符来分配和释放动态内存。运算符new分配内存,delete释放new分配的内存。 **相对于智能指针,使用这两个运算符管理内存非常容易出错**,随着我们逐步详细介绍这两个运算符,这一点会更为清楚。而且,自己直接管理内存的类与使用智能指针的类不同,它们不能依赖类对象拷贝、赋值和销毁操作的任何默认定义。因此,使用智能指针的程序更容易编写和调试。 **WARNING**:在学习第13章之前,除非使用智能指针来管理内存,否则不要分配动态内存。 #### 使用new动态分配和初始化对象 #### 在自由空间分配的内存是无名的,因此**new**无法为其分配的对象命名,而是**返回一个指向该对象的指针**: int *pi = new int; // pi指向一个动态分配的、未初始化的无名对象 此new表达式在自由空间构造一个int型对象,并返回指向该对象的指针。 默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化: string *ps = new string; //初始化为空string int *pi = new int;// pi指向一个未初始化的int 我们可以使用直接初始化方式来初始化一个动态分配的对象。我们可以使用传统的构造方式(使用圆括号),在新标准下,也可以使用列表初始化(使用花括号): int *pi = new int(1024) ;// pi指向的对象的值为1024 string *ps = new string(10, '9') ;// *ps 为"9999999999" // vector有10个元素,值依次从0到9 vector<int> *pv = new vector<int>{ 0,1,2,3,4,5,6,7,8,9}; 也可以对动态分配的对象进行值初始化(参见第3章),只需在类型名之后跟一对空括号即可: string *ps1 = new string; //默认初始化为空string string *ps = new string(); //值初始化为空string int *pi1 = new int;//默认初始化; *pi1的值未定义 int *pi2 = new int();//值初始化为0; *pi2为0 * **对于定义了自己的构造函数(参见第7章)的类类型(例如string)来说,要求值初始化是没有意义的;不管采用什么形式,对象都会通过默认构造函数来初始化**。 * 但对于内置类型,两种形式的差别就很大了: * 值初始化的内置类型对象有着良好定义的值, * 而默认初始化的对象的值则是未定义的。 类似的,对于类中那些依赖于编译器合成的默认构造函数的内置类型成员,如果它们未在类内被初始化,那么它们的值也是未定义的。 **Best Practices**:出于与变量初始化相同的原因,对动态分配的对象进行初始化通常是个好主意。 如果我们提供了一个括号包围的初始化器,就可以使用auto从此初始化器来推断我们想要分配的对象的类型。但是,由于编译器要用初始化器的类型来推断要分配的类型,只有当括号中仅有单一初始化器时才可以使用auto: auto p1 = new auto(obj);// p指向一个与obj类型相同的对象 //该对象用obj进行初始化 auto p2 = new auto{ a, b, c};//错误:括号中只能有单个初始化器 p1的类型是一个指针,指向从obj自动推断出的类型。若obj是一个int,那么p1就是int\*;若obj是一个string,那么p1是一个string\*;依此类推。新分配的对象用obj的值进行初始化。 #### 动态分配的const对象 #### 用new 分配 const对象是合法的: //分配并初始化一个const int const int *pci = new const int (1024); //分配并默认初始化一个const的空string const string *pcs = new const string; 类似其他任何const对象,一个动态分配的const对象必须进行初始化。对于一个定义了默认构造函数的类类型,其const动态对象可以隐式初始化,而其他类型的对象就必须显式初始化。由于分配的对象是const的,new返回的指针是一个指向const的指针。 #### 内存耗尽 #### 虽然现代计算机通常都配备大容量内存,但是自由空间被耗尽的情况还是有可能发生。一旦一个程序用光了它所有可用的内存,new表达式就会失败。默认情况下,如果new不能分配所要求的内存空间,它会抛出一个类型为bad\_alloc的异常。我们可以改变使用new的方式来阻止它抛出异常: //如果分配失败,new返回一个空指针 int *p1 = new int; //如果分配失败,new 抛出std: : bad_alloc int *p2 = new (nothrow) int;//如果分配失败,new返回一个空指针 我们称这种形式的new为**定位new**(placement new),其原因我们将在第19章中解释。定位new表达式允许我们向new传递额外的参数。 在此例中,我们传递给它一个由标准库定义的名为nothrow的对象。如果将nothrow传递给new,**我们的意图是告诉它不能抛出异常**。如果这种形式的new不能分配所需内存,它会返回一个空指针。bad\_alloc和nothrow都定义在头文件new中。 #### 释放动态内存 #### 为了防止内存耗尽,在动态内存使用完毕后,必须将其归还给系统。我们通过**delete表达式**(delete expression)来将动态内存归还给系统。**delete表达式接受一个指针**,指向我们想要释放的对象: delete p; // p必须指向一个动态分配的对象或是一个空指针 与new类型类似,delete表达式也执行两个动作:销毁给定的指针指向的对象,释放对应的内存。 #### 指针值和delete #### 我们传递给delete的指针必须指向动态分配的内存,或者是一个空指针。释放一块并非new分配的内存,或者将相同的指针值释放多次,其行为是未定义的: int i, *pi1 = &i, *pi2 = nullptr; double *pd = new double(33), *pd2 = pd; delete i;//错误:i不是一个指针//1. delete pil;//未定义:pi1指向一个局部变量//2. delete pd;//正确 delete pd2;//未定义:pd2指向的内存已经被释放了//2. delete pi2;//正确:释放一个空指针总是没有错误的 1. 对于delete i的请求,编译器会生成一个错误信息,因为它知道i不是一个指针。 2. 执行delete pil和 pd2所产生的错误则更具潜在危害:通常情况下,编译器不能分辨一个指针指向的是静态还是动态分配的对象。 类似的,编译器也不能分辨一个指针所指向的内存是否已经被释放了。对于这些delete表达式,大多数编译器会编译通过,尽管它们是错误的。 **虽然一个const对象的值不能被改变,但它本身是可以被销毁的**。如同任何其他动态对象一样,想要释放一个const动态对象,只要 delete指向它的指针即可: const int *pci = new const int(1024); delete pci;//正确:释放一个const对象* #### 动态对象的生存期直到被释放时为止 #### 由shared\_ptr管理的内存在最后一个shared\_ptr销毁时会被自动释放。但对于通过内置指针类型来管理的内存,就不是这样了。对于一个由内置指针管理的动态对象,直到被显式释放之前它都是存在的。 返回指向动态内存的指针(而不是智能指针)的函数给其调用者增加了一个额外负担——**调用者必须记得释放内存**: // factory返回一个指针,指向一个动态分配的对象 Foo* factory(T arg) //视情况处理arg return new Foo(arg);//调用者负责释放此内存 } 类似我们之前定义的factory函数,这个版本的factory分配一个对象,但并不delete它。factory的调用者负责在不需要此对象时释放它。**不幸的是**,调用者经常忘记释放对象: void use_factory(T arg) { Foo *p = factory(arg);//使用p但不delete它 } // p离开了它的作用域,但它所指向的内存没有被释放! 此处,use\_factory函数调用factory,后者分配一个类型为 Foo 的新对象。当use\_factory返回时,局部变量p被销毁。此变量是一个内置指针,而不是一个智能指针。 与类类型不同,内置类型的对象被销毁时什么也不会发生。特别是,当一个指针离开其作用域时,它所指向的对象什么也不会发生。如果这个指针指向的是动态内存,那么内存将不会被自动释放。 **WARNING:由内置指针(而不是智能指针)管理的动态内存在被显式释放前一直都会存在。** 在本例中,p是指向factory分配的内存的唯一指针。一旦use\_factory返回,程序就没有办法释放这块内存了。根据整个程序的逻辑,修正这个错误的正确方法是在use\_factory中记得释放内存: void use_factory(T arg){ Foo *p = factory(arg);//使用p delete p;//现在记得释放内存,我们已经不需要它了 } 还有一种可能,我们的系统中的其他代码要使用use\_factory所分配的对象,我们就应该修改此函数,让它返回一个指针,指向它分配的内存: Foo* use_factory(T arg){ Foo *p = factory(arg);//使用p return p;//调用者必须释放内存 } #### 小心:动态内存的管理非常容易出错 #### 使用new和delete管理动态内存存在三个常见问题: 1. 忘记 delete内存。忘记释放动态内存会导致人们常说的“**内存泄漏**”问题,因为这种内存永远不可能被归还给自由空间了。查找内存泄露错误是非常困难的,因为通常应用程序运行很长时间后,真正耗尽内存时,才能检测到这种错误。 2. 使用已经释放掉的对象。通过在释放内存后将指针置为空,有时可以检测出这种错误。 3. 同一块内存释放两次。当有两个指针指向相同的动态分配对象时,可能发生这种错误。如果对其中一个指针进行了delete操作,对象的内存就被归还给自由空间了。如果我们随后又delete第二个指针,自由空间就可能被破坏。 相对于查找和修正这些错误来说,制造出这些错误要简单得多。 **Best Practices:坚持只使用智能指针,就可以避免所有这些问题。对于一块内存,只有在没有任何智能指针指向它的情况下,智能指针才会自动释放它。** #### delete之后重置指针值…… #### 当我们delete一个指针后,指针值就变为无效了。虽然指针已经无效,但在很多机器上指针仍然保存着(已经释放了的)动态内存的地址。在delete之后,指针就变成了人们所说的**空悬指针**(dangling pointer),即,**指向一块曾经保存数据对象但现在已经无效的内存的指针**。 未初始化指针(参见第二章)的所有缺点空悬指针也都有。 * 有一种方法可以避免空悬指针的问题:**在指针即将要离开其作用域之前释放掉它所关联的内存**。这样,在指针关联的内存被释放掉之后,就没有机会继续使用指针了。 * 如果我们需要保留指针,可以在 delete之后将nullptr赋予指针,这样就清楚地指出指针不指向任何对象。 #### ……这只是提供了有限的保护 #### 动态内存的一个基本问题是可能有多个指针指向相同的内存。在delete内存之后重置指针的方法只对这个指针有效,对其他任何仍指向(已释放的)内存的指针是没有作用的。例如: int *p(new int (42)) ;// p指向动态内存 auto q = p; // p和q指向相同的内存 delete p; // p和q均变为无效 p = nullptr; //指出p不再绑定到任何对象 本例中p和q指向相同的动态分配的对象。我们delete此内存,然后将p置为nullptr,指出它不再指向任何对象。但是,**重置p对q没有任何作用**,在我们释放p所指向的(同时也是q所指向的!)内存时,q也变为无效了。(MyNote:q还是空悬指针)在实际系统中,查找指向相同内存的所有指针是异常困难的。 ### shared\_ptr和new结合使用 ### 如前所述,如果我们不初始化一个智能指针,它就会被初始化为一个空指针。 <table> <thead> <tr> <th>定义和改变shared_ptr的其他方法</th> <th>说明</th> </tr> </thead> <tbody> <tr> <td>shared_ptr<T> p(q)</td> <td>p管理内置指针q所指向的对象;q必须指向new分配的内存,且能够转换为T*类型</td> </tr> <tr> <td>shared_ptr<T> p(u)</td> <td>p从unique_ptr u那里接管了对象的所有权;将u置为空</td> </tr> <tr> <td>shared_ptr<T> p(q, d)</td> <td>p接管了内置指针g所指向的对象的所有权。q必须能转换为T*类型。p将使用可调用对象d来代替delete</td> </tr> <tr> <td>shared_ptr<T> p(p2, a)</td> <td>p是shared_ptr p2的拷贝,唯一的区别是p将用可调用对象d来代替delete</td> </tr> <tr> <td>p.reset()<br>p.reset(q)<br>p.reset(g, d)</td> <td>若p是唯一指向其对象的shared_ptr,reset会释放此对象。若传递了可选的参数内置指针q,会令p指向q,否则会将p置为空。若还传递了参数d,将会调用d而不是delete来释放q</td> </tr> </tbody> </table> 如表所示,我们还可以用new返回的指针来初始化智能指针: shared_ptr<double> p1; // shared_ptr 可以指向一个double shared_ptr<int> p2(new int(42)); // p2指向一个值为42的int 接受指针参数的智能指针构造函数是explicit的(第7章)。因此,我们不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式(第3章)来初始化一个智能指针: shared_ptr<int> p1 = new int (1024);//错误:必须使用直接初始化形式 shared_ptr<int> p2(new int (1024)); //正确:使用了直接初始化形式 p1的初始化隐式地要求编译器用一个new返回的int\*来创建一个shared\_ptr。由于**我们不能进行内置指针到智能指针间的隐式转换**,因此这条初始化语句是错误的。 出于相同的原因,一个返回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)); } 默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象。 我们可以将智能指针绑定到一个指向其他类型的资源的指针上,但是为了这样做,必须提供自己的操作来替代 delete。我们将在随后介绍如何定义自己的释放操作。 #### 不要混合使用普通指针和智能指针… #### shared\_ptr可以协调对象的析构,但这仅限于其自身的拷贝(也是shared \_ptr)之间。这也是为什么我们推荐使用make\_shared而不是new的原因。这样,我们就能在分配对象的同时就将shared\_ptr 与之绑定,从而避免了无意中将同一块内存绑定到多个独立创建的shared\_ptr 上。 考虑下面对shared ptr进行操作的函数: //在函数被调用时ptr被创建并初始化 void process(shared_ptr<int> ptr){ //使用ptr } // ptr离开作用域,被销毁 process的参数是传值方式传递的,因此实参会被拷贝到ptr中。拷贝一个shared\_ptr会递增其引用计数,因此,在process运行过程中,引用计数值至少为2。当process结束时,ptr的引用计数会递减,但不会变为0。因此,当局部变量ptr被销毁时,ptr指向的内存不会被释放。 使用此函数的正确方法是传递给它一个shared\_ptr: shared_ptr<int> p(new int (42)); // 引用计数为1 process(p); //拷贝p会递增它的引用计数;在process中引用计数值为2 int i = *p; //正确:引用计数值为1 虽然不能传递给process一个内置指针,但可以传递给它一个(临时的)shared \_ptr,这个shared\_ptr是用一个内置指针显式构造的。 但是,这样做很可能会导致错误: int *x (new int (1024));//危险:x是一个普通指针,不是一个智能指针 process(x);//错误:不能将int*转换为一个shared_ptr<int> process(shared_ptr<int> (x)) ; //合法的,但内存会被释放! int j = *x; //未定义的:x是一个空悬指针! 在上面的调用中,我们将一个临时shared\_ptr传递给process。当这个调用所在的表达式结束时,这个临时对象就被销毁了。销毁这个临时变量会递减引用计数,此时引用计数就变为0了。因此,当临时对象被销毁时,它所指向的内存会被释放。 但x继续指向(已经释放的)内存,从而变成一个**空悬指针**。如果试图使用x的值,其行为是未定义的。 当将一个shared\_ptr绑定到一个普通指针时,我们就将内存的管理责任交给了这个shared\_ptr。一旦这样做了,我们就不应该再使用内置指针来访问shared\_ptr所指向的内存了。 **WARNING**:使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时会被销毁。 #### …也不要使用get初始化另一个智能指针或为智能指针赋值 #### 智能指针类型定义了一个名为 get的函数(参见前文表格),它返回一个内置指针,指向智能指针管理的对象。此函数是为了这样一种情况而设计的:**我们需要向不能使用智能指针的代码传递一个内置指针**。 **使用get返回的指针的代码不能delete此指针**。 虽然编译器不会给出错误信息,但将另一个智能指针也绑定到get 返回的指针上是错误的: shared_ptr<int> p(new int (42)); //引用计数为1 int *q = p.get(); //正确:但使用q时要注意,不要让它管理的指针被释放 { //新程序块 //未定义:两个独立的shared_ptr指向相同的内存 shared_ptr<int>(q); }//程序块结束,q被销毁,它指向的内存被释放 int foo = *p; //未定义:p指向的内存已经被释放了 在本例中,p和q指向相同的内存。由于它们是相互独立创建的,因此各自的引用计数都是1。当q所在的程序块结束时,q被销毁,这会导致q指向的内存被释放。从而p变成一个空悬指针,意味着当我们试图使用p时,将发生未定义的行为。而且,当p被销毁时,这块内存会被第二次delete。 **WARNING**:get用来将指针的访问权限传递给代码,你只有在确定代码不会delete指针的情况下,才能使用 get。特别是,永远不要用 get初始化另一个智能指针或者为另一个智能指针赋值。 #### 其它shared\_ptr操作 #### 我们可以用reset来将一个新的指针赋予一个shared\_ptr: p = new int (1024) ; //错误:不能将一个指针赋予shared_ptr p.reset(new int (1024));//正确:p指向一个新对象 与赋值类似,reset会更新引用计数,如果需要的话,会释放p指向的对象。 reset成员经常与unique一起使用,来控制多个shared\_ptr共享的对象。在改变底层对象之前,我们检查自己是否是当前对象仅有的用户。如果不是,在改变之前要制作一份新的拷贝: if (!p.unique()) p.reset(new string(*p)); //我们不是唯一用户;分配新的拷贝* p += newVal; //现在我们知道自己是唯一的用户,可以改变对象的值 * p.unique():若p.use\_count()为1,返回true;否则返回false * p.use\_count():返回与p共享对象的智能指针数量;可能很慢,主要用于调试 ### 智能指针和异常 ### 第5章介绍了使用异常处理的程序能在异常发生后令程序流程继续,我们注意到,这种程序需要确保在异常发生后资源能被正确地释放。一个简单的确保资源被释放的方法是使用智能指针。 如果使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放: void f() { shared_ptr<int> sp(new int (42)) ; //分配一个新对象 //这段代码抛出一个异常,且在f中未被捕获 }//在函数结束时shared_ptr自动释放内存 函数的退出有两种可能,正常处理结束或者发生了异常,无论哪种情况,局部对象都会被销毁。在上面的程序中,sp是一个shared\_ptr,因此 sp 销毁时会检查引用计数。在此例中,sp是指向这块内存的唯一指针,因此**内存会被释放掉**。 与之相对的,当发生异常时,我们直接管理的内存是不会自动释放的。**如果使用内置指针管理内存,且在new之后在对应的delete之前发生了异常,则内存不会被释放**: void f (){ int * ip = new int (42);//动态分配一个新对象 //这段代码抛出一个异常,且在f中未被捕获 delete ip;//在退出之前释放内存 } 如果在new和delete之间发生异常,且异常未在f中被捕获,则内存就永远不会被释放了。在函数f之外没有指针指向这块内存,因此就无法释放它了。 #### 智能指针和哑类 #### 包括所有标准库类在内的很多C++类都定义了析构函数,负责清理对象使用的资源。但是,不是所有的类都是这样良好定义的。特别是那些为C和C++两种语言设计的类,通常都要求用户显式地释放所使用的任何资源。 那些分配了资源,而又没有定义析构函数来释放这些资源的类,可能会遇到与使用动态内存相同的错误——程序员非常容易忘记释放资源。类似的,如果在资源分配和释放之间发生了异常,程序也会发生资源泄漏。 与管理动态内存类似,我们通常可以使用类似的技术来管理不具有良好定义的析构函数的类。例如,假定我们正在使用一个C和C++都使用的网络库,使用这个库的代码可能是这样的: struct destination; //表示我们正在连接什么 struct connection; //使用连接所需的信息 connection connect (destination* ) ; //打开连接 void disconnect (connection) ; //关闭给定的连接 void f (destination &d /*其他参数*/){ //获得一个连接;记住使用完后要关闭它 connection c = connect(&d); //使用连接 //如果我们在f退出前忘记调用disconnect,就无法关闭c了 } 如果connection有一个析构函数,就可以在f结束时由析构函数自动关闭连接。**但是,connection没有析构函数**。 这个问题与我们上一个程序中**使用shared\_ptr避免内存泄漏**几乎是等价的。使用shared\_ptr来保证connection被正确关闭,已被证明是一种有效的方法。 #### 使用我们自己的释放操作 #### 默认情况下,shared \_ptr假定它们指向的是动态内存。因此,当一个shared\_ptr被销毁时,它默认地对它管理的指针进行delete操作。为了用shared\_ptr来管理一个connection,我们必须首先定义一个函数来代替delete。这个**删除器**(deleter)函数必须能够完成对shared\_ptr中保存的指针进行释放的操作。在本例中,我们的删除器必须接受单个类型为connection\*的参数: void end_connection (connection *p){ disconnect(*p);} 当我们创建一个shared\_ptr时,可以传递一个(可选的)指向删除器函数的参数: void f(destination &d / *其他参数*/){ connection c = connect(&d); shared_ptr<connection> p(&c, end_connection) ; //使用连接 //当f退出时(即使是由于异常而退出), connection会被正确关闭 } **当p被销毁时,它不会对自己保存的指针执行delete,而是调用end\_connection**。接下来,end\_connection会调用disconnect,从而确保连接被关闭。如果f正常退出,那么p的销毁会作为结束处理的一部分。如果发生了异常,p同样会被销毁,从而连接被关闭。 #### 注意:智能指针陷阱 #### 智能指针可以提供对动态分配的内存安全而又方便的管理,但这建立在正确使用的前提下。为了正确使用智能指针,我们必须坚持一些基本规范: * 不使用相同的内置指针值初始化(或reset)多个智能指针。不delete get()返回的指针。 * 不使用get()初始化或reset另一个智能指针。 * 如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了。 * 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。 ### unique\_ptr ### 一个unique ptr“拥有”它所指向的对象。与shared \_ptr不同,某个时刻只能有一个unique\_ptr指向一个给定对象。当unique\_ptr被销毁时,它所指向的对象也被销毁。下表列出了unique\_ptr特有的操作。与shared\_ptr相同的操作列在本文开头第一个表中。 <table> <thead> <tr> <th>unique_ptr特有操作</th> <th>说明</th> </tr> </thead> <tbody> <tr> <td>unique_ptr u1<br>unique_ptr<T,D> u2</td> <td>空unique_ptr,可以指向类型为T的对象。ul会使用delete来释放它的指针;u2会使用一个类型为D的可调用对象来释放它的指针</td> </tr> <tr> <td>unique_ptr<T,D> u(d)</td> <td>空unique_ptr,指向类型为T的对象,用类型为D的对象d代替delete</td> </tr> <tr> <td>u = nullptr</td> <td>释放u指向的对象,将u置为空</td> </tr> <tr> <td>u.release()</td> <td>u放弃对指针的控制权,返回指针,并将u置为空</td> </tr> <tr> <td>u.reset()</td> <td>释放u指向的对象</td> </tr> <tr> <td>u.reset(q)</td> <td>如果提供了内置指针q,令u指向这个对象;</td> </tr> <tr> <td>u.reset(nullptr)</td> <td>将u置为空</td> </tr> </tbody> </table> 与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("Stegosaurus")); unique_ptr<string> p2(p1);//错误: unique_ptr不支持拷贝 unique_ptr<string> p3; p3 = p2;//错误:unique _ptr不支持赋值 虽然我们不能拷贝或赋值unique\_ptr,但可以通过调用release或reset将指针的所有权从一个(非const)unique\_ptr转移给另一个unique: //将所有权从p1(指向string stegosaurus)转移给 p2 unique_ptr<string> p2(p1.release()); //release 将p1置为空 unique_ptr<string> p3(new string("Trex")); //将所有权从p3转移给p2 p2.reset(p3.release());//reset释放了p2原来指向的内存 release 成员返回unique\_ptr当前保存的指针并将其置为空。因此,p2被初始化为p1原来保存的指针,而p1被置为空。 reset成员接受一个可选的指针参数,令unique\_ptr重新指向给定的指针。如果unique\_ptr不为空,它原来指向的对象被释放。因此,对p2调用reset释放了用"Stegosaurus"初始化的string所使用的内存,将p3对指针的所有权转移给p2,并将p3置为空。 调用release会切断unique\_ptr和它原来管理的对象间的联系。release返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。在本例中,管理内存的责任简单地从一个智能指针转移给另一个。但是,如果我们不用另一个智能指针来保存release返回的指针,我们的程序就要负责资源的释放: p2.release();//错误:p2不会释放内存,而且我们丢失了指针 auto p = p2.release ();//正确,但我们必须记得delete(p) #### 传递unque\_ptr参数和返回unique\_ptr #### 不能拷贝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; } 对于两段代码,编译器都知道要返回的对象将要被销毁。在此情况下,编译器执行一种特殊的“拷贝”,我们将在第13章中介绍它。 #### 向后兼容:auto\_ptr #### 标准库的较早版本包含了一个名为auto\_ptr的类,它具有unique\_ptr的部分特性,但不是全部。特别是,我们不能在容器中保存auto\_ptr,也不能从函数中返回auto\_ptr。 虽然auto\_ptr仍是标准库的一部分,但编写程序时应该使用unique\_ptr。 #### 向unique\_ptr传递删除器 #### 类似shared\_ptr,unique\_ptr 默认情况下用delete释放它指向的对象。与shared\_ptr一样,我们可以重载一个unique\_ptr中默认的删除器。但是,unique\_ptr管理删除器的方式与shared\_ptr不同,其原因我们将在第16章中介绍。 重载一个unique\_ptr 中的删除器会影响到unique\_ptr类型以及如何构造(或reset)该类型的对象。与重载关联容器的比较操作(参见第11章)类似,我们必须在尖括号中unique\_ptr指向类型之后提供删除器类型。在创建或reset一个这种unique\_ptr类型的对象时,必须提供一个指定类型的可调用对象(删除器): // p指向一个类型为objT的对象,并使用一个类型为delT的对象释放objT对象 //它会调用一个名为fcn 的delT类型对象 unique_ptr<objT,delT> p (new objT, fcn) ; 作为一个更具体的例子,我们将重写连接程序,用unique\_ptr来代替shared\_ptr,如下所示: void f (destination &d /*其他需要的参数*/){ connection c = connect (&d); //打开连接//当 p被销毁时,连接将会关闭 unique_ptr<connection,decltype (end_connection) *> p ( &c, end_connection) ; //使用连接 //当f退出时(即使是由于异常而退出),connection会被正确关闭* } 在本例中我们使用了 decltype(参见第2章)来指明函数指针类型。由于decltype (end\_connection)返回一个函数类型,所以我们必须添加一个\*来指出我们正在使用该类型的一个指针(参见第6章)。 ### weak\_ptr ### weak\_ptr(见下表)是**一种不控制所指向对象生存期的智能指针**,它指向由一个shared\_ptr管理的对象。将一个 weak\_ptr绑定到一个shared\_ptr不会改变shared\_ptr的引用计数。一旦最后一个指向对象的shared\_ptr被销毁,对象就会被释放。即使有weak\_ptr指向对象,对象也还是会被释放,因此,weak \_ptr 的名字抓住了这种智能指针“弱”共享对象的特点。 <table> <thead> <tr> <th>操作</th> <th>说明</th> </tr> </thead> <tbody> <tr> <td>weak_ptr<T> w</td> <td>空weak_ptr可以指向类型为T的对象</td> </tr> <tr> <td>weak_ptr<T> w(sp)</td> <td>与shared_ptr sp指向相同对象的weak_ptr。T必须能转换为sp指向的类型</td> </tr> <tr> <td>w = p</td> <td>p可以是一个shared_ptr或一个weak_ptr。赋值后w与p共享对象</td> </tr> <tr> <td>w.reset()</td> <td>将w置为空</td> </tr> <tr> <td>w.use_count()</td> <td>与w共享对象的shared_ptr的数量</td> </tr> <tr> <td>w.expired()</td> <td>若w.use_count()为0,返回true,否则返回false</td> </tr> <tr> <td>w.lock()</td> <td>如果expired为true,返回一个空shared_ptr;否则返回一个指向w的对象的shared_ptr</td> </tr> </tbody> </table> 当我们创建一个weak \_ptr时,要用一个shared\_ptr来初始化它: auto p = make_shared<int> (42); weak_ptr<int> wp(p); // wp弱共享p;p的引用计数未改变 本例中wp和p指向相同的对象。由于是弱共享,创建wp不会改变p的引用计数;wp指向的对象可能被释放掉。 由于对象可能不存在,我们不能使用weak\_ptr直接访问对象,而必须调用lock。此函数检查weak\_ptr指向的对象是否仍存在。如果存在,lock返回一个指向共享对象的shared\_ptr。与任何其他shared\_ptr类似,只要此shared\_ptr存在,它所指向的底层对象也就会一直存在。例如: if (shared_ptr<int> np = wp.lock()) { //如果np不为空则条件成立 //在if中,np 与p共享对象 } 在这段代码中,只有当lock调用返回true时我们才会进入if语句体。在if中,使用np访问共享对象是安全的。 #### 核查指针类 #### 作为weak\_ptr用途的一个展示,我们将为StrBlob类(本文的定义StrBlob类章节)定义一个伴随指针类。我们的指针类将命名为StrBlobPtr,会保存一个weak\_ptr,指向StrBlob的data成员,这是初始化时提供给它的。**通过使用weak\_ptr,不会影响一个给定的StrBlob所指向的vector的生存期。但是,可以阻止用户访问一个不再存在的vector的企图**。 StrBlobPtr会有两个数据成员: 1. wptr,或者为空,或者指向一个strBlob 中的vector; 2. curr,保存当前对象所表示的元素的下标。 类似它的伴随类 StrBlob,我们的指针类也有一个check成员来检查解引用strBlobPtr是否安全: // StrBlobPtr throws an exception on attempts to access a nonexistent element class StrBlobPtr { public: StrBlobPtr(): curr(0) { } StrBlobPtr(StrBlob &a, size_t sz = 0): wptr(a.data), curr(sz) { } std::string& deref() const; StrBlobPtr& incr(); // prefix version private: // check returns a shared_ptr to the vector if the check succeeds std::shared_ptr<std::vector<std::string>> check(std::size_t, const std::string&) const; // store a weak_ptr, which means the underlying vector might be destroyed//重点关注处 std::weak_ptr<std::vector<std::string>> wptr; std::size_t curr; // current position within the array }; 1. 默认构造函数生成一个空的StrBlobPtr。其构造函数初始化列表将curr显式初始化为0,并将wptr隐式初始化为一个空weak\_ptr。 2. 第二个构造函数接受一个strBlob引用和一个可选的索引值。此构造函数初始化 wptr,令其指向给定StrBlob对象的shared\_ptr中的vector,并将curr初始化为sz的值。我们使用了默认参数,表示默认情况下将curr初始化为第一个元素的下标。我们将会看到,StrBlob的end成员将会用到参数sz。 值得注意的是,我们不能将StrBlobPtr绑定到一个const StrBlob对象。这个限制是由于构造函数接受一个非const strBlob对象的引用而导致的。 StrBlobPtr的check成员与StrBlob中的同名成员不同,它还要**检查指针指向的vector是否还存在**: std::shared_ptr<std::vector<std::string>> StrBlobPtr::check(std::size_t i, const std::string &msg) const { auto ret = wptr.lock(); // is the vector still around? if (!ret) throw std::runtime_error("unbound StrBlobPtr"); if (i >= ret->size()) throw std::out_of_range(msg); return ret; // otherwise, return a shared_ptr to the vector } 由于一个 weak\_ptr不参与其对应的shared\_ptr的引用计数,StrBlobPtr指向的vector可能已经被释放了。如果vector已销毁,lock将返回一个空指针。在本例中,任何vector 的引用都会失败,于是抛出一个异常。否则,check 会检查给定索引,如果索引值合法,check返回从lock 获得的shared\_ptr。 (MyNote:防御性函数) #### 指针操作 #### 将在第14章学习如何定义自己的运算符。现在,我们将定义名为deref和incr的函数,分别用来解引用和递增StrBlobPtr。 deref成员调用check,检查使用vector是否安全以及curr是否在合法范围内: std::string& StrBlobPtr::deref() const { auto p = check(curr, "dereference past end"); return (*p)[curr]; // (*p) is the vector to which this object points } 如果check 成功,p就是一个shared\_ptr,指向strBlobPtr所指向的vector。表达式(\*p)\[curr\]解引用shared\_ptr来获得vector,然后使用下标运算符提取并返回curr位置上的元素。 incr成员也调用check: // prefix: return a reference to the incremented object StrBlobPtr& StrBlobPtr::incr() { // if curr already points past the end of the container, can't increment it check(curr, "increment past end of StrBlobPtr"); ++curr; // advance the current state return *this; } 当然,为了访问data成员,我们的指针类必须声明为StrBlob 的friend(MyNote:**好朋友声明**。StrBlob:StrBlobPtr是我的好朋友,我就可以调用StrBlobPtr成员啦。)。我们还要为StrBlob类定义begin和end操作,返回一个指向它自身的StrBlobPtr: // forward declaration needed for friend declaration in StrBlob class StrBlobPtr; class StrBlob { friend class StrBlobPtr; // other members as in § 12.1.1 (p. 456) // return StrBlobPtr to the first and one past the last elements StrBlobPtr begin() { return StrBlobPtr(*this); } StrBlobPtr end(){ auto ret = StrBlobPtr(*this, data->size()); return ret; } }; ## 动态数组 ## new和delete运算符一次分配/释放一个对象,但某些应用需要**一次为很多对象分配内存的功能**。例如,vector和 string 都是在连续内存中保存它们的元素,因此,当容器需要重新分配内存时,必须一次性为很多元素分配内存。 为了支持这种需求,C++语言和标准库提供了两种**一次分配一个对象数组的方法**。 1. C++语言定义了另一种new表达式语法,可以分配并初始化一个对象数组。 2. 标准库中包含一个名为allocator的类,允许我们将分配和初始化分离。使用allocator通常会提供更好的性能和更灵活的内存管理能力,原因随后介绍。 很多(可能是大多数)应用都**没有直接访问动态数组**的需求。当一个应用需要可变数量的对象时,我们在StrBlob中所采用的方法几乎总是更简单、更快速并且更安全的——即,使用**vector**(或其他标准库容器)。 如我们将在第13章中看到的,使用标准库容器的优势在新标准下更为显著。在支持新标准的标准库中,容器操作比之前的版本要快速得多。 **Best Practices**:大多数应用应该使用标准库容器而不是动态分配的数组。使用容器更为简单、更不容易出现内存管理错误并且可能有更好的性能。 如前所述,使用容器的类可以使用默认版本的拷贝、赋值和析构操作。分配动态数组的类则必须定义自己版本的操作,在拷贝、复制以及销毁对象时管理所关联的内存。 **WARNING**:直到学习完第13章,不要在类内的代码中分配动态内存。 ### new和数组 ### 为了让 new分配一个对象数组,我们要在类型名之后跟一对方括号,在其中指明要分配的对象的数目。在下例中,new分配要求数量的对象并(假定分配成功后)返回指向第一个对象的指针: //调用get_size确定分配多少个int int *pia = new int[get_size()]; // pia指向第一个int 方括号中的大小必须是整型,但不必是常量。 也可以用一个表示数组类型的类型别名(参见第2章)来分配一个数组,这样,new表达式中就不需要方括号了: typedef int arrT [42];// arrT表示42个int的数组类型 int *p = new arrT;//分配一个42个int的数组;p指向第一个int 在本例中,new分配一个int 数组,并返回指向第一个int的指针。即使这段代码中没 有方括号,编译器执行这个表达式时还是会用new\[ \]。即,编译器执行如下形式: int *p = new int [42]; #### 分配一个数组会得到一个元素类型的指针 #### 虽然我们通常称new T\[\]分配的内存为“动态数组”,但这种叫法某种程度上有些误导。当用new分配一个数组时,我们并未得到一个数组类型的对象,而是**得到一个数组元素类型的指针**。即使我们使用类型别名定义了一个数组类型,new也不会分配一个数组类型的对象。在上例中,我们正在分配一个数组的事实甚至都是不可见的——连\[num\]都没有。new返回的是一个元素类型的指针。 由于分配的内存并不是一个数组类型,因此不能对动态数组调用begin或end(参见第3章)。这些函数使用数组维度(回忆一下,维度是数组类型的一部分)来返回指向首元素和尾后元素的指针。出于相同的原因,也不能用范围for语句(for (declaration : expression)statement)来处理(所谓的)动态数组中的元素。 **WARNING**:要记住我们所说的动态数组并不是数组类型,这是很重要的。 #### 初始化动态分配对象的数组 #### 默认情况下,new分配的对象,不管是单个分配的还是数组中的,都是默认初始化的。可以对数组中的元素进行**值初始化**(第3章),方法是在大小之后跟一对空括号。 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 在C++11新标准中,我们还可以提供一个元素初始化器的花括号列表: // 10个int分别用列表中对应的初始化器初始化 int *pia3 = new int [10] { 0,1,2,3,4,5,6,7,8,9}; // 10个string,前4个用给定的初始化器初始化,剩余的进行值初始化 string *psa3 = new string [10] { "a","an","the",string (3,'x’)}; 与内置数组对象的列表初始化(参见第3章)一样,初始化器会用来初始化动态数组中开始部分的元素。如果初始化器数目小于元素数目,剩余元素将进行值初始化。如果初始化器数目大于元素数目,则new表达式失败,不会分配任何内存。在本例中,new会抛出一个类型为bad\_array\_new\_length的异常。类似bad\_alloc,此类型定义在头文件new中。 虽然我们用空括号对数组中元素进行值初始化,但不能在括号中给出初始化器,这意味着不能用auto分配数组。 #### 动态分配一个空数组是合法的 #### 可以用任意表达式来确定要分配的对象的数目: size_t n = get_size();//get_size返回需要的元素的数目 int* p = new int[n];//分配数组保存元素 for (int* q = p; q != p + n; ++q) /*处理数组*/; 这产生了一个有意思的问题:如果get\_size返回0,会发生什么? 答案是代码仍能正常工作。虽然我们不能创建一个大小为0的静态数组对象,但当n等于0时,调用new\[n\]是合法的: char arr[0];//错误:不能定义长度为0的静态数组 char *cp = new char[0];//正确:但cp不能解引用 当我们用new分配一个大小为0的数组时,new返回一个合法的非空指针。此指针保证与new返回的其他任何指针都不相同。对于零长度的数组来说,此指针就像尾后指针一样(参见第3章),我们可以像使用尾后迭代器一样使用这个指针。可以用此指针进行比较操作,就像上面循环代码中那样。可以向此指针加上(或从此指针减去)0,也可以从此指针减去自身从而得到0。但**此指针不能解引用**——毕竟它不指向任何元素。 (MyNote:那怎么用?\[\]) 回到刚才的循环中,若get\_size返回0,则n也是0,new会分配0个对象。for循环中的条件会失败(p等于q+n,因为n为0)。因此,循环体不会被执行。 #### 释放动态数组 #### 为了释放动态数组,我们使用一种特殊形式的delete——在指针前加上一个空方括号对: delete p;// p必须指向一个动态分配的对象或为空 delete [] pa;// pa必须指向一个动态分配的数组或为空 第二条语句销毁pa指向的数组中的元素,并释放对应的内存。**数组中的元素按逆序销毁**,即,最后一个元素首先被销毁,然后是倒数第二个,依此类推。 当我们释放一个指向数组的指针时,**空方括号对是必需的**:它指示编译器此指针指向一个对象数组的第一个元素。如果我们在delete一个指向数组的指针时忽略了方括号(或者在delete一个指向单一对象的指针时使用了方括号),其行为是未定义的。 回忆一下,当我们使用一个类型别名来定义一个数组类型时,在 new表达式中不使用\[\]。即使是这样,在释放一个数组指针时也必须使用方括号: typedef int arrT [42] ;// arrT是42个int的数组的类型别名 int *p = new arrT; //分配一个42个int的数组;p指向第一个元素 delete [] p;//方括号是必需的,因为我们当初分配的是一个数组 不管外表如何,p指向一个对象数组的首元素,而不是一个类型为arrT的单一对象。因此,在释放p时我们必须使用\[\]。 **WARNING**:如果我们在delete一个数组指针时忘记了方括号,或者在delete一个单一对象的指针时使用了方括号,编译器很可能不会给出警告。我们的程序可能在执行过程中在没有任何警告的情况下行为异常。 #### 智能指针和动态数组 #### 标准库提供了一个可以管理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指向一个数组时,我们可以使用下标运算符来访问数组中的元素: for (size_t i = 0; i != 10; ++i) up[i] = i; //为每个元素赋予一个新值 指向数组的unique\_ptr提供的操作: 注意: * 指向数组的unique\_ptr不支持成员访问运算符(点和箭头运算符)。 * 其他unique\_ptr操作不变。 <table> <thead> <tr> <th>操作</th> <th>说明</th> </tr> </thead> <tbody> <tr> <td>unique_ptr<T[]> u</td> <td>u可以指向一个动态分配的数组,数组元素类型为T</td> </tr> <tr> <td>unique_ptr<T[]> u§</td> <td>u指向内置指针p所指向的动态分配的数组。p必须能转换为类型T*</td> </tr> <tr> <td>u[i]</td> <td>返回u拥有的数组中位置i处的对象,u必须指向一个数组</td> </tr> </tbody> </table> 与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一个lambda(参见第10章)作为删除器,它使用delete\[\]释放数组。 如果未提供删除器,这段代码将是未定义的。默认情况下,shared\_ptr使用delete销毁它指向的对象。如果此对象是一个动态数组,对其使用delete所产生的问题与释放一个动态数组指针时忘记\[\]产生的问题一样(参见第12章)。 shared\_ptr不直接支持动态数组管理这一特性会影响我们如何访问数组中的元素: //shared_ptr未定义下标运算符,并且不支持指针的算术运算 for (size_t i = 0; i != 10; ++i) *(sp.get() + i)= i; //使用get获取一个内置指针 shared\_ptr未定义下标运算符,而且智能指针类型不支持指针算术运算。因此,为了访问数组中的元素,必须用get获取一个内置指针,然后用它来访问数组元素。 ### allocator类 ### new有一些灵活性上的局限,其中一方面表现在它将**内存分配**和**对象构造**组合在了一起。类似的,delete将**对象析构**和**内存释放**组合在了一起。我们分配单个对象时,通常希望将内存分配和对象初始化组合在一起。因为在这种情况下,我们几乎肯定知道对象应有什么值。 当分配一大块内存时,我们通常计划在这块内存上按需构造对象。在此情况下,**我们希望将内存分配和对象构造分离**。这意味着我们可以分配大块内存,但只在真正需要时才真正执行对象创建操作(同时付出一定开销)。 一般情况下,将内存分配和对象构造组合在一起可能会导致不必要的浪费。例如: string *const p = new string[n] ; //构造n个空string string s; string *q = p; // q指向第一个string while (cin >> s && q != p +n) *q++ = s; //赋予*q一个新值 const size_t size = q - p; //记住我们读取了多少个string //使用数组 delete[] p; //p指向一个数组;记得用delete[]来释放 new表达式分配并初始化了n 个string。但是,我们可能不需要n个string,少量string可能就足够了。这样,我们就可能创建了一些永远也用不到的对象。而且,对于那些确实要使用的对象,我们也在初始化之后立即赋予了它们新值。每个使用到的元素都被赋值了两次:第一次是在默认初始化时,随后是在赋值时。 更重要的是,那些没有默认构造函数的类就不能动态分配数组了。 #### allocator类:将**内存分配**和**对象构造**分离 #### 标准库**allocator**类定义在头文件memory中,它帮助我们将**内存分配**和**对象构造**分离开来。它提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。下表概述了allocator支持的操作。在本节中,我们将介绍这些allocator操作。在第13章,我们将看到如何使用这个类的典型例子。 类似vector,allocator是一个模板(参见第3章)。为了定义一个allocator对象,我们必须指明这个allocator可以分配的对象类型。当一个allocator对象分配内存时,它会根据给定的对象类型来确定恰当的内存大小和对齐位置: allocator<string> alloc; //可以分配string的allocator对象 auto const p = alloc.allocate(n); //分配n个未初始化的string 这个allocate调用为n个string分配了内存。 **标准库allocator类及其算法** <table> <thead> <tr> <th>操作</th> <th>说明</th> </tr> </thead> <tbody> <tr> <td>allocator<T> a</td> <td>定义了一个名为a的allocator对象,它可以为类型为T的对象分配内存</td> </tr> <tr> <td>a.allocate(n)</td> <td>分配一段原始的、未构造的内存,保存n个类型为T的对象</td> </tr> <tr> <td>a.deallocate(p, n)</td> <td>释放从T*指针p中地址开始的内存,这块内存保存了n个类型为T的对象;p必须是一个先前由allocate返回的指针,且n必须是p创建时所要求的大小。在调用deallocate之前,用户必须对每个在这块内存中创建的对象调用destroy</td> </tr> <tr> <td>a.construct(p, args)</td> <td>p必须是一个类型为T*的指针,指向一块原始内存;arg被传递给类型为T的构造函数,用来在p指向的内存中构造一个对象</td> </tr> <tr> <td>a.destroy§</td> <td>p为T*类型的指针,此算法对p指向的对象执行析构函数</td> </tr> </tbody> </table> #### allocator分配未构造的内存 #### allocator分配的内存是未构造的(unconstructed)。我们按需要在此内存中构造对象。在新标准库中,construct成员函数接受一个指针和零个或多个额外参数,在给定位置构造一个元素。额外参数用来初始化构造的对象。类似make\_shared的参数,这些额外参数必须是与构造的对象的类型相匹配的合法的初始化器: auto q = p; // q指向最后构造的元素之后的位置 alloc.construct(q++); // *q为空字符串 alloc.construct(q++, 10, 'c'); // *q为cccccccccc alloc.construct(q++, "hi"); //*q为hi! 在早期版本的标准库中,construct 只接受两个参数:指向创建对象位置的指针和一个元素类型的值。因此,我们只能将一个元素拷贝到未构造空间中,而不能用元素类型的任何其他构造函数来构造一个元素。 还未构造对象的情况下就使用原始内存是错误的: cout << *p << endl; //正确:使用string的输出运算符 cout << *q << endl; //灾难:q指向未构造的内存! **WARNING**:为了使用allocate返回的内存,我们必须用construct构造对象。使用未构造的内存,其行为是未定义的。 当我们用完对象后,必须对每个构造的元素调用destroy来销毁它们。函数destroy接受一个指针,对指向的对象执行析构函数: while (q != p) alloc.destroy(--q); //释放我们真正构造的string 在循环开始处,q指向最后构造的元素之后的位置。我们在调用destroy之前对q进行了递减操作。因此,第一次调用destroy时,q指向最后一个构造的元素。最后一步循环中我们destroy了第一个构造的元素,随后q将与p相等,循环结束。 **WARNING**:我们只能对真正构造了的元素进行destroy操作。 一旦元素被销毁后,就可以重新使用这部分内存来保存其他string,也可以将其归还给系统。释放内存通过调用deallocate来完成: alloc.deallocate(p, n); 我们传递给deallocate的指针不能为空,它必须指向由allocate分配的内存。而且,传递给deallocate 的大小参数必须与调用allocated分配内存时提供的大小参数具有一样的值。 #### 拷贝和填充未初始化内存的算法 #### 标准库还为allocator类定义了两个伴随算法,可以在未初始化内存中创建对象。下表描述了这些函数,它们都定义在头文件memory中。 **allocator算法** 这些函数在给定目的位置创建元素,而不是由系统分配内存给它们。 <table> <thead> <tr> <th>操作</th> <th>说明</th> </tr> </thead> <tbody> <tr> <td>uninitialized_copy(b, e, b2)</td> <td>从迭代器b和e指出的输入范围中拷贝元素到迭代器b2指定的未构造的原始内存中。b2指向的内存必须足够大,能容纳输入序列中元素的拷贝</td> </tr> <tr> <td>uninitialized_copy_n(b, n, b2)</td> <td>从迭代器b指向的元素开始,拷贝n个元素到b2开始的内存中</td> </tr> <tr> <td>uninitialized_fill(b, e, t)</td> <td>在迭代器b和e指定的原始内存范围中创建对象,对象的值均为t的拷贝</td> </tr> <tr> <td>uninitialized_fill_n(b, n, t)</td> <td>从迭代器b指向的内存地址开始创建n个对象。b 必须指向足够大的未构造的原始内存,能够容纳给定数量的对象</td> </tr> </tbody> </table> 作为一个例子,假定有一个int的vector,希望将其内容拷贝到动态内存中。我们将分配一块比vector中元素所占用空间大一倍的动态内存,然后将原vector中的元素拷贝到前一半空间,对后一半空间用一个给定值进行填充: //分配比vi中元素所占用空间大一倍的动态内存 auto p = alloc.allocate(vi.size() *2); //通过拷贝vi中的元素来构造从p开始的元素 auto q = uninitialized_copy(vi.begin(), vi.end(), p); //将剩余元素初始化为42 uninitialized_fill_n(a, vi.size(), 42) ; 类似拷贝算法(参见第10章),uninitialized\_copy接受三个迭代器参数。前两个表示输入序列,第三个表示这些元素将要拷贝到的目的空间。传递给uninitialized\_copy 的目的位置迭代器必须指向未构造的内存。与 copy 不同,uninitialized\_copy在给定目的位置构造元素。 类似copy,uninitialized\_copy返回(递增后的)目的位置迭代器。因此,一次uninitialized\_copy调用会返回一个指针,指向最后一个构造的元素之后的位置。 在本例中,我们将此指针保存在q中,然后将q传递给uninitialized\_fill\_n。此函数类似fill\_n(参见第10章),接受一个指向目的位置的指针、一个计数和一个值。它会在目的位置指针指向的内存中创建给定数目个对象,用给定值对它们进行初始化。 ## 使用标准库:文本查询程序 ## 我们将实现一个简单的文本查询程序,作为标准库相关内容学习的总结。**我们的程序允许用户在一个给定文件中查询单词。查询结果是单词在文件中出现的次数及其所在行的列表**。(MyNote:设计目标)如果一个单词在一行中出现多次,此行只列出一次。行会按照升序输出——即,第7行会在第9行之前显示,依此类推。 例如,我们可能读入一个包含本章内容(指英文版中的文本)的文件,在其中寻找单词element。输出结果的前几行应该是这样的: 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 接下来还有大约100行,都是单词element出现的位置。 ### 文本查询程序设计 ### 开始一个程序的设计的一种好方法是列出程序的操作。了解需要哪些操作会帮助我们分析出需要什么样的数据结构。从需求入手,我们的文本查询程序需要完成如下**任务要求**: * 当程序读取输入文件时,它必须记住单词出现的每一行。因此,程序需要逐行读取输入文件,并将每一行分解为独立的单词 * 当程序生成输出时, * 它必须能提取每个单词所关联的行号 * 行号必须按升序出现且无重复 * 它必须能打印给定行号中的文本。 利用多种标准库设施,我们可以很漂亮地实现这些要求: * 我们将使用一个vector<string>来保存整个输入文件的一份拷贝。输入文件中的每行保存为vector中的一个元素。当需要打印一行时,可以用行号作为下标来提取行文本。 * 我们使用一个istringstream(参见第8章)来将每行分解为单词。我们使用一个 set来保存每个单词在输入文本中出现的行号。这保证了每行只出现一次且行号按升序保存。 * 我们使用一个map来将每个单词与它出现的行号set关联起来。这样我们就可以方便地提取任意单词的set。 * 我们的解决方案还使用了shared\_ptr,原因稍后进行解释。 #### 数据结构 #### 虽然我们可以用vector、set 和 map来直接编写文本查询程序,但如果定义一个更 为抽象的解决方案,会更为有效。我们将从定义一个保存输入文件的类开始,这会令文件查询更为容易。我们将这个类命名为TextQuery,它包含一个vector和一个map。 * vector用来保存输入文件的文本, * map用来关联每个单词和它出现的行号的set。 这个类将会有一个用来读取给定输入文件的构造函数和一个执行查询的操作。 查询操作要完成的任务非常简单:查找map 成员,检查给定单词是否出现。设计这个函数的难点是确定应该返回什么内容。一旦找到了一个单词,我们需要知道它出现了多少次、它出现的行号以及每行的文本。 返回所有这些内容的最简单的方法是定义另一个类,可以命名为QueryResult,来保存查询结果。这个类会有一个print函数,完成结果打印工作。 #### 在类之间共享数据 #### 我们的QueryResult类要表达查询的结果。这些结果包括与给定单词关联的行号的set和这些行对应的文本。这些数据都保存在TextQuery类型的对象中。 由于 QueryResult所需要的数据都保存在一个TextQuery对象中,我们就必须确定如何访问它们。我们可以拷贝行号的set,但这样做可能很耗时。而且,我们当然不希望拷贝vector,因为这可能会引起整个文件的拷贝,而目标只不过是为了打印文件的一小部分而已(通常会是这样)。 通过返回指向TextQuery对象内部的迭代器(或指针),我们可以避免拷贝操作。但是,这种方法开启了一个陷阱:如果TextQuery对象在对应的QueryResult对象之前被销毁,会发生什么?在此情况下,QueryResult就将引用一个不再存在的对象中的数据。 对于QueryResult对象和对应的TextQuery对象的生存期应该同步这一观察结果,其实已经暗示了问题的解决方案。考虑到这两个类概念上“共享”了数据,可以使用shared\_ptr来反映数据结构中的这种共享关系。 (MyNote:太多文字了,还是结合源码再看吧!) ### 文本查询程序类的定义 ### 我们以TextQuery类的定义开始。 * 用户创建此类的对象时会提供一个istream,用来读取输入文件。这个类还提供一个query操作,接受一个 string,返回一个QueryResult表示string出现的那些行。 设计类的数据成员时,需要考虑与QueryResult 对象共享数据的需求。QueryResult类需要共享保存输入文件的vector和保存单词关联的行号的set。因此,这个类应该有两个数据成员: 1. 一个指向动态分配的 vector(保存输入文件)的shared\_ptr 2. 一个string到 shared\_ptr<set>的map。map将文件中每个单词关联到一个动态分配的set 上,而此set 保存了该单词所出现的行号。 为了使代码更易读,我们还会定义一个类型成员来引用行号,即string的vector中的下标: class QueryResult;//为了定义函数query的返回类型,这个定义是必需的 class TextQuery { public: using line_no = std::vector<std::string>::size_type; TextQuery(std::ifstream&); QueryResult query (const std::string&) const; private: std::shared_ptr<std::vector<std::string>> file;// 输入文件 //每个单词到它所在的行号的集合的映射 std::map<std::string, std::shared ptr<std::set<line_no>>>wm; }; 这个类定义最困难的部分是解开类名。与往常一样,对于可能置于头文件中的代码,在使用标准库名字时要加上std::。在本例中,我们反复使用了std::, 使得代码开始可能有些难读。例如, std::map<std::string,std::shared_ptr<std::set<line_no>>> wm; 如果写成下面的形式可能就更好理解一些 map<string, shared_ptr<set<line_no>>> wm; #### TextQuery构造函数 #### TextQuery 的构造函数接受一个ifstream,逐行读取输入文件: //读取输入文件并建立单词到行号的映射 TextQuery::TextQuery(ifstream &is): file(new vector<string>)//1. { string text; while (getline(is, text)) { //对文件中每一行 file->push_back (text) ; //保存此行文本// int n = file->size() - 1; //当前行号 istringstream line(text) ; //将行文本分解为单词//2. string word; while (line >> word) { //对行中每个单词 //如果单词不在wm 中,以之为下标在wm中添加一项 //map<string, shared_ptr<set<line_no>>> wm; auto &lines = wm[word];// lines是一个shared_ptr//3. if (!lines) //在我们第一次遇到这个单词时,此指针为空 lines.reset (new set<line_no>); //分配一个新的set lines->insert (n);//将此行号插入set中//4. } } } 1. 构造函数的初始化器分配一个新的vector来保存输入文件中的文本。我们用getline逐行读取输入文件,并存入vector中。由于file是一个shared\_ptr,我们用->运算符解引用file来提取file指向的vector对象的push\_back成员。 2. 接下来我们用一个istringstream来处理刚刚读入的一行中的每个单词。内层while循环用istringstream 的输入运算符来从当前行读取每个单词,存入 word 中。在 while循环内,我们用map下标运算符提取与word相关联的shared\_ptr<set>,并将lines绑定到此指针。注意,lines是一个引用,因此改变lines也会改变wm中的元素。 3. 若word不在map中,下标运算符会将word添加到 wm中,与word关联的值进行值初始化。这意味着,如果下标运算符将word添加到wm中,lines 将是一个空指针。如果 lines为空,我们分配一个新的set,并调用reset更新lines引用的shared\_ptr,使其指向这个新分配的set。 4. 不管是否创建了一个新的set,我们都调用insert将当前行号添加到set 中。由于lines是一个引用,对insert的调用会将新元素添加到wm中的set中。如果一个给定单词在同一行中出现多次,对insert的调用什么都不会做。 #### QueryResult类 #### QueryResult类有三个数据成员: 1. 一个string,保存查询单词; 2. 一个shared\_ptr,指向保存输入文件的vector; 3. 一个shared\_ptr,指向保存单词出现行号的set。 它唯一的一个成员函数是一个构造函数,初始化这三个数据成员: class QueryResult { friend std::ostream& print(std::ostream&, const QueryResult&); public: QueryResult (std::string s, std::shared_ptr<std::set<line_no>> p, std::shared_ptr<std::vector<std::string>> f): sought(s), lines(p), file(f) { } private: std::string sought; //查询单词 std::shared_ptr<std::set<line_no>> lines; //出现的行号 std::shared_ptr<std::vector<std::string>> file; //输入文件 }; 构造函数的唯一工作是将参数保存在对应的数据成员中,这是在其初始化器列表中完成的。 #### query函数 #### query函数接受一个string参数,即查询单词,query用它来在map 中定位对应的行号set。如果找到了这个string,query函数构造一个QueryResult,保存给定string、TextQuery 的file成员以及从wm中提取的set。 唯一的问题是?如果给定string未找到,我们应该返回什么?在这种情况下,没有可返回的set。为了解决此问题,我们定义了一个局部static对象,它是一个指向空的行号set的shared\_ptr。当未找到给定单词时,我们返回此对象的一个拷贝: QueryResult TextQuery::query(const string &sought) const{ //如果未找到sought,我们将返回一个指向此set的指针 static shared_ptr<set<line_no>> nodata(new set<line_no>);//使用find而不是下标运算符来查找单词,避免将单词添加到wm中! auto loc = wm.find(sought) ; if (loc == wm.end()) return QueryResult(sought, nodata, file); //未找到 else return QueryResult(sought, loc->second, file); } #### 打印结果 #### print函数在给定的流上打印出给定的QueryResult对象: ostream &print (ostream & os, const QueryResult &qr) { //如果找到了单词,打印出现次数和所有出现的位置 os << qr.sought << " occurs " << qr.lines->size() << " " << make_plural(qr.lines->size(), "time", "s") << endl;//打印单词出现的每一行 for (auto num : *qr.lines) //对set中每个单词 //避免行号从0开始给用户带来的困惑 os <<" \t (line " << num + 1 << ") " << *(qr.file->begin() + num) << endl; return os; } 我们调用qr.lines指向的set的size成员来报告单词出现了多少次。由于set是一个shared ptr,必须解引用lines。调用make\_plural来根据大小是否等于1打印time或times。 //如果ctr的值大于1,返回word的复数形式 string make_plural(size_t ctr, const string &word, const string &ending) { return (ctr > 1) ? word + ending : word; } 在for循环中,我们遍历lines所指向的set。for循环体打印行号,并按人们习惯的方式调整计数值。set中的数值就是vector中元素的下标,从0开始编号。但大多数用户认为第一行的行号应该是1,因此我们对每个行号都加上1,转换为人们更习惯的形式。 我们用行号从file指向的vector中提取一行文本。回忆一下,当给一个迭代器加上一个数时,会得到vector中相应偏移之后位置的元素。因此,file->begin()+num即为file指向的vector中第 num个位置的元素。 注意此函数能正确处理未找到单词的情况。在此情况下,set为空。第一条输出语句会注意到单词出现了0次。由于\*res.lines为空,for循环一次也不会执行。
相关 C++ Primer:第12章 动态内存 -------------------- C++ Primer:第12章 动态内存 第12章 动态内存 12.1 动态内存与智能指针 超、凢脫俗/ 2023年03月13日 13:48/ 0 赞/ 47 阅读
相关 《C++ Primer 5th》笔记(8 / 19):IO库 文章目录 IO类 IO类型间的关系 IO对象无拷贝或赋值 条件状态 素颜马尾好姑娘i/ 2023年01月19日 08:19/ 0 赞/ 31 阅读
相关 《C++ Primer 5th》笔记(7 / 19):类 文章目录 定义抽象数据类型 设计Sales\_data类 关键概念:不同的编程角色 缺乏、安全感/ 2023年01月19日 04:47/ 0 赞/ 27 阅读
相关 《C++ Primer 5th》笔记(5 / 19):语句 文章目录 简单语句 空语句 别漏写分号,也别多写分号 复合语句(块) 语句作用域 墨蓝/ 2023年01月18日 04:28/ 0 赞/ 140 阅读
相关 《C++ Primer 5th》笔记(4 / 19):表达式 文章目录 基础 基本概念 组合运算符和运算对象 运算对象转换 缺乏、安全感/ 2023年01月17日 15:00/ 0 赞/ 168 阅读
相关 《C++ Primer 5th》笔记(1 / 19):C++基础 文章目录 编写一个简单的C++程序 编译、运行程序 初识输入输出 注释简介 控制流 爱被打了一巴掌/ 2023年01月14日 15:56/ 0 赞/ 178 阅读
相关 《C++ Primer 5th》笔记(12 / 19):动态内存 文章目录 动态内存与智能指针 shared\_ptr类 make\_shared函数 痛定思痛。/ 2022年10月16日 07:33/ 0 赞/ 230 阅读
相关 《C++ Primer 5th》笔记(11 / 19):关联容器 文章目录 使用关联容器 使用map 使用set 关联容器概述 定义关联容器 一时失言乱红尘/ 2022年10月14日 05:41/ 0 赞/ 385 阅读
相关 C++ primer 12章 动态内存 这一章,由于本身对C语言动态分配以及指针比较熟悉,所以看起来会很轻松的。 自动分配,static分配,以及动态分配对象,动态分配对象的生存期和在哪来创建无关,只有被显式 本是古典 何须时尚/ 2022年06月02日 03:39/ 0 赞/ 252 阅读
还没有评论,来说两句吧...