C++ Primer 总结索引 | 第十二章:动态内存

news2025/1/16 5:48:57

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

2、局部static变量 是指 在函数内部声明的静态变量。这种变量 具有局部作用域,但其生命周期 从声明开始 直到程序结束。这意味着,即使函数 执行完毕,局部static变量的值 也不会消失,而是 保持上次函数调用结束时的值。在下一次 调用同一函数时,局部static变量 将使用上次留下的值,而不是 重新初始化

使用局部static变量的好处包括:
1)保持 函数内变量的状态,无需 使用外部变量
2)避免 变量的频繁创建 和 销毁,提高性能(尤其是对于复杂的对象)
3)实现单例模式等 设计模式时,局部static变量非常有用

#include <iostream>

void counter() {
    static int count = 0; // 局部static变量
    count++;
    std::cout << "当前计数: " << count << std::endl;
}

int main() {
    counter(); // 输出: 当前计数: 1
    counter(); // 输出: 当前计数: 2
    counter(); // 输出: 当前计数: 3
    return 0;
}

在这个例子中,每次调用 counter函数时,count变量 都会递增。由于count是 局部static变量,它不会在 每次调用counter函数时重新初始化,而是 保留上一次调用结束时的值

如果局部static变量 没有显式初始化,它将 被自动初始化为零(对于基本数据类型)
在多线程环境中,局部static变量的初始化 可能需要特别注意,以确保 线程安全。C++11及以后的标准中,局部static变量的初始化 是线程安全的,但在此之前的老版本中可能不是

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

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

4、我们的程序 到目前为止 只使用过 静态内存 或 栈内存
静态内存 用来保存 局部static对象、类static数据成员 以及 定义在任何函数之外的变量,以及 常量
栈内存 用来保存 定义在函数内的非static对象,用于存储函数调用时的局部变量和函数的参数。当函数被调用时,系统会为函数的局部变量分配内存空间,当函数执行结束时,这些内存空间会被自动释放

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

静态内存具有以下特点:
1)生命周期长:静态内存的生命周期从程序启动到程序结束,变量的值在整个程序运行期间保持不变
2)作用域广:静态内存中的数据 可以在程序的任何地方访问,具有全局性
3)一次分配,多次使用:静态内存 只需分配一次,之后可以 多次读取和写入

栈内存的特点包括:
1)后进先出:栈内存采用后进先出(LIFO)的存储方式,最后进入的数据首先被取出
2)生命周期短:栈内存中的数据的生命周期 随着函数的调用和结束 而动态变化,局部变量的生命周期 仅限于函数的执行期间

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

1、动态内存 和 智能指针

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

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

3、为了更容易(同时也更安全)地使用动态内存,新的标准库 提供了两种智能指针类型 来管理动态对象。智能指针的行为 类似常规指针,重要的区别是 它负责自动释放 所指向的对象。新标准库 提供的这两种智能指针的区别 在于管理底层指针的方式shared_ptr 允许多个指针指向同一个对象;unique_ptr 则 “独占” 所指向的对象
标准库还定义了 一个名为 weak_ptr 的伴随类,它是一种弱引用,指向 shared_ptr 所管理的对象

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

1.1 shared_ptr类

1、智能指针也是模板。因此,当我们 创建一个智能指针时,必须提供额外的信息——指针可以指向的类型。与vector一样,我们在尖括号内 给出类型,之后是 所定义的这种智能指针的名字

shared_ptr<string> p1;    // shared_ptr,可以指向string
shared_ptr<list<int>> p2; // shared_ptr,可以指向int的list

默认初始化的智能指针 中保存着 一个空指针

智能指针的使用方式 与普通指针类似。解引用一个智能指针 返回它指向的对象。如果 在一个条件判断中 使用智能指针,效果就是 检测它是否为空

// 如果p1不为空,检查 它是否指向一个空string
if (p1 && pl->empty())
	*p1 = "hi"; // 如果p1指向一个空string,解引用p1,将一个新值 赋予string

2、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中的指针

p.get() 用法

#include <iostream>
#include <memory>

int main() {
    // 创建一个 shared_ptr,指向动态分配的整型对象
    std::shared_ptr<int> ptr(new int(42));

    // 使用 .get() 方法获取指向被 shared_ptr 管理对象的原始指针
    int *rawPtr = ptr.get();

    // 打印原始指针的值
    std::cout << "原始指针的值: " << rawPtr << std::endl;

    // 注意:不要使用原始指针释放内存!
    // 不要手动 delete rawPtr;

    return 0;
}

shared_ptr 独有的操作

操作解释
make_shared<T> (args)返回一个shared_ptr,指向 一个动态分配的 类型为T的对象。使用args 初始化此对象
shared_ptr<T> p(q)p是 shared_ptr q 的拷贝:此操作 会递增q中的计数器。q中的指针 必须能转换为T
p=qp和q 都是shared_ptr,所保存的指针 必须能相互转换。此操作会递减p的引用计数,递增q的引用计数;若p的引用计数变为0,则将其管理的原内存释放
p.unique()若p.use_count() 为1,返回true;否则返回false
p.use_count()返回与p共享对象的智能指针数量:可能很慢,主要用于调试

3、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 用其参数 来构造给定类型的对象
如果我们不传递任何参数,对象就会进行 值初始化

通常用auto 定义一个对象 来保存 make_shared 的结果

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

4、当进行 拷贝或赋值操作 时,每个 shared_ptr 都会记录有多少个 其他 shared_ptr 指向相同的对象

auto p = make_shared<int>(42); // p指向的对象 只有p一个引用者
auto q(p);                     // p和q指向相同对象,此对象有两个引用者

可以认为 每个 shared_ptr 都有一个关联的计数器,通常称其为引用计数
无论何时 我们拷贝一个 shared_ptr,计数器都会递增。例如,当用一个 shared_ptr 初始化另一个shared_ptr,或 将它作为参数传递给一个函数 以及 作为函数的返回值 时,它所关联的计数器 就会递增
当 给shared_ptr 赋予一个新值 或是 shared_ptr 被销毁(例如 局部的 shared_ptr 离开其作用域)时,计数器就会递减

下面是一个示例,演示了当 shared_ptr 作为函数的返回值时,关联的计数器是如何递增的:

#include <iostream>
#include <memory>

std::shared_ptr<int> createSharedPtr() {
    std::shared_ptr<int> ptr(new int(42));
    std::cout << "创建 shared_ptr,计数器:" << ptr.use_count() << std::endl;
    return ptr;
}

int main() {
    // 调用函数创建 shared_ptr,并接收返回值
    std::shared_ptr<int> returnedPtr = createSharedPtr();
    
    // 打印返回的 shared_ptr 的计数器值
    std::cout << "返回的 shared_ptr,计数器:" << returnedPtr.use_count() << std::endl;

    return 0;
}

当运行这个程序时,你会看到如下输出:

创建 shared_ptr,计数器:1
返回的 shared_ptr,计数器:1

可以看到,创建 shared_ptr 的函数中计数器值为 1,而返回的 shared_ptr 的计数器值为 1。这说明了当 shared_ptr 作为函数返回值时,关联的计数器会递增(本来 ptr 都销毁了,应该为0了),以确保内存中的对象在 至少有一个 shared_ptr 指向它时不会被销毁

就算函数 换成

std::shared_ptr<int> createSharedPtr() {
    return std::make_shared<int>(42);
}

结果还是:返回的 shared_ptr,计数器:1

一旦 一个shared_ptr的计数器变为0,它就会 自动释放自己所管理的对象

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

分配了一个int,将其指针保存在r中

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

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

析构函数 一般用来 释放对象所分配的资源。例如,string的构造函数 会分配内存 来保存构成string的字符。string的析构函数 就负责释放 这些内存

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

6、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,所以 我们可以确保 它分配的对象会 在恰当的时刻被释放。例如,下面的函数 将factory返回的shared_ptr 保存在局部变量中:

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

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

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

拷贝一个 shared_ptr 会增加 所管理对象的引用计数值。现在 当p被销毁时,它所指向的内存 还有其他使用者。对于一块内存,shared_ptr 类 保证只要有任何 shared_ptr对象 引用它,它就不会 被释放掉

由于在 最后一个shared_ptr销毁前 内存都不会释放,保证shared_ptr在无用之后 不再保留就非常重要了。如果 你忘记了销毁程序 不再需要的 shared_ptr,程序仍会正确执行,但会浪费内存。share_ptr 在无用之后 仍然保留的一种可能情况是,你将shared_ ptr 存放在一个容器中,随后 重排了容器,从而 不再需要某些元素。在这种情况下,你应该 确保用erase删除那些不再需要的shared_ptr元素

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

int main() {
    // 创建一个存放 std::shared_ptr 的容器
    std::vector<std::shared_ptr<int>> ptrContainer;

    // 向容器中添加一些 std::shared_ptr
    ptrContainer.push_back(std::make_shared<int>(1));
    ptrContainer.push_back(std::make_shared<int>(2));
    ptrContainer.push_back(std::make_shared<int>(3));

    // 在这里对容器进行重排或者其他操作

    // 假设在这之后不再需要第一个指针
    ptrContainer.erase(ptrContainer.begin()); // 删除第一个元素

    // 假设在这之后不再需要第三个指针
    auto it = std::find(ptrContainer.begin(), ptrContainer.end(), nullptr); // 找到需要删除的指针
    if (it != ptrContainer.end()) {
        ptrContainer.erase(it); // 删除指定元素
    }

    // 在这之后,确保容器中存放的都是仍然需要的指针

    return 0;
}

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

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

