C++ string(reserve , resize , insert , erase)
目录
1. size和length
2. size和capacity
1. size(或length)
2.capacity
3. 总结:
3. reserve
1. reserve 的作用
2. 示例
4. clear
5. shrink_to_fit
6. resize
7. insert
1. 在指定位置插入单个字符
2. 在指定位置插入多个相同字符
3. 在指定位置插入另一个字符串
4. 在指定位置插入另一个字符串的部分内容
5. 在指定位置插入C风格字符串(以 '\0' 结尾的字符数组)
6. 在指定位置插入 C 风格字符串的部分内容
7. 缺点
8. erase
1. 删除指定位置的单个字符
2. 从指定位置删除指定长度的子串
3. 删除迭代器范围 [first, last) 内的字符
4. 底层原理与效率分析
5. 使用注意事项
6. 总结:
9. 总结:
1. size和length
在 C++ 的 string 类中 , size() 和 length() 几乎没有区别 , 它们的功能完全一致。
- 相同点:两者都返回字符串当前实际存储的字符数量(不包含结尾的空字符 \0)。
- 例如:string s = "hello" , s.size() 和 s.length() 的返回值都是 5。
- 细微差异:
- length() 是早期 C++ 为了与 C 语言的字符串处理函数(如 strlen )保持风格一致而引入的。
- size() 是后来为了统一容器(如 vector , list 等)的接口而添加的 , 更符合 C++ 标准容器的命名习惯。
实际使用中 , 两者可以互换 , 选择哪个主要看个人或团队的编码风格。
2. size和capacity
在 C++ 的 string 类中 , size 和 capacity 是两个不同的成员函数 , 用于描述字符串的不同属性 , 核心区别如下:
1. size(或length)
- 含义:返回字符串当前实际存储的字符数量(不包含结尾的空字符 \0)。
- 示例:若 string s = "abc" , 则s.size() 的值为 3 , 因为实际存储了 'a'、'b'、'c' 三个字符。
2.capacity
- 含义:返回字符串当前在内存中分配的存储空间能容纳的最大字符数量(同样不包含结尾的空字符) , 即当前缓冲区的大小。
- 特点:capacity 通常大于或等于 size , 预留的空间是为了避免每次添加字符时都重新分配内存(提升效率)。
- 示例:当 s.size() 为 3 时 , s.capacity() 可能是15(具体值取决于编译器实现) , 表示当前最多能存储 15 个字符而无需重新分配内存。
3. 总结:
- size 反映实际使用的字符数 , capacity 反映当前分配的总容量。
- 当 size 超过 capacity 时 , string 会自动扩容(通常翻倍或按一定策略增加) , 此时 capacity 会变大。
3. reserve
1. reserve 的作用
- reserve 是 string 类的一个成员函数 , 它的主要作用是预先为字符串分配指定大小的存储空间。这样做的目的是为了避免后续添加字符时频繁地进行扩容操作 , 从而提高程序的效率。因为每次扩容都需要重新分配内存 , 复制原有数据 , 是比较耗时的操作。
- reserve 不能进行缩容操作。reserve(n) 的核心作用是确保容器的容量(capacity)至少为 n。
- 如果当前容量小于 n , 则会进行扩容(重新分配更大的内存空间) , 将容量提升到 n 或更大(具体取决于实现)。
- 如果当前容量大于或等于 n , 则不会做任何操作 , 既不会缩小容量 , 也不会改变容器的大小( size )和内容。
- 也就是说 , reserve 是一个“单向操作”——只能保证容量不小于指定值 , 无法主动缩小容量。如果需要缩容 , 需要借助 shrink_to_fit (C++11 及以上) , 它的作用就是尝试将容量缩小到与实际大小( size )匹配。
2. 示例
string s2; // 确定知道需要多少空间,提前开好,避免扩容,提高效率 s2.reserve(100);
- 首先定义了一个空的 string 对象 s2 。
- 然后调用 s2.reserve(100) , 这一步是预先为 s2 分配能容纳 100 个字符的存储空间(不包含结尾的 \0) , 此时 , s2 的 capacity (容量)会至少变成 100 (具体可能因编译器等因素略有差异 , 但会满足至少能存 100 个字符) , 而 size (实际字符数)还是 0 , 因为还没有添加字符。
- 接下来的循环:
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();} }
- 先记录下调用 reserve 后的初始容量 old 并输出。
- 然后循环 100 次 , 每次向 s2 中添加一个字符 'x' 。
- 在循环中 , 判断当前 s2 的容量是否发生变化。由于之前已经用 reserve 预先分配了至少能存 100 个字符的空间 , 所以在添加这 100 个字符的过程中 , s2 不需要再进行扩容 , capacity 不会发生变化 , 所以内部的 if 语句块不会执行(除非编译器的 reserve 实现分配的容量刚好是 100 , 且严格按这个容量来 , 不过通常会预留一定冗余 , 但这里代码逻辑是演示如果提前 reserve 足够空间 , 就不会扩容)。这就体现了 reserve 避免扩容、提高效率的作用。如果没有调用 reserve(100) , 直接循环添加 100 个字符 , s2 会多次扩容 , 每次扩容都要消耗额外的时间和资源。
补充说明:
- reserve 只改变字符串的容量(capacity) , 不改变实际存储的字符数量(size)。
- 当需要向字符串中添加大量字符 , 且能提前预估所需空间时 , 使用 reserve 可以显著提升性能 , 减少内存分配和数据复制的开销。
4. clear
string s1("123456"); cout << s1 << endl; s1.clear(); cout << s1 << endl;
- 首先输出 s1 原本的内容(s1 被初始化为 "123456" , 所以会输出 123456)。
- 然后调用 s1.clear() , clear 函数的作用是清空 s1 中实际存储的字符(即让 s1 的 size 变为0) , 但不会释放 s1 之前分配的容量( capacity 保持不变)。
- 最后再次输出 s1, 此时 s1 已经被清空 , 所以输出空字符串。
5. shrink_to_fit
shrink_to_fit 是 C++ 标准库中 string 类(以及 vector、deque 等容器)的成员函数 , 作用是尝试将容器的容量( capacity )缩小到与实际存储的元素数量( size )相匹配 , 从而释放多余的内存空间。
核心逻辑:
- 容器(如 string )的容量( capacity )是指当前分配的 , 能容纳的最大元素数量;大小( size )是指当前实际存储的元素数量。通常 , 为了避免频繁扩容(扩容需重新分配内存、复制数据 , 开销大) , 容器会预先分配比 size 更大的容量。
- 当容器中的元素被大量删除 , size 远小于 capacity 时 , shrink_to_fit 会尝试重新分配内存 , 让 capacity 尽可能接近 size , 从而回收未被利用的内存。
演示 string 不会自动缩容及 shrink_to_fit 的作用:
string s2; // 确定知道需要多少空间,提前开好,避免扩容,提高效率 s2.reserve(100); 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();} }// 不会缩容,缩容要重新开空间,代价很大 cout << "size:" << s2.size() << endl; cout << "capacity:" << s2.capacity() << endl; //s2.clear(); for (size_t i = 0; i < 50; i++) {s2.pop_back(); } s2.shrink_to_fit(); cout << "size:" << s2.size() << endl; cout << "capacity:" << s2.capacity() << endl;
- 首先输出 s2 的 size (此时 s2 经过前面的循环 , 已经添加了 100 个 'x' , 所以 size 是100)和 capacity (是之前 reserve(100) 后分配的容量 , 比如 100 或更大的值) , 注释说明 string 不会自动缩容 , 因为缩容需要重新分配内存空间 . 代价很大。
- 然后通过循环 , 使用 pop_back 函数弹出 50 个 'x' , 此时 s2 的 size 变为 100 - 50 =50 , 但 capacity 仍然保持之前的大小(因为没有自动缩容)。
- 接着调用 s2.shrink_to_fit() , 这个函数的作用是尝试将 s2 的容量缩小到与实际 size 相匹配的大小(即让 capacity 尽可能接近 size)。
- 最后输出 s2 经过弹出操作和 shrink_to_fit 后的 size (50)和 capacity(此时 capacity 会被调整为接近或等于 50 的值 , 具体取决于编译器 , 但会比之前的容量小很多)。
6. resize
resize 成员函数。resize 主要用于调整字符串的大小( size ) , 并可以指定调整后新增字符的默认值。
作用与行为:
- 调整大小:如果指定的新大小 n 大于当前字符串的 size , string 会在末尾添加足够的字符 , 使 size 变为 n;如果 n 小于当前 size , 则会删除末尾的字符 , 使 size 变为 n。
- 指定默认字符:当需要扩容时 , 可以指定第二个参数(字符类型) , 新增的字符会被初始化为该字符;如果不指定 , 默认新增字符是 '\0'(空字符)。
示例:
string s2("123456");cout << s2 << endl;cout << "size:" << s2.size() << endl;cout << "capacity:" << s2.capacity() << endl;// 插入数据,让size到n个// n > capacity > size;s2.resize(20, 'x');cout << s2 << endl;cout << "size:" << s2.size() << endl;cout << "capacity:" << s2.capacity() << endl;// capacity > n > size;//s2.resize(25, 'x');s2.resize(25); // 插入的是\0s2.push_back('y');cout << s2 << endl;cout << "size:" << s2.size() << endl;cout << "capacity:" << s2.capacity() << endl;// 删除数据// n < size;s2.resize(5);cout << s2 << endl;cout << "size:" << s2.size() << endl;cout << "capacity:" << s2.capacity() << endl;
这段代码主要围绕 string 类的 resize 和 push_back (尾插)操作 , 展示字符串在大小 , 容量方面的变化:
1. 初始字符串与基本属性输出
string s2("123456"); cout << s2 << endl; cout << "size:" << s2.size() << endl; cout << "capacity:" << s2.capacity() << endl;
- 定义 string 对象 s2 并初始化为 "123456"。
- 输出 s2 的内容、size (实际字符数 , 这里是 6)和 capacity (当前分配的容量 , 能容纳的最大字符数 , 通常大于 size , 具体值由编译器决定)。
2. resize 扩容(为尾插做准备 , 或直接改变大小)
s2.resize(20, 'x'); cout << s2 << endl; cout << "size:" << s2.size() << endl; cout << "capacity:" << s2.capacity() << endl;
- resize(20, 'x') :把 s2 的 size 调整为 20。因为原 size (6)小于 20 , 所以会在字符串末尾添加 20 - 6 = 14 个 'x'。
- 此时 s2 内容变为 "123456xxxxxxxxxx" , size 是 20。若原 capacity 小于 20 , string 会扩容(重新分配更大内存) , capacity 会增大;若原 capacity 已大于等于 20 , 则 capacity 不变。
3. resize 与 push_back 结合的尾插
s2.resize(25); // 插入的是\0 s2.push_back('y'); cout << s2 << endl; cout << "size:" << s2.size() << endl; cout << "capacity:" << s2.capacity() << endl;
- s2.resize(25) :把 s2 的 size 调整为 25。原 size (20)小于 25 , 会在末尾添加 25 - 20 = 5 个字符。由于没指定第二个参数 , 默认添加空字符 '\0'。
- s2.push_back('y') :在字符串末尾再插入一个 'y' , 此时 size 变为 25 + 1 = 26。
- 若 26 超过当前 capacity , string 会扩容 , capacity 增大;否则 capacity 不变。
4. resize 缩容(不影响尾插的“潜力” , 因为容量还在)
s2.resize(5); cout << s2 << endl; cout << "size:" << s2.size() << endl; cout << "capacity:" << s2.capacity() << endl;
- s2.resize(5) :把 s2 的 size 调整为 5。原 size (26)大于 5 , 会删除末尾 26 - 5 = 21 个字符 , s2 内容变为 "12345" , size 是 5。
- 注意:resize 缩容时 , 不会改变 capacity (缩容需要重新分配内存 , 代价大 , string 不会自动缩容)。所以后续若要尾插 , 只要新增字符数不超过 capacity - size , 就不需要扩容 , 效率更高。
尾插相关核心总结:
- -push_back 是在字符串末尾插入单个字符 , 每次插入会让 size 加 1。
- 若插入前 size 接近 capacity , push_back 会触发扩容(重新分配更大内存 , 复制原数据 , 开销大);若提前用 resize 或 reserve 预留足够容量 , 能减少扩容次数 , 提升尾插效率。
- resize 可以先调整字符串大小(相当于批量尾插或删尾) , 后续再用 push_back 精细尾插 , 且能通过控制容量优化性能。
7. insert
在 C++ 的 string 类中 , insert 函数用于在字符串的指定位置插入字符或子字符串 , 它有多种重载形式 , 以下是一些常见的用法:
1. 在指定位置插入单个字符
函数原型:
string& insert (size_t pos, char c);
参数说明:
- pos :表示要插入字符的位置 , 从 0 开始计数。如果 pos 大于字符串的长度 , 会抛出 out_of_range 异常。
- c :要插入的字符。
示例:
#include <iostream> #include <string>int main() {std::string str = "hello";str.insert(2, 'w'); // 在索引为 2 的位置(即 'l' 前面)插入字符 'w'std::cout << str << std::endl; // 输出 "hewllo"return 0; }
2. 在指定位置插入多个相同字符
函数原型:
string& insert (size_t pos, size_t n, char c);
参数说明:
- pos :插入置 , 从 0 开始计数。
- n :要插入字符 c 的数量。
- c :要插入的字符。
示例:
#include <iostream> #include <string>int main() {std::string str = "world";str.insert(1, 3, '*'); // 在索引为 1 的位置(即 'o' 前面)插入 3 个 '*'std::cout << str << std::endl; // 输出 "w***orld"return 0; }
3. 在指定位置插入另一个字符串
函数原型:
string& insert (size_t pos, const string& str);
参数说明:
- pos :插入位置 , 从 0 开始计数。
- str :要插入的字符串。
示例:
#include <iostream> #include <string>int main(){std::string str1 = "hello";std::string str2 = " world";str1.insert(5, str2); // 在 str1 索引为 5 的位置(即末尾)插入 str2std::cout << str1 << std::endl; // 输出 "hello world"return 0; }
4. 在指定位置插入另一个字符串的部分内容
函数原型:
string& insert (size_t pos, const string& str, size_t subpos, size_t sublen);
参数说明:
- pos :在目标字符串中要插入的位置 , 从 0 开始计数。
- str :源字符串。
- subpos :在源字符串 str 中开始提取子串的位置 , 从 0 开始计数。
- sublen :要从源字符串 str 中提取的子串长度。
示例:
#include <iostream> #include <string>int main() {std::string str1 = "hello";std::string str2 = " world, nice to meet you";// 在 str1 索引为 5 的位置(即末尾)插入 str2 中从索引 6 开始,长度为 4 的子串str1.insert(5, str2, 6, 4); std::cout << str1 << std::endl; // 输出 "hello nice"return 0; }
5. 在指定位置插入C风格字符串(以 '\0' 结尾的字符数组)
函数原型:
string& insert (size_t pos, const char* s);
参数说明:
- pos :插入位置 , 从 0 开始计数。
- s :指向 C 风格字符串的指针。
示例:
#include <iostream> #include <string>int main() {std::string str = "hello";const char* cstr = " world";str.insert(5, cstr); // 在 str 索引为 5 的位置(即末尾)插入 cstr 指向的 C 风格字符串std::cout << str << std::endl; // 输出 "hello world"return 0; }
6. 在指定位置插入 C 风格字符串的部分内容
函数原型:
string& insert (size_t pos, const char* s, size_t n);
参数说明:
- pos :插入位置 , 从 0 开始计数。
- s :指向 C 风格字符串的指针。
- n :要从 C 风格字符串 s 中插入的字符数量。
示例:
#include <iostream> #include <string>int main(){std::string str = "hello";const char* cstr = " world, nice to meet you";str.insert(5, cstr, 6); // 在 str 索引为 5 的位置(即末尾)插入 cstr 前 6 个字符std::cout << str << std::endl; // 输出 "hello world"return 0; }
insert 函数在处理字符串拼接 , 修改等操作时非常灵活 , 合理使用可以高效地对字符串进行各种编辑。
7. 缺点
- 底层数据挪动导致效率问题:
- C++ 中 string 类的 insert 函数在执行插入操作时 , 如果插入位置之后还有数据 , 那么从插入点开始 , 后面的所有字符都需要向后移动 , 为新插入的字符或子字符串腾出空间。而且当插入导致字符串长度超过当前已分配的容量(capacity)时 , 还会触发内存重新分配 , 要把原字符串数据复制到新的内存空间 , 这进一步加大了开销 , 所以很容易导致效率低下。
- 时间复杂度 : 假设要在长度为 n 的字符串中插入长度为 m 的子字符串 , 通常情况下:
- 普通插入且不涉及扩容:当不发生容量重新分配时 , 在字符串中间插入数据 , 需要将插入位置之后的字符依次向后挪动 , 时间复杂度为 O(n) 。因为最坏情况下 , 需要移动插入位置之后的所有字符 , 移动次数与原字符串长度相关 。
- 插入导致扩容:如果插入操作导致了内存重新分配 , 除了移动原有字符 , 还涉及新内存的分配和旧数据的整体复制 , 时间复杂度会达到 O(n + m) , 这里 n 是原字符串长度 , m 是要插入的子字符串长度。
- 不过 , 在一些特殊场景 , 比如在字符串末尾插入(不涉及数据挪动 , 仅增加长度) , 时间复杂度可能会是 O(m) , 但这属于比较理想的情况 , 总体来说 , insert 函数在常规插入操作下时间复杂度较高 , 存在效率瓶颈。
8. erase
在 C++ 中 , erase 是 string 类(以及容器如 vector , list 等)提供的成员函数 , 用于删除字符串中的字符或子串:
erase 的常见用法 : string 的 erase 主要有三种重载形式 , 适用于不同的删除需求:1. 删除指定位置的单个字符
函数原型:(C++11 前返回 void , C++11 后返回指向删除位置下一个字符的迭代器)
iterator erase(iterator pos);
功能:删除迭代器 pos 指向的字符。
示例:#include <iostream> #include <string> using namespace std;int main() {string str = "hello";// 删除索引为 1 的字符('e')str.erase(str.begin() + 1); cout << str << endl; // 输出 "hllo"return 0; }
2. 从指定位置删除指定长度的子串
函数原型:
string& erase(size_t pos = 0, size_t count = npos);
- pos 是起始位置
- count 是要删除的字符数;
- 若 count 超过剩余字符数 , 则删除到字符串末尾
功能:从索引 pos 开始 , 删除 count 个字符。
示例:string str = "abcdefg"; str.erase(2, 3); // 从索引 2 开始删除 3 个字符('c','d','e') cout << str << endl; // 输出 "abfg"
3. 删除迭代器范围 [first, last) 内的字符
函数原型:
iterator erase(iterator first, iterator last);
- 返回指向 last 位置的迭代器
功能:删除 [first, last) 范围内的所有字符(包含 first , 不包含 last)。
示例:string str = "123456"; // 删除从第 2 个字符到第 5 个字符(即 '2','3','4') str.erase(str.begin() + 1, str.begin() + 4); cout << str << endl; // 输出 "156"
4. 底层原理与效率分析
- 底层数据挪动:
- 与 insert 类似 , erase 也会导致删除位置后的字符向前挪动(覆盖被删除的部分) , 以保证字符串的连续性。例如 , 删除字符串中间的字符后 , 其右侧的所有字符都需要向左移动 , 填补空缺。
- 时间复杂度:假设字符串长度为 n , 删除 k 个字符:
- 若删除位置在中间 , 需要挪动 n - pos - k 个字符 , 时间复杂度为 O(n)(与字符串长度成正比)。
- 若删除末尾字符(如 erase(str.size() - 1, 1)) , 无需挪动数据 , 时间复杂度为 O(1)(仅修改长度)。
- 内存分配:
- erase 不会主动释放字符串的容量( capacity ) , 只会减少长度( size )。若需释放多余内存 , 可结合 shrink_to_fit()(C++11 后)使用。
5. 使用注意事项
1. 边界检查:pos不能超过字符串的长度( size()) , 否则会触发未定义行为(通常是程序崩溃)。建议使用前判断:
if (pos < str.size()) {str.erase(pos, count); }
2. 迭代器失效:
执行 erase 后 , 指向被删除字符及之后的迭代器会失效 , 需通过返回值获取新的有效迭代器:auto it = str.begin(); while (it != str.end()) {if (*it == 'a') {it = str.erase(it); // 用返回值更新迭代器} else {++it;} }
3. 与其他函数配合:
常与 find 结合 , 删除指定子串。例如 , 删除字符串中第一个出现的 "ab":size_t pos = str.find("ab"); if (pos != string::npos) {str.erase(pos, 2); // "ab" 长度为 2 }
6. 总结:
erase 是 C++ 中处理字符串删除操作的核心函数 , 支持按位置 , 长度或迭代器范围删除 , 但其效率受底层数据挪动影响(中间删除为 O(n) , 末尾删除为 O(1))。使用时需注意迭代器失效和边界检查 , 常与 find 配合处理复杂删除需求。
9. 总结:
本文详细介绍了C++中string类的常用操作及其底层原理。主要内容包括:size()和length()的功能一致性;size与capacity的区别,前者表示实际字符数,后者代表内存容量;reserve()用于预分配空间提升效率;clear()清空内容但不释放内存;shrink_to_fit()释放多余内存;resize()调整字符串大小;insert()的各种插入操作及其效率问题;erase()的多种删除方式及其时间复杂度。这些操作涉及字符串的内存管理 , 扩容机制和性能优化 , 是高效使用string类的基础。
感谢大家的观看!