C++ Primer 第十二章 动态内存

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

动态内存与智能指针

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

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

智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。

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

都定义在 memory 头文件中,区别在于管理底层指针的方式

shared_ptr 类

shared_ptr 和 unique_ptr 都支持的操作

shared_ptr<T> sp         空智能指针,可以指向类型为 T 的对象
unique_ptr<T> up
p                        将 p 用作一个条件判断,若 p 指向一个对象,则为 true
*p                       解引用 p,获得它所指向的对象
p -> mem                 等价于 (*p).mem
p.get()                  返回 p 中保存的指针。要小心使用,若只能指针释放了其对象,返回的指针所指向的对象也就消失了
swap(p, q)               交换 p 和 q 中的指针
p.swap(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 函数

    // 指向一个值为 42 的 int 的 shared_ptr    
    std::shared_ptr<int> p3 = std::make_shared<int>(42);

    // p4 指向一个值为 "9999999999" 的 string
    std::shared_ptr<string> p4 = std::make_shared<string>(10, '9');

    // p5 指向一个值初始化的 int,即,值为 0
    std::shared_ptr<int> p5 = std::make_shared<int>();

    // p6 指向一个动态分配的空 vector<string>
    auto p6 = std::make_shared<vector<string>>();

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

shared_ptr 的拷贝和赋值

  • 我们可以认为每个 shared_ptr 都有一个关联的计数器,通常称其为 引用计数。无论何时我们拷贝一个 shared_ptr,计数器都会递增。当我们给 shared_ptr 赋予一个新值或是 shared_ptr 被销毁时,计数器就会递减。
  • 一旦一个 shared_ptr 的计数器变为 0,它就会自动释放自己所管理的对象
    auto r = std::make_shared<int>(42); // r 指向的 int 只有一个引用者
    r = q; // 给 r 赋值,令它指向另一个地址
           // 递增 q 指向的对象的引用计数
           // 递减 r 原来指向的对象的引用计数
           // r 原来指向的对象已没有引用者,会自动释放

shared_ptr 自动销毁所管理的对象 ......

  • 它是通过另一个特殊的成员函数——析构函数完成销毁工作的
  • 析构函数一般用来释放对象所分配的资源
  • shared_ptr 的析构函数会递减它所指向的对象的引用计数。如果引用计数变为 0,shared_ptr 的析构函数就会销毁对象,并释放它占用的内存

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

std::shared_ptr<Foo> factory(T arg) {
    // 恰当地处理 arg
    // shared_ptr 负责释放内存
    return std::make_shared<Foo>(arg);
}

由于 factory 返回一个 shared_ptr,所以我们可以确保它分配的对象会在恰当的时刻被释放。

void use_factory(T arg) {
    std::shared_ptr<Foo> p = factory(arg);
    // 使用了 p
} // p 离开了作用域,它指向的内存会被自动释放

当 p 被销毁时,将递减其引用计数并检查它是否为 0.
在此例中,p 是唯一引用 factory 返回的内存的对象。由于 p 将要销毁,p 指向的这个对象也会被销毁,所占用的内存会被释放

但如果有其他 shared_ptr 也指向这块内存,它就不会被释放

void use_factory(T arg) {
    std::shared_ptr<Foo> p = factory(arg);
    // 使用 p
    return p; // 当我们返回 p 时,引用计数进行了递增操作
} // p 离开了作用域,但它指向的内存不会被释放

如果你忘记了销毁程序不再需要的 shared_ptr,程序仍会正确执行,但会浪费内存

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

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

  • 程序不知道自己需要使用多少对象
  • 程序不知道所需对象的准确类型
  • 程序需要在多个对象间共享数据
    vector<string> v1; {
        // 新作用域
        vector<string> v2 = {"a", "an", "the"};
        v1 = v2; // 从 v2 拷贝元素到 v1 中
    } // v2 被销毁,其中的元素也被销毁
    // v1 有三个元素,是原来 v2 中元素的拷贝

但某些类分配的资源具有与原对象相独立的生存期。即,当我们拷贝一个 Blob 时,原 Blob 对象及其拷贝应该引用相同的底层元素。
一般而言,如果两个对象共享底层的数据,当某个对象被销毁时,我们不能单方面地销毁底层数据。

    Blob<string> b1; {
        Blob<string> b2 = {"a", "an", "the"};
        b1 = b2; // b1 和 b2 共享相同的元素
    } // b2 被销毁了,但 b2 中的元素不能销毁
    // b1 指向最初由 b2 创建的元素

在此例中,b1 和 b2 共享相同的元素。当 b2 离开作用域时,这些元素必须保留,因为 b1 仍然在使用它们。
使用动态内存的一个常见原因是允许多个对象共享相同的状态

定义 StrBlob 类

#pragma once
#ifndef STRBLOB_H
#define STRBOLB_H

#include <iostream>
#include <string>
#include <vector>
#include <memory>
#include <initializer_list>

class StrBlob {
public:
	typedef std::vector<std::string>::size_type size_type;
	StrBlob();
	StrBlob(std::initializer_list<std::string> il);
	size_type size() const { return data -> size(); }
	bool empty() const { return data -> empty(); }
	// 添加和删除元素
	void push_back(const std::string &t) { data -> push_back(t); }
	void pop_back();
	// 元素访问
	std::string &front();
	std::string &back();
private:
	std::shared_ptr<std::vector<std::string>> data;
	// 如果 data[i] 不合法,抛出一个异常
	void check(size_type i, const std::string &msg) const;
};

#endif

StrBlob 构造函数

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

元素访问成员函数

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

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

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

void StrBlob::pop_back() {
	check(0, "pop_back on empty StrBlob");
	data -> pop_back();
}

StrBlob 的拷贝、赋值和修改

  • 类似 Sales_data 类,StrBlob 使用默认版本的拷贝、赋值和销毁成员函数来对此类型的对象进行这些操作。

直接管理内存

C++ 语言定义了两个运算符来分配和释放动态内存。运算符 new 分配内存,delete 释放 new 分配的内存。

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

在自由空间分配的内存是无名的,因此 new 无法为其分配的对象命名,而是返回一个指向该对象的指针

    int *pi = new int; // pi 指向一个动态分配的、未初始化的无名对象

默认情况下,动态分配内存的对象是默认初始化的,这意味着内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化

    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};

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

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

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

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