容器类是出于第一种原因而使用动态内存的典型例子

8、到目前为止,我们使用过的类中,分配的资源 都与对应对象生存期一致。例如,每个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仍然 在使用它们

9、定义 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),如果用户试图 访问不存在的元素,这些操作会抛出一个异常

类有一个默认构造函数和一个构造函数,接受单一的 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的元素

std::initializer_list 是 C++11 引入的一种新特性,它允许你用花括号 {} 初始化列表的方式来初始化对象。std::initializer_list 是一个模板类,用于表示一个特定类型 T 的值的数组
std::initializer_list 常用于构造函数和函数参数,使得函数可以接受任意数量的参数,只要这些参数是同一类型的。这对于初始化容器类如 std::vector、std::array、std::map 等非常有用

#include <iostream>
#include <string>
#include <initializer_list>

class StringList {
public:
    StringList(std::initializer_list<std::string> initList) {
        for (const auto& str : initList) {
            std::cout << str << std::endl;
        }
    }
};

int main() {
    // 使用 initializer_list 来初始化 StringList 对象
    StringList list{"Hello", "World", "Initializer", "List"};
    return 0;
}

特点
1)自动推导类型:你不需要指定列表中元素的数量,编译器会自动根据初始化列表中的元素数量来推导
2)只读访问:通过 std::initializer_list 提供的元素只能进行只读访问。它提供的迭代器是常量迭代器,这意味着你不能修改列表中的元素
3)生命周期:std::initializer_list 对象的生命周期通常很短,它只是临时存储和传递初始化值的一种手段。因此,它适合用于初始化但不适合用作存储容器

元素访问成员函数:定义了一个名为check的 private工具函数,它检查 一个给定索引是否在合法范围内。除了索引,check还接受一个string参数,它会 将此参数传递给异常处理程序,这个string 描述了错误内容
pop.back 和 元素访问成员函数 首先调用check。如果check成功,这些成员函数 继续利用底层vector的操作 来完成自己的工作

front和back应该对const进行重载

在C++中,使用const关键字 可以实现 对成员函数的重载,这允许你 根据对象是否为常量 来调用不同的函数实现。这种技术常用于提供对常量和非常量对象的不同操作,确保对常量对象的访问不会修改其状态
下面是一个使用const重载成员函数的例子:

class MyClass {
public:
    void display() const {
        // 对于const对象调用的版本
        std::cout << "Display const" << std::endl;
    }
    
    void display() {
        // 对于非const对象调用的版本
        std::cout << "Display non-const" << std::endl;
    }
};

在这个例子中,display函数 被重载了两次:一次是 带有const修饰的,一次是 不带const的。当你尝试 在常量对象上调用display时,编译器 会选择带有const修饰的版本;而在 非常量对象上调用时,会选择 不带const的版本

int main() {
    MyClass obj;
    const MyClass cObj;
    
    obj.display();   // 调用非const版本
    cObj.display();  // 调用const版本
    
    return 0;
}

通过合理使用const重载,你可以提高代码的安全性和灵活性,确保在适当的上下文中以正确的方式访问对象

StrBlob的拷贝、赋值和销毁:拷贝一个 shared_ptr 会递增 其引用计数;将一个 shared_ptr 赋予另一个 shared_ptr 会递增赋值号右侧 shared_ptr 的引用计数,而递减左侧 shared_ptr 的引用计数。如果一个 shared_ptr 的引用计数变为0,它所指向的对象
会被自动销毁。因此,对于 由StrBlob构造函数分配的vector,当最后一个指向它的 StrBlob对象 被销毁时,它会 随之被自动销毁

实现:
StrBlob.h

#pragma once
#ifndef STRBLOB_H
#define STRBLOB_H

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

class StrBlob {
public:
	typedef std::vector<std::string>::size_type size_type;
	StrBlob() :data(std::make_shared<std::vector<std::string>>()) {};
	StrBlob(std::initializer_list<std::string> il);
	size_type size() const { return data->size(); } // data是指针
	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& front() const;
	std::string& back();
	std::string& back() const;

private:
	std::shared_ptr<std::vector<std::string>> data;
	void check(size_type i, const std::string& msg) const;
};

StrBlob::StrBlob(std::initializer_list<std::string> il):data(std::make_shared<std::vector<std::string>>(il)) {} // 构造函数也要StrBlob::

void StrBlob::check(size_type i, const std::string& msg) const{ // 实现的时候也需要加const
	if (i >= data->size())
		throw std::out_of_range(msg);
}

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

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

std::string& StrBlob::front() const{ // 对const进行重载
	check(0, "front on empty StrBlob");
	return data->front();
}

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

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

#endif

12.2.cpp

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


int main()
{
	StrBlob b1({ "a", "an", "the" });
	const StrBlob b2 = { "a", "b", "c" };

	std::cout << b1.back() << std::endl;
	std::cout << b2.back() << std::endl;

	return 0;
}

为什么在initial_list的参数里不能直接加引用,需要加const 引用

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

在C++中,std::initializer_list 通常被设计为一种轻量级容器,用于初始化列表的传递和访问。它的设计初衷是为了实现轻量级的、不可变的列表,即元素都是 const 类型
这就导致了在初始化列表的参数中只添加引用是无效的

10、在此代码的结尾,b1 和 b2 各包含多少个元素(注意第二行 不是两个 shared_ptr<vector<string>> 赋值,所以b1减b2增不成立)

StrBlob b1;
{
	StrBlob b2 = {"a", "an", "the"};
	b1 = b2;
	b2.push_back("about");
}

代码第 3 行创建 b2 时提供了 3 个 string 的列表,因此会创建一个包含 3 个 string 的 vector 对象,并创建一个 shared_ptr 指向此对象(引用计数为 1)
第 4 行将 b2 赋予 b1 时,创建一个 shared_ptr 也指向刚才创建的 vector 对象,引用计数变为 2
因此,第 4 行向 b2 添加一个 string 时,会向两个 StrBlob 共享的 vector 中添加此 string。最终,在代码结尾,b1 和 b2 均包含 4 个 string
右花括号结束,b2 销毁;b1 仍有效,包含 4 个 string

11、StrBlob 需要const 版本的push_back 和 pop_back吗
通常情况下,push_back 和 pop_back 这样的函数用于修改对象的状态,因此它们通常不应该是 const 成员函数。const 成员函数声明了不修改对象状态的保证,因此在设计上不应该将修改对象状态的操作放在 const 成员函数中
如果你的设计中希望在对象是 const 的情况下也能够修改对象的状态,那么可以提供 const 版本的 push_back 和 pop_back。这种情况通常较少见,而且需要特别小心,因为它违反了常规的对象语义

12、在我们的 check 函数中,没有检查 i 是否大于0。为什么可以忽略这个检查
因为 vector<string>::size_type 是一个unsigned,任何小于0的数 传进来 就会转成大于0的数

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

使用explicit之后
优点:我们可以清楚地知道使用的是哪种类型;
缺点:不易使用,需要显式地初始化

未编写接受一个初始化列表参数的显式构造函数,意味着可以进行列表向 StrBlob 的隐式类型转换,亦即在需要 StrBlob 的地方(如函数的参数),可以使用列表进行代替。而且,可以进行拷贝形式的初始化(如赋值)。这令程序编写更为简单方便
但这种隐式转换并不总是好的。例如,列表中可能并非都是合法的值。再如,对于接受 StrBlob 的函数,传递给它一个列表,会创建一个临时的 StrBlob 对象,用列表对其初始化,然后将其传递给函数,当函数完成后,此对象将被丢弃,再也无法访问了。对于这些情况,我们可以定义显式的构造函数,禁止隐式类类型转换

隐式初始化

class MyClass {
public:
    std::vector<int> values;

    MyClass(std::initializer_list<int> initList) : values(initList) {
        std::cout << "MyClass initialized with a list of " << values.size() << " elements.\n";
    }
};

void func(MyClass obj) {
    // 函数内容...
}

int main() {
    MyClass obj1 {5, 6, 7, 8};   // 隐式
    MyClass obj2 {{5, 6, 7, 8}}; // 隐式
	MyClass obj3 = {1, 2, 3, 4}; // 复制列表初始化(隐式)
    func({9, 10, 11});  // 隐式地构造 MyClass 对象并传递给 func
}

显式初始化
加了explict

class MyClass {
public:
    std::vector<int> values;

    explicit MyClass(std::initializer_list<int> initList) : values(initList) {
        std::cout << "MyClass initialized with a list of " << values.size() << " elements.\n";
    }
};

void func(MyClass obj) {
    // 函数内容...
}

int main() {
    MyClass obj1({1, 2, 3, 4});  // 显式初始化,合法

    // func({9, 10, 11});  // 编译错误,不能隐式地使用初始化列表构造 MyClass 对象
    func(MyClass({9, 10, 11}));  // 显式构造 MyClass 对象
}

1.2 直接管理内存

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

