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=q | p和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 pi1
和 pd2
所产生的错误 则更具潜在危害:通常情况下,编译器不能分辨一个指针指向的是 静态 还是动态分配的对象。类似的,编译器 也不能分辨 一个指针所指向的内存 是否已经被释放了。对于 这些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 = 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 时,要用一个 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[ ]> u | u可以指向 一个动态分配的数组,数组元素类型为 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不要抛出异常