动态分配的 const 对象

用 new 分配 const 对象是合法的
类似其他任何 const 对象,一个动态分配的 const 对象必须进行初始化

    // 分配并初始化一个 const int
    const int *pci = new const int(1024);
    // 分配并默认初始化一个 const 的空 string
    const string *pcs = new const string;

内存耗尽

默认情况下,如果 new 不能分配所要求的内存空间,它会抛出一个类型为 bad_alloc 的异常。
我们可以改变使用 new 的方式来阻止它抛出的异常。

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

我们称这种形式的 new 为定位 new。定位 new 表达式允许我们向 new 传递额外的参数

bad_alloc 和 nothrow 都定义在头文件 new

释放动态内存

我们通过 delete 表达式来将动态内存归还给系统。delete 表达式接受一个指针,指向我们想要释放的对象。(销毁给定的指针指向的对象;释放对应的内存)

    delete p; // p 必须指向一个动态分配的对象或是一个空指针

指针值和 delete

我们传递给 delete 的指针必须指向动态分配的内存,或者是一个空指针

    int i, *pi1 = &i, *pi2 = nullptr;
    double *pd = new double(33), *pd2 = pd;
    delete i; // 错误:i 不是一个指针
    delete pi1; // 未定义:pi1 指向一个局部变量
    delete pd; // 正确
    delete pd2; // 未定义:pd2 指向的内存已经被释放了
    delete pi2; // 正确:释放一个空指针总是没有错误的

虽然一个 const 对象的值不能被改变,但它本身是可以被销毁的。如同任何其他动态对象一样,想要释放一个 const 动态对象,只要 delete 指向它的指针即可

    const int *pci = new const int(1024);
    delete pci; // 正确:释放一个 const 对象

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