2、相对于智能指针,使用这两个运算符管理内存非常容易出错。而且,自己直接管理内存的类 与使用智能指针的类不同,它们 不能依赖类对象拷贝、赋值和销毁操作的任何默认定义

3、使用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
// *ps 为"9999999999"
string *ps = new string(10, 9);
// vector有10个元素,值依次从0到9

vector<int> *pv = new vector<int>{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

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

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

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

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

// p指向一个 与obj类型相同的对象
auto p1 = new auto(obj);
// 该对象用obj进行初始化

// 错误:括号中只能有单个初始化器
auto p2 = new auto{a, b, c};

p1的类型 是一个指针,指向 从obj自动推断出的类型

5、动态分配的const对象:用 new分配 const对象是合法的

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

对于一个定义了 默认构造函数的类 类型,其const动态对象 可以隐式初始化,而 其他类型的对象 就必须显式初始化。由于分配的对象是 const的,new返回的指针 是一个指向const的指针

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

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

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

7、释放动态内存:为了 防止内存耗尽,在动态内存使用完毕后,必须 将其归还给系统。我们通过 delete表达式 来将动态内存归还给系统。delete表达式 接受一个指针,指向 我们想要释放的对象

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

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

8、指针值和delete:传递给delete的指针 必须指向动态分配的内存,或者是 一个空指针
释放一块 并非new分配的内存,或者 将相同的指针值 释放多次,其行为是未定义的:

int i, *pi1 = &i, *pi2 = nullptr;
double *pd = new double(33), *pd2 = pd;
delete i;   // 错误:i不是一个指针

delete pi1; // 未定义:pi1指向一个局部变量

delete pd;  // 正确
delete pd2; // 未定义:pd2指向的内存 已经被释放了

delete pi2; // 正确:释放一个空指针总是没有错误的

delete pi1pd2 所产生的错误 则更具潜在危害:通常情况下,编译器不能分辨一个指针指向的是 静态 还是动态分配的对象。类似的,编译器 也不能分辨 一个指针所指向的内存 是否已经被释放了。对于 这些delete表达式,大多数编译器 会编译通过,尽管它们是错误的

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

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

9、动态对象的生存期 直到被释放时为止:由 shared_ptr 管理的内存 在最后一个 shared_ptr 销毁时会被自动释放。但对于通过内置指针类型 来管理的内存,就不是这样了。对于一个 由内置指针管理的动态对象,直到 被显式释放之前 它都是存在的

返回指向动态内存的指针(而不是智能指针)的函数 给其调用者 增加了一个额外负担——调用者必须记得释放内存

Foo* factory(T arg)
{
	// 视情况处理arg
	return new Foo(arg); // 调用者负责释放此内存
}

factory的调用者 负责在不需要此对象时 释放它

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

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

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; // 调用者必须释放内存
}

10、动态内存的管理非常容易出错:
1)忘记delete内存
2)使用已经释放掉的对象
3)同一块内存释放两次

坚持只使用智能指针,就可以避免所有这些问题

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

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

12、这只是提供了有限的保护:动态内存的一个基本问题是 可能有多个指针 指向相同的内存。在delete内存之后 重置指针的方法 只对这个指针有效,对其他任何 仍指向(已释放的)内存的指针 是没有作用的。例如:

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

重置p对q 没有任何作用,在我们释放p所指向的(同时也是q所指向的!)内存时,q也变为无效了。在实际系统中,查找 指向相同内存的所有指针 是异常困难的

13、返回一个动态分配的 int 的vector。将此vector 传递给另一个函数,这个函数 读取标准输入,将读入的值 保存在 vector 元素中。再将 vector传递给另一个函数,打印读入的值。记得在恰当的时刻delete vector
第二次 使用 shared_ptr 而不是内置指针

不使用智能指针(在前,需要 手动delete)使用智能指针操作 注意注释

在标准输入流 std::cin 遇到文件结束 (EOF) 之后,它会处于错误状态,此时再次尝试从其读取输入会失败
这是因为 C++ 标准库 将文件结束看作是一个不可恢复的输入状态,因此 在此之后,对输入流的进一步读取操作 都会失败
为了 使程序能够重复 从标准输入中读取数据,你需要 清除错误标志 并重置输入流的状态
可以 使用 std::cin.clear() 来清除错误标志,然后 使用 std::cin.ignore() 来清除输入缓冲区

std::numeric_limits<std::streamsize>::max() 返回了 std::streamsize 类型的最大值
std::streamsize 是一种用于表示输入/输出操作的字节数或字符数的整数类型
std::numeric_limits 是 C++ 标准库中的一个模板类,用于获取数值类型的特性信息,比如最大值、最小值等

因此,std::numeric_limits<std::streamsize>::max() 返回了 std::streamsize 类型的最大值,表示输入缓冲区的最大尺寸

std::cin.ignore() 函数用于从输入缓冲区中提取字符并丢弃它们。其第一个参数 表示最大读取的字符数,第二个参数 表示要忽略的特定字符
在这里,我们使用 std::numeric_limits<std::streamsize>::max() 作为第一个参数,以确保 尽可能地读取输入缓冲区中的所有字符,而 ‘\n’ 表示忽略换行符
std::streamsize 通常用于 表示 I/O 操作中的缓冲区大小、读取或写入的字节数等。它是一个平台无关的类型,因此 可以在不同平台上 保证一致的行为

这个操作的目的是 清空输入缓冲区中的所有剩余字符,以便 在下一次从标准输入中读取输入时,不会受到 之前输入操作的影响

#include <iostream>
#include <vector>
#include <string>
#include <memory> // 使用智能指针

using namespace std;

vector<int>* create()
{
	return new vector<int>;  // 注意返回值,是一个指针
}

void read(vector<int> *vecp) // 操作都是基于指针
{
	int i;
	while (cin >> i)
	{
		vecp->push_back(i);
	}
}

void print(vector<int>* vecp)
{
	for (auto i : (*vecp))
		cout << i << " ";
}

// 使用智能指针,参数 和 返回值都需要改
shared_ptr<vector<int>> create_smartp()
{
	return make_shared<vector<int>>();
}

void read_smartp(shared_ptr<vector<int>> sp)
{
	int i;
	while (cin >> i) // 这里读不进去了,因为已经遇到EOF了
	{
		sp->push_back(i);
	}
}

void print_smartp(shared_ptr<vector<int>> sp)
{
	for (auto i : (*sp))
		cout << i << " ";
}

int main()
{
	vector<int>* p = create();
	read(p);
	print(p);
	delete p;

	// 注意
	cin.clear();
	cin.ignore(numeric_limits<streamsize>::max(), '\n');

	shared_ptr<vector<int>> sp = create_smartp();
	read_smartp(sp);
	print_smartp(sp);
	return 0;
}

运行结果
运行结果
14、下面的函数是否有错误

bool b() {
	int* p = new int;
	// ...
	return p;
}

意图是通过 new 返回的指针值来区分内存分配成功或失败 —— 成功返回一个合法指针,转换为整型是一个非零值,可转换为 bool 值 true;分配失败,p 得到 nullptr,其整型值是 0,可转换为 bool 值 false
但普通 new 调用在分配失败时抛出一个异常 bad_alloc,而不是返回 nullptr,因此程序不能达到预想的目的
可将 new int 改为 new (nothrow) int 来令 new 在分配失败时不抛出异常,而是返回 nullptr

15、r=q后r所指的内存 没有释放,应该先delete r,再r=q;第二段代码内存会自动释放

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 的内存再无指针管理,变成 “孤儿内存”,从而造成内存泄漏。
其次是一个 “空悬指针” 问题。由于 r 和 q 指向同一个动态对象,如果程序编写不当,很容易产生释放了其中一个指针,而继续使用另一个指针的问题。继续使用的指针指向的是一块已经释放的内存,是一个空悬指针,继续读写它指向的内存可能导致程序崩溃甚至系统崩溃的严重问题

1.3 shared_ptr和new结合使用

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

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

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

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

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

3、不要混合使用普通指针 和 智能指针:shared_ptr 可以 协调对象的析构,但 这仅限于 其自身的拷贝(也是shared_ptr)之间。这是 推荐使用make_shared 而不是new 的原因。这样,我们就能 在分配对象的同时 就将shared_ptr与之绑定,从而避免了无意中将同一块内存绑定到 多个独立创建的shared_ptr上

4、

// 在函数被调用时 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,出来就是1了
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 所指向的内存了

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

5、不要使用 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

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

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

在多用户(或者说多个指针共享同一资源)的情况下,确保在修改资源之前 每个用户都有自己的私有拷贝是很重要的。这个概念在共享资源管理中 非常重要,尤其是 在使用智能指针如 std::shared_ptr 管理共享资源时。实现这样的机制 可以避免意外地修改了其他用户 正依赖的资源,从而引入难以发现的bug

