Skip to content

面向对象编程基础

本课程入选教育部产学合作协同育人项目 课程主页:http://cpp.njuer.org 课程老师:陈明 http://cv.mchen.org

ppt和代码下载地址
git clone https://gitee.com/cpp-njuer-org/book

第12章

动态内存

动态内存

到目前为止我们编写的程序中所使用的对象都有着严格定义的生存期
- 全局对象在程序启动时分配在程序结束时销毁
- 对于局部自动对象当我们进入其定义所在的程序块时被创建在离开块时销毁
- 局部static对象在第一次使用前分配在程序结束时销毁

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

动态内存

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

静态内存用来保存局部static对象类static数据成员以及定义在任何函数之外的变量
栈内存用来保存定义在函数内的非static对象

分配在静态或栈内存中的对象由编译器自动创建和销毁
对于栈对象仅在其定义的程序块运行时才存在
static对象在使用之前分配在程序结束时销毁

动态内存

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

虽然使用动态内存有时是必要的但众所周知正确地管理动态内存是非常棘手的

动态内存与智能指针

动态内存的管理是通过一对运算符来完成的
- new在动态内存中为对象分配空间并返回一个指向该对象的指针
  我们可以选择对对象进行初始化
- delete接受一个动态对象的指针销毁该对象并释放与之关联的内存

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

动态内存与智能指针

新的标准库提供了两种智能指针smart pointer类型来管理动态对象
智能指针的行为类似常规指针重要的区别是它负责自动释放所指向的对象

新标准库提供的这两种智能指针的区别在于管理底层指针的方式
- shared_ptr允许多个指针指向同一个对象
- unique_ptr则独占所指向的对象
- 标准库还定义了一个名为weak_ptr的伴随类
  它是一种弱引用指向shared_ptr所管理的对象

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

shared_ptr类

类似vector智能指针也是模板
创建一个智能指针时必须提供额外的信息——指针可以指向的类型

    shared_ptr<string> p1;
    shared_ptr<list<int>> p2;
//默认初始化的智能指针中保存着一个空指针
智能指针的使用方式与普通指针类似
//解引用一个智能指针返回它指向的对象。
//如果在一个条件判断中使用智能指针,效果就是检测它是否为空:
//如果p1不为空 检查是否指向空string
if(p1 && p1->empty()){
    *p1 = "hi";
}

shared_ptr和unique_ptr都支持的操作

 shared_ptr<T> sp  unique_ptr<T> 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<T>(args)   返回一个shared_ptr指向一个动态分配的类型为T的对象
                        使用args初始化此对象 
 shared_ptr<T>p(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共享对象的智能指针数量可能很慢主要用于调试

make_shared函数

最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数
此函数在动态内存中分配一个对象并初始化它返回指向此对象的shared_ptr
与智能指针一样make_shared也定义在头文件memory中

当要用make_shared时必须指定想要创建的对象的类型
定义方式与模板类相同在函数名之后跟一个尖括号在其中给出类型
    shared_ptr<int> p3 = make_shared<int>(42);
    shared_ptr<string> p4 = make_shared<string>(10,'9');
    shared_ptr<int> p5 = make_shared<int>();
//make_shared用其参数来构造给定类型的对象。
//例如,调用make_shared<string>时传递的参数必须与string的某个构造函数相匹配,
//调用make_shared<int>时传递的参数必须能用来初始化一个int,依此类推。
//如果我们不传递任何参数,对象就会进行值初始化
通常用auto定义一个对象来保存make_shared的结果这种方式较为简单
    auto p6 = make_shared<vector<string>>();

shared_ptr的拷贝和赋值

当进行拷贝或赋值操作时每个shared_ptr都会记录
有多少个其他shared_ptr指向相同的对象
    auto p = make_shared<int>(42);
    auto q(p);

我们可以认为每个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 = q;
//我们分配了一个int,将其指针保存在r中。
//接下来,我们将一个新值赋予r。
//r是唯一指向此int的shared_ptr,在把q赋给r的过程中,此int被自动释放。

到底是用一个计数器还是其他数据结构来记录有多少指针共享对象
完全由标准库的具体实现来决定

关键是智能指针类能记录有多少个shared_ptr指向相同的对象
并能在恰当的时候自动释放对象

shared_ptr自动销毁所管理的对象

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

析构函数一般用来释放对象所分配的资源
- 例如string的构造函数以及其他string成员会分配内存来保存构成string的字符
  string的析构函数就负责释放这些内存
- 类似的vector的若干操作都会分配内存来保存其元素
  vector的析构函数就负责销毁这些元素并释放它们所占用的内存

shared_ptr的析构函数会递减它所指向的对象的引用计数
- 如果引用计数变为0shared_ptr的析构函数就会销毁对象并释放它占用的内存

shared_ptr还会自动释放相关联的内存

当动态对象不再被使用时shared_ptr类会自动释放动态对象
这一特性使得动态内存的使用变得非常容易
- 例如我们可能有一个函数它返回一个shared_ptr指向一个Foo类型的动态分配
  的对象对象是通过一个类型为T的参数进行初始化的
  shared_ptr<foo> factory(T arg){
    //恰当处理arg shared_ptr负责释放内存
    return shared_ptr<Foo>(arg);
  }
  //由于factory返回一个shared_ptr,
  //所以我们可以确保它分配的对象会在恰当的时刻被释放。
- 例如下面的函数将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也指向这块内存它就不会被释放掉
  shared_ptr<Foo> 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元素

//如果你将shared_ptr存放于一个容器中,而后不再需要全部元素,
//而只使用其中一部分,要记得用erase删除不再需要的那些元素。

使用了动态生存期的资源的类

程序使用动态内存出于以下三种原因之一
1.程序不知道自己需要使用多少对象
2.程序不知道所需对象的准确类型
3.程序需要在多个对象间共享数据

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

到目前为止,我们使用过的类中,

分配的资源都与对应对象生存期一致。

例如每个vector拥有其自己的元素
当我们拷贝一个vector时原vector和副本vector中的元素是相互分离的
vector<string> v1; // empty vector
{ // new scope
    vector<string> v2 = {"a", "an", "the"};
    v1 = v2; // copies the elements from v2 into v1
} // v2 is destroyed, which destroys the elements in v2
  // v1 has three elements, which are copies of the ones originally in v2
由一个vector分配的元素只有当这个vector存在时才存在
当一个vector被销毁时这个vector中的元素也都被销毁

使用动态内存 允许多个对象共享相同的状态

但某些类分配的资源具有与原对象相独立的生存期
例如假定我们希望定义一个名为Blob的类保存一组元素
与容器不同我们希望Blob对象的不同拷贝之间共享相同的元素
当我们拷贝一个Blob时原Blob对象及其拷贝应该引用相同的底层元素
如果两个对象共享底层的数据当某个对象被销毁时我们不能单方面地销毁底层数据
Blob<string> b1;    // empty Blob
{ // new scope
    Blob<string> b2 = {"a", "an", "the"};
    b1 = b2; // b1 and b2 share the same elements
} // b2 is destroyed, but the elements in b2 must not be destroyed
  // b1 points to the elements originally created in b2
在此例中b1和b2共享相同的元素
当b2离开作用域时这些元素必须保留因为b1仍然在使用它们

定义StrBlob类

现在我们先定义一个管理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):
        - 如果用户试图访问不存在的元素这些操作会抛出一个异常

定义StrBlob类

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;
};
//我们的类有一个默认构造函数和一个构造函数,
//接受单一的initializer_list<string>类型参数
//此构造函数可以接受一个初始化器的花括号列表。

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构造函数
//此构造函数通过拷贝列表中的值来初始化vector的元素。

元素访问成员函数

//名为check的private工具函数,它检查一个给定索引是否在合法范围内
void StrBlob::check(size_type i, const string &msg) const
{
    if (i >= data->size())
        throw out_of_range(msg);
}

元素访问成员函数