对于一个由内置指针管理的动态对象,直到被显式释放之前它都是存在的

// factory 返回一个指针,指向一个动态分配的对象
Foo* factory(T arg) {
    return new Foo(arg); // 调用者负责释放此内存
}

void use_factory(T arg) {
    Foo *p = factory(arg);
    // 使用 p 但不 delete 它
} // p 离开了它的作用域,但它所指向的内存没有被释放!

由内置指针(而不是智能指针)管理的动态内存在被显式释放前一直都会存在
根据整个程序的逻辑,修正这个错误的正确方法是在 use_factory 中记得释放内存

void use_factory(T arg) {
    Foo *p = factory(arg);
    // 使用 p
    delete p; // 现在记得释放内存,我们已经不需要它了
} 

还有一种可能,我们的系统中的其他代码要使用 use_factory 所分配的对象,我们就应该修改此函数,让他返回一个指针,指向它分配的内存

void use_factory(T arg) {
    Foo *p = factory(arg);
    // 使用 p
    return p; // 调用者必须释放内存
} 

delete 之后重置指针值......

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

可以在 delete 之后将 nullptr 赋予指针,这样就清楚地指出指针不指向任何对象

......这只是提供了有限的保护

在 delete 内存之后重置指针的方法只对这个指针有效,对其他任何仍指向(已释放的)内存的指针是没有作用的。

    int *p(new int(42)); // p 指向动态内存
    auto q = p; // p 和 q 指向相同的内存
    delete p; // p 和 q 均变为无效
    p = nullptr; // 指出 p 不再绑定到任何对象

shared_ptr 和 new 结合使用

我们还可以用 new 返回的指针来初始化智能指针

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

接受指针参数的智能指针构造函数是 explicit 的。因此,我们不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针

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

不能进行内置指针到智能指针间的隐式转换

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

shared_ptr<int> clone(int p) {
    return shared_ptr<int>(new int(p)); // 正确:显式地用 int* 创建 shared_ptr<int>
}

定义和改变 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_ptr, reset 会释放此对
p.reset(q)                   象。若传递了可选的参数内置指针 q,会令 p 指向 q,否则会
p.reset(q, d)                将 p 置为空。若还传递了参数 d,将会调用 d 而不是 delete 来释放 q

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

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

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

  • get() 函数:返回一个内置指针,指向智能指针管理的对象
  • 此函数是为了这样一种情况而设计的:我们需要向不能使用智能指针的代码传递一个内置指针。使用 get 返回的指针的代码不能 delete 此指针
    shared_ptr<int> p(new int(42)); // 引用计数为 1
    int *q = p.get(); // 正确:但使用 q 时要注意,不要让它管理的指针被释放
    {
        // 未定义:两个独立的 shared_ptr 指向相同的内存
        shared_ptr<int>(q);
    } // 程序块结束,q 被销毁,它指向的内存被释放
    int foo = *p; // 未定义:p 指向的内存已经被释放了

其他 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; // 现在我们知道自己是唯一的用户,可以改变对象的值

智能指针和异常

使用异常处理的程序能在异常发生后令程序流程继续,我们注意到,这种程序需要确保在异常发生后资源能被正确的释放。一个简单的确保资源被释放的方法是使用智能指针。

void f() {
    shared_ptr<int> sp(new int(42)); // 分配一个新对象
    // 这段代码抛出一个异常,且在 f 中未被捕获
} // 在函数结束时 shared_ptr 自动释放内存

如果使用内置指针管理内存,且在 new 之后在对应的 delete 之前发生了异常,则内存不会被释放

void f() {
    int *ip = new int(42); // 动态分配一个新对象
    // 这段代码抛出一个异常,且在 f 中未被捕获
    delete ip; // 在退出之前释放内存
} // 如果在 new 和 delete 之间发生异常,且异常未在 f 中被捕获,则内存就永远不会被释放了。
// 在函数 f 之外没有指针指向这块内存,因此就无法释放它了

智能指针和哑类

不是所有的类都定义了良好的析构函数。特别是那些为 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 结束时由析构函数自动关闭连接。
使用 shared_ptr 来保证 connection 被正确关闭,已被证明是一种有效的方法

