这一章在干嘛?
前面各章你已经用过
string,这一章把它彻底讲透:构造、输入、查找、容量。接着介绍三个”会自己删内存”的智能指针,解决new/delete配对的老大难问题。本章的重头戏是 STL(标准模板库)——C++ 里最常用的一套”容器 + 迭代器 + 算法”组合拳,它体现的泛型编程思想将贯穿你后续的 C++ 生涯。最后补上 C++11 的initializer_list。
16.1 string 类详解
string 其实是模板 basic_string<char> 的 typedef,头文件是 <string>(注意不是 C 的 <cstring>)。它重载了 =、+、+=、[]、<<、==、< 等一大票运算符,所以用起来像普通变量一样自然。
七种构造方式,一次看全:
// str1.cpp -- introducing the string class
#include <iostream>
#include <string>
int main()
{
using namespace std;
string one("Lottery Winner!"); // ctor #1: C 风格字符串
cout << one << endl; // 重载的 <<
string two(20, '$'); // ctor #2: 20 个 '$'
cout << two << endl;
string three(one); // ctor #3: 复制构造
one += " Oops!"; // 重载的 +=
two = "Sorry! That was "; // 重载的 =
three[0] = 'P'; // 重载的 [],可读可写
string four; // ctor #4: 默认空串
four = two + three; // 重载的 + 和 =
char alls[] = "All's well that ends well";
string five(alls, 20); // ctor #5: 取前 20 个字符
string six(alls + 6, alls + 10); // ctor #6: 指针范围 [begin, end)
cout << six << ", ";
string seven(&five[6], &five[10]); // ctor #6 再来一次
string eight(four, 7, 16); // ctor #7: 从位置 7 取 16 个字符
cout << eight << " in motion!" << endl;
return 0;
}范围构造 [begin, end):string six(alls+6, alls+10) 中,begin 包含、end 不包含——end 指向”最后一个要用元素的后一个位置”。这个数学式的半开区间约定,之后会贯穿整个 STL,务必现在就刻进脑子。
▲ 图 16.1 用范围 [begin, end) 构造字符串
注意:string seven(five + 6, five + 10) 是错的——对象名不像数组名,不是地址。要写 &five[6]。C++11 又补了两个构造函数:移动构造 string(string && str)(第 18 章详解)和接受 initializer_list<char> 的版本(string comp_lang {'L','i','s','p'};)。
string 的输入:C 风格字符串有 cin >> info、cin.getline(info, 100) 两种;string 对象则是 cin >> stuff(读一个词)和 getline(cin, stuff)(读一行)。核心区别在于:string 版会自动调整大小,永远不会截断你的输入,所以 getline 不需要第二个长度参数。这也是为什么 C 版的 getline 是 istream 的方法(cin.getline(...)),而 string 版是独立函数(getline(cin, stuff))。
string 版 getline 停止读取的三种情形:读到文件尾(设 eofbit);遇到分隔符(默认 \n,读走但不存);读到 string::npos 上限(设 failbit)。
查找方法:find() 有四个重载版本,都返回 string::npos(无符号整型最大值)表示”没找到”。家族成员还有 rfind()(从后往前找)、find_first_of()(找参数中任意字符首次出现)、find_last_of()、find_first_not_of()、find_last_not_of()。
一个实战味十足的游戏程序:猜单词(Hangman)。
// hangman.cpp -- some string methods
#include <iostream>
#include <string>
#include <cstdlib>
#include <ctime>
#include <cctype>
using std::string;
const int NUM = 26;
const string wordlist[NUM] = { "apiary", "beetle", "cereal",
"danger", "ensign", "florid", "garage", "health", "insult",
"jackal", "keeper", "loaner", "manage", "nonce", "onset",
"plaid", "quilt", "remote", "stolid", "train", "useful",
"valid", "whence", "xenon", "yearn", "zippy" };
int main()
{
using std::cout;
using std::cin;
using std::tolower;
using std::endl;
std::srand(std::time(0));
char play;
cout << "Will you play a word game? <y/n>";
cin >> play;
play = tolower(play);
while (play == 'y')
{
string target = wordlist[std::rand() % NUM];
int length = target.length();
string attempt(length, '-'); // 与谜底等长的连字符串
string badchars;
int guesses = 6;
cout << "Guess my secret word. It has " << length
<< " letters.\nYour word: " << attempt << endl;
while (guesses > 0 && attempt != target)
{
char letter;
cout << "Guess a letter: ";
cin >> letter;
if (badchars.find(letter) != string::npos
|| attempt.find(letter) != string::npos)
{
cout << "You already guessed that. Try again.\n";
continue;
}
int loc = target.find(letter);
if (loc == string::npos)
{
cout << "Oh, bad guess!\n";
--guesses;
badchars += letter; // += 可以追加单个字符
}
else
{
cout << "Good guess!\n";
attempt[loc] = letter;
loc = target.find(letter, loc + 1); // 从下一处继续找
while (loc != string::npos)
{
attempt[loc] = letter;
loc = target.find(letter, loc + 1);
}
}
cout << "Your word: " << attempt << endl;
}
if (guesses > 0)
cout << "That's right!\n";
else
cout << "Sorry, the word is " << target << ".\n";
cout << "Will you play another? <y/n> ";
cin >> play;
play = tolower(play);
}
cout << "Bye\n";
return 0;
}容量机制:每次 += 都重新分配内存太浪费,所以实现通常预留超额空间。capacity() 返回当前分配块大小,reserve() 请求最小容量。最后,需要 C 风格字符串时(如 fout.open(...)),用 filename.c_str() 转换。
常见坑:string::npos 不是 -1
find()找不到时返回string::npos,它定义成无符号整型的最大值。如果你写if (str.find('x') < 0)永远不成立,因为无符号数不会小于 0。正确写法是!= string::npos。另外string five(alls, 40)这种”长度超过原串”的构造会把后面的内存垃圾也拷进来。
通关标准:
能不查资料写出七种 string 构造方式;能解释
[begin, end)半开区间的含义;知道c_str()的用途;理解capacity()与size()的区别。
16.2 智能指针模板类
先看一段内存泄漏的反面教材:
void remodel(std::string & str)
{
std::string * ps = new std::string(str);
...
if (weird_thing())
throw exception(); // 异常一抛,下面的 delete 永远执行不到
str = *ps;
delete ps;
return;
}“记得 delete”从来不是好方案。智能指针的思路:让指针本身是个类对象,对象消亡时析构函数自动 delete 所指向的内存。头文件 <memory>,三个模板:
std::auto_ptr<std::string> ps (new std::string(str)); // C++98,已弃用
std::unique_ptr<std::string> pdu (new std::string(str)); // C++11,独占
std::shared_ptr<std::string> pss (new std::string(str)); // C++11,共享
▲ 图 16.2 普通指针与 auto_ptr 的行为对比:auto_ptr 过期时自动释放内存
三者用法几乎一样,区别在赋值时的策略。两个指针指向同一块内存、析构时 delete 两次,是灾难,所以:
- auto_ptr / unique_ptr 采用”所有权”模型:赋值即转移所有权。区别是 auto_ptr 转移后原指针悄悄变成空指针(用起来就崩),而 unique_ptr 要求源对象必须是临时右值才允许赋值,否则编译期报错。
- shared_ptr 采用引用计数:赋值计数 +1,指针过期计数 -1,减到 0 才真正 delete。
看一个 auto_ptr 的”翻车现场”:
// fowl.cpp -- auto_ptr a poor choice(节选)
auto_ptr<string> films[5] = {
auto_ptr<string> (new string("Fowl Balls")),
auto_ptr<string> (new string("Duck Walks")),
auto_ptr<string> (new string("Chicken Runs")),
auto_ptr<string> (new string("Turkey Errors")),
auto_ptr<string> (new string("Goose Eggs"))
};
auto_ptr<string> pwin;
pwin = films[2]; // films[2] 交出所有权,变成空指针
for (int i = 0; i < 5; i++)
cout << *films[i] << endl; // 打到 films[2] 时直接段错误!换成 shared_ptr 程序正常(引用计数 1→2→1→0);换成 unique_ptr 则在 pwin = films[2] 这行编译不过——把运行期崩溃提前到编译期,这正是它优于 auto_ptr 的地方。若确实想转移,用 std::move() 显式表态:
ps2 = move(ps1); // 显式移交,ps1 之后的值未定义,但这是你自己选的选型建议:多处共用同一对象选 shared_ptr;独占式管理选 unique_ptr(尤其适合做函数返回值,把所有权交给调用方);auto_ptr 别再用了。另外三者都只能管 new 出来的内存——栈变量、new [] 分配的数组都不能交给它们(unique_ptr 有 unique_ptr<double[]> 数组特化版除外)。
常见坑:把栈地址交给智能指针
string vacation("hello"); shared_ptr<string> pvac(&vacation); // 大错!——pvac 过期时会对栈内存执行 delete,直接未定义行为。智能指针只认new返回的堆地址。
16.3 STL 与 vector
STL 是一套”容器 + 迭代器 + 算法 + 函数对象”的模板集合,由 Alex Stepanov 等人在 HP 实验室开发,1994 年被纳入 C++ 标准。它代表的不是面向对象,而是泛型编程(generic programming)。
vector 是最常用的容器——可动态伸缩的数组:
// vect1.cpp -- introducing the vector template(节选)
#include <vector>
vector<int> ratings(NUM); // 5 个 int
vector<string> titles(NUM); // 5 个 string
for (int i = 0; i < NUM; i++)
{
cout << "Enter title #" << i + 1 << ": ";
getline(cin, titles[i]);
cin >> ratings[i];
cin.get();
}所有容器共有的基本方法:size()(元素个数)、swap()(交换两个容器)、begin()(指向首元素的迭代器)、end()(指向超尾的迭代器)。
▲ 图 16.3 STL 的范围概念:[begin, end) 覆盖整个容器
迭代器可以理解为”泛化的指针”:
vector<double>::iterator pd; // 声明迭代器
pd = scores.begin(); // 指向首元素
*pd = 22.3; // 解引用赋值
++pd; // 前移
for (pd = scores.begin(); pd != scores.end(); pd++)
cout << *pd << endl; // 遍历整个容器C++11 可以偷懒:auto pd = scores.begin();。
vector 专属的常用方法:push_back() 尾部追加(自动扩容)、erase(begin, end) 删除区间、insert(pos, first, last) 在某位置前插入区间。超尾的价值在这里体现了——insert(old_v.end(), ...) 就是”追加到末尾”。
STL 的算法大多定义成非成员函数而非容器方法。8 个容器 × 10 个操作,若做成成员函数要写 80 个;做成通用函数只需 10 个,而且你自己的新容器也能直接享用。当然某些容器会额外提供特化版成员函数(如 list 的 sort()),因为类内部知道数据布局,可以做得更快。
常见坑:copy() 不扩容
copy(casts, casts+10, dice.begin())是覆写,要求目标容器已有足够空间。往空 vector 里 copy 会直接内存越界。想边拷边长,得用插入迭代器(见 16.4)或push_back。
16.4 迭代器与泛型编程
为什么需要迭代器?设想给”数组”和”链表”各写一个 find 函数:数组用下标走,链表用 p_next 指针走,算法逻辑完全不同。但抽象一层看,两者都是”逐个比较元素直到命中”。迭代器就是把”在容器中移动”这件事抽象成统一接口:能解引用 *p、能比较 p == q、能前进 ++p。普通指针天然满足这些要求,所以指针就是迭代器的一种(模型)。
每个容器类都定义了自己的 iterator 类型(类内 typedef),并提供 begin()/end()。于是同一段遍历代码,把 vector<double>::iterator 换成 list<double>::iterator 就能原样用于链表:
for (auto pr = scores.begin(); pr != scores.end(); pr++)
cout << *pr << endl; // vector 和 list 通用五种迭代器(能力递增的层级):
| 类型 | 能力 | 典型算法 |
|---|---|---|
| 输入迭代器 | 单遍、只读、只进 | find() |
| 输出迭代器 | 单遍、只写、只进 | copy() 的目标 |
| 正向迭代器 | 多遍、固定顺序、读写 | replace() |
| 双向迭代器 | 正向 + 反向 -- | list 内部算法 |
| 随机访问迭代器 | 可跳跃 +n、下标 a[n]、比较大小 | sort() |
关键点:算法按”最低需求”声明参数。find() 只要输入迭代器,所以任何容器都能用;sort() 要随机访问迭代器,list 就用不了通用 sort(),只能用类内的 list::sort() 成员。注意这些”概念”不是 C++ 类型,编译器不会替你检查,文档用 InputIterator、RandomAccessIterator 这类参数名提醒你。
常用的预定义迭代器(头文件 <iterator>):
// copyit.cpp -- copy() 与迭代器(节选)
int casts[10] = {6, 7, 2, 9, 4, 11, 8, 7, 10, 5};
vector<int> dice(10);
copy(casts, casts + 10, dice.begin()); // 数组 → vector
ostream_iterator<int, char> out_iter(cout, " ");
copy(dice.begin(), dice.end(), out_iter); // 容器 → 屏幕
copy(dice.rbegin(), dice.rend(), out_iter); // 逆序输出,一行搞定
// 插入迭代器:把"覆写"变成"插入"
copy(s2, s2 + 2, back_insert_iterator<vector<string>>(words)); // 尾插
copy(s3, s3 + 2, insert_iterator<vector<string>>(words, words.begin())); // 头插back_insert_iterator 要求容器能在尾部常数时间插入(vector 可以,list 也可以);front_insert_iterator 要求头部常数时间插入(vector 不行,deque/list 行);insert_iterator 无限制。它们能通过容器的 push_back() 等方法自动扩容,绕开了 copy() 不扩容的限制。
16.5 容器大点兵
序列容器(元素有确定线性顺序):vector、deque、list、forward_list(C++11)、queue、priority_queue、stack。前五种要求支持 insert()、erase() 等序列操作。
- vector:可变长数组,随机访问快;尾插尾删常数时间,头插中间插是线性时间。默认之选。
- deque:双端队列,读作”deck”。头尾插入删除都是常数时间,也支持随机访问。两端操作多时选它。
- list:双向链表。任意位置插入删除都是常数时间,但不支持随机访问和下标。迭代器在插入删除后依然指向原元素(vector 插入元素后迭代器可能失效)。
▲ 图 16.4 push_front() 与 push_back():list 和 deque 支持头部常数时间插入,vector 不支持
list 的专属工具箱:
// list.cpp -- using a list(核心片段)
list<int> one(5, 2); // 5 个 2
two.insert(two.begin(), stuff, stuff + 5); // 区间插入
three.remove(2); // 删除所有 2
three.splice(three.begin(), one); // 剪切拼接,one 变空
three.unique(); // 折叠相邻重复值
three.sort(); // list 专用排序
three.merge(two); // 合并两个有序链表注意 unique() 只折叠相邻的重复值,所以”先 sort 再 unique”才能全量去重。splice() 与 insert() 不同:它是搬移而非复制,源链表被搬空。
- queue:适配器(adapter),默认基于 deque。只许队尾进、队头出,提供
push()/pop()/front()/back()/size()/empty()。pop()只删不取,取值要先front()。 - priority_queue:最大值永远排队首(默认用
<比较),可用priority_queue<int, vector<int>, greater<int>>改成最小值优先。 - stack:适配器,默认基于 vector。
push()/pop()/top()三板斧,后进先出。 - array(C++11):定长,不是完全的 STL 容器(不能 resize),但支持 begin()/end(),可用 STL 算法。
关联容器:把值和键(key)绑在一起,用键快速查找,底层通常是树,插入时自动排序,不允许你指定插入位置。
- set:键就是值,键唯一,自动排序。
- multiset:一个键可以有多个值。
- map:键值对,键唯一。
- multimap:一键多值。
// setops.cpp -- some set operations(节选)
string s1[N] = {"buffoon", "thinkers", "for", "heavy", "can", "for"};
set<string> A(s1, s1 + N); // "for" 只留一份,且已排序
set_union(A.begin(), A.end(), B.begin(), B.end(), out); // 并集
set_intersection(A.begin(), A.end(), B.begin(), B.end(), out); // 交集
set_difference(A.begin(), A.end(), B.begin(), B.end(), out); // 差集
set<string> C;
set_union(A.begin(), A.end(), B.begin(), B.end(),
insert_iterator<set<string>>(C, C.begin())); // 写回集合 C
C.lower_bound("ghost"); // 第一个 >= "ghost" 的元素
C.upper_bound("spook"); // 第一个 > "spook" 的元素为什么写回 C 要用 insert_iterator 而不是 C.begin()?两个原因:集合的键是常量,begin() 返回的是常量迭代器不能当输出迭代器;且 C 是空的,copy 型算法要求目标有足够空间。
multimap 用 pair<const Key, T> 存元素,用 equal_range() 取同键的所有值:
// multmap.cpp -- use a multimap(节选)
typedef std::pair<const KeyType, std::string> Pair;
typedef std::multimap<KeyType, std::string> MapCode;
MapCode codes;
codes.insert(Pair(718, "Brooklyn"));
codes.insert(Pair(718, "Staten Island")); // 同一个键 718
cout << codes.count(718); // 2
auto range = codes.equal_range(718); // 返回一对迭代器
for (auto it = range.first; it != range.second; ++it)
cout << (*it).second << endl;C++11 还新增了基于哈希表的无序关联容器:unordered_set、unordered_multiset、unordered_map、unordered_multimap——增删更快、查找平均常数时间。
16.6 函数对象
函数对象(functor)泛指”能像函数一样加 () 调用的东西”:函数名、函数指针、重载了 operator()() 的类对象。类的版本有个杀手锏——可以用成员变量携带额外信息:
// functor.cpp -- using a functor(核心)
template<class T>
class TooBig
{
private:
T cutoff;
public:
TooBig(const T & t) : cutoff(t) {}
bool operator()(const T & v) { return v > cutoff; }
};
TooBig<int> f100(100);
yadayada.remove_if(f100); // 删除所有 > 100 的元素
etcetera.remove_if(TooBig<int>(200)); // 匿名对象,阈值 200普通函数 bool tooBig(int n) 只能有一个参数,想换个阈值就得再写一个函数;functor 用构造函数存阈值,一份代码通吃。STL 概念上把 functor 分为:generator(无参)、一元函数(一参)、二元函数(两参),返回 bool 的一元/二元函数分别叫 predicate(谓词)和 binary predicate。
STL 预定义了一批 functor(头文件 <functional>),与运算符一一对应:plus、minus、multiplies、divides、modulus、negate;equal_to、not_equal_to、greater、less、greater_equal、less_equal;logical_and、logical_or、logical_not。典型用法是给 transform() 当参数:
// funadap.cpp -- using function adapters(节选)
vector<double> sum(LIM);
transform(gr8.begin(), gr8.end(), m8.begin(), sum.begin(),
plus<double>()); // 逐元素相加
vector<double> prod(LIM);
transform(gr8.begin(), gr8.end(), prod.begin(),
bind1st(multiplies<double>(), 2.5)); // 每个元素 × 2.5bind1st/bind2nd 是函数适配器:把二元函数的一个参数绑定成常量,降级成一元函数。C++11 里它们已被更灵活的 bind 和 lambda 取代,但老代码里常见。C++11 的 lambda 是更现代的替代方案(第 18 章详述)。
16.7 STL 算法
STL 算法分四组:非修改式(find、for_each——只看不改)、修改式(transform、random_shuffle、copy)、排序及相关(sort、各种 set 操作)、通用数值运算(在 <numeric> 里,其余在 <algorithm>)。
两个通用命名约定:
_copy后缀:复制版本,结果写到新位置。如replace()原地替换,replace_copy()结果拷到别处并返回”最后拷贝位置之后”的迭代器。_if后缀:接受一个谓词决定是否执行。如replace_if()、replace_copy_if()。
string 虽不属于 STL,但设计了 begin()/end() 等接口,可以直接吃 STL 算法:
// strgstl.cpp -- applying the STL to a string(核心)
string letters;
cin >> letters;
sort(letters.begin(), letters.end()); // 先排成最小字典序
do {
cout << letters << endl;
} while (next_permutation(letters.begin(), letters.end()));成员函数 vs 通用算法,该选谁?通常成员函数更好:针对容器特化过、能自动调整容器大小。最经典的对比例子:
// listrmv.cpp -- 核心对比
la.remove(4); // 方法版:真删,list 从 10 个变 6 个
list<int>::iterator last;
last = remove(lb.begin(), lb.end(), 4); // 算法版:不能改大小!
// lb 仍是 10 个元素:5 2 2 3 8 1 4 8 1 4(后 4 个是残留垃圾)
lb.erase(last, lb.end()); // 需要手动 erase 掉残留段通用算法的优势在于通用:数组、string、以及跨容器类型混合操作(vector 数据存进 set)都行。
来一个 STL 组件协作的完整示例——统计输入单词的出现频率:
// usealgo.cpp -- using several STL elements(核心)
vector<string> words; // 按输入顺序记录
while (cin >> input && input != "quit")
words.push_back(input);
set<string> wordset; // 自动排序 + 自动去重
transform(words.begin(), words.end(),
insert_iterator<set<string>>(wordset, wordset.begin()), ToLower);
map<string, int> wordmap; // 单词 → 次数
for (auto si = wordset.begin(); si != wordset.end(); si++)
wordmap[*si] = count(words.begin(), words.end(), *si);vector 管顺序、set 管排序去重、map 管键值统计——三个容器各司其职,一行算法干一件事。用 STL 的正确姿势是能不写的代码就不写。
16.8 vector、valarray 与 array
为什么 C++ 有三个数组模板?因为出身不同、使命不同:
- vector:STL 容器家族成员,强在排序、插入、搜索、容器间搬运。
- valarray:面向数值计算,不是 STL。重载了全部算术运算符,写公式最爽。
- array:定长数组的替身,紧凑高效且接口更安全,支持 begin()/end()。
对比一目了然:
// vector/array 版:逐元素相加要靠算法
transform(ved1.begin(), ved1.end(), ved2.begin(), ved3.begin(), plus<double>());
// valarray 版:直接写算式
vad3 = vad1 + vad2;
vad3 = 2.5 * vad3; // 整体缩放
vad3 = log(vad1); // 数学函数整体作用
vad3 = 10.0 * ((vad1 + vad2) / 2.0 + vad1 * cos(vad2)); // 多步算式一行写完但 valarray 没有 begin()/end(),通用 sort() 用不了;C++11 补了 sort(begin(vad), end(vad)) 的模板函数版本救场。valarray 还有个看家本领 slice 切片——用 slice(start, number, stride) 当下标,把一维数组当二维矩阵用:
// vslice.cpp -- valarray slices(节选)
vint valint(12); // 当作 4 行 × 3 列
vint vcol(valint[slice(1, 4, 3)]); // 取第 2 列:下标 1,4,7,10
valint[slice(2, 4, 3)] = 10; // 第 3 列整体赋值为 10
valint[slice(0,4,3)] = vint(valint[slice(1,4,3)])
+ vint(valint[slice(2,4,3)]); // 第 1 列 = 第 2 列 + 第 3 列16.9 initializer_list 模板
C++11 让花括号列表初始化几乎无处不在:
std::vector<double> payments {45.99, 39.23, 19.95, 89.01};能这么做是因为容器类有了接受 initializer_list<T> 的构造函数。规则:只要类有 initializer_list 构造函数,{} 语法就优先匹配它——
vector<int> a1(10); // 10 个未初始化元素(圆括号版)
vector<int> a2{10}; // 只有 1 个元素,值为 10(花括号版走 initializer_list)
vector<int> a3{4,6,1}; // 3 个元素列表内元素会隐式转换到元素类型({45.99, 39.23, 19, 89} 里两个 int 转 double),但禁止收窄转换:vector<int> values = {10, 8, 5.5}; 是编译错误,5.5 转 int 会丢信息。
自己也能用:头文件 <initializer_list>,有 begin()/end()/size(),元素是 const 不可改:
// ilist.cpp -- use initializer_list
double sum(std::initializer_list<double> il);
double average(const std::initializer_list<double> & ril);
double sum(std::initializer_list<double> il)
{
double tot = 0;
for (auto p = il.begin(); p != il.end(); p++)
tot += *p;
return tot;
}
// 调用:sum({2,3,4}),也可传入具名的 initializer_list 变量定长类的构造函数(如 Position(int x, int y, int z))不要提供 initializer_list 版本——元素个数固定的类用花括号反而会走错构造函数。
常见坑:
vector<int> v{10}不是"10 个元素"花括号触发的 initializer_list 构造让你得到1 个值为 10 的元素;想要 10 个元素请写
vector<int> v(10)。同理auto声明加大括号会推导出initializer_list类型,别踩。
自测:find() 找不到目标时返回什么?怎么判断?
返回
string::npos(无符号整型最大值)。判断必须用str.find(ch) != string::npos,不能与 -1 或 0 比较,更不能判断< 0(无符号数恒 false)。
自测:为什么 unique_ptr 比 auto_ptr 安全?
auto_ptr 赋值时悄悄转移所有权,原指针变空,后续解引用直接崩溃。unique_ptr 只允许用临时右值赋值(转移后临时对象立刻销毁,不会被误用),把普通左值赋给它会得到编译期错误——把运行期事故提前到编译期拦截。
自测:list 为什么不能用通用 sort() 算法,却有自己的 sort() 方法?
通用 sort() 要求随机访问迭代器(需要 +n、a[n] 等操作),而 list 是链表只有双向迭代器。类内部知道链式结构,自己实现了基于双向迭代器的归并排序成员版本,还附赠 merge()、splice()、unique() 等链表特化操作。
自测:set_union() 的结果想写进一个空 set C,为什么不能直接用 C.begin() 当输出迭代器?
两个原因:① set 的键是常量,C.begin() 返回常量迭代器,不能用于输出;② set_union 像 copy() 一样覆写目标,要求目标空间足够,空 set 不满足。正确做法是构造
insert_iterator<set<string>>(C, C.begin()),它通过 insert() 自动扩容。
自测:copy() 算法和 list::remove() 成员函数在"删除元素"上有什么本质区别?
通用 remove() 不是成员函数,无权修改容器大小——它只把不需要删的元素搬到容器前部,返回新逻辑末尾的迭代器,尾部残留垃圾需要再调 erase() 清掉。成员 remove() 直接调整容器大小,一步到位。所以对 list 删除元素优先用成员方法。