//pop_back和元素访问成员函数首先调用check。
//如果check成功,这些成员函数继续利用底层vector的操作来完成自己的工作:
string& StrBlob::front()
{
    // if the vector is empty, check will throw
    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进行重载,这些版本的定义留作练习。

StrBlob的拷贝、赋值和销毁

StrBlob使用默认版本的拷贝赋值和销毁成员函数来对此类型的对象进行这些操作
默认情况下这些操作拷贝赋值和销毁类的数据成员
当我们拷贝赋值或销毁一个StrBlob对象时它的shared_ptr成员会被拷贝赋值或销毁

如前所见拷贝一个shared_ptr会递增其引用计数
- 将一个shared_ptr赋予另一个shared_ptr会递增赋值号右侧shared_ptr的引用计数
- 而递减左侧shared_ptr的引用计数

如果一个shared_ptr的引用计数变为0它所指向的对象会被自动销毁
- 对于由StrBlob构造函数分配的vector当最后一个指向它的StrBlob对象被销毁时
  它会随之被自动销毁

练习

在此代码的结尾b1  b2 各包含多少个元素
StrBlob b1;
{
    StrBlob b2 = {"a", "an", "the"};
    b1 = b2;
    b2.push_back("about");
}

它们实际操作的是同一个vector都包含4个元素
在代码的结尾b2 被析构了不影响 b1 的元素

练习

编写你自己的StrBlob 包含const 版本的 front  back
//StrBlb.h 文件:
#include <vector>
#include <string>
#include <initializer_list>
#include <memory>
#include <exception>

using std::vector; using std::string;

class StrBlob {
public:
    using size_type = vector<string>::size_type;

    StrBlob():data(std::make_shared<vector<string>>()) { }
    StrBlob(std::initializer_list<string> il
                ):data(std::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();
    }

    std::string& front() {
        check(0, "front on empty StrBlob");
        return data->front();
    }

    std::string& back() {
        check(0, "back on empty StrBlob");
        return data->back();
    }

练习

    const std::string& front() const {
        check(0, "front on empty StrBlob");
        return data->front();
    }
    const std::string& back() const {
        check(0, "back on empty StrBlob");
        return data->back();
    }

private:
    void check(size_type i, const string &msg) const {
        if (i >= data->size()) throw std::out_of_range(msg);
    }

private:
    std::shared_ptr<vector<string>> data;
};

练习

主函数

#include "StrBlb.h"
#include <iostream>

int main()
{
    const StrBlob csb{ "hello", "world", "pezy" };
    StrBlob sb{ "hello", "world", "Mooophy" };

    std::cout << csb.front() << " " << csb.back() << std::endl;
    sb.back() = "pezy";
    std::cout << sb.front() << " " << sb.back() << std::endl;
}

练习

StrBlob 需要const 版本的push_back  pop_back吗
如需要添加进去否则解释为什么不需要

不需要push_back  pop_back 会改变对象的内容
 const 对象是只读的因此不需要

练习

在我们的 check 函数中没有检查 i 是否大于0为什么可以忽略这个检查

因为 size_type 是一个无符号整型
当传递给 check 的参数小于 0 的时候参数值会转换成一个正整数

练习

我们未编写接受一个 initializer_list explicit 参数的构造函数
讨论这个设计策略的优点和缺点


构造函数不是 explicit 意味着可以从 initializer_list 隐式转换为 StrBlob
 StrBlob 对象中只有一个数据成员 data
 StrBlob 对象本身的含义也是一个管理字符串的序列
因此 initializer_list  StrBlob 的转换在逻辑上是可行的

而这个设计策略的缺点可能在某些地方我们确实需要 initializer_list
而编译器仍会将之转换为 StrBlob

直接管理内存

C++语言定义了两个运算符来分配和释放动态内存
- 运算符new分配内存
- delete释放new分配的内存
- 使用这两个运算符管理内存非常容易出错
- 直接管理内存的类与使用智能指针的类不同
  它们不能依赖类对象拷贝赋值和销毁操作的任何默认定义

使用智能指针的程序更容易编写和调试

使用new动态分配和初始化对象

在自由空间分配的内存是无名的因此new无法为其分配的对象命名
而是返回一个指向该对象的指针
int *pi = new int;      // pi points to a dynamically allocated,
                        // unnamed, uninitialized int
//此new表达式在自由空间构造一个int型对象,并返回指向该对象的指针。
默认情况下动态分配的对象是默认初始化的
这意味着内置类型或组合类型的对象的值将是未定义的
而类类型对象将用默认构造函数进行初始化
string *ps = new string;  // initialized to empty string
int *pi = new int;        // pi points to an uninitialized int

使用new动态分配和初始化对象

我们可以使用直接初始化方式来初始化一个动态分配的对象
我们可以使用传统的构造方式使用圆括号),
在新标准下也可以使用列表初始化使用花括号):
int *pi = new int(1024); // object to which pi points has value 1024
string *ps = new string(10, '9');   // *ps is "9999999999"
// vector with ten elements with values from 0 to 9
vector<int> *pv = new vector<int>{0,1,2,3,4,5,6,7,8,9};

也可以对动态分配的对象进行值初始化只需在类型名之后跟一对空括号即可
string *ps1 = new string;  //default initialized to the empty string
string *ps = new string(); //value initialized to the empty string
int *pi1 = new int;        //default initialized; *pi1 is undefined
int *pi2 = new int();      //value initialized to 0; *pi2 is 0

使用new动态分配和初始化对象

对于定义了自己的构造函数的类类型例如string来说要求值初始化是没有意义的
- 不管采用什么形式对象都会通过默认构造函数来初始化
但对于内置类型两种形式的差别就很大了
- 值初始化的内置类型对象有着良好定义的值
- 而默认初始化的对象的值则是未定义的类似的

对于类中那些依赖于编译器合成的默认构造函数的内置类型成员
- 如果它们未在类内被初始化那么它们的值也是未定义的

出于与变量初始化相同的原因对动态分配的对象进行初始化通常是个好主意

使用new动态分配和初始化对象

如果我们提供了一个括号包围的初始化器就可以使用auto,
从此初始化器来推断我们想要分配的对象的类型

但是由于编译器要用初始化器的类型来推断要分配的类型
只有当括号中仅有单一初始化器时才可以使用auto
auto p1 = new auto(obj);   // p points to an object of the type of obj
                           // that object is initialized from obj
auto p2 = new auto{a,b,c}; // error:  括号中只能有单个初始化器
//p1的类型是一个指针,指向从obj自动推断出的类型。
//若obj是一个int,那么p1就是int*;
//若obj是一个string,那么p1是一个string*;
//依此类推。新分配的对象用obj的值进行初始化。

动态分配的const对象

用new分配const对象是合法的
// allocate and initialize a const int
const int *pci = new const int(1024);
// allocate a default-initialized const empty string
const string *pcs = new const string;
类似其他任何const对象一个动态分配的const对象必须进行初始化
对于一个定义了默认构造函数的类类型其const动态对象可以隐式初始化
而其他类型的对象就必须显式初始化
由于分配的对象是const的new返回的指针是一个指向const的指针

内存耗尽

虽然现代计算机通常都配备大容量内存但是自由空间被耗尽的情况还是有可能发生
一旦一个程序用光了它所有可用的内存new表达式就会失败
- 默认情况下如果new不能分配所要求的内存空间它会抛出一个类型为bad_alloc的异常
- 我们可以改变使用new的方式来阻止它抛出异常
// if allocation fails, new returns a null pointer
int *p1 = new int; // if allocation fails, new throws std::bad_alloc
int *p2 = new (nothrow) int; // if allocation fails, new returns a null pointer
我们称这种形式的new为定位newplacement new
定位new表达式允许我们向new传递额外的参数
//在此例中,我们传递给它一个由标准库定义的名为nothrow的对象
//如果将nothrow传递给new,我们的意图是告诉它不能抛出异常。
//如果这种形式的new不能分配所需内存,它会返回一个空指针。
bad_alloc和nothrow都定义在头文件new中

释放动态内存

为了防止内存耗尽在动态内存使用完毕后必须将其归还给系统
我们通过delete表达式delete expression来将动态内存归还给系统
delete表达式接受一个指针指向我们想要释放的对象
delete p;      // p must point to a dynamically allocated object or be null

与new类型类似delete表达式也执行两个动作
- 销毁给定的指针指向的对象
- 释放对应的内存

指针值和delete

我们传递给delete的指针必须指向动态分配的内存或者是一个空指针
释放一块并非new分配的内存或者将相同的指针值释放多次其行为是未定义的
int i,*pi1 = &i, *pi2 = nullptr;
double *pd = new double(33), *pd2 = pd;
delete i;   // error: i is not a pointer
delete pi1; // undefined: pi1 refers to a local
delete pd;  // ok
delete pd2; // undefined: the memory pointed to by pd2 was already freed
delete pi2; // ok: it is always ok to delete a null pointer
//对于delete i的请求,编译器会生成一个错误信息,因为它知道i不是一个指针。
//执行delete pi1和pd2所产生的错误则更具潜在危害:
//通常情况下,编译器不能分辨一个指针指向的是静态还是动态分配的对象。
//类似的,编译器也不能分辨一个指针所指向的内存是否已经被释放了。
//对于这些delete表达式,大多数编译器会编译通过,尽管它们是错误的

指针值和delete

虽然一个const对象的值不能被改变但它本身是可以被销毁的
如同任何其他动态对象一样想要释放一个const动态对象
只要delete指向它的指针即可
const int *pci = new const int(1024);
delete pci;  // ok: deletes a const object

动态对象的生存期直到被释放时为止

由shared_ptr管理的内存在最后一个shared_ptr销毁时会被自动释放
但对于通过内置指针类型来管理的内存就不是这样了
对于一个由内置指针管理的动态对象直到被显式释放之前它都是存在的
返回指向动态内存的指针而不是智能指针的函数给其调用者增加了一个额外负担
 调用者必须记得释放内存
// factory returns a pointer to a dynamically allocated object
Foo* factory(T arg)
{
    // process arg as appropriate
    return new Foo(arg); // caller is responsible for deleting this memory
}
//这个版本的factory分配一个对象,但并不delete它。
//factory的调用者负责在不需要此对象时释放它。
//不幸的是,调用者经常忘记释放对象:

动态对象的生存期直到被释放时为止

void use_factory(T arg)
{
    Foo *p = factory(arg);
    // use p but do not delete it
} // p goes out of scope, but the memory to which p points is not freed!

//此处,use_factory函数调用factory,后者分配一个类型为Foo的新对象。
//当use_factory返回时,局部变量p被销毁。
//此变量是一个内置指针,而不是一个智能指针。
与类类型不同内置类型的对象被销毁时什么也不会发生
特别是当一个指针离开其作用域时它所指向的对象什么也不会发生
如果这个指针指向的是动态内存那么内存将不会被自动释放