使用我们自己的释放操作

为了用 shared_ptr 来管理一个 connection,我们必须首先定义一个函数来代替 delete。这个删除器函数必须能够完成对 shared_ptr 中保存的指针进行释放的操作.

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

void f(distination &d /* 其他参数 */) {
    connection c = connect(&d);
    shared_ptr<connection> p(&c, end_connection);
    // 使用连接
    // 当 f 退出时(即使是由于异常而退出),connection 会被正确关闭
}

unique_ptr

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

    unique_ptr<double> p1; // 可以指向一个 double 的 unqiue_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 特有的操作

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)                如果提供了内置指针 q,令 u 指向这个对象;否则将 u 置为空
u.reset(nullptr)
	// 将所有权从 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 返回的指针,我们的程序就要负责资源的释放

	p2.release(); // 错误:p2 不会释放内存,而且我们丢失了指针
	auto p = p2.release(); // 正确,但我们必须记得 delete(p);

传递 unique_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;
}

向 unique_ptr 传递删除器

// p 指向一个类型为 objT 的对象,并使用一个类型为 delT 的对象释放 objT 对象
// 它会调用一个名为 fcn 的 delT 类型对象
unique_ptr<objT, delT> p (new objT, fcn);

例:重写连接程序

void f(ddedstination &d /* 其他需要的参数 */) {
	connection c = connect(&d); // 打开连接
	// 当 p 被销毁时,连接将会关闭
	unique_ptr<connection, decltype(end_connection)*> p(&c, end_connection);
	// 使用连接
	// 当 f 退出时(即使是由于异常而退出),connection 会被正确关闭别学了
}

weak_ptr

weak_ptr 是一种不控制所指向对象生存期的只能指针,它指向由一个 shared_ptr 管理的对象。将一个 weak_ptr 绑定到一个 shared_ptr 不会改变 shared_ptr 的引用计数。一旦最后一个指向对象的 shared_ptr 被销毁,对象就会被释放。即使有 weak_ptr 指向对象,对象也还是会被释放,因此,weak_ptr 的名字抓住了这种只能指针“弱”共享对象的特点。

weak_ptr

weak_ptr<T> w           空 weak_ptr 可以指向类型为 T 的对象
weak_ptr<T> w(ps)       与 shared_ptr sp 指向相同对象的 weak_ptr。T 必须能转换为 sp 指向的类型

w = p                   p 可以是一个 shared_ptr 或一个 weak_ptr 。赋值后 w 与 p 共享对象

w.reset()               将 w 置空
w.use_count()           与 w 共享对象的 shared_ptr 的数量
w.expired()             若 w.use_count() 为 0,返回 true,否则返回 false
w.lock()                如果 expired 为 true,返回一个空 shared_ptr;否则返回一个指向 w 的对象的 shared_ptr

当我们创建一个 weak_ptr s时,要用一个 shared_ptr 来初始化它:

    auto p = make_shared<int>(42);
    weak_ptr<int> wp(p); // wp 弱共享 p;p 的引用计数未改变

由于对象可能不存在,我们不能使用 weak_ptr 直接访问对象,而必须调用 lock。此函数检查 weak_ptr 指向的对象是否仍存在。

    if (shared_ptr<int> np = wp.lock()) { // 如果 np 不为空则条件成立
        // 在 if 中,np 与 p 共享对象
    }

核查指针类

作为 weak_ptr 用途的一个展示,我们将为 StrBlob 类定义一个伴随指针类。

#pragma once
#ifndef STRBLOB_H
#define STRBOLB_H

#include <iostream>
#include <string>
#include <vector>
#include <memory>
#include <initializer_list>
#include <stdexcept>

using std::vector; using std::string; using std::initializer_list;
using size_type = vector<string>::size_type;