演示 如何在不是唯一用户的情况下,为修改操作 分配新的资源拷贝。我们将使用 std::shared_ptr 来管理 一个简单的资源(例如,一个 std::vector<int>

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

// 模拟资源类
class Resource {
public:
    std::vector<int> data;

    // 添加数据的方法,为了简化就直接在这里实现
    void add(int value) {
        data.push_back(value);
    }

    // 打印数据的方法
    void print() const {
        for (auto val : data) {
            std::cout << val << " ";
        }
        std::cout << "\n";
    }
};

// 确保在修改前拥有自己的副本
void ensureOwnCopy(std::shared_ptr<Resource>& resourcePtr) {
    if (!resourcePtr.unique()) { // 如果不是唯一的用户
        resourcePtr = std::make_shared<Resource>(*resourcePtr); // 创建一个新的资源拷贝
    }
    // 现在可以安全地修改 resourcePtr 指向的资源,不影响其他用户
}

int main() {
    auto ptr1 = std::make_shared<Resource>(); // 创建一个资源
    ptr1->add(1); // 添加一些数据
    ptr1->add(2);

    auto ptr2 = ptr1; // ptr2 现在共享 ptr1 指向的相同资源

    // 在修改前确保 ptr1 有自己的副本
    ensureOwnCopy(ptr1);
    ptr1->add(3); // 现在这个修改不会影响 ptr2

    std::cout << "ptr1: ";
    ptr1->print();
    std::cout << "ptr2: ";
    ptr2->print();

    return 0;
}

在这个例子中,ensureOwnCopy 函数检查 std::shared_ptr 是否是指向其资源的唯一指针(即没有其他共享指针指向同一个资源)。如果不是唯一的,函数 创建一个新的资源拷贝,并更新 std::shared_ptr 以指向这个新资源,这样 就可以在不影响其他共享指针的情况下 修改资源了。在 main 函数中,我们演示了 如何使用 ensureOwnCopy 在修改资源前 确保我们有自己的资源拷贝

7、智能指针和普通指针使用上的问题

shared_ptr<int> p(new int(42));
process(shared_ptr<int>(p));

此调用是正确的,利用 p 创建一个临时的 shared_ptr 赋予 process 的参数 ptr,p 和 ptr 都指向相同的 int 对象,引用计数被正确地置为 2。process 执行完毕后,ptr 被销毁,int 对象 42 引用计数减 1,这是正确的 —— 只有 p 指向它

process(shared_ptr<int>(p.get()));

此调用是错误的。p.get() 获得一个普通指针,指向 p 所共享的 int 对象。利用此指针(普通指针)创建一个 shared_ptr 赋予 process 的参数 ptr,而不是利用 p 创建一个 shared_ptr 赋予 process 的参数 ptr,这样的话将不会形成正确的动态对象共享。编译器会认为 p 和 ptr 是使用两个地址(虽然它们相等)创建的两个不相干的 shared_ptr,而非共享同一个动态对象。这样,两者的引用计数均为 1。当 process 执行完毕后,ptr 的引用计数减为 0,所管理的内存地址被释放,而此内存就是 p 所管理的。p 成为一个管理空悬指针的 shared_ptr

auto p = new int();
auto sp = make_shared<int>();
void process(shared_ptr<int> ptr);

(a) process(sp);
(b) process(new int());
(c) process(p);
(d) process(shared_ptr<int>(p));

(a)合法。sp 是一个共享指针,指向一个 int 对象。对 process 的调用会拷贝 sp,传递给 process 的参数 ptr,两者都指向相同的 int 对象,引用计数变为 2。当 process 执行完毕时,ptr 被销毁,引用计数变回 1

(b)不合法。普通指针不能隐式转换为智能指针

(c)不合法。原因同(b)

(d)合法,但是是错误的程序。p 是一个指向 int 对象的普通指针,被用来创建一个临时 shared_ptr,传递给 process 的参数 ptr,引用计数为 1。当 process 执行完毕,ptr 被销毁,引用计数变为 0,int 对象被销毁。p 变为空悬指针

auto sp = make_shared<int>();
auto p = sp.get();
delete p;

第二行用 get 获取了 sp 指向的 int 对象的地址,第三行用 delete 释放这个地址。这意味着 sp 的引用计数仍为 1,但其指向的 int 对象已经被释放了。sp 成为类似空悬指针的 shared_ptr

1.4 智能指针和异常

1、以前介绍了 使用异常处理的程序 能在异常发生后令程序流程继续,这种程序 需要确保 在异常发生后 资源能被正确地释放。一个简单的确保资源被释放的方法是 使用智能指针

使用智能指针,即使 程序块过早结束,智能指针类 也能确保 在内存不再需要时将其释放

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之外 没有指针指向这块内存,因此 就无法释放它了

2、智能指针和哑类:包括所有标准库类在内的 很多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 被正确关闭,已被证明是一种有效的方法

3、使用我们自己的释放操作:shared_ptr 假定它们指向的是 动态内存。因此,当一个 shared _ptr 被销毁时,它默认地 对它管理的指针 进行delete操作。为了用 shared_ptr 来管理一个 connection,我们必须 首先定义一个函数 来代替 delete

这个删除器函数 必须能够完成 对 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同样 会被销毁,从而连接被关闭

4、智能指针陷阱:为了 正确使用智能指针,我们必须坚持一些基本规范

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

5、编写 自己版本的用 shared_ptr 管理 connection 的函数(使用智能指针释放)

#include <iostream>
#include <memory>

using namespace std;

struct destination {};
struct connection {};

connection connect(destination*) {
	cout << "连接建立" << endl;
	return connection();
}

void disconnect(connection) {
	cout << "断开连接" << endl;
}

// 直接管理指针,未使用 shared_ptr,忘记调用disconnect
void f(destination& d)
{
	connection c = connect(&d);
	// 在退出前 忘记调用disconnect,就无法关闭连接了
	cout << "f结束" << endl;
}

// 使用 shared_ptr
void close_connect(connection* cp) {
	return disconnect(*cp);
}

void f1(destination& d)
{
	connection c = connect(&d);
	shared_ptr<connection> p(&c, close_connect);
	// 别忘了指出 智能指针 名字p
	// 退出函数前调用 close_connect,注意 close_connect参数是指针,shared_ptr第一个参数 是指针 不是引用
	cout << "f1结束" << endl;
}

// 使用 shared_ptr,同时使用 lambda
void f2(destination& d)
{
	connection c = connect(&d);
	shared_ptr<connection> p(&c, [](connection* cp) { return disconnect(*cp); });
	cout << "f2结束" << endl;
}

int main()
{
	destination d;
	f(d);
	cout << endl;
	f1(d);
	cout << endl;
	f2(d);
	return 0;
}

运行结果
运行结果

1.5 unique_ptr

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

2、与 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不支持赋值
#include <memory>
#include <iostream>

int main()
{
	std::unique_ptr<int> up1(new int(1));
	
	// 报错,无法引用
	std::unique_ptr<int> up2(up1);

	// 报错,无法引用
	std::unique_ptr<int> up2 = up1;

	// 正确
	std::unique_ptr<int> up2;
	up2.reset(up1.release());

	std::cout << *up2 << std::endl;

	return 0;
}

3、unique_ptr 操作

操作解释
unique_ptr<T> u1, unique_ptr<T, D> u2空unique_ptr,可以指向 类型为T的对象。u1会使用delete 来释放它的指针;u2会使用 一个类型为D的可调用对象 来释放它的指针
unique_ptr<T, D> u(d)空unique_ptr,指向类型为T的对象,用类型为D的对象d 代替delete
u = nullptr释放 u指向的对象,将u置为空
u.release()u放弃 对指针的控制权,返回指针,并将u置为空
u.reset(), u.reset(q), u.reset(nullptr)释放u指向的对象,如果提供了内置指针q,令u指向这个对象;否则将u置为空

虽然我们 不能拷贝或赋值 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("T rex"));
//将所有权从p3转移给p2
p2.reset(p3.release()); // reset释放了p2原来指向的内存,将p3置为空

release成员 返回 unique_ptr 当前保存的指针 并将其置为空。因此,p2被初始化为 p1 原来保存的指针,而p1 被置为空
reset成员 接受一个可选的指针参数,令 unique_ptr 重新指向给定的指针。如果 unique_ptr 不为空,它原来指向的对象 被释放

调用 release会切断 unique_ptr 和 它原来管理的对象间的联系。release 返回的指针 通常 被用来初始化 另一个智能指针 或 给另一个智能指针赋值

如果 用另一个智能指针 来保存release返回的指针,程序就要 负责资源的释放

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

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

对于 两段代码,编译器 都知道 要返回的对象将要被销毁。在此情况下,编译器执行一种特殊的“拷贝”

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

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

重载一个 unique_ptr 中的删除器 会影响到 unique_ptr类型以及如何构造(或 reset)该类型的对象
与重载关联容器的比较操作 类似,我们必须在 尖括号中 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(end_connection)返回一个函数类型,所以我们 必须添加一个* 来指出我们 正在使用该类型的一个指针
同时 初始化p的第一个参数 &c 也是一个地址

6、下面的 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 需要用一个指针初始化,无法将 int 转换为指针
(b)编译时合法,运行时会报错,因为pi不是new出来的,销毁时使用默认的delete会出错;

(c)编译时合法,但是运行时会导致空悬指针,unique_ptr释放空间时,使用pi2指针会出错;
在将原始指针 赋给 unique_ptr 之后,不再使用 原始指针,如果 需要再次访问 该内存,应该再次通过 unique_ptr 来访问
pi2 是一个原始指针,指向一个动态分配的 int。当 使用 pi2 初始化 p2(一个 std::unique_ptr<int> 类型的对象)时,p2 接管了 pi2 指向的内存的所有权