由内置指针而不是智能指针管理的动态内存在被显式释放前一直都会存在

p是指向factory分配的内存的唯一指针
一旦use_factory返回程序就没有办法释放这块内存了
根据整个程序的逻辑修正这个错误的正确方法是在use_factory中记得释放内存
void use_factory(T arg)
{
    Foo *p = factory(arg);
    // use p
    delete p;  // remember to free the memory now that we no longer need it
}
还有一种可能我们的系统中的其他代码要使用use_factory所分配的对象
我们就应该修改此函数让它返回一个指针指向它分配的内存
Foo* use_factory(T arg)
{
    Foo *p = factory(arg);
    // use p
    return p;  // caller must delete the memory
}

动态内存的管理非常容易出错

使用new和delete管理动态内存存在三个常见问题
1.忘记delete内存
  忘记释放动态内存会导致人们常说的内存泄漏问题
  因为这种内存永远不可能被归还给自由空间了查找内存泄露错误是非常困难的
  因为通常应用程序运行很长时间后真正耗尽内存时才能检测到这种错误
2.使用已经释放掉的对象
  通过在释放内存后将指针置为空有时可以检测出这种错误
3.同一块内存释放两次
  当有两个指针指向相同的动态分配对象时可能发生这种错误
  如果对其中一个指针进行了delete操作对象的内存就被归还给自由空间了
  如果我们随后又delete第二个指针自由空间就可能被破坏
  相对于查找和修正这些错误来说制造出这些错误要简单得多

坚持只使用智能指针就可以避免所有这些问题
对于一块内存只有在没有任何智能指针指向它的情况下智能指针才会自动释放它

delete之后重置指针值

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

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

这只是提供了有限的保护

动态内存的一个基本问题是可能有多个指针指向相同的内存
在delete内存之后重置指针的方法只对这个指针有效
对其他任何仍指向已释放的内存的指针是没有作用的例如
int *p(new int(42));  // p points to dynamic memory
auto q = p;           // p and q point to the same memory
delete p;    // invalidates both p and q
p = nullptr; // indicates that p is no longer bound to an object
//本例中p和q指向相同的动态分配的对象。
//我们delete此内存,然后将p置为nullptr,指出它不再指向任何对象。
//但是,重置p对q没有任何作用,
//在我们释放p所指向的(同时也是q所指向的!)内存时,q也变为无效了。
//在实际系统中,查找指向相同内存的所有指针是异常困难的。

练习

编写函数返回一个动态分配的 int 的vector将此vector 传递给另一个函数
这个函数读取标准输入将读入的值保存在 vector 元素中
再将vector传递给另一个函数打印读入的值记得在恰当的时刻delete vector

#include <iostream>
#include <vector>
using std::vector;

vector<int>* alloc_vector()
{
    return new vector<int>();
}

练习

void assign_vector(vector<int>* p)
{
    int i;
    while (std::cin >> i)
    {
        p->push_back(i);
    }
}

void print_vector(vector<int>* p)
{
    for (auto i : *p)
    {
        std::cout << i << std::endl;
    }
}

练习

int main()
{
    auto p = alloc_vector();
    assign_vector(p);
    print_vector(p);
    delete p;
    return 0;
}

练习

重做上一题这次使用 shared_ptr 而不是内置指针

#include <iostream>
#include <vector>
#include <memory>

using std::vector;

std::shared_ptr<vector<int>> alloc_vector()
{
    return std::make_shared<vector<int>>();
}

void assign_vector(std::shared_ptr<vector<int>> p)
{
    int i;
    while (std::cin >> i)
    {
        p->push_back(i);
    }
}

练习

void print_vector(std::shared_ptr<vector<int>> p)
{
    for (auto i : *p)
    {
        std::cout << i << std::endl;
    }
}

int main()
{
    auto p = alloc_vector();
    assign_vector(p);
    print_vector(p);
    return 0;
}

练习

下面的函数是否有错误如果有解释错误原因
bool b() {
    int* p = new int;
    // ...
    return p;
}


有错误p会被强制转换成bool继而没有释放指针 p 指向的对象

练习

解释下面代码执行的结果
int *q = new int(42), *r = new int(100);
r = q;
auto q2 = make_shared<int>(42), r2 = make_shared<int>(100);
r2 = q2;

r  q 指向 42
而之前 r 指向的 100 的内存空间并没有被释放因此会发生内存泄漏
r2  q2 都是智能指针当对象空间不被引用的时候会自动释放

定义和改变shared_ptr的其他方法

 shared_ptr<T> p(q)     p管理内置指针q所指向的对象
                        q必须指向new分配的内存且能够转换为T*类型 
 shared_ptr<T> p(u)     p从unique_ptr u那里接管了对象的所有权将u置为空 
 shared_ptr<T> p(q, d)  p接管了内置指针q所指向的对象的所有权
                        q必须能转换为T*类型p将使用可调用对象d来代替delete 
 shared_ptr<T> p(p2, d) p是shared_ptr p2的拷贝
                        唯一的区别是p将可调用对象d来代替delete 
 p.reset()              若p是唯一指向其对象的shared_ptrreset会释放此对象
                        若传递了可选的参数内置指针q会令p指向q否则会将p置空
                        若还传递了参数d则会调用d而不是delete来释放q 
 p.reset(q)             同上 
 p.reset(q, d)          同上 

shared_ptr和new结合使用

如果我们不初始化一个智能指针它就会被初始化为一个空指针
我们还可以用new返回的指针来初始化智能指针
shared_ptr<double> p1; // shared_ptr that can point at a double
shared_ptr<int> p2(new int(42)); // p2 points to an int with value 42

接受指针参数的智能指针构造函数是explicit的
因此我们不能将一个内置指针隐式转换为一个智能指针
必须使用直接初始化形式来初始化一个智能指针
shared_ptr<int> p1 = new int(1024);  // error: must use direct initialization
shared_ptr<int> p2(new int(1024));   // ok: uses direct initialization
//p1的初始化隐式地要求编译器用一个new返回的int*来创建一个shared_ptr。
//由于我们不能进行内置指针到智能指针间的隐式转换,因此这条初始化语句是错误的。

shared_ptr和new结合使用

出于相同的原因一个返回shared_ptr的函数不能在其返回语句中隐式转换一个普通指针
shared_ptr<int> clone(int p) {
    return new int(p); // error: implicit conversion to shared_ptr<int>
}
我们必须将shared_ptr显式绑定到一个想要返回的指针上
shared_ptr<int> clone(int p) {
    // ok: explicitly create a shared_ptr<int> from int*
    return shared_ptr<int>(new int(p));
}

shared_ptr和new结合使用

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

不要混合使用普通指针和智能指针

shared_ptr可以协调对象的析构但这仅限于其自身的拷贝也是shared_ptr之间
这也是为什么我们推荐使用make_shared而不是new的原因
这样我们就能在分配对象的同时就将shared_ptr与之绑定
从而避免了无意中将同一块内存绑定到多个独立创建的shared_ptr上

不要混合使用普通指针和智能指针

下面对shared_ptr进行操作的函数
// ptr is created and initialized when process is called
void process(shared_ptr<int> ptr)
{
    // use ptr
} // ptr goes out of scope and is destroyed

//process的参数是传值方式传递的,因此实参会被拷贝到ptr中。
//拷贝一个shared_ptr会递增其引用计数,
//因此,在process运行过程中,引用计数值至少为2。
//当process结束时,ptr的引用计数会递减,但不会变为0。
//因此,当局部变量ptr被销毁时,ptr指向的内存不会被释放。

使用此函数的正确方法是传递给它一个shared_ptr
shared_ptr<int> p(new int(42)); // reference count is 1
process(p); // copying p increments its count; 
            //in process the reference count is 2
int i = *p; // ok: reference count is 1

不要混合使用普通指针和智能指针

这样做很可能会导致错误
int *x(new int(1024)); // dangerous: x is a plain pointer, not a smart pointer
process(x);  // error: cannot convert int* to shared_ptr<int>
process(shared_ptr<int>(x)); // legal, but the memory will be deleted!
int j = *x;  // undefined: x is a dangling pointer!

//将一个临时shared_ptr传递给process。
//当这个调用所在的表达式结束时,这个临时对象就被销毁了。
//销毁这个临时变量会递减引用计数,此时引用计数就变为0了。
//因此,当临时对象被销毁时,它所指向的内存会被释放。
//但x继续指向(已经释放的)内存,从而变成一个空悬指针。
//如果试图使用x的值,其行为是未定义的。

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

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

不要使用get初始化另一个智能指针或为智能指针赋值

智能指针类型定义了一个名为get的函数它返回一个内置指针指向智能指针管理的对象
此函数是为了这样一种情况而设计的
- 我们需要向不能使用智能指针的代码传递一个内置指针
- 使用get返回的指针的代码不能delete此指针