class StrBlobPtr;
class StrBlob {
	friend class StrBlobPtr;
public:
	StrBlob();
	StrBlob(std::initializer_list<std::string> il);
	size_type size() const { return data -> size(); }
	bool empty() const { return data -> empty(); }
	// 添加和删除元素
	void push_back(const std::string &t) { data -> push_back(t); }
	void pop_back();		
	// 元素访问
	std::string &front();
	const std::string &front() const;
	std::string &back();
	const std::string &back() const;
private:
	std::shared_ptr<std::vector<std::string>> data;
	// 如果 data[i] 不合法,抛出一个异常
	void check(size_type i, const std::string &msg) const;
};

class StrBlobPtr {
public:
	StrBlobPtr() : curr(0) { }
	StrBlobPtr(StrBlob &a, std::size_t sz = 0) : wptr(a.data), curr(sz) { }
	std::string& deref() const;
	StrBlobPtr& incr(); // 前缀递增
private:
	// 若检查成功,check 返回一个指向 vector 的 shared_ptr
	std::shared_ptr<std::vector<std::string>> check(std::size_t, const std::string&) const;
	// 保存一个 weak_ptr,意味着底层 vector 可能被销毁
	std::weak_ptr<std::vector<std::string>> wptr;
	std::size_t curr; // 在数组中的当前位置
};


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

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

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

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

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

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

void StrBlob::pop_back() {
	check(0, "pop_back on empty StrBlob");
	data -> pop_back();
}

std::shared_ptr<std::vector<std::string>> StrBlobPtr::check(std::size_t i, const std::string &msg) const {
	auto ret = wptr.lock(); // vector 还存在吗
	if (!ret) {
		throw std::runtime_error("unbound StrBlobPtr");
	}
	if (i >= ret->size()) {
		throw std::out_of_range(msg);
	}
	return ret; // 否则,返回指向 vector 的 shared_ptr
}

#endif

指针操作

deref 成员调用 check,检查使用 vector 是否安全以及 curr 是否在合法范围内

std::string& StrBlobPtr::deref() const {
	auto p = check(curr, "dereference past end");
	return (*p)[curr]; // (*P) 是对象所指向的 vector
}

incr 成员也调用 check

StrBlobPtr& StrBlobPtr::incr() {
	// 如果 curr 已经指向容器的尾后位置,就不能递增它
	check(curr, "increment past end of StrBlobPtr");
	++ curr; // 推进当前位置
	return *this;
}

我们还要为 StrBlob 类定义 begin 和 end 操作,返回一个指向它自身的 StrBlobPtr

	// 返回指向首元素和尾后元素的 StrBlobPtr
	StrBlobPtr begin() { return StrBlobPtr(*this); }
	StrBlobPtr end() {
		auto ret = StrBlobPtr(*this, data -> size());
		return ret;
	}

动态数组

标准库中包含一个名为 allocator 的类,允许我们将分配和初始化分离。使用 allocator 通常会提供更好的性能和更灵活的内存管理能力。

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

new 和数组

为了让 new 分配一个对象数组,我们要在类型名之后跟一对方括号,在其中指明要分配的对象的数目。在下例中,new 分配要求数量的对象并(假定分配成功后)返回指向第一个对象的指针。

    // 调用 get_size 确定分配多少个 int
    int *pia = new int[get_size()]; // pia 指向第一个 int,方括号中的大小必须是整型,但不必是常量

也可以用一个表示数组类型的类型别名来分配一个数组,这样,new 表达式中就不需要方括号了

    typedef int arrT[42]; // arrT 表示 42 个 int 的数组类型
    int *p = new arrT; // 分配一个 42 个 int 的数组;p 指向第一个 int

     |
     v

    int *p = new int[42];

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

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

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

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

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

在新标准中,我们还提供一个元素初始化器的花括号列表

    // 10 个 int 分别用列表中对应的初始化器初始化
    int *pia3 = new int[10]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    // 10 个 string,前 4 个用给定的初始化器初始化,剩余的进行值初始化
    string *psa3 = new string[10]{"a", "an", "the", string(3, 'X')};

如果初始化器数目大于元素数目,则 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,代码仍能正常工作

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