int* pi2 = new int(2048); // 动态分配一个 int
std::unique_ptr<int> p2(pi2); // p2 现在拥有这个 int 的所有权

这之后,应该避免 直接使用 pi2,因为当 p2 被销毁(例如,当它的作用域结束时)或者 显式地调用 p2.reset() 时,p2 所拥有的内存将被释放。此时,如果 尝试通过 pi2 访问该内存位置,将遇到未定义行为,因为 pi2 现在是一个悬挂指针。这可能导致程序崩溃或其他不可预测的行为

(d)编译时合法,运行时会报错,因为指针不是new出来的,销毁时使用默认的delete会出错;
(e)合法;
(f)编译时合法,但是会导致两次delete或者一个delete后另一个变为空悬指针

7、shared_ptr 为什么没有 release 成员:unique_ptr “独占” 对象的所有权,不能拷贝和赋值。release 操作是用来将对象的所有权 转移给另一个 unique_ptr 的
而多个 shared_ptr 可以 “共享” 对象的所有权。需要共享时,可以简单拷贝和赋值。因此,并不需要 release 这样的操作来转移所有权

1.6 weak_ptr

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

weak_ptr解释
weak_ptr<T> w空 weak_ptr 可以 指向类型为T的对象
weak_ptr<T> w(sp)与shared_ptr sp 指向相同对象的 weak_ptr。T必须 能转换为 sp指向的类型
w = pp可以是一个 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 时,要用一个 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访问共享对象 是安全的

2、作为weak_ptr用途的一个展示,我们 将为StrBlob类 定义一个伴随指针类。定义的指针类 将命名为 StrBlobPtr,会保存一个weak_ptr,指向StrBlob的data成员

初始化时提供给它的我们通过使用 weak_ptr,不会影响 一个给定的StrBlob所指向 的vector的生存期。但是,可以 阻止用户访问一个 不再存在的vector的企图

// 对于访问一个不存在StrBlob,StrBlobPtr抛出异常
class StrBlobPtr {
public:
	StrBlobPtr() :curr(0) {} // 构造函数后面不需要加;
	StrBlobPtr(StrBlob& a, size_t sz = 0) :wptr(a.data), curr(sz) {}
	std::string& deref() const; // 解引用StrBlobPtr
	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可能被销毁,直接用vector初始化即可
	std::weak_ptr<std::vector<std::string>> wptr;
	std::size_t curr; // 在数组中的当前位置
};

不能将 StrBlobPtr 绑定到一个 const StrBlob 对象。这个限制是 由于构造函数 接受一个非 const StrBlob 对象的引用 而导致的。它还要 检查指针指向的vector是否还存在

StrBlobPtr的check成员 与strBlob中的同名成员不同,它还要检查指针指向的vector 是否还存在

std::shared_ptr<std::vector<std::string>> StrBlobPtr::check(std::size_t i, const std::string& msg) const
 // msg不同错误不同信息
{
	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
}

3、指针操作:将定义名为 deref 和 incr的函数,分别用来 解引用和递增StrBlobPtr
deref成员 调用check,检查使用 vector是否安全 以及curr是否在合法范围内

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

如果check成功,p就是一个shared_ptr,指向StrBlobPtr所指向的vector

incr成员 也调用check

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

为了访问data成员,我们的指针类必须声明为StrBlob的friend

4、完整定义 StrBlobPtr,更新 StrBlob 类,加入恰当的 friend 声明以及 begin 和 end 成员,并逐行读入一个输入文件,将内容存入一个 StrBlob 中,用一个 StrBlobPtr 打印出 StrBlob 中的每个元素
StrBlob_20.h

#pragma once
#ifndef STRBLOB_20_H
#define STRBLOB_20_H

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

class StrBlobPtr;

class StrBlob {
public:
	friend class StrBlobPtr; // 声明友元,这样可以使用StrBlob中的数据
	typedef std::vector<std::string>::size_type size_type;
	StrBlob() :data(std::make_shared<std::vector<std::string>>()) {};
	StrBlob(const std::initializer_list<std::string>& il);
	size_type size() const { return data->size(); } // data是指针
	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& front() const;
	std::string& back();
	std::string& back() const;
	// StrBlobPtr还没定义,只是声明,所以直接使用构造函数是不对的
	/*StrBlobPtr begin() { return StrBlobPtr(*this); }
	StrBlobPtr end()
	{
		auto ret = StrBlobPtr(*this, data->size());
		return ret;
	}*/
	StrBlobPtr begin();
	StrBlobPtr end();

private:
	std::shared_ptr<std::vector<std::string>> data;
	void check(size_type i, const std::string& msg) const;
};

// 对于访问一个不存在StrBlob,StrBlobPtr抛出异常
// 除了检查之外,还负责随机取出 StrBlob中存的信息
class StrBlobPtr {
public:
	StrBlobPtr() :curr(0) {} // 构造函数后面不需要加;
	StrBlobPtr(StrBlob& a, size_t sz = 0) :wptr(a.data), curr(sz) {}
	std::string& deref() const; // 解引用StrBlobPtr
	StrBlobPtr& incr(); // 前缀递增
	bool operator!=(const StrBlobPtr& p) { return p.curr != curr; } // 重新定义运算符

private:
	// 若检查确实存在,check返回一个 指向vector的shared_ptr
	std::shared_ptr<std::vector<std::string>> check(std::size_t, const std::string&) const;
	// 保存一个weak_ptr,意味着 底层vector可能被销毁,直接用vector初始化即可
	std::weak_ptr<std::vector<std::string>> wptr;
	std::size_t curr; // 在数组中的当前位置
};

StrBlob::StrBlob(const std::initializer_list<std::string>& il) :data(std::make_shared<std::vector<std::string>>(il)) {} // 构造函数也要StrBlob::

void StrBlob::check(size_type i, const std::string& msg) const { // 实现的时候也需要加const
	if (i >= data->size())
		throw std::out_of_range(msg);
}

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

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

std::string& StrBlob::front() const { // 对const进行重载
	check(0, "front on empty StrBlob");
	return data->front();
}

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

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

std::shared_ptr<std::vector<std::string>> StrBlobPtr::check(std::size_t i, const std::string& msg) const // msg不同错误不同信息
{
	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
}

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

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

StrBlobPtr StrBlob::begin() {
	return StrBlobPtr(*this);
}

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

#endif

12.20.cpp

#include <iostream>
#include <fstream>
#include "StrBlob_20.h"

using namespace std;

int main()
{
	ifstream ifs("data_20.txt");
	string s;
	StrBlob sb;
	while (getline(ifs, s))
	{
		sb.push_back(s);
	}
	for (StrBlobPtr beg = StrBlobPtr(sb, 0), end = StrBlobPtr(sb, sb.size()); beg != end; beg.incr()) 
		// 注意beg,end,以及重载的运算符 !=
		cout << beg.deref() << endl;

	// 等价
	for (StrBlobPtr beg = sb.begin(), ed = sb.end(); beg != ed; beg.incr())
		// sb.begin()和sb.end()本来就构造好了,当然包括curr
		std::cout << beg.deref() << std::endl;
	return 0;
}

data_20.txt

c++ primer 5th
C++ Primer 5th

运行结果
运行结果

2、动态数组

1、new和delete运算符 一次分配 / 释放一个对象,但某些应用 需要一次 为很多对象分配内存的功能。例如,vector 和 string都是 在连续内存中保存它们的元素,因此,当容器 需要重新分配内存时,必须一次性 为很多元素分配内存

C++语言 定义了 另一种new表达式语法,可以分配 并 初始化一个对象数组。标准库中 包含一个名为 allocator的类,允许我们将分配 和 初始化分离

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

2.1 new和数组

1、要在类型名之后 跟一对方括号,在其中指明 要分配的对象的数目。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

2、分配一个数组 会得到一个元素类型的指针:当 用new分配一个数组时,我们 并未得到一个数组类型的对象,而是得到一个数组元素类型的指针。即使 我们使用类型别名 定义了一个数组类型,new 也不会分配一个数组类型的对象。在上例中,我们正在分配一个数组的事实 甚至都是不可见的——连[num] 都没有。new 返回的是 一个元素类型的指针

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

动态数组并不是数组类型,这是很重要的

3、初始化动态分配对象的数组:默认情况下,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分配数组

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

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

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

4、释放动态数组:使用一种特殊形式的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; //方括号是必需的,因为我们当初分配的是一个数组

5、智能指针和动态数组:标准库提供了 一个可以管理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指向一个数组时,我们可以 使用下标运算符 来访问数组中的元素

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

操作解释
unique_ptr<T[ ]> uu可以指向 一个动态分配的数组,数组元素类型为 T
unique_ptr<T[ ]> u§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未定义下标运算符,并且不支持指针的算术运算
for (size_t i = 0; i != 10; ++i)
	*(sp.get() + i) = i; // 使用get获取一个内置指针

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

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

用 new char[xx] 即可分配用来保存结果的 char 数组,其中 xx 应该足以保存结果字符串。由于 C 风格字符串以 \0 结尾,因此 xx 应不小于字符数加 1

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
#include <cstring>

using namespace std;