虽然编译器不会给出错误信息但将另一个智能指针也绑定到get返回的指针上是错误的
shared_ptr<int> p(new int(42)); // reference count is 1
int *q = p.get();  // ok: but don't use q in any way that might delete its pointer
{ // new block
// undefined: two independent shared_ptrs point to the same memory
shared_ptr<int>(q);
} // block ends, q is destroyed, and the memory to which q points is freed
int foo = *p; // undefined; the memory to which p points was freed
//p和q指向相同的内存。由于它们是相互独立创建的,因此各自的引用计数都是1。
//当q所在的程序块结束时,q被销毁,这会导致q指向的内存被释放。
//从而p变成一个空悬指针,意味着当我们试图使用p时,将发生未定义的行为。
//而且,当p被销毁时,这块内存会被第二次delete。

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

其他shared_ptr操作

可以用reset来将一个新的指针赋予一个shared_ptr
p = new int(1024);       // error: cannot assign a pointer to a shared_ptr
p.reset(new int(1024));  // ok: p points to a new object

与赋值类似reset会更新引用计数如果需要的话会释放p指向的对象
reset成员经常与unique一起使用来控制多个shared_ptr共享的对象
在改变底层对象之前我们检查自己是否是当前对象仅有的用户
如果不是在改变之前要制作一份新的拷贝

if (!p.unique())
    p.reset(new string(*p)); // we aren't alone; allocate a new copy
*p += newVal; // now that we know we're the only pointer, okay to change this object

练习

下面的代码调用了process 函数解释此调用是否正确如果不正确应如何修改
shared_ptr<int> p(new int(42));
process(shared_ptr<int>(p));

正确shared_ptr<int>(p) 会创建一个临时的智能指针
这个智能指针与 p 引用同一个对象此时引用计数为 2
当表达式结束时临时的智能指针被销毁此时引用计数为 1

练习

如果我们像下面这样调用 process会发生什么
process(shared_ptr<int>(p.get()));

这样会创建一个新的智能指针它的引用计数为 1
这个智能指针所指向的空间与 p 相同
在表达式结束后这个临时智能指针会被销毁引用计数为 0
所指向的内存空间也会被释放
而导致 p 所指向的空间被释放使得 p 成为一个空悬指针

练习

p  sp 的定义如下对于接下来的对 process 的每个调用
如果合法解释它做了什么如果不合法解释错误原因
auto p = new int();
auto sp = make_shared<int>();
(a) process(sp);
(b) process(new int());
(c) process(p);
(d) process(shared_ptr<int>(p));


(a) 合法将sp 拷贝给 process函数的形参
    在函数里面引用计数为 2函数结束后引用计数为 1
(b) 不合法不能从内置指针隐式转换为智能指针
(c) 不合法不能从内置指针隐式转换为智能指针
(d) 合法但是智能指针和内置指针一起使用可能会出现问题
    在表达式结束后智能指针会被销毁它所指向的对象也被释放
    而此时内置指针 p 依旧指向该内存空间
    之后对内置指针 p 的操作可能会引发错误

练习

如果执行下面的代码会发生什么
auto sp = make_shared<int>();
auto p = sp.get();
delete p;

智能指针 sp 所指向空间已经被释放再对 sp 进行操作会出现错误

智能指针和异常

程序需要确保在异常发生后资源能被正确地释放
一个简单的确保资源被释放的方法是使用智能指针
如果使用智能指针即使程序块过早结束智能指针类也能确保在内存不再需要时将其释放
void f()
{
    shared_ptr<int> sp(new int(42)); // allocate a new object
   // code that throws an exception that is not caught inside f
} // shared_ptr freed automatically when the function ends
//函数的退出有两种可能,正常处理结束或者发生了异常,
//无论哪种情况,局部对象都会被销毁。
//在上面的程序中,sp是一个shared_ptr,因此sp销毁时会检查引用计数。
//在此例中,sp是指向这块内存的唯一指针,因此内存会被释放掉。

智能指针和异常

当发生异常时我们直接管理的内存是不会自动释放的
如果使用内置指针管理内存且在new之后在对应的delete之前发生了异常则内存不会被释放
void f()
{
    int *ip = new int(42);     // dynamically allocate a new object
    // code that throws an exception that is not caught inside f
    delete ip;                 // free the memory before exiting
}
//如果在new和delete之间发生异常,且异常未在f中被捕获,则内存就永远不会被释放了。
//在函数f之外没有指针指向这块内存,因此就无法释放它了。

智能指针和哑类

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

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

智能指针和哑类

与管理动态内存类似,可以使用类似的技术来管理不具有良好定义的析构函数的类
struct destination;  // represents what we are connecting to
struct connection;   // information needed to use the connection
connection connect(destination*);  // open the connection
void disconnect(connection);       // close the given connection
void f(destination &d /* other parameters */)
{
    // get a connection; must remember to close it when done
    connection c = connect(&d);
    // use the connection
    // if we forget to call disconnect before exiting f, there will be no way to close c
}
如果connection有一个析构函数就可以在f结束时由析构函数自动关闭连接
但是connection没有析构函数
这个问题与我们上一个程序中使用shared_ptr避免内存泄漏几乎是等价的
使用shared_ptr来保证connection被正确关闭已被证明是一种有效的方法

使用我们自己的释放操作

当一个shared_ptr被销毁时它默认地对它管理的指针进行delete操作
为了用shared_ptr来管理一个connection我们必须首先定义一个函数来代替delete
这个删除器deleter函数必须能够完成对shared_ptr中保存的指针进行释放的操作
void end_connection(connection *p) { disconnect(*p); }
当我们创建一个shared_ptr时可以传递一个可选的指向删除器函数的参数
void f(destination &d /* other parameters */)
{
    connection c = connect(&d);
    shared_ptr<connection> p(&c, end_connection);
    // use the connection
    // when f exits, even if by an exception, the connection will be properly closed
}
//当p被销毁时,它不会对自己保存的指针执行delete,而是调用end_connection。
//接下来,end_connection会调用disconnect,从而确保连接被关闭。
//如果f正常退出,那么p的销毁会作为结束处理的一部分。
//如果发生了异常,p同样会被销毁,从而连接被关闭。

注意:智能指针陷阱

智能指针可以提供对动态分配的内存安全而又方便的管理但这建立在正确使用的前提下
为了正确使用智能指针我们必须坚持一些基本规范
- 不使用相同的内置指针值初始化或reset多个智能指针。· 
- 不delete get()返回的指针。· 
- 不使用get()初始化或reset另一个智能指针。· 
- 如果你使用get()返回的指针记住当最后一个对应的智能指针销毁后
  你的指针就变为无效了。· 
- 如果你使用智能指针管理的资源不是new分配的内存记住传递给它一个删除器

练习

编写你自己版本的用 shared_ptr 管理 connection 的函数

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

struct connection
{
    std::string ip;
    int port;
    connection(std::string i, int p) : ip(i), port(p) {}
};

struct destination
{
    std::string ip;
    int port;
    destination(std::string i, int p) : ip(i), port(p) {}
};

练习

connection connect(destination* pDest)
{
    std::shared_ptr<connection> 
            pConn(new connection(pDest->ip, pDest->port));
    std::cout << "creating connection(" 
            << pConn.use_count() << ")" << std::endl;
    return *pConn;
}

void disconnect(connection pConn)
{
    std::cout << "connection close(" << pConn.ip << ":" 
            << pConn.port << ")" << std::endl;    
}

void end_connection(connection* pConn)
{
    disconnect(*pConn);
}

练习

void f(destination &d)
{
    connection conn = connect(&d);
    std::shared_ptr<connection> p(&conn, end_connection);
    std::cout << "connecting now(" << p.use_count() << ")" << std::endl;
}

int main()
{
    destination dest("220.181.111.111", 10086);
    f(dest);

    return 0;
}

练习

重写上一题的程序 lambda 代替end_connection 函数

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

struct connection
{
    std::string ip;
    int port;
    connection(std::string i, int p) : ip(i), port(p) {}
};

struct destination
{
    std::string ip;
    int port;
    destination(std::string i, int p) : ip(i), port(p) {}
};

练习

connection connect(destination* pDest)
{
    std::shared_ptr<connection> 
                pConn(new connection(pDest->ip, pDest->port));
    std::cout << "creating connection(" 
                << pConn.use_count() << ")" << std::endl;
    return *pConn;
}

void disconnect(connection pConn)
{
    std::cout << "connection close(" 
                << pConn.ip << ":" << pConn.port << ")" << std::endl;
}

练习

void f(destination &d)
{
    connection conn = connect(&d);
    std::shared_ptr<connection> 
                p(&conn, [] (connection* p){ disconnect(*p); });
    std::cout << "connecting now(" << p.use_count() << ")" << std::endl;
}

int main()
{
    destination dest("220.181.111.111", 10086);
    f(dest);

    return 0;
}

unique_ptr

一个unique_ptr拥有它所指向的对象
与shared_ptr不同某个时刻只能有一个unique_ptr指向一个给定对象
当unique_ptr被销毁时它所指向的对象也被销毁

当我们定义一个unique_ptr时需要将其绑定到一个new返回的指针上
unique_ptr<double> p1;  // unique_ptr that can point at a double
unique_ptr<int> p2(new int(42)); // p2 points to int with value 42

由于一个unique_ptr拥有它指向的对象因此unique_ptr不支持普通的拷贝或赋值操作
unique_ptr<string> p1(new string("Stegosaurus"));
unique_ptr<string> p2(p1);  // error: no copy for unique_ptr
unique_ptr<string> p3;
p3 = p2;                    // error: no assign for unique_ptr

unique_ptr操作

