☰
[C++] std::string:接口、算法技巧与权衡 (速查)
2026/10/25 1:54:52 网站建设 项目流程

定位:

本质是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()/ 范围 forc_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()体系。

三、算法常用技巧

  1. 拼接用+=,不要用+:s += "abc"原地追加;s = s + "abc"会先造临时 string 再拷贝回来,白付一次 O(n)。
  2. 预分配:循环拼接前s.reserve(总长),避免多次扩容加拷贝。
  3. reserve/reverse别混:reserve是留空间,reverse是std::reverse(s.begin(), s.end())(反转),后者是算法不是成员。
  4. string 的迭代器是随机访问迭代器,所以sort、reverse、binary_search都能直接作用在字符上:std::sort(s.begin(), s.end())。
  5. 读整行用getline(cin, s);cin >> s遇空格就断。混用时先cin.ignore(numeric_limits<streamsize>::max(), '\n')吃掉残留换行,否则getline会读到空行。
  6. 判定查找结果写!= string::npos。npos就是(size_t)-1,所以!= -1数值上等价也能编过——但用npos才表达意图。
  7. substr会拷贝。只读切片用std::string_view(C++17)零拷贝;注意它不拥有数据,原串一改/一销毁它就悬空。
  8. stoi的三种结局:正常转换、非法输入抛std::invalid_argument、溢出抛std::out_of_range,所以循环里读数字要 try-catch。它不检查尾部:stoi("123abc")得 123。
  9. to_string(浮点)只给 6 位有效数字:to_string(3.1415926)→"3.141593"。要精度自己格式化。另外float与double的to_string结果可能不同,因为二者存储精度本就不同。
  10. 按分隔符切串:istringstream iss(s); while (getline(iss, token, ',')) {...}——用getline的第三个参数指定分隔符。
  11. 前缀/后缀:C++20 有s.starts_with(p)/ends_with(p);更早用s.compare(0, p.size(), p) == 0。
  12. 传参一律const string&,返回靠移动/NRVO。
  13. 彻底释放内存:string().swap(s);clear()只清内容不还内存。shrink_to_fit()是非强制请求。
  14. tolower/toupper先转unsigned char:std::tolower(static_cast<unsigned char>(c)),直接传负char是 UB。
  15. 字符映射常客:s[i] - 'a'当 0~25 的下标,用于计数排序、哈希、状态压缩。

四、权衡利弊

1. string vs vector<char>

stringvector<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 库。

五、易错速答

  1. s.size()不含'\0',而capacity()也不含——缓冲区是capacity()+1字节。别把两者混算。
  2. s.c_str() == s2.c_str()比的是地址,不是内容。要比内容用s == s2或compare。
  3. c_str()/data()返回的指针会被后续修改失效:任何扩容、insert、+=之后都要重新取。别把它存起来跨操作使用。
  4. insert/erase/reserve造成扩容时,所有迭代器、指针、引用全部失效;不扩容时只有修改点之后的失效——和 vector 同一条规律。
  5. 手写 erase/resize 收尾时要在_str[size]补'\0'。这里最容易的笔误是把字符'0'当成'\0'写进去——两者只差一个反斜杠,编译照过,运行出错。
  6. 比较是按unsigned char语义(与 C 的strcmp一致,底层走memcmp)。所以手写operator<时如果用char(x86 上默认有符号)逐字节比,遇到 ≥0x80 的字节会和标准库结果不一致。
  7. size_t的下标别参与减法:s[s.size() - 1]在空串上下溢成巨大值。先判!s.empty()。
  8. substr(pos)的pos > size()会抛out_of_range(pos == size()合法,返回空串),而s[pos]越界则是 UB——同一件事两种失败方式,别搞混。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询