当我们用 new 分配一个大小为 0 的数组时,new 返回一个合法的非空指针。次指针保证与 new 返回的其他任何指针都不相同。对于零长度的数组来说,此指针就像尾后指针一样,我们可以像使用尾后迭代器一样使用这个指针。可以用此指针进行比较操作,就像上面循环代码中那样。可以向此指针加上(或从此指针减去)0,也可以从此指针减去自身从而得到 0。但此指针不能解引用 —— 毕竟它不指向任何元素。

释放动态数组

我们使用一种特殊形式的 delete —— 在指针前加上一个空方括号对:

    delete p; // p 必须指向一个动态分配的对象或为空
    delete [] pa; // pa 必须指向一个动态分配的数组或为空

数组中的元素按逆序销毁,即,最后一个元素首先被销毁,然后是倒数第二个,以此类推。

    typedef int arrT[42]; // arrT 是 42 个 int 的数组的类型别名
    int *p = new arrT; // 分配一个 42 个 int 的数组;p 指向第一个元素
    delete [] p; // 方括号是必需的,因为我们当初分配的是一个数组

智能指针和动态数组

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

    -----------------------------------------------
    // 当一个 unique_ptr 指向一个数组时,我们可以使用下标运算符来访问数组中的元素
    for (size_t i = 0; i != 10; ++ i) up[i] = i;

指向数组的 unique_ptr

指向数组的 unique_ptr 不支持成员访问运算符(点和箭头运算符)
其他 unique_ptr 操作不变

unique_ptr<T[]> u            u 可以指向一个动态分配的数组,数组元素类型为 T
unique_ptr<T[]> u(p)         u 指向内置指针 p 所指向的动态分配的数组。p 必须能转换为类型 T*

u[i]                         返回 u 拥有的数组中位置 i 处的对象,u 必须指向一个数组

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

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

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

shared_ptr 不直接支持动态数组管理这一特性会影响我们如何访问数组中的元素

    // shared_ptr 未定义下标运算符,并且不支持指针的算术运算
    for (size_t i = 0; i != 10; ++ i)
        *(sp.get() + i) = i; // 使用 get 获取一个内置指针

allocator 类

一般情况下,将内存分配和对象构造组合在一起可能会导致不必要的浪费。例如:

    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<string> alloc; // 可以分配 string 的 allocator 对象
    auto const p = alloc.allocate(n); // 分配 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* 的指针,指向一块原始内存;arg 被传递
                          给类型为 T 的构造函数,用来在 p 指向的内存中构造一个对象

a.destroy(p)              p 为 T* 类型的指针,此算法对 p 指向的对象执行析构函数

allocator 分配未构造的内存

allocator 分配的内存是未构造的。我们按需要在此内存中构造对象。

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

    // 还未构造对象的情况下就使用原始内存是错误的
    cout << *p << endl; // 正确:使用 string 的输出运算符
    cout << *q << endl; // 错误:q 指向未构造的内存

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

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

一旦元素被销毁后,就可以重新使用这部分内存来保存其他 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 必
                                   须指向足够大的未构造的原始内存,能够容纳给定数量的
                                   对象

例:将原 vector 中的元素拷贝到前一半空间,对后一半空间用一个给定值进行填充

    // 分配比 vi 中元素所占空间大一倍的动态内存
    auto p = alloc.allocate(vi.size() * 2);
    // 通过拷贝 vi 中的元素来构造从 p 开始的元素
    auto q = uninitialized_copy(vi.begin(), vi.end(), p);
    // 将剩余元素初始化为 42
    uninitialized_fill_n(q, vi.size(), 42);

    for (auto it = p; it != p + vi.size() * 2; ++ it) cout << *it << ' ';
    cout << endl;

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

实现一个简单的文本查询程序,作为标准库相关内容学习的总结

文本查询程序设计

#pragma once
#ifndef TEXTQUERY_H
#define TEXTQUERY_H

#include <iostream>
#include <string>
#include <vector>
#include <cstdlib>
#include <fstream> // getline
#include <map>
#include <set>
#include <memory>
#include <sstream> // istringstream

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

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;
};

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; // 输入文件
};