unique_ptr<T> u1        空unique_ptr可以指向类型是T的对象
                        u1会使用delete来是释放它的指针 
unique_ptr<T, D> u2     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.reset(q)              令u指向q指向的对象 
u.reset(nullptr)        将u置空 

unique_ptr

虽然我们不能拷贝或赋值unique_ptr但可以通过调用release或reset将
指针的所有权从一个非constunique_ptr转移给另一个unique
// transfers ownership from p1 (which points to the string Stegosaurus) to p2
unique_ptr<string> p2(p1.release()); // release makes p1 null
unique_ptr<string> p3(new string("Trex"));
// transfers ownership from p3 to p2
p2.reset(p3.release()); // reset deletes the memory to which p2 had pointed
//release成员返回unique_ptr当前保存的指针并将其置为空。
//因此,p2被初始化为p1原来保存的指针,而p1被置为空。

reset成员接受一个可选的指针参数令unique_ptr重新指向给定的指针
如果unique_ptr不为空它原来指向的对象被释放
因此对p2调用reset释放了用"Stegosaurus"初始化的string所使用的内存
将p3对指针的所有权转移给p2并将p3置为空

unique_ptr

调用release会切断unique_ptr和它原来管理的对象间的联系
release返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值
在本例中管理内存的责任简单地从一个智能指针转移给另一个
如果我们不用另一个智能指针来保存release返回的指针我们的程序就要负责资源的释放
p2.release(); // WRONG: p2 won't free the memory and we've lost the pointer
auto p = p2.release(); // ok, but we must remember to delete(p)

传递unique_ptr参数和返回unique_ptr

不能拷贝unique_ptr的规则有一个例外
我们可以拷贝或赋值一个将要被销毁的unique_ptr
最常见的例子是从函数返回一个unique_ptr
unique_ptr<int> clone(int p) {
    // ok: explicitly create a unique_ptr<int> from int*
    return unique_ptr<int>(new int(p));
}
还可以返回一个局部对象的拷贝
unique_ptr<int> clone(int p) {
    unique_ptr<int> ret(new int (p));
    // . . .
    return ret;
}
//对于两段代码,编译器都知道要返回的对象将要被销毁。
//在此情况下,编译器执行一种特殊的“拷贝”

向后兼容:auto_ptr

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

向unique_ptr传递删除器

unique_ptr默认情况下用delete释放它指向的对象
与shared_ptr一样我们可以重载一个unique_ptr中默认的删除器
但是unique_ptr管理删除器的方式与shared_ptr不同

重载一个unique_ptr中的删除器会影响到unique_ptr类型以及
如何构造或reset该类型的对象与重载关联容器的比较操作类似
我们必须在尖括号中unique_ptr指向类型之后提供删除器类型
在创建或reset一个这种unique_ptr类型的对象时
必须提供一个指定类型的可调用对象删除器):
// p points to an object of type objT 
//and uses an object of type delT to free that object
// it will call an object named fcn of type delT
unique_ptr<objT, delT> p (new objT, fcn);

重写连接程序,用unique_ptr来代替shared_ptr

void f(destination &d /* other needed parameters */)
{
    connection c = connect(&d);  // open the connection
    // when p is destroyed, the connection will be closed
    unique_ptr<connection, decltype(end_connection)*>
        p(&c, end_connection);
    // use the connection
    // when f exits, even if by an exception, 
        //the connection will be properly closed
}
//在本例中我们使用了decltype来指明函数指针类型。
//由于decltype(end_connection)返回一个函数类型,
//所以我们必须添加一个*来指出我们正在使用该类型的一个指针

练习

如果你试图拷贝或赋值 unique_ptr编译器并不总是能给出易于理解的错误信息
编写包含这种错误的程序观察编译器如何诊断这种错误

#include <iostream>
#include <string>
#include <memory>
using std::string; using std::unique_ptr;
int main()
{
    unique_ptr<string> p1(new string("pezy"));
    // unique_ptr<string> p2(p1); // copy
    //                      ^
    // Error: Call to implicitly-deleted copy constructor of 'unique_ptr<string>'
    //
    // unique_ptr<string> p3 = p1; // assign
    //                      ^
    // Error: Call to implicitly-deleted copy constructor of 'unique_ptr<string>'
    std::cout << *p1 << std::endl;
    p1.reset(nullptr);
}

练习

下面的 unique_ptr 声明中哪些是合法的哪些可能导致后续的程序错误
解释每个错误的问题在哪里
int ix = 1024, *pi = &ix, *pi2 = new int(2048);
typedef unique_ptr<int> IntP;
(a) IntP p0(ix);
(b) IntP p1(pi);
(c) IntP p2(pi2);
(d) IntP p3(&ix);
(e) IntP p4(new int(2048));
(f) IntP p5(p2.get());

(a) 不合法在定义一个 unique_ptr 需要将其绑定到一个new 返回的指针上
(b) 不合法理由同上
(c) 合法但是也可能会使得 pi2 成为空悬指针
(d) 不合法 p3 被销毁时它试图释放一个栈空间的对象
(e) 合法
(f) 不合法p5  p2 指向同一个对象 p5  p2 被销毁时会使得同一个指针被释放两次

练习

shared_ptr 为什么没有 release 成员

release 成员的作用是放弃控制权并返回指针
因为在某一时刻只能有一个 unique_ptr 指向某个对象unique_ptr 不能被赋值
所以要使用 release 成员将一个 unique_ptr 的指针的所有权传递给另一个 unique_ptr
 shared_ptr 允许有多个 shared_ptr 指向同一个对象因此不需要 release 成员

weak_ptr

weak_ptr是一种不控制所指向对象生存期的智能指针
它指向由一个shared_ptr管理的对象

将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数
一旦最后一个指向对象的shared_ptr被销毁对象就会被释放

即使有weak_ptr指向对象对象也还是会被释放
因此weak_ptr的名字抓住了这种智能指针共享对象的特点

当我们创建一个weak_ptr时要用一个shared_ptr来初始化它
auto p = make_shared<int>(42);
weak_ptr<int> wp(p);  // wp weakly shares with p; use count in p is unchanged
//本例中wp和p指向相同的对象。由于是弱共享,创建wp不会改变p的引用计数;
//wp指向的对象可能被释放掉。

weak_ptr操作

weak_ptr<T> w       空weak_ptr可以指向类型为T的对象 
weak_ptr<T> w(sp)   与shared_ptr指向相同对象的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 

由于对象可能不存在我们不能使用weak_ptr直接访问对象而必须调用lock
此函数检查weak_ptr指向的对象是否仍存在
如果存在lock返回一个指向共享对象的shared_ptr
与任何其他shared_ptr类似只要此shared_ptr存在
它所指向的底层对象也就会一直存在例如
if (shared_ptr<int> np = wp.lock()) { // true if np is not null
    // inside the if, np shares its object with p
}
//在这段代码中,只有当lock调用返回true时我们才会进入if语句体。
//在if中,使用np访问共享对象是安全的。

核查指针类

weak_ptr用途的一个展示
// 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
};

核查指针类

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
}

指针操作

定义名为deref和incr的函数分别用来解引用和递增StrBlobPtr
std::string& StrBlobPtr::deref() const
{
    auto p = check(curr, "dereference past end");
    return (*p)[curr];  // (*p) is the vector to which this object points
}
// 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;
}

为StrBlob类定义begin和end操作返回一个指向它自身的StrBlobPtr
// forward declaration needed for friend declaration in StrBlob
class StrBlobPtr;
class StrBlob {
    friend class StrBlobPtr;
    // other members ...
    // 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; }
};

练习

定义你自己版本的 StrBlobPtr更新 StrBlob 加入恰当的 friend 声明以及 begin  end 成员
#include <string>
#include <vector>
#include <initializer_list>
#include <memory>
#include <stdexcept>

using std::vector; using std::string;

class StrBlobPtr;

class StrBlob
{
public:
    using size_type = vector<string>::size_type;
    friend class StrBlobPtr;

    StrBlobPtr begin();
    StrBlobPtr end();

练习

    StrBlob() : data(std::make_shared<vector<string>>()) {}
    StrBlob(std::initializer_list<string> il) : data(std::make_shared<vector<string>>(il)) {}

    size_type size() const { return data->size(); }
    bool empty() const { return data->empty(); }

    void push_back(const string& s) { data->push_back(s); }
    void pop_back()
    {
        check(0, "pop_back on empty StrBlob");
        data->pop_back();
    }

    std::string& front()
    {
        check(0, "front on empty StrBlob");
        return data->front();
    }

练习

    std::string& back()
    {
        check(0, "back on empty StrBlob");
        return data->back();
    }

    const std::string& front() const
    {
        check(0, "front on empty StrBlob");
        return data->front();
    }
    const std::string& back() const
    {
        check(0, "back on empty StrBlob");
        return data->back();
    }

练习

private:
    void check(size_type i, const string& msg) const
    {
        if (i >= data->size())
            throw std::out_of_range(msg);
    }

private:
    std::shared_ptr<vector<string>> data;
};

练习

