本章讨论STL容器适配器,包括
- 容器适配器入门
- 如何使用std::stack
- 如何使用std::queue
- 如何使用std::priority_queue
- 扁平化容器适配器
容器适配器入门指南
容器适配器是一种模板类,它通过重新利用STL序列容器类来提供特定功能。STL包含两类容器适配器。
第一类包括std::stack、std::queue和std::priority_queue这些经典容器适配器。它们定义了刻意保持简洁直观的公共接口。第二类容器适配器包含std::flat_set、std::flat_multiset、std::flat_map和std::flat_multimap。这些C++23新增的类利用STL序列容器实现了与对应关联容器类(参见第7章)相似的功能行为,但在某些使用场景下具有更优性能。表9-1总结了这些容器适配器的若干显著特征。

在阅读本章后续内容时,请牢记以下几点:
正如前文所述,所有容器适配器都在内部操作序列容器;但它们本身并非序列容器。应将其视为底层序列容器的包装类,这些包装类提供特定但受限的功能。
表9-1中,"默认容器"列指明了适配器默认使用的底层序列容器,而"替代容器"列则列出了支持的其他可选容器。
对于大多数使用场景,默认序列容器能提供最佳性能。只有当特定数据类型需要优化元素插入或删除性能时,才会指定使用替代容器。
函数无法访问适配器底层序列容器,必须使用公共接口函数。
迭代器不能用于访问std::stack、std::queue或std::priority_queue的元素,这意味着大多数STL算法不适用于这些类。
使用std::stack
std::stack是一种容器适配器,通过push()和pop()操作实现后进先出(LIFO)功能。软件栈的常见视觉隐喻是一摞盘子:新盘子只能被插入(压入)到堆叠顶部,且只有最顶部的盘子能被移除(弹出),堆叠内部的盘子不可访问。图9-1展示了使用std::deque作为底层序列容器的std::stack逻辑结构,该图同时演示了push()和pop()操作的执行过程。