int main()
{
	const char a[] = "aaa";
	const char b[] = "bbb";
	char *c = new char[strlen(a) + strlen(b) + 1]; // new一个char数组,返回指针
	strcpy(c, a);
	strcat(c, b); // C风格字符数组的拷贝和拼接
	cout << string(c) << endl;
	delete[]c;

	string sa = "aaa";
	string sb = "bbb";
	char* c2 = new char[sa.size() + sb.size() + 1];
	// 使用 std::string 的 c_str() 函数获取 C 风格的字符串
	strcpy(c2, (sa + sb).c_str());
	cout << c2 << endl;
	delete[]c2;

	std::string* c3 = new std::string;
	*c3 = sa + sb;
	std::cout << *c3 << std::endl;
	delete c3;
	return 0;
}

从标准输入读取一个字符串,存入一个动态分配的字符数组中。输入一个超出你分配的数组长度的字符串,只会读入 指定大小的字符数组(即采取了截断的方式。还可以采取其他处理方式,如抛出异常)

在动态分配字符数组的空间大小时,记得加上字符数组结束符 \0 所占的空间

#include <iostream>
#include <cstring>

using namespace std;

int main()
{
	int size = 0;
	cin >> size;
	char* s = new char[size + 1];
	cin.ignore();
	cin.get(s, size + 1);
	cout << s << endl;
	delete[]s;
	return 0;
}

2.2 allocator类

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

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

将内存分配和对象构造组合在一起 可能会导致不必要的浪费

string *const p = new string[n]; //构造n个空string
string s;
string *q = p;  //指向第一个string
while (cin >> s && q != p + n)
	*q++ = s;   //赋予*q一个新值
const size_t size = q - p; //记住我们读取了多少个string
//使用数组
delete[] p; //p指向一个数组;记得用delete[]来释放

可能不需要 n个string,少量 string 可能就足够了。这样,我们就可能 创建了 一些永远也用不到的对象
而且,对于 那些确实要使用的对象,我们也在 初始化之后 立即赋予了它们新值。每个使用到的元素 都被赋值了两次:第一次 是在默认初始化时,随后 是在赋值时

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

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

allocator 是一个模板。为了定义一个 allocator对象,我们必须指明 这个 allocator 可以分配的 对象类型。当一个 allocator对象 分配内存时,它会根据 给定的对象类型 来确定恰当的内存大小 和 对齐位置:

allocator<string> alloc; //可以分配string的allocator对象
auto const p = alloc.allocate(n); //分配n个未初始化的string

这个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*的指针,指向一块原始内存:arg被传递给 类型为T的构造函数,用来在p指向的内存中 构造一个对象
a.destroy§p为T*类型的指针,此算法 对p指向的对象 执行析构函数

3、allocator 分配未构造的内存:allocator 分配的内存 是未构造的。我们按需要 在此内存中构造对象。在新标准库中,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指向未构造的内存

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

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

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

第一次 调用destroy时,q指向 最后一个构造的元素。最后一步循环中 我们destroy了 第一个构造的元素,随后 q将与p相等,循环结束

只能对真正构造了的元素进行destroy操作

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

alloc.deallocate(p, n);

传递给 deallocate的指针 不能为空,它必须指向 由allocate分配的内存。而且,传递给 deallocate的大小参数 必须与调用allocate 分配内存时 提供的大小参数 具有一样的值

4、拷贝和填充 未初始化内存的算法:标准库 还为allocator类 定义了 两个伴随算法,可以在 未初始化内存中 创建对象

allocator算法,都定义在头文件memory中
这些函数 在给定目的位置 创建元素,而不是 由系统分配内存给它们

操作解释
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中元素所占用空间 大一倍的动态内存,然后将 原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);

uninitialized_copy 接受三个迭代器参数。前两个 表示输入序列,第三个 表示这些元素 将要拷贝到的目的空间。传递给uninitialized_copy 的目的位置迭代器 必须指向 未构造的内存

uninitialized_copy 返回(递增后的)目的位置迭代器。因此,一次 uninitialized_copy 调用 会返回一个指针,指向 最后一个构造的元素之后的位置

在本例中,我们将 此指针保存在q中,然后将q传递给 uninitialized_fill_n
此函数类似 fill_n,接受 一个指向目的位置的指针、一个计数 和 一个值。它会在 目的位置指针 指向的内存中 创建给定数目个对象,用给定值 对它们进行初始化

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

using namespace std;

int main()
{
	allocator<string> alloc;
	auto const p = alloc.allocate(3);
	auto q = p;
	alloc.construct(q++);
	alloc.construct(q++, 10, 'c');
	alloc.construct(q++, "hi");
	while (q != p) // 注意输出所有string写法
	{
		cout << *(--q) << endl;
		alloc.destroy(q); // 同时删除
	}
	alloc.deallocate(p, 3);
	return 0;
}

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

允许用户在一个给定文件中查询单词。查询结果是单词在文件中出现的次数及其所在行的列表

3.1 文本查询程序设计

1、开始一个程序的设计的一种好方法是列出程序的操作

  • 当程序读取输入文件时,它必须记住单词出现的每一行。因此,程序需要逐行读取输入文件,并将每一行分解为独立的单词
  • 当程序读取输入文件时
    – 它必须能提取每个单词所关联的行号
    – 行号必须按升序出现且无重复
    –它必须能打印给定行号中的文本

实现这些要求:

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

2、数据结构:
从定义一个保存输入文件的类 开始,将这个类命名为TextQuery,它包含 一个vector和 一个map。vector用来保存 输入文件的文本,map用来关联 每个单词和它出现的行号的set。这个类 将会有一个用来读取给定输入文件的构造函数 和一个执行查询的操作

查询操作 要完成的任务非常简单:查找map成员,检查给定单词是否出现。设计这个函数的难点 是确定应该返回什么内容。一旦找到了一个单词,我们需要知道 它出现了多少次、它出现的行号以及每行的文本

返回所有这些内容的最简单的方法 是定义另一个类,可以命名为 QueryResult,来保存 查询结果。这个类会有 一个print函数,完成 结果打印工作

3、在类之间共享数据:由于 QueryResult 所需要的数据 都保存在 一个TextQuery对象中,我们就必须 确定如何访问它们。我们可以 拷贝行号的set,但这样做可能很耗时。而且,我们当然不希望 拷贝vector,因为这可能会 引起整个文件的拷贝,而目标只不过是 为了打印文件的一小部分而已

通过返回指向 TextQuery对象内部的迭代器(或指针),我们可以避免 拷贝操作。如果 TextQuery对象 在对应的 QueryResult对象之前被销毁,QueryResult 就将 引用一个不再存在的对象中的数据

对于 QueryResult对象 和对应的 TextQuery对象的生存期 应该同步这一观察结果,考虑到这两个类概念上“共享”了数据,可以使用 shared_ptr 来反映 数据结构中的这种共享关系

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

#include "Query.h"

using namespace std;

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

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

3.2 文本查询程序类的定义

1、设计类的数据成员时,需要考虑 与QueryResult对象共享数据的需求。QueryResult类 需要共享 保存输入文件的vector 和 保存单词关联的行号的set
因此,这个类 应该有两个数据成员:一个指向 动态分配的vector(保存输入文件)的 shared_ptr 和 一个string 到 shared_ptr<set>的map。map将文件中 每个单词关联到 一个动态分配的set上,而此set 保存了 该单词所出现的行号

class QueryResult;

class TextQuery {
public:
	friend class QueryResult;
	TextQuery(std::ifstream& ifs);
	QueryResult query(std::string& word);
private:
	std::shared_ptr<std::vector<std::string>> vec; // 文件
	std::map<std::string, std::shared_ptr<std::set<int>>> m; // value也需要std::shared_ptr
};

2、TextQuery构造函数:由于vec是一个shared_ptr,我们用->运算符解引用vec 来提取vec指向的vector对象的push_back成员

若str_after不在map中,下标运算符 会将str_after添加到m中,与str_after关联的值 进行值初始化。如果m[str_after] 为空,我们分配 一个新的set,并调用reset 更新m[str_after]的 shared_ptr,使其 指向这个新分配的set

不管 是否创建了一个新的set,我们都调用insert 将当前行号添加到set中

TextQuery::TextQuery(std::ifstream& ifs):vec(new std::vector<std::string>) // new的那个vector被shared_ptr管理了
{
	std::string s;
	while (getline(ifs, s)) {
		std::istringstream iss(s);
		std::string str;
		vec->push_back(s);
		int lineNo = vec->size();
		while (iss >> str) {
			std::string str_after;
			std::copy_if(str.begin(), str.end(), std::back_inserter(str_after), isalpha);
			if (m.find(str_after) == m.end()) {
				m[str_after].reset(new std::set<int>); // shared_ptr用法
			}
			m[str_after]->insert(lineNo);
		}
	}
}

3、QueryResult类:

class QueryResult {
public:
	QueryResult(std::string s, std::shared_ptr<std::set<int>> l, std::shared_ptr<std::vector<std::string>> v) :word(s), lines(l), vec(v) {}
	void print();
private:
	std::shared_ptr<std::vector<std::string>> vec; // 输入文件
	std::string word; // 查询单词
	std::shared_ptr<std::set<int>> lines; // 出现的行号(把map拆开来了)
};