class StrBlobPtr
{
public:
    StrBlobPtr() :curr(0) {}
    StrBlobPtr(StrBlob &a, size_t sz = 0) :wptr(a.data), curr(sz) {}
    bool operator!=(const StrBlobPtr& p) { return p.curr != curr; }
    string& deref() const
    {
        auto p = check(curr, "dereference past end");
        return (*p)[curr];
    }
    StrBlobPtr& incr()
    {
        check(curr, "increment past end of StrBlobPtr");
        ++curr;
        return *this;
    }

练习

private:
    std::shared_ptr<vector<string>> check(size_t i, const string &msg) const
    {
        auto ret = wptr.lock();
        if (!ret) throw std::runtime_error("unbound StrBlobPtr");
        if (i >= ret->size()) throw std::out_of_range(msg);
        return ret;
    }
    std::weak_ptr<vector<string>> wptr;
    size_t curr;
};

StrBlobPtr StrBlob::begin()
{
    return StrBlobPtr(*this);
}
StrBlobPtr StrBlob::end()
{
    return StrBlobPtr(*this, data->size());
}

练习

编写程序逐行读入一个输入文件将内容存入一个 StrBlob 
用一个 StrBlobPtr 打印出 StrBlob 中的每个元素
#include <iostream>
#include <fstream>
#include "StrBlob.h"
using namespace std;
int main(){
    ifstream ifs("books.txt");
    StrBlob sb;
    string s;
    while (getline(ifs, s)){
        sb.push_back(s);
    }
    for (StrBlobPtr sbp = sb.begin(); sbp != sb.end(); sbp.incr()){
        cout << sbp.deref() << endl;
    }
    return 0;
}

练习

也可以这样编写 StrBlobPtr  deref 成员
std::string& deref() const {
    return (*check(curr, "dereference past end"))[curr];
}
你认为哪个版本更好为什么

原来的版本更好可读性更高

练习

为了能让 StrBlobPtr 使用 const StrBlob你觉得应该如何修改
定义一个名为ConstStrBlobPtr 的类使其能够指向 const StrBlob

构造函数改为接受 const Strblob & 
然后给 Strblob 类添加两个 const 成员函数 cbegin  cend返回 ConstStrBlobPtr

动态数组

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

当一个应用需要可变数量的对象时
- 使用vector或其他标准库容器更简单更快速并且更安全

大多数应用应该使用标准库容器而不是动态分配的数组
使用容器更为简单更不容易出现内存管理错误并且可能有更好的性能

使用容器的类可以使用默认版本的拷贝赋值和析构操作
分配动态数组的类则必须定义自己版本的操作在拷贝复制以及销毁对象时管理所关联的内存

new和数组

为了让new分配一个对象数组我们要在类型名之后跟一对方括号
在其中指明要分配的对象的数目。(假定分配成功后返回指向第一个对象的指针
// call get_size to determine how many ints to allocate
int *pia = new int[get_size()]; // pia points to the first of these ints
方括号中的大小必须是整型但不必是常量

可以用一个表示数组类型的类型别名来分配一个数组new表达式中就不需要方括号了
typedef int arrT[42]; // arrT names the type array of 42 ints
int *p = new arrT;    // allocates an array of 42 ints; p points to the first one
//int *p = new int[42];

分配一个数组会得到一个元素类型的指针

当用new分配一个数组时到一个数组元素类型的指针

由于分配的内存并不是一个数组类型因此不能对动态数组调用begin或end
这些函数使用数组维度来返回指向首元素和尾后元素的指针
出于相同的原因也不能用范围for语句来处理所谓的动态数组中的元素

初始化动态分配对象的数组

默认情况下new分配的对象不管是单个分配的还是数组中的都是默认初始化的
可以对数组中的元素进行值初始化方法是在大小之后跟一对空括号
int *pia = new int[10];          //block of ten uninitialized ints
int *pia2 = new int[10]();       //block of ten ints value initialized to 0
string *psa = new string[10];    //block of ten empty strings
string *psa2 = new string[10](); //block of ten empty strings
在新标准中我们还可以提供一个元素初始化器的花括号列表
// block of ten ints each initialized from the corresponding initializer
int *pia3 = new int[10]{0,1,2,3,4,5,6,7,8,9};
// block of ten strings; the first four are initialized from the given initializers
// remaining elements are value initialized
string *psa3 = new string[10]{"a","an",string(3,'x')};

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

动态分配一个空数组是合法的

可以用任意表达式来确定要分配的对象的数目
size_t n = get_size(); // get_size returns the number of elements needed
int* p = new int[n];   // allocate an array to hold the elements
for (int* q = p; q != p + n; ++q)
     /* process the array */ ;

char arr[0];            // error: cannot define a zero-length array
char *cp = new char[0]; // ok: but cp can't be dereferenced

释放动态数组

为了释放动态数组我们使用一种特殊形式的delete——在指针前加上一个空方括号对
delete p;     // p must point to a dynamically allocated object or be null
delete [] pa; // pa must point to a dynamically allocated array or be null
//第二条语句销毁pa指向的数组中的元素,并释放对应的内存。
//数组中的元素按逆序销毁

当我们释放一个指向数组的指针时空方括号对是必需的
它指示编译器此指针指向一个对象数组的第一个元素
如果我们在delete一个指向数组的指针时忽略了方括号
或者在delete一个指向单一对象的指针时使用了方括号),其行为是未定义的

typedef int arrT[42];  // arrT names the type array of 42 ints
int *p = new arrT;     // allocates an array of 42 ints; p points to the first one
delete [] p;           // brackets are necessary because we allocated an array

如果我们在delete一个数组指针时忘记了方括号或者在delete一个单一对象的指针
时使用了方括号编译器很可能不会给出警告我们的程序可能在执行过程中在没有
任何警告的情况下行为异常

智能指针和动态数组

标准库提供了一个可以管理new分配的数组的unique_ptr版本
为了用一个unique_ptr管理动态数组我们必须在对象类型后面跟一对空方括号
// up points to an array of ten uninitialized ints
unique_ptr<int[]> up(new int[10]);
up.release();   // automatically uses delete[] to destroy its pointer
//由于up指向一个数组,当up销毁它管理的指针时,会自动使用delete[]。
当一个unique_ptr指向一个数组时我们不能使用点和箭头成员运算符
- 毕竟unique_ptr指向的是一个数组而不是单个对象因此这些运算符是无意义的
当一个unique_ptr指向一个数组时我们可以使用下标运算符来访问数组中的元素
for (size_t i = 0; i != 10; ++i)
    up[i] = i; // assign a new value to each of the elements

指向数组的unique_ptr

- 指向数组的unique_ptr不支持成员访问运算符点和箭头)。
 unique_ptr<T[]> u      u可以指向一个动态分配的数组整数元素类型为T 
 unique_ptr<T[]> u(p)   u指向内置指针p所指向的动态分配的数组
                        p必须能转换为类型T* 
 u[i]                   返回u拥有的数组中位置i处的对象u必须指向一个数组 

智能指针和动态数组

与unique_ptr不同shared_ptr不直接支持管理动态数组
如果希望使用shared_ptr管理一个动态数组必须提供自己定义的删除器
shared_ptr<int> sp(new int[10], [](int *p) { delete[] p; });
sp.reset(); // uses the lambda we supplied that uses delete[] to free the array
//如果未提供删除器,这段代码将是未定义的。
//默认情况下,shared_ptr使用delete销毁它指向的对象。
//如果此对象是一个动态数组,对其使用delete所产生的问题
//与释放一个动态数组指针时忘记[]产生的问题一样
shared_ptr不直接支持动态数组管理这一特性会影响我们如何访问数组中的元素
// shared_ptrs don't have subscript operator and don't support pointer arithmetic
for (size_t i = 0; i != 10; ++i)
    *(sp.get() + i) = i;  // use get to get a built-in pointer
//shared_ptr未定义下标运算符,而且智能指针类型不支持指针算术运算。
//为了访问数组中的元素,必须用get获取一个内置指针,然后用它来访问数组元素。

练习

编写一个程序连接两个字符串字面常量将结果保存在一个动态分配的char数组中
重写这个程序连接两个标准库string对象

#include <iostream>
#include <string>
#include <cstring>
#include <memory>
using namespace std;
int main() {
    const char *c1 = "Hello ";
    const char *c2 = "World";
    unsigned len = strlen(c1) + strlen(c2) + 1;
    char *r = new char[len]();
    strcat(r,  c1);
    strcat(r, c2);
    std::cout << r << std::endl;

    std::string s1 = "Hello ";
    std::string s2 = "World";
    strcpy(r,  (s1 + s2).c_str());
    std::cout << r << std::endl;

    delete[] r;

    return 0;
}

练习

编写一个程序从标准输入读取一个字符串存入一个动态分配的字符数组中
描述你的程序如何处理变长输入测试你的程序输入一个超出你分配的数组长度的字符串
#include <iostream>
int main()
{
    std::cout << "How long do you want the string? ";
    int size{ 0 };
    std::cin >> size;
    char *input = new char[size + 1]();
    std::cin.ignore();
    std::cout << "input the string: ";
    std::cin.get(input, size + 1);
    std::cout << input;
    delete[] input;

    return 0;
}

练习

给定下面的new表达式你应该如何释放pa
int *pa = new int[10];

delete [] pa;

allocator类

new有一些灵活性上的局限其中一方面表现在它将内存分配和对象构造组合在了一起