从表9-1可知,std::stack也可以选择std::vector或std::list作为其底层序列容器。这些容器与std::deque都定义了push_back()、pop_back()和back()成员函数。容器适配器std::stack正是利用这些函数分别实现了自身的push()、pop()和top()操作。
代码清单9-1-1展示了示例函数Ch09_01_ex1()的源代码。该函数演示了如何使用std::stack进行基础操作。Ch09_01_ex1()的开头代码块定义了一个名为print_words()的lambda表达式,用于打印std::stack<std::string>的元素。在该函数中,表达式auto stk{words}通过拷贝构造创建了参数words的新实例。之所以要创建参数words的副本,是因为适配器std::stack既不支持迭代器,也没有定义任何允许直接访问其元素的公共成员函数。目前1,打印std::stack元素的唯一直接方法是连续调用top()和pop()。
//-------------------------------------------------------------------------
// Ch09_01_ex.cpp
//-------------------------------------------------------------------------
#include <deque>
#include <ranges>
#include <stack>
#include <string>
#include "Ch09_01.h"
#include "TowerOfHanoi.h"
void Ch09_01_ex1() {
// 打印文字 lambda
auto print_words = [](const char *msg, const std::stack<std::
string> &words) {
auto stk{words};
std::println("{:s} (size = {:d})", msg, stk.size());
while (!stk.empty()) {
// 使用 top() 和 pop()
std::print("| {:s} ", stk.top());
stk.pop();
}
std::println("|");
};
// 创建栈(std::stack 不支持初始化列表)
const auto il1 = {
"brought ...", "fathers", "our", "ago",
"years", "and"
};
std::stack<std::string> words1(il1.begin(), il1.end());
print_words("\nwords1 (initial values)", words1);
// 使用 push 和 emplace
words1.push("score");
words1.emplace("four");
print_words("\nwords1 (after push and emplace)", words1);
// 创建栈(使用 std::deque 初始化的构造函数)
const std::deque<std::string> deq2
{"apple", "banana cream", "blueberry"};
std::stack<std::string> words2(deq2);
print_words("\nwords2 (initial values)", words2);
// 使用push_range
std::stack<std::string> words3{words2};
print_words("\nwords3 (initial values)", words3);
const auto il3 = {"cherry", "key lime", "peach", "pecan", "turtle"};
#ifdef __cpp_lib_containers_ranges
words3.push_range(il3);
print_words("\nwords3 (after push_range)", words3);
#else
for (const auto &s: il3)
words3.push(s);
print_words("\nwords3 (after push operations)", words3);
#endif
}
print_words()函数中的下一个代码块使用while循环来打印stk的元素。成员函数stk.empty()在stk包含零个元素时返回true。随后的std::println()语句通过stk.top()访问并打印stk的顶部元素,紧接着调用stk.pop()移除stk顶部的std::string。您可能会疑惑为何将top()和pop()设计为独立函数而非合并为单一操作。定义top_and_pop()成员函数将强制进行元素拷贝操作,即便某些场景只需pop()功能。当然,您可以自定义top_and_pop()函数,后文将展示具体示例。
需要特别说明的是:对空std::stack调用top()或pop()会导致未定义行为。使用std::stack适配器的函数应当始终通过empty()或size()进行有效性检查。若检测到空栈属于异常情况,抛出异常也是合理的处理方式。
回到Ch09_01_ex1()的代码:在print_words()定义之后的首个代码块实例化了名为words1的std::stack<std::string>适配器。注意该代码块显式定义了文本字符串的初始化列表,并将迭代器il1.begin()和il1.end()传递给words1的构造函数。这是因为适配器类std::stack不支持直接使用初始化列表构造对象(例如std::stack<std::string> stk {"one", "two"}是无效语法)。实例化words1后,Ch09_01_ex1()调用print_words()打印其元素。
随后的代码块中,语句words1.push("score")和words1.emplace("four")向words1追加了两个元素。接下来的代码块实例化了另一个名为words2的std::stack<std::string>适配器,值得注意的是这里向words2的构造函数传递了std::deque<std::string>类型的对象——这是可行的,因为适配器words2的底层容器正是std::deque<std::string>。
Ch09_01_ex1()的最终代码块演示了words2.push_range()(C++23)的用法,该操作将il3的元素压入栈words3。il3的元素按照初始化列表中的顺序被压入(即首先压入"cherry",接着是"key lime"等)。实际压入words3的元素当然是通过il3的const char*成员构造的std::string对象。
栈使用的典型范例是著名的汉诺塔(TOH)算法。在该算法中,编号圆盘必须通过中间柱子从源柱子移动到目标柱子。只有当圆盘数值小于目标柱子最顶部圆盘时,才能将其移动到另一根柱子。圆盘也可以移动到空柱子上。图9-2更详细地展示了汉诺塔柱子与圆盘的排列方式。

TOH算法通常如图9-2所示,将所有圆盘初始放置在A柱上。若圆盘总数为偶数,则目标柱为C柱;若为奇数,则目标柱为B柱。将N个圆盘从A柱移至目标柱所需的最少移动次数为次。
代码清单9-1-2-1展示了汉诺塔类(TowerOfHanoi)的源代码实现。在类声明顶部定义了名为uint_t的类型别名,用于指定圆盘的整数类型。该类的唯一公开成员函数是TowerOfHanoi::run(),该函数需要接收两个参数:圆盘数量和一个详细模式标志位(该标志位用于在执行过程中启用堆栈转储及其他状态信息输出)。
类型别名peg_t表示汉诺塔的柱体结构,其本质是基于std::stack<uint, std::vector<uint_t>>的容器适配器。该适配器选择std::vector<uint_t>作为底层容器,这是因为当处理uint_t这类基础数据类型时,向量容器通常比std::deque<uint_t>具有更优的性能表现。
汉诺塔类的私有成员函数声明紧随其后。该类包含三个peg_t类型的成员对象:m_PegA、m_PegB和m_PegC,以及一个布尔型成员变量m_Verbose用于控制详细输出模式。
//-------------------------------------------------------------------------
// TowerOfHanoi.h
//-------------------------------------------------------------------------
#ifndef TOWER_OF_HANOI_H_
#define TOWER_OF_HANOI_H_
#include <cstdint>
#include <stack>
#include <vector>
class Tow


1509

被折叠的 条评论
为什么被折叠?