query函数:接受 一个string参数,即 查询单词,query用它来 在map中定位 对应的行号set。如果找到了 这个string,query函数构造一个QueryResult,保存给定 string、TextQuery的vec成员 以及 从m中提取的set

如果给定string未找到,我们应该返回什么?定义一个局部static对象(自己实现中没有整成static)emp,它是一个指向空的行号set 的shared_ptr。当未找到给定单词时,我们返回此对象的一个拷贝

QueryResult TextQuery::query(std::string& word) {
	std::shared_ptr<std::set<int>> emp(new std::set<int>); // new的那个set被shared_ptr管理了
	if (m.find(word) == m.end())
		return QueryResult(word, emp, vec);
	else
		return QueryResult(word, m[word], vec);
}

4、打印结果

void QueryResult::print()
{
	std::cout << word << " occurs " << lines->size() << " times" << std::endl;
	int t = lines->size();
	for (int i : *lines)
	{
		std::cout << "(line " << i << ") " << *(vec->begin() + i - 1) << std::endl;
	}
}

vec->begin() + i - 1 即为vec指向的vector中的第i个位置的元素
此函数能正确处理 未找到单词的情况。在此情况下,set为空。第一条输出语句 会注意到单词出现了0次。由于*res.lines为空,for循环 一次也不会执行

5、完整程序
Query.h

#pragma once
#ifndef QUERY_H
#define QUERY_H

#include <string>
#include <iostream>
#include <fstream>
#include <vector>
#include <sstream>
#include <set>
#include <map>
#include <algorithm>
#include <memory>

class QueryResult;

class TextQuery {
public:
	friend class QueryResult;
	TextQuery(std::ifstream& ifs);
	QueryResult query(std::string& word);
private:
	std::shared_ptr<std::vector<std::string>> vec;
	std::map<std::string, std::shared_ptr<std::set<int>>> m; // value也需要std::shared_ptr
};

class QueryResult {
public:
	QueryResult(std::string s, std::shared_ptr<std::set<int>> l, std::shared_ptr<std::vector<std::string>> v) :word(s), lines(l), vec(v) {}
	void print();
private:
	std::shared_ptr<std::vector<std::string>> vec;
	std::string word;
	std::shared_ptr<std::set<int>> lines;
};

TextQuery::TextQuery(std::ifstream& ifs):vec(new std::vector<std::string>) // new的那个vector被shared_ptr管理了
{
	std::string s;
	while (getline(ifs, s)) {
		std::istringstream iss(s);
		std::string str;
		vec->push_back(s);
		int lineNo = vec->size();
		while (iss >> str) {
			std::string str_after;
			std::copy_if(str.begin(), str.end(), std::back_inserter(str_after), isalpha);
			if (m.find(str_after) == m.end()) {
				m[str_after].reset(new std::set<int>); // shared_ptr用法
			}
			m[str_after]->insert(lineNo);
		}
	}
}

QueryResult TextQuery::query(std::string& word) {
	std::shared_ptr<std::set<int>> emp(new std::set<int>); // new的那个set被shared_ptr管理了
	if (m.find(word) == m.end())
		return QueryResult(word, emp, vec);
	else
		return QueryResult(word, m[word], vec);
}

void QueryResult::print()
{
	std::cout << word << " occurs " << lines->size() << " times" << std::endl;
	int t = lines->size();
	for (int i : *lines)
	{
		std::cout << "(line " << i << ") " << *(vec->begin() + i - 1) << std::endl;
	}
}

#endif

12.27.cpp

#include "Query.h"

using namespace std;

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

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

data_27.txt

c++ primer 5th
c++ primer 3th
example. Cplusplus
primer example, 
example primer

运行结果
运行结果
6、如果用vector 代替 set 保存行号,会有什么差别?哪个方法更好?
vector 更好。因为,虽然 vector 不会维护元素值的序,set 会维护关键字的序,但注意到,我们是逐行读取输入文本的,因此每个单词出现的行号是自然按升序加入到容器中的,不必特意用关联容器来保证行号的升序。而从性能角度,set 是基于红黑树实现的,插入操作时间复杂性为 O(logn)(n 为容器中元素数目),而 vector 的 push_back 可达到常量时间

另外,一个单词在同一行中可能出现多次。set 自然可保证关键字不重复,但对 vector 这也不成为障碍 —— 每次添加行号前与最后一个行号比较一下即可。总体性能仍然是 vector 更优

7、重写 TextQuery 和 QueryResult类,用StrBlob 代替 vector 保存输入文件
Query_32.h

#pragma once
#ifndef QUERY_32_H
#define QUERY_32_H

#include <string>
#include <iostream>
#include <fstream>
#include <vector>
#include <sstream>
#include <set>
#include <map>
#include <algorithm>
#include <memory>
#include "StrBlob_20.h"

class QueryResult;

class TextQuery {
public:
	friend class QueryResult;
	TextQuery(std::ifstream& ifs);
	QueryResult query(std::string& word);
private:
	StrBlob file;
	std::map<std::string, std::shared_ptr<std::set<int>>> m; // value也需要std::shared_ptr
};

class QueryResult {
public:
	QueryResult(std::string s, std::shared_ptr<std::set<int>> l, StrBlob sb) :word(s), lines(l), file(sb) {}
	void print();
private:
	StrBlob file;
	std::string word;
	std::shared_ptr<std::set<int>> lines;
};

TextQuery::TextQuery(std::ifstream& ifs) :file(StrBlob()) // new的那个vector被shared_ptr管理了
{
	std::string s;
	while (getline(ifs, s)) {
		std::istringstream iss(s);
		std::string str;
		file.push_back(s);
		int lineNo = file.size();
		while (iss >> str) {
			std::string str_after;
			std::copy_if(str.begin(), str.end(), std::back_inserter(str_after), isalpha);
			if (m.find(str_after) == m.end()) {
				m[str_after].reset(new std::set<int>); // shared_ptr用法
			}
			m[str_after]->insert(lineNo);
		}
	}
}

QueryResult TextQuery::query(std::string& word) {
	std::shared_ptr<std::set<int>> emp(new std::set<int>); // new的那个set被shared_ptr管理了
	if (m.find(word) == m.end())
		return QueryResult(word, emp, file);
	else
		return QueryResult(word, m[word], file);
}

void QueryResult::print()
{
	std::cout << word << " occurs " << lines->size() << " times" << std::endl;
	int t = lines->size();
	for (int i : *lines)
		// 利用StrBlobPtr类 取 指定行的数据,file只是为了记录对应行的数据
	{
		StrBlobPtr sbp(file, i - 1);
		std::cout << "(line " << i << ") " << sbp.deref() << std::endl;
	}
}

#endif

StrBlob_20.h

#pragma once
#ifndef STRBLOB_20_H
#define STRBLOB_20_H

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

class StrBlobPtr;

class StrBlob {
public:
	friend class StrBlobPtr; // 声明友元,这样可以使用StrBlob中的数据
	typedef std::vector<std::string>::size_type size_type;
	StrBlob() :data(std::make_shared<std::vector<std::string>>()) {};
	StrBlob(const std::initializer_list<std::string>& il);
	size_type size() const { return data->size(); } // data是指针
	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& front() const;
	std::string& back();
	std::string& back() const;
	// StrBlobPtr还没定义,只是声明,所以直接使用构造函数是不对的
	/*StrBlobPtr begin() { return StrBlobPtr(*this); }
	StrBlobPtr end()
	{
		auto ret = StrBlobPtr(*this, data->size());
		return ret;
	}*/
	StrBlobPtr begin();
	StrBlobPtr end();

private:
	std::shared_ptr<std::vector<std::string>> data;
	void check(size_type i, const std::string& msg) const;
};

// 对于访问一个不存在StrBlob,StrBlobPtr抛出异常
// 除了检查之外,还负责随机取出 StrBlob中存的信息
class StrBlobPtr {
public:
	StrBlobPtr() :curr(0) {} // 构造函数后面不需要加;
	StrBlobPtr(StrBlob& a, size_t sz = 0) :wptr(a.data), curr(sz) {}
	std::string& deref() const; // 解引用StrBlobPtr
	StrBlobPtr& incr(); // 前缀递增
	bool operator!=(const StrBlobPtr& p) { return p.curr != curr; } // 重新定义运算符

private:
	// 若检查确实存在,check返回一个 指向vector的shared_ptr
	std::shared_ptr<std::vector<std::string>> check(std::size_t, const std::string&) const;
	// 保存一个weak_ptr,意味着 底层vector可能被销毁,直接用vector初始化即可
	std::weak_ptr<std::vector<std::string>> wptr;
	std::size_t curr; // 在数组中的当前位置
};

StrBlob::StrBlob(const std::initializer_list<std::string>& il) :data(std::make_shared<std::vector<std::string>>(il)) {} // 构造函数也要StrBlob::

void StrBlob::check(size_type i, const std::string& msg) const { // 实现的时候也需要加const
	if (i >= data->size())
		throw std::out_of_range(msg);
}

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

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

std::string& StrBlob::front() const { // 对const进行重载
	check(0, "front on empty StrBlob");
	return data->front();
}

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

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

std::shared_ptr<std::vector<std::string>> StrBlobPtr::check(std::size_t i, const std::string& msg) const // msg不同错误不同信息
{
	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
}

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

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

