定位:
本质是
char* _str+size_t _size+size_t _capacity,外加短字符串优化两个恒等式:
_size不含结尾的'\0';实际缓冲区是_capacity + 1字节(那 1 字节留给'\0')。一条主线:string 同时背了两套长度来源——
size()体系(可信,能表示含'\0'的内容)和'\0'终止体系(C 语言遗产)。绝大多数 string 的坑,都是这两个体系👆混用造成的。
一、接口速查
构造与赋值
| 写法 | 结果 |
|---|---|
string s; | 空串,size()==0 |
string s("abc")/s = "abc" | 从 C 字符串构造(隐式转换) |
string s(5, 'a') | 5 个a→"aaaaa" |
string s("abcdef", 3) | 取前 3 个 →"abc" |
string s(other, 2, 4) | 从other下标 2 起取 4 个 |
string s(first, last) | 迭代器区间,左闭右开 |
string s(1, 'c') | 把 char 变成 string(string 没有单 char 构造) |
容量
| 接口 | 语义 |
|---|---|
size()/length() | 完全等价,返回字节数,不含'\0' |
capacity() | 可存的有效字符数,不含结尾'\0';缓冲区实为capacity()+1 |
empty() | size()==0 |
reserve(n) | 只改 capacity,不改 size;n < capacity 时是否缩容由实现决定 |
resize(n, c) | 改 size:变大补c(默认'\0'),变小截断,capacity 不变 |
shrink_to_fit() | 非强制缩容请求 |
clear()只把 size 置 0,capacity 不变。
元素访问
| 接口 | 越界行为 |
|---|---|
s[i] | 无检查 |
s.at(i) | 抛std::out_of_range |
front()/back() | 空串上是 UB |
c_str()/data() | 返回带'\0'的 C 串指针,O(1) |
string 独有的一条:s[i]中i == size()是合法的,返回结尾'\0'的引用(改它成非'\0'是 UB)。所以空串s[0]也合法。而at(size())会抛异常(at要求i < size())。
修改
| 接口 | 复杂度 | 返回值 |
|---|---|---|
push_back / pop_back | 均摊 O(1) | void |
append/operator+= | 均摊 O(len) | string& |
insert(pos, ...) | O(n) | 下标版返回string&,迭代器版返回iterator |
erase(pos, len) | O(n) | 下标版返回string&;迭代器版返回下一个迭代器 |
replace(pos, len, str) | O(n) | string& |
swap(other) | O(1) | 只换三个成员,不深拷贝 |
注意erase的len默认是npos(删到结尾);insert/erase/replace都同时提供下标版和迭代器版——下标版不涉及迭代器失效,日常优先用它。
查找与子串(vector / list 都没有,这是 string 的护城河)
| 接口 | 作用 |
|---|---|
find / rfind | 找子串或字符,返回下标,找不到返回npos |
find_first_of / find_last_of | 找集合中任一字符(find_first_not_of反之) |
substr(pos, len) | 返回新 string;len默认到结尾 |
compare | 字典序比较,返回<0 / 0 / >0 |
非成员
operator+(拼接,产生新对象)、operator<< / >>、getline(cin, s)、stoi / stod / stof、to_string。
二、两套'\0'体系 —— 后面所有技巧的根
std::string能存含'\0'的字节序列(因为它按size()记账),但很多接口内部仍调用 C 函数,会在'\0'处提前截止。这不是缺陷,是两套体系的边界,必须分清:
信任size()的(含'\0'也正常) | 信任'\0'的(遇到就截止) |
|---|---|
size()/capacity()/empty() | string(const char*)——未显式给长度时 |
s[i]/at(i)/front()/back() | find(const char*) |
begin()/end()/ 范围 for | c_str()、data()(遍历到首个'\0'停) |
swap/ 移动 | cin >> s(且'\0'会被当成空白) |
assign(p, n)/append(p, n)(显式给长度) | s += "x\0y"(未给长度,只加"x") |
拷贝构造 /operator=(整个对象) | s.compare("a\0b")(C 串那侧被截断) |
string s1 = "a\0b"; // 未给长度 → size()==1,只存了 'a' string s2("a\0b", 3); // 显式给长度 → size()==3,完整存了 3 个字节一句话规则:参数是const char*且没给长度 → 走'\0'体系;给了长度或传的是string→ 走size()体系。
三、算法常用技巧
- 拼接用
+=,不要用+:s += "abc"原地追加;s = s + "abc"会先造临时 string 再拷贝回来,白付一次 O(n)。 - 预分配:循环拼接前
s.reserve(总长),避免多次扩容加拷贝。 reserve/reverse别混:reserve是留空间,reverse是std::reverse(s.begin(), s.end())(反转),后者是算法不是成员。- string 的迭代器是随机访问迭代器,所以
sort、reverse、binary_search都能直接作用在字符上:std::sort(s.begin(), s.end())。 - 读整行用
getline(cin, s);cin >> s遇空格就断。混用时先cin.ignore(numeric_limits<streamsize>::max(), '\n')吃掉残留换行,否则getline会读到空行。 - 判定查找结果写
!= string::npos。npos就是(size_t)-1,所以!= -1数值上等价也能编过——但用npos才表达意图。 substr会拷贝。只读切片用std::string_view(C++17)零拷贝;注意它不拥有数据,原串一改/一销毁它就悬空。stoi的三种结局:正常转换、非法输入抛std::invalid_argument、溢出抛std::out_of_range,所以循环里读数字要 try-catch。它不检查尾部:stoi("123abc")得 123。to_string(浮点)只给 6 位有效数字:to_string(3.1415926)→"3.141593"。要精度自己格式化。另外float与double的to_string结果可能不同,因为二者存储精度本就不同。- 按分隔符切串:
istringstream iss(s); while (getline(iss, token, ',')) {...}——用getline的第三个参数指定分隔符。 - 前缀/后缀:C++20 有
s.starts_with(p)/ends_with(p);更早用s.compare(0, p.size(), p) == 0。 - 传参一律
const string&,返回靠移动/NRVO。 - 彻底释放内存:
string().swap(s);clear()只清内容不还内存。shrink_to_fit()是非强制请求。 tolower/toupper先转unsigned char:std::tolower(static_cast<unsigned char>(c)),直接传负char是 UB。- 字符映射常客:
s[i] - 'a'当 0~25 的下标,用于计数排序、哈希、状态压缩。
四、权衡利弊
1. string vs vector<char>
| string | vector<char> | |
|---|---|---|
结尾'\0' | 保证有,c_str()可直接喂给 C API | 无此约定 |
| 字符串算法 | find/substr/replace/compare全套 | 只有通用算法 |
| SSO | 有,短串不碰堆 | 无,一有元素就分配 |
| 语义 | 文本 | 纯字节缓冲区 |
要处理文本用 string;要纯字节缓冲(二进制、网络包)用vector<char>或vector<uint8_t>——string 的'\0'约定反而碍事。
2. SSO:短字符串优化
短串(libstdc++ 约 ≤15 字节)直接存对象内部的联合体里,不碰堆;长串才退化成堆指针。
| 无 SSO(24 字节) | 有 SSO(32 字节) | |
|---|---|---|
| 成员 | 指针 + size + capacity | 联合体(16) + size(8) + capacity(8) |
| 短串 | 每次都堆分配 | 栈内直接放,快得多 |
| 代价 | — | sizeof变大,string 对象移动/拷贝成本略升 |
权衡:SSO 用固定的对象体积换短串的免堆分配,对"大量短字符串"的场景收益极大,对"单个超长字符串"没有收益还白占空间。sizeof(std::string)是实现定义的,别写死 24 或 32。
3. 扩容倍率:实现定义,别背结论
string 的扩容倍率不是标准规定的,MSVC 与 libstdc++ 不同,且随版本变化。可靠的做法是实测——打印capacity()观察变化点:
string s; size_t old = s.capacity(); for (size_t i = 0; i < 200; ++i) { s.push_back('x'); if (s.capacity() != old) { cout << s.capacity() << endl; old = s.capacity(); } }扩容的代价是固定的四步:开新空间 → 拷贝旧数据 → 释放旧空间 → 更新指针容量。所以reserve的价值不在"省几次扩容",而在消掉这条链上的拷贝和迭代器失效。
4. COW 的历史:曾经有,现在没了
早期实现用引用计数 + 写时拷贝:拷贝只加计数不复制数据,写的时候才真复制。它和 C++11 的接口契约冲突——s[i]返回可写引用、c_str()要求指针稳定、多线程下计数还得原子化。现在说"string 拷贝是 O(n) 深拷贝"是对的,"string 用写时拷贝"是过时的。
5.size()是字节数,不是字符数
string是字节容器,不是字符容器:string("中文").size() == 6(UTF-8 下一个汉字 3 字节)。所以substr、s[i]、reverse都可能从多字节字符中间切开,得到非法编码。
所以std::string只能按字节处理。真要做字符级处理(按"字"截断、数"字数"),得换按编码分离的兄弟类型:u16string(UTF-16,2 字节/元素)、u32string(UTF-32,4 字节/元素),或用专门的 Unicode 库。
五、易错速答
s.size()不含'\0',而capacity()也不含——缓冲区是capacity()+1字节。别把两者混算。s.c_str() == s2.c_str()比的是地址,不是内容。要比内容用s == s2或compare。c_str()/data()返回的指针会被后续修改失效:任何扩容、insert、+=之后都要重新取。别把它存起来跨操作使用。insert/erase/reserve造成扩容时,所有迭代器、指针、引用全部失效;不扩容时只有修改点之后的失效——和 vector 同一条规律。- 手写 erase/resize 收尾时要在
_str[size]补'\0'。这里最容易的笔误是把字符'0'当成'\0'写进去——两者只差一个反斜杠,编译照过,运行出错。 - 比较是按
unsigned char语义(与 C 的strcmp一致,底层走memcmp)。所以手写operator<时如果用char(x86 上默认有符号)逐字节比,遇到 ≥0x80 的字节会和标准库结果不一致。 size_t的下标别参与减法:s[s.size() - 1]在空串上下溢成巨大值。先判!s.empty()。substr(pos)的pos > size()会抛out_of_range(pos == size()合法,返回空串),而s[pos]越界则是 UB——同一件事两种失败方式,别搞混。