当分配一大块内存时我们通常计划在这块内存上按需构造对象
在此情况下我们希望将内存分配和对象构造分离
这意味着我们可以分配大块内存
但只在真正需要时才真正执行对象创建操作同时付出一定开销)。
//将内存分配和对象构造组合在一起可能会导致不必要的浪费。
string *const p = new string[n]; // construct n empty strings
string s;
string *q = p;                   // q points to the first string
while (cin >> s && q != p + n)
    *q++ = s;                      // assign a new value to *q
const size_t size = q - p;       // remember how many strings we read
// use the array
delete[] p;  // p points to an array; must remember to use delete[]
//new表达式分配并初始化了n个string。但是,我们可能不需要n个string,
//少量string可能就足够了。这样,我们就可能创建了一些永远也用不到的对象。
//而且,对于那些确实要使用的对象,我们也在初始化之后立即赋予了它们新值。
//每个使用到的元素都被赋值了两次:第一次是在默认初始化时,随后是在赋值时。
//更重要的是,那些没有默认构造函数的类就不能动态分配数组了。

allocator类

标准库allocator类定义在头文件memory中它帮助我们将内存分配和对象构造分离开来
它提供一种类型感知的内存分配方法它分配的内存是原始的未构造的

类似vectorallocator是一个模板
为了定义一个allocator对象我们必须指明这个allocator可以分配的对象类型
当一个allocator对象分配内存时它会根据给定的对象类型来
确定恰当的内存大小和对齐位置
allocator<string> alloc;          // object that can allocate strings
auto const p = alloc.allocate(n); // allocate n unconstructed strings
//这个allocate调用为n个string分配了内存。

标准库allocator类及其算法

allocator<T> 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分配未构造的内存

allocator分配的内存是未构造的unconstructed)。我们按需要在此内存中构造对象
在新标准库中construct成员函数接受一个指针和零个或多个额外参数
在给定位置构造一个元素额外参数用来初始化构造的对象
类似make_shared的参数这些额外参数必须是与构造的对象的类型相匹配的合法的初始化器
auto q = p; // q will point to one past the last constructed element
alloc.construct(q++);            // *q is the empty string
alloc.construct(q++, 10, 'c');   // *q is cccccccccc
alloc.construct(q++, "hi");      // *q is hi!

在早期版本的标准库中construct只接受两个参数
指向创建对象位置的指针和一个元素类型的值
因此我们只能将一个元素拷贝到未构造空间中
而不能用元素类型的任何其他构造函数来构造一个元素

allocator分配未构造的内存

还未构造对象的情况下就使用原始内存是错误的
cout << *p << endl; // ok: uses the string output operator
cout << *q << endl; // disaster: q points to unconstructed memory!

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

当我们用完对象后必须对每个构造的元素调用destroy来销毁它们
函数destroy接受一个指针对指向的对象执行析构函数
while (q != p)
    alloc.destroy(--q);         // free allocated

allocator分配未构造的内存

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

allocator算法

标准库还为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必须指向足够大的未构造的原始内存能够容纳给定数量的对象 

// allocate twice as many elements as vi holds
auto p = alloc.allocate(vi.size() * 2);
// construct elements starting at p as copies of elements in vi
auto q = uninitialized_copy(vi.begin(), vi.end(), p);
// initialize the remaining elements to 42
uninitialized_fill_n(q, vi.size(), 42);
//一次uninitialized_copy调用会返回一个指针,指向最后一个构造的元素之后的位置。

 allocator 重写的程序
#include <iostream>
#include <string>
#include <memory>
using namespace std;
int main(){
    int n = 5;
    allocator<string> alloc;
    auto p = alloc.allocate(n);
    string s;
    auto q = p;
    while (cin >> s && q != p + n){
        alloc.construct(q++, s);
    }
    while (q != p){
        std::cout << *--q << " ";
        alloc.destroy(q);
    }
    alloc.deallocate(p, n);
    return 0;
}

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

实现一个简单的文本查询程序作为标准库相关内容学习的总结
我们的程序允许用户在一个给定文件中查询单词
查询结果是单词在文件中出现的次数及其所在行的列表
如果一个单词在一行中出现多次此行只列出一次行会按照升序输出

文本查询程序设计

开始一个程序的设计的一种好方法是列出程序的操作
了解需要哪些操作会帮助我们分析出需要什么样的数据结构
从需求入手我们的文本查询程序需要完成如下任务

当程序读取输入文件时
- 它必须记住单词出现的每一行
- 因此程序需要逐行读取输入文件并将每一行分解为独立的单词

当程序生成输出时
- 它必须能提取每个单词所关联的行号
- 行号必须按升序出现且无重复
- 它必须能打印给定行号中的文本

利用多种标准库设施

vector<string>来保存整个输入文件的一份拷贝
- 输入文件中的每行保存为vector中的一个元素
- 当需要打印一行时可以用行号作为下标来提取行文本
istringstream将每行分解为单词
set来保存每个单词在输入文本中出现的行号
- 这保证了每行只出现一次且行号按升序保存
map来将每个单词与它出现的行号set关联起来
- 这样我们就可以方便地提取任意单词的set
方案还使用了shared_ptr

数据结构

虽然我们可以用vectorset和map来直接编写文本查询程序
但如果定义一个更为抽象的解决方案会更为有效

保存输入文件的类TextQuery它包含一个vector和一个map
- vector用来保存输入文件的文本map用来关联每个单词和它出现的行号的set
- 这个类将会有一个用来读取给定输入文件的构造函数和一个执行查询的操作
- 查询操作要完成的任务非常简单查找map成员检查给定单词是否出现
  设计这个函数的难点是确定应该返回什么内容一旦找到了一个单词我们需要
  知道它出现了多少次它出现的行号以及每行的文本
- 返回所有这些内容的最简单的方法是定义另一个类可以命名为QueryResult
  来保存查询结果这个类会有一个print函数完成结果打印工作

在类之间共享数据

QueryResult类要表达查询的结果这些结果包括与给定单词关联的行号的set和
这些行对应的文本这些数据都保存在TextQuery类型的对象中

QueryResult所需要的数据都保存在一个TextQuery对象中我们就必须确定如何访问它们
- 不希望拷贝
- 通过返回指向TextQuery对象内部的迭代器或指针),我们可以避免拷贝操作
- 对于QueryResult对象和对应的TextQuery对象的生存期应该同步这一观察结果
  其实已经暗示了问题的解决方案
  - 考虑到这两个类概念上共享了数据
    可以使用shared_ptr来反映数据结构中的这种共享关系

使用TextQuery类

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

下面的程序使用了TextQuery和QueryResult类这个函数接受一个指向要处理的
文件的ifstream并与用户交互打印给定单词的查询结果
void runQueries(ifstream &infile)
{
    // infile is an ifstream that is the file we want to query
    TextQuery tq(infile);  //  store the file and build the query map
    // iterate with the user: prompt for a word to find and print results
    while (true) {
        cout << "enter word to look for, or q to quit: ";
        string s;
        // stop if we hit end-of-file on the input or if a 'q' is entered
        if (!(cin >> s) || s == "q") break;
        // run the query and print the results
        print(cout, tq.query(s)) << endl;
    }
}

练习

TextQuery  QueryResult 类只使用了我们已经介绍过的语言和标准库特性
不要提前看后续章节内容只用已经学到的知识对这两个类编写你自己的版本
头文件
#ifndef EX12_27_H
#define EX12_27_H
#include <fstream>
#include <memory>
#include <vector>
#include <string>
#include <map>
#include <set>
class QueryResult;
class TextQuery
{
public:
    using line_no = std::vector<std::string>::size_type;
    TextQuery(std::ifstream&);
    QueryResult query(const std::string& s) const;

练习

private:
    std::shared_ptr<std::vector<std::string>> file;
    std::map<std::string, std::shared_ptr<std::set<line_no>>> wm;
};
class QueryResult
{
public:
    friend std::ostream& print(std::ostream&, const QueryResult&);
    QueryResult(std::string s,
                std::shared_ptr<std::set<TextQuery::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<TextQuery::line_no>> lines;
    std::shared_ptr<std::vector<std::string>> file;
};
std::ostream& print(std::ostream&, const QueryResult&);
#endif

练习

实现
#include "ex_12_27.h"
#include <sstream>
#include <fstream>
#include <vector>
#include <string>
using namespace std;
TextQuery::TextQuery(ifstream& ifs) : file(new vector<string>)
{
    string text;
    while (getline(ifs, text))
    {
        file->push_back(text);
        int n = file->size() - 1;
        istringstream line(text);
        string word;

练习

        while (line >> word)
        {
            auto &lines = wm[word];
            if (!lines)
                lines.reset(new set<line_no>);
            lines->insert(n);
        }
    }
}

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

练习

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

练习

主函数
#include <iostream>
#include <string>
#include <fstream>
#include "ex_12_27.h"

using namespace std;

void runQueries(ifstream& infile)
{
    TextQuery tq(infile);
    while (true)
    {
        cout << "enter word to look for, or q to quit: ";
        string s;
        if (!(cin >> s) || s == "q") break;
        print(cout, tq.query(s)) << endl;
    }
}

练习

int main()
{
    ifstream ifs("storyDataFile.txt");
    runQueries(ifs);
    return 0;
}

练习

编写程序实现文本查询不要定义类来管理数据
你的程序应该接受一个文件并与用户交互来查询单词
使用vectormap  set 容器来保存来自文件的数据并生成查询结果

#include <string>
using std::string;

#include <vector>
using std::vector;

#include <memory>
using std::shared_ptr;

#include <iostream>
#include <fstream>
#include <sstream>
#include <map>
#include <set>
#include <algorithm>

练习

int main()
{
    std::ifstream file("storyDataFile.txt");
    vector<string> input;
    std::map<string, std::set<decltype(input.size())>> dictionary;
    decltype(input.size()) lineNo{ 0 };

    for (string line; std::getline(file, line); ++lineNo)
    {
        input.push_back(line);
        std::istringstream line_stream(line);
        for (string text, word; line_stream >> text; word.clear())
        {
            std::remove_copy_if(text.begin(), text.end(), 
                        std::back_inserter(word), ispunct);
            dictionary[word].insert(lineNo);
        }
    }

练习

    while (true)
    {
        std::cout << "enter word to look for, or q to quit: ";
        string s;
        if (!(std::cin >> s) || s == "q") break;
        auto found = dictionary.find(s);
        if (found != dictionary.end())
        {
            std::cout << s << " occurs " << found->second.size() << 
             (found->second.size() > 1 ? " times" : " time") << std::endl;
            for (auto i : found->second)
                std::cout << "\t(line " << i + 1 << ") " 
                                << input.at(i) << std::endl;
        }
        else std::cout << s << " occurs 0 time" << std::endl;
    }
}

练习

我们曾经用do while 循环来编写管理用户交互的循环
用do while 重写本节程序解释你倾向于哪个版本为什么


do {
    std::cout << "enter word to look for, or q to quit: ";
    string s;
    if (!(std::cin >> s) || s == "q") break;
    print(std::cout, tq.query(s)) << std::endl;
} while ( true );

更喜欢 while这可能是习惯

文本查询程序类的定义

我们以TextQuery类的定义开始
用户创建此类的对象时会提供一个istream用来读取输入文件
还提供一个query操作接受一个string返回一个QueryResult表示string出现的那些行
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;
private:
    std::shared_ptr<std::vector<std::string>> file;  // input file
    // map of 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;
};