StrBlobPtr StrBlob::begin() {
	return StrBlobPtr(*this);
}

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

#endif

12.32.cpp(跟12.27.cpp一致,数据结构的变化 不影响使用)

#include "Query_32.h"

using namespace std;

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

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

术语表

1、空悬指针:指向 曾经保存一个对象 但现在已释放的内存。空悬指针 引起的程序错误 非常难以调试

2、析构函数:特殊的成员函数,负责 在对象离开作用域 或 被释放时完成 清理工作

3、动态分配:在自由空间中 分配的对象。在自由空间中分配的对象 直到被显式释放 或 被释放时 完成清理工作

4、自由空间:程序可用的内存池,保存 动态分配的对象

5、定位new:一种new表达式形式,接受一些额外参数,在new关键字 后面的括号中给出。例如,new(nothrow) int 告诉new不要抛出异常

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1565237.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

文心一言 VS 讯飞星火 VS chatgpt (227)-- 算法导论16.3 9题

九、证明&#xff1a;对于一个由随机生成的8位字符组成的文件&#xff0c;没有任何压缩方法可以望将其压缩、哪怕只是压缩一位。(提示&#xff1a;比较可能的文件数量和可能的编码文件数量。) 文心一言&#xff1a; 确实&#xff0c;对于一个由随机生成的8位字符组成的文件&a…

蓝桥杯相关算法学习(Python)

一、排序 排序算法是指将一组数据按照某种规则重新排列&#xff0c;使得数据呈现出递增或递减的顺序。常见的排序算法包括冒泡排序、选择排序、插入排序、快速排序、归并排序、堆排序等。 1.冒泡排序 解释&#xff1a; 冒泡排序通过不断交换相邻两个元素的位置&#xff0c;使…

如何辨别蜂蜜的好坏?具体看哪些指标?

蜂蜜的好坏是通过检测指标反应出来的。 衡量蜂蜜质量的指标很多&#xff0c;主要分五类。 第一类是蜂蜜的基础性检测。如&#xff1a;感官、水分、灰分、酸度、蜂蜜的真实性&#xff08;SM-X&#xff09;等&#xff1b; 第二类是营养成分检测。如&#xff1a;果糖与葡萄糖含…

GC Roots

JVM垃圾回收的时候如何确定垃圾&#xff1f; 在说GC Roots之前&#xff0c;我们先看看JVM是如何确定垃圾的&#xff0c;从而进行回收 什么是垃圾 简单来说就是内存中已经不再被使用的空间就是垃圾 如何判断一个对象是否可以被回收 引用计数法 Java中&#xff0c;引用和对象…

node res.end返回json格式数据

使用 Node.js 内置 http 模块的createServer()方法创建一个新的HTTP服务器并返回json数据&#xff0c;代码如下&#xff1a; const http require(http);const hostname 127.0.0.1; const port 3000;const data [{ name: 测试1号, index: 0 },{ name: 测试2号, index: 1 },…

【R】Error in library(foreach) : 不存在叫‘foreach’这个名字的程辑包

Error in library(foreach) : 不存在叫‘foreach’这个名字的程辑包 此外: Warning message: package ‘parallel’ is a base package, and should not be updated 解决方法 缺少名为 foreach 的包&#xff0c;使用install.packages("foreach")将名为foreach 的包…

Flask Python:模糊查询filter和filter_by,数据库多条件查询

数据库&#xff08;sqlalchemy&#xff09;多条件查询 前言一、filter、filter_by实现过滤查询1、filter_by()基础查询并且查询&#xff08;多条件查询&#xff09; 2、filter()like&#xff1a;模糊查询and&#xff1a;并且查询or&#xff1a;或者查询 二、all(),first(),get(…

【其他】灾害预警,科技助力:手机地震预警功能设置指导

22024年4月3日7时58分在台湾花莲县海域遭遇了一场7.3级的强烈地震&#xff0c;震源深度12公里&#xff0c;震中位于北纬23.81度&#xff0c;东经121.74度&#xff0c;距台湾岛约14公里。震中5公里范围内平均海拔约-3560米。这场突如其来的自然灾害给当地居民的生活带来了巨大的…

2024妈妈杯数学建模思路ABCD题思路汇总分析 MathorCup建模思路分享

1 赛题思路 (赛题出来以后第一时间在群内分享&#xff0c;点击下方群名片即可加群) 2 比赛日期和时间 报名截止时间&#xff1a;2024年4月11日&#xff08;周四&#xff09;12:00 比赛开始时间&#xff1a;2024年4月12日&#xff08;周五&#xff09;8:00 比赛结束时间&…

【鹅厂摸鱼日记(一)】(工作篇)认识八大技术架构

&#x1f493;博主CSDN主页:杭电码农-NEO&#x1f493;   ⏩专栏分类:重生之我在鹅厂摸鱼⏪   &#x1f69a;代码仓库:NEO的学习日记&#x1f69a;   &#x1f339;关注我&#x1faf5;带你学习更多知识   &#x1f51d;&#x1f51d; 认识八大架构 1. 前言2. 架构简介&…

SAD法(附python实现)和Siamese神经网络计算图像的视差图

1 视差图 视差图&#xff1a;以左视图视差图为例&#xff0c;在像素位置p的视差值等于该像素在右图上的匹配点的列坐标减去其在左图上的列坐标 视差图和深度图&#xff1a; z f b d z \frac{fb}{d} zdfb​ 其中 d d d 是视差&#xff0c; f f f 是焦距&#xff0c; b b…

redis数据类型介绍

字符串string&#xff1a; 字符串类型是Redis中最为基础的数据存储类型&#xff0c;是一个由字节组成的序列&#xff0c;他在Redis中是二进制安全的&#xff0c;这便意味着该类型可以接受任何格式的数据&#xff0c;如JPEG图像数据货Json对象描述信息等&#xff0c;是标准的key…

每日五道java面试题之消息中间件MQ篇(二)

目录&#xff1a; 第一题. RabbitMQ的工作模式第二题. 如何保证RabbitMQ消息的顺序性&#xff1f;第三题. 消息如何分发&#xff1f;第四题. 消息怎么路由&#xff1f;第五题. 如何保证消息不被重复消费&#xff1f;或者说&#xff0c;如何保证消息消费时的幂等性&#xff1f; …

触想四代ARM架构工业一体机助力手功能康复机器人应用

一、行业发展背景 手功能康复机器人是医疗机器人的一个分支&#xff0c;设计用于帮助肢体障碍患者进行手部运动和力量训练&#xff0c;在医疗健康领域有着巨大的成长空间。 手功能康复机器人融合了传感、控制、计算、AI视觉等智能科技与医学技术&#xff0c;能够帮助患者改善康…

Vue的学习之旅-part1

Vue的学习之旅-part1 vue介绍vue读音编程范式ES6中不用var声明变量vue的声明、初始化传参使用data中数据时要用this指向 vue中的语法糖MVVM在Vue中&#xff0c; MVVM的各层的对应位置 方法、函数的不同之处 vue介绍 vue读音 Vue 读作 /vju:/ 不要读成v u e Vuex 的x读作叉 不…

scratch买蛋糕 2024年3月中国电子学会图形化编程 少儿编程 scratch编程等级考试一级真题和答案解析

目录 scratch买蛋糕 一、题目要求 1、准备工作 2、功能实现 二、案例分析 1、角色分析 2、背景分析 3、前期准备 三、解题思路 1、思路分析 2、详细过程 四、程序编写 五、考点分析 六、 推荐资料 1、入门基础 2、蓝桥杯比赛 3、考级资料 4、视频课程 5、py…

Predict the Next “X” ,第四范式发布先知AIOS 5.0

今天&#xff0c;第四范式发布了先知AIOS 5.0&#xff0c;一款全新的行业大模型平台。 大语言模型的原理是根据历史单词去不断预测下一个单词&#xff0c;换一句常见的话&#xff1a;Predict the Next “Word”。 当前对于行业大模型的普遍认知就是沿用这种逻辑&#xff0c;用大…

蓝桥杯刷题第八天(dp专题)

这道题有点像小学奥数题&#xff0c;解题的关键主要是&#xff1a; 有2种走法固走到第i级阶梯&#xff0c;可以通过计算走到第i-1级和第i-2级的走法和&#xff0c;可以初始化走到第1级楼梯和走到第2级楼梯。分别为f[1]1;f[2]1(11)1(2)2.然后就可以循环遍历到后面的状态。 f[i…

obsidian常用插件,实现高效知识管理,打造最强第二大脑(更新中)

obsidian的精髓就在于其强大的社区插件。但是其插件市场太过于庞大&#xff0c;各式插件五花八门。 我们应该把核心放在知识的管理上&#xff0c;插件只是为知识管理服务的。而不是花费大量的时间去研究插件怎么用&#xff0c;做事情不能本末倒置&#xff01; 下面笔者结合自己…

界面控件DevExtreme JS ASP.NET Core 2024年度产品规划预览(一)

在本文中我们将介绍今年即将发布的v24.1附带的主要特性&#xff0c;这些特性既适用于DevExtreme JavaScript (Angular、React、Vue、jQuery)&#xff0c;也适用于基于DevExtreme的ASP.NET MVC/Core控件。 注意&#xff1a;本文中列出的功能和特性说明官方当前/预计的发展计划&a…