前言
std::string 是 C++ 最常用的字符串容器,很多开发者只会简单调用 push_back、substr,却不了解其容量扩容、迭代器分类、内存伸缩、性能优化底层逻辑;同时字符串大数运算也是面试高频算法题。本文结合图示拆解 string 底层特性,并对比两种大数相加写法的性能差距。
一、string 迭代器体系:正向、反向、const 区分
1. 正向迭代器 begin /end
string s = "123456";
auto it = s.begin(); // 指向首字符'1'
while(it != s.end()) // end指向末尾'\0',不可解引用
{
cout << *it;
++it;
}
内存示意图: [1,2,3,4,5,6,\0] begin → 首元素地址,end → 末尾占位符,区间 [begin, end) 左闭右开。
2. const_iterator 常量迭代器
-
iterator:可修改迭代器指向的数据*it = 'x'; -
const_iterator:仅可读,禁止修改容器内字符,常用于const string&参数场景。
3. 反向迭代器 rbegin /rend
反向迭代器用于逆序遍历字符串,逻辑和正向完全相反:
string s1 = "123456";
string::reverse_iterator it3 = s1.rbegin();
while (it3 != s1.rend())
{
cout << *it3 << " ";
++it3;
}
// 输出:6 5 4 3 2 1
内存逻辑:
-
rbegin():指向最后一个有效字符6; -
rend():指向第一个字符前的占位; -
自增
++it等价于正向迭代器--it,实现倒序遍历。
二、string 容量机制:size、capacity、扩容规则
两个核心成员变量:
-
_size:当前有效字符长度,s.size()获取; -
_capacity:底层数组总容量,包含预留空闲空间,s.capacity()获取; 扩容本质:底层是动态字符数组,空间耗尽时开辟新数组、拷贝旧数据、释放旧内存,开销极大。
1. 不同编译器扩容策略对比
① VS2019(MSVC)
初始空串容量 15;首次填满后,后续按 1.5 倍 扩容:
15 → 22 → 33 → 49 ...
测试代码:
string s2;
size_t old = s2.capacity();
cout << "capacity:" << old << endl;
for (size_t i = 0; i < 100; i++)
{
s2.push_back('x');
if (s2.capacity() != old)
{
cout << "capacity:" << s2.capacity() << endl;
old = s2.capacity();
}
}
② g++(GCC Linux)
初始容量 15,扩容采用 2 倍扩容:
15 → 30 → 60 → 120 ...
2. 缩容:shrink_to_fit
string 默认不会自动缩容,pop_back、clear 只会修改 _size,底层 capacity 不变; 若需要释放空闲内存,手动调用 shrink_to_fit(),会重新开辟等于 size 的数组拷贝数据,代价高:
// 先填满100个字符 for (size_t i = 0; i < 100; i++) s2.push_back('x'); // 删除50个字符,size=50,capacity仍为120 for (size_t i = 0; i < 50; i++) s2.pop_back(); // 手动缩容,capacity变为50 s2.shrink_to_fit();
设计取舍:以空间换时间,频繁增删字符串时,保留冗余容量避免反复扩容拷贝;只有确定不再扩容时才缩容。
3. reserve 预分配空间(性能优化核心)
已知字符串最大长度时,提前用 reserve(n) 开辟足够容量,全程不会触发扩容,大幅提升效率:
// 先填满100个字符
for (size_t i = 0; i < 100; i++) s2.push_back('x');
// 删除50个字符,size=50,capacity仍为120
for (size_t i = 0; i < 50; i++) s2.pop_back();
// 手动缩容,capacity变为50
s2.shrink_to_fit();
面试考点:处理长字符串拼接、大数运算时,优先 reserve 预分配,避免 insert 头插带来的 O (N²) 时间复杂度。
4. resize 修改有效长度
resize(n, c='\0') 两种行为:
-
n < size:截断字符串,保留前 n 个字符,减小_size,capacity不变; -
n > size:尾部填充字符c,增大有效长度,空间不足则触发扩容。
三、高频算法:415. 字符串相加(LeetCode)
题目要求
输入两个字符串格式非负整数 num1="456"、num2="77",输出和字符串 "533"; 禁止转 long long/int(数值过长会溢出),只能逐位模拟竖式加法。
思路:竖式加法
-
双指针分别指向两个字符串末尾(个位);
-
每次取对应位数字相加,加上进位
carry; -
计算当前位值、新进位;
-
处理剩余未遍历字符;
-
最后处理最高位进位。
写法 1:头插 insert(低效 O (N²))
class Solution {
public:
string addStrings(string num1, string num2) {
string str;
int end1 = num1.size()-1, end2 = num2.size()-1;
int carry = 0;
while(end1 >= 0 || end2 >= 0)
{
int x1 = end1 >= 0 ? num1[end1--] - '0' : 0;
int x2 = end2 >= 0 ? num2[end2--] - '0' : 0;
int val = x1 + x2 + carry;
carry = val / 10;
val = val % 10;
// 头插:每次在字符串最前面插入字符
str.insert(str.begin(), ('0' + val));
}
if(carry == 1)
str.insert(str.begin(), '1');
return str;
}
};
性能缺陷: string::insert(begin(), ch) 会让已有字符全部向后挪动一位,长度为 N 时总操作次数 1+2+3+...+N = N(N+1)/2,时间复杂度 O(N²),超长数字会超时。
写法 2:尾插 + 反转(高效 O (N),工程推荐)
class Solution {
public:
string addStrings(string num1, string num2) {
string str;
// 预分配最大所需容量,避免扩容
str.reserve(max(num1.size(), num2.size()) + 1);
int end1 = num1.size()-1, end2 = num2.size()-1;
int carry = 0;
while(end1 >= 0 || end2 >= 0)
{
int x1 = end1 >= 0 ? num1[end1--] - '0' : 0;
int x2 = end2 >= 0 ? num2[end2--] - '0' : 0;
int val = x1 + x2 + carry;
carry = val / 10;
val = val % 10;
// 尾插,仅追加,无数据挪动 O(1)均摊
str += ('0' + val);
}
if(carry == 1)
str += '1';
// 反转字符串得到正确顺序
reverse(str.begin(), str.end());
return str;
}
};
优化点拆解:
-
reserve预分配空间,全程无扩容拷贝; -
push_back/+=尾插仅追加字符,均摊 O (1); -
仅最后一次全局反转,总时间复杂度 O(N);
-
工业代码标准写法,处理超大数字无性能瓶颈。
四、总结开发最佳实践
-
迭代器:只读场景优先
const_iterator,逆序遍历用rbegin/rend; -
容量优化:已知字符串最大长度必须调用
reserve,杜绝频繁扩容; -
增删性能:避免
begin()位置insert/erase,优先尾部操作,需要逆序结果先尾插再reverse; -
内存伸缩:
pop_back/clear不会释放容量,大量空闲空间不再使用时调用shrink_to_fit; -
算法选型:字符串大数、长文本拼接场景,一律采用「尾插 + 反转」代替头插,规避 O (N²) 性能陷阱。

2476

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