TextQuery构造函数

TextQuery的构造函数接受一个ifstream逐行读取输入文件
// 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
            // 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
        }
    }
}

QueryResult类

QueryResult类有三个数据成员
一个string保存查询单词
一个shared_ptr指向保存输入文件的vector
一个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;  // word this query represents
    std::shared_ptr<std::set<line_no>> lines; // lines it's on
    std::shared_ptr<std::vector<std::string>> file; // input file
};

query函数

query函数接受一个string参数即查询单词,query用它来在map中定位对应的行号set
- 如果找到了这个stringquery函数构造一个QueryResult
  保存给定stringTextQuery的file成员以及从wm中提取的set
- 给定string未找到,定义一个局部static对象指向空的行号set的shared_ptr
  返回此对象的一个拷贝
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(sought);
    if (loc == wm.end())
        return QueryResult(sought, nodata, file); // not found
    else
        return QueryResult(sought, loc->second, file);
}

打印结果

print函数在给定的流上打印出给定的QueryResult对象
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() + num) << endl;
    return os;
}

练习

定义你自己版本的 TextQuery  QueryResult 并执行runQueries 函数

同之前练习

练习

如果用vector 代替 set 保存行号会有什么差别哪个方法更好为什么

如果用 vector 则会有单词重复的情况出现
而这里保存的是行号不需要重复元素所以 set 更好

练习

重写 TextQuery  QueryResult类用StrBlob 代替 vector<string> 保存输入文件

TextQuery  QueryResult 类中的 file 成员改为 指向 StrBlob 的智能指针
在访问 StrBlob 要使用 StrBlobPtr

练习

我们将扩展查询系统 QueryResult 类中将会需要一些额外的成员
添加名为 begin  end 的成员返回一个迭代器
指向一个给定查询返回的行号的 set 中的位置
再添加一个名为 get_file 的成员返回一个 shared_ptr
指向 QueryResult 对象中的文件

class QueryResult{
public:
    using Iter = std::set<line_no>::iterator;    
    // ...
    Iter begin() const { return lines->begin(); }
    Iter end() const { return lines->end(); }
    shared_ptr<std::vector<std::string>> get_file() const 
    { 
        return std::make_shared<std::vector<std::string>>(file); 
    }
private:
    // ...
};

小结

在C++内存是通过new表达式分配通过delete表达式释放的

标准库还定义了一个allocator类来分配动态内存块
分配动态内存的程序应负责释放它所分配的内存
内存的正确释放是非常容易出错的地方
- 要么内存永远不会被释放要么在仍有指针引用它时就被释放了

新的标准库定义了智能指针类型——shared_ptrunique_ptr和weak_ptr
可令动态内存管理更为安全
对于一块内存当没有任何用户使用它时智能指针会自动释放它
现代C++程序应尽可能使用智能指针

实践课

  • 从课程主页 cpp.njuer.org 打开 《面向对象编程基础》实验课 cloud shell界面 https://developer.aliyun.com/adc/scenario/476f2ff6afad4221bac08e33cf6984fc
  • 使用g++编译代码
  • 编辑一个 readme.md 文档,键入本次实验心得.
  • 使用git进行版本控制 可使用之前的gitee代码仓库
      - 云服务器elastic compute service,简称ecs
      - aliyun linux 2是阿里云推出的 linux 发行版
      - vim是从vi发展出来的一个文本编辑器.
      - g++ 是c++编译器
    
习题1
使用 shared_ptr 而不是内置指针,编写函数返回一个动态分配的 int 的vector
将此vector 传递给另一个函数这个函数读取标准输入
将读入的值保存在 vector 元素中再将vector传递给另一个函数打印读入的值


习题2
编写用 shared_ptr 管理 connection 的函数

习题3
TextQuery  QueryResult 类只使用了我们已经介绍过的语言和标准库特性
对这两个类编写你自己的版本
编辑c++代码和markdown文档,使用git进行版本控制
yum install -y git gcc-c++
使用git工具进行版本控制
git clone你之前的网络git仓库test(或其它名字)
cd test 进入文件夹test
(clone的仓库,可移动旧文件到目录weekN:  mkdir -p weekN ; mv 文件名 weekN;)

vim test1.cpp
g++ ./test1.cpp 编译
./a.out 执行程序

vim test2.cpp
g++ ./test2.cpp 编译
./a.out 执行程序

vim test3.cpp
g++ ./test3.cpp 编译
./a.out 执行程序
git add . 加入当前文件夹下所有文件到暂存区
git config --global user.email "you@example.com"
git config --global user.name "Your Name"
vim readme.md 键入新内容(实验感想),按ESC 再按:wq退出
git add .
git commit –m "weekN" 表示提交到本地,备注weekN

git push 到你的git仓库

git log --oneline --graph 可看git记录
键入命令并截图或复制文字,并提交到群作业.
cat test* readme.md

提交

  • 截图或复制文字,提交到群作业.
  • 填写阿里云平台(本实验)的网页实验报告栏,发布保存.本次报告不需要分享提交
  • 填写问卷调查 https://rnk6jc.aliwork.com/o/cppinfo

关于使用tmux

sudo yum install -y tmux
cd ~ && wget https://cpp.njuer.org/tmux && mv tmux .tmux.conf
tmux 进入会话 .
前缀按键prefix= ctrl+a, 
prefix+c创建新面板,
prefix+"分屏,
prefix+k选上面,prefix+j选下面,
prefix+1选择第一,prefix+n选择第n,
prefix+d脱离会话
tmux attach-session -t 0 回到会话0

vim 共分为三种模式

图片1

- 命令模式
  - 刚启动 vim,便进入了命令模式.其它模式下按ESC,可切换回命令模式
    - i 切换到输入模式,以输入字符.
    - x 删除当前光标所在处的字符.
    - : 切换到底线命令模式,可输入命令.
- 输入模式
  - 命令模式下按下i就进入了输入模式.
    - ESC,退出输入模式,切换到命令模式
- 底线命令模式
  - 命令模式下按下:英文冒号就进入了底线命令模式.
    - wq 保存退出

vim 常用按键说明

除了 i, Esc, :wq 之外,其实 vim 还有非常多的按键可以使用.命令模式下
- 光标移动
  - j下 k上 h左 l右
  - w前进一个词 b后退一个词
  - Ctrl+d 向下半屏  ctrl+u 向上半屏
  - G 移动到最后一行 gg 第一行 ngg 第n行
- 复制粘贴
  - dd 删一行 ndd 删n行
  - yy 复制一行 nyy复制n行
  - p将复制的数据粘贴在下一行 P粘贴到上一行
  - u恢复到前一个动作 ctrl+r重做上一个动作
- 搜索替换
  - /word 向下找word     word 向上找
  - n重复搜索 N反向搜索
  - :1,$s/word1/word2/g从第一行到最后一行寻找 word1 字符串,并将该字符串
    取代为 word2

vim 常用按键说明

底线命令模式下
- :set nu   显示行号
- :set nonu 取消行号
- :set paste    粘贴代码不乱序
把caps lock按键映射为ctrl,能提高编辑效率.

Markdown 文档语法

# 一级标题
## 二级标题
*斜体* **粗体**
- 列表项
  - 子列表项
> 引用
[超链接](http://asdf.com)
![图片名](http://asdf.com/a.jpg)

|表格标题1|表格标题2|
 |-|-|
|内容1|内容2|

谢谢