TextQuery::TextQuery(std::ifstream &is) : file(new std::vector<std::string>) {
	std::string text;
	while (getline(is, text)) { // 对文件中每一行
		file -> push_back(text); // 保存此行文本
		int n = (int)(file -> size()) - 1; // 当前行号
		std::istringstream line(text); // 将行文本分解为单词
		std::string word;
		while (line >> word) { // 对行中每个单词
			// 如果单词不在 wn 中,以之为下标在 wn 中添加一项
			auto &lines = wm[word]; // lines 是一个 shared_ptr
			if (!lines) // 在我们第一次遇到这个单词时,此指针为空
				lines.reset(new std::set<line_no>); // 分配一个新的 set
			lines -> insert(n); // 将此行号插入 set 中
		}
	}
}

QueryResult TextQuery::query(const std::string &sought) const {
	// 如果未找到 sought,我们将返回一个指向此 set 的指针
	static std::shared_ptr<std::set<line_no>> nodata(new std::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);
}

std::string make_plural(std::set<line_no>::size_type size, std::string time, std::string s) {
	return size == 1 ? time : time + s;
}

std::ostream &print(std::ostream& os, const QueryResult &qr) {
	// 如果找到了单词,打印出现次数和所有出现的位置
	os << qr.sought << " occurs " << qr.lines -> size() << " " << make_plural(qr.lines -> size(), "time", "s") << std::endl;
	// 打印单词出现的每一行
	for (auto num : *qr.lines) { // 对 set 中的每个单词
		// 避免行号从 0 开始给用户带来的困惑
		os << "\t(line " << num + 1 << ") " << *(qr.file -> begin() + num) << std::endl;
	}
	return os;
}

void runQuery(std::ifstream &infile) {
	// infile 是一个 ifstream,指向我们要处理的文件
	TextQuery tq(infile); // 保存文件并建立查询 map
	// 与用户交互:提示用户输入要查询的单词,完成查询并打印结果
	while (true) {
		std::cout << "enter word to look for, or q to quit: ";
		std::string s;
		// 若遇到文件尾或用户输入了 'q' 时循环终止
		if (!(std::cin >> s) || s == "q") break;
		// 指向查询并打印结果
		print(std::cout, tq.query(s)) << std::endl;
	}
}

#endif

//#define NDEBUG
#include <iostream>
#include <string>
#include <vector>
#include <cstring>
#include <cstddef>
#include <iterator>
#include <stdexcept>
#include <initializer_list>
#include <cstdlib>
#include <cassert>
#include <fstream>
#include <sstream>
#include <list>
#include <deque>
#include <forward_list>
#include <array>
#include <stack>
#include <numeric>
#include <algorithm>
#include <functional>
#include <map>
#include <set>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <memory>
#include <new>

//#include "Chapter6.h"
//#include "Sales_data.h"
//#include "Person.h"
//#include "Screen.h"
//#include "Debug.h"
//#include "Account.h"
//#include "Sales_item.h"
//#include "StrBlob.h"
#include "TextQuery.h"

using std::cin; using std::cout; using std::endl; using std::cerr;
using std::vector; using std::string;
using std::begin; using std::end;
using std::ends; using std::flush;
using std::ifstream; using std::ofstream; using std::istringstream; using std::ostringstream;
using std::list; using std::deque; using std::forward_list; using std::array; using std::stack;
using std::map; using std::set; using std::multimap; using std::multiset; 
using std::unordered_map; using std::unordered_set; using std::unordered_multimap; using std::unordered_multiset;
using std::pair;
using std::shared_ptr; using std::make_shared; using std::unique_ptr; using std::weak_ptr;
using std::allocator; using std::uninitialized_copy; using std::uninitialized_copy_n; using std::uninitialized_fill; using std::uninitialized_fill_n;

using namespace std::placeholders;

const int n = 10;

int main(int argc, char *argv[]) {

    ifstream input("Text");
    if (input.is_open()) {
        runQuery(input);
    } else {
        std::cerr << "Failed to open file" << std::endl;
        return EXIT_FAILURE;
    }

    return 0;
}
posted @ 2023-02-13 16:27  HuiPuKui  阅读(54)  评论(0)    收藏  举报