大纲
容器
string 容器
string 的概念
string 是 STL 的字符串类型,通常用来表示字符串。而在使用 string 之前,字符串通常是用 char* 表示的。string 与 char* 都可以用来表示字符串,两者的区别如下:
string 是一个类,char* 是一个指向字符的指针string 封装了 char* 来管理字符串,本质是一个 char* 类型的容器string 不用考虑内存释放和越界的问题string 负责管理 char* 所分配的内存。每一次 string 的复制,取值都由 string 类负责维护,不用担心复制越界和取值越界等问题string 提供了一系列的字符串操作函数,例如:查找(find)、拷贝(copy)、删除(erase)、替换(replace)、插入(insert)
string 的构造函数
- 默认构造函数:
string(); - 带参数的构造函数:
string(const char *s);,用字符串 s 初始化string(int n, char c);,用 n 个字符 c 初始化
- 拷贝构造函数:
string(const string &str);
字符串头文件说明
- 在标准 C++ 中,使用
string 必须显式包含 <string> 头文件。任何在未包含 <string> 的情况下仍然能够使用 string 的代码,都依赖于标准库实现的间接包含行为,属于非标准、不可移植的用法。 - 一些标准库实现(例如 libstdc++)在
<iostream> 等头文件的内部实现中,可能间接包含了 <string>,从而使代码在特定编译器和版本下 "看起来可以正常工作"。然而,这种行为是实现细节,不受 C++ 标准保证,随着编译器、标准库版本或编译选项(如不同的 -std= 标准级别)变化,代码都有可能无法通过编译。
string 的初始化方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| char str1[100] = "C++";
char str2[100] = {'C', '+', '+', '\0'};
char str3[] = "C++";
char str4[] = {'C', '+', '+', '\0'};
char str5[100] = {'C', '+', '+'};
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| string s1;
string s2 = "I Love C++";
string s3("I Love C++");
string s4{"I Love C++"};
string s5 = s2;
string s6(5, 'a');
string s7(s2, 2);
string s8(s2, 2, 4);
char buf[] = {'a', 'b', 'c', '\0'}; string s9(buf);
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| const char* p1 = "Modern C++"; string s1 = p1;
string s2(p1);
char buf[] = {'a', 'b', 'c', '\0'}; string s3(buf);
char buf2[] = {'a', 'b', 'c', '\0'}; char* p2 = buf2; string s4 = p2;
char buf3[] = {'A', 'B', 'C', 'D', 'E'}; string s5(buf3, 3);
char buf4[] = "Hello C++ World"; string s6(buf4 + 6, 3);
char buf5[] = {'A', 'B', 'C', 'D', 'E', 'F', 'G', '\0'}; string s7(buf5 + 3);
|
通过 char[] 或者 char * 初始化 string 的写法 | 传入的真实类型 | 是否依赖 '\0' | 说明 |
|---|
string(p) | const char* | 依赖 | C 风格字符串,必须包含 '\0',遇 '\0' 结束拷贝 |
string(p, n) | const char* + size_t | 不依赖 | 精确拷贝 n 个字节,p 可以没有 '\0' |
string(arr) | const char*(arr 衰变) | 依赖 | arr 必须包含 '\0',遇 '\0' 结束拷贝 |
string(arr, n) | const char* + size_t | 不依赖 | 精确拷贝 n 个字节,arr 可以没有 '\0' |
char 与 string 之间的转换
- 在 C++ 中,
char[] 与 char* 可以隐式转换为 string,而 string 不能自动转换为 char[] 或者 char*,只能通过 string::c_str() 显式获取 const char *。 string 的构造函数并不区分 char* 和 char[],因为字符数组在参数传递时会退化为字符指针;是否依赖 '\0',只取决于开发者调用的是 string(const char*) 还是 string(const char*, size_t)。
从 string 取得 char*
从 string 取得 char*,可以使用:
const char *c_str() const;,返回一个以 \0 结尾的字符串的首地址
特别注意
- 在 C++ 中,
char[] 与 char * 可以隐式转换为 string 类型,反过来则不可以,例如右边这种写法是合法的: char *p = "abc"; string str = p;
将 string 拷贝到 char*
将 string 拷贝到 char* 指向的内存空间,可以使用:
int copy(char *s, int n, int pos=0) const;
将当前串中以 pos 位置开始的 n 个字符拷贝到以 s 为起始位置的字符数组中,返回实际拷贝的字符数量。特别注意,要保证指针 s 所指向的内存空间足以容纳当前的字符串,不然可能会发生越界。
从 string 与范围 for 使用
1 2 3 4 5 6 7 8 9 10 11 12 13
| string s10 = "hello c++";
for (const char& c : s10) { cout << c; } cout << endl;
for (char& c : s10) { c = toupper(c); cout << c; } cout << endl;
|
程序运行输出的结果如下:
vector 容器
vector 的概念
vector 的数据存储以及操作方式,与 Array 非常相似,两者的唯一差别在于空间运用的灵活性。Array 是静态空间,一旦配置了就不能改变,要换大一点或者小一点的空间,可以,一切琐碎的细节得由自己来实现;首先配置一块新的空间,然后将旧空间的数据搬往新空间,再释放原来的空间。Vector 是动态空间,随着元素的加入,它的内部机制会自动扩充空间以容纳新元素。因此 vector 的运用对于内存的合理利用与运用的灵活性有很大的帮助,我们再也不必害怕空间不足而一开始就初始化一个大的 Array 了。Vector 的实现技术,关键在于其对大小的控制以及重新配置时的数据移动效率,一旦 vector 旧空间满了,如果客户每新增一个元素 vector 内部只是扩充一个元素的空间,实为不智,因为所谓的扩充空间(不论多大),一如刚才所说,是 “配置新空间 - 数据移动 - 释放旧空间” 的大工程,时间成本很高,应该加入某种未雨绸缪的考虑。
总结
- vector 是将元素置于一个动态数组中加以管理的容器。
- vector 可以随机存取元素(支持使用索引值直接存取, 用
[] 操作符或 at() 函数。 - vector 尾部添加或移除元素非常快速,但是在头部或中部插入元素或移除元素比较耗时。
vector 的结构
![]()
vector 底层所采用的数据结构是线性连续空间(单向开口的连续内存空间),可以理解为支持动态开辟内存空间的数组(单向开口);它以两个迭代器(_Myfirst 和 _Mylast)分别指向配置得来的连续空间中目前已被使用的范围,并以迭代器 _Myend 指向整块连续内存空间的尾端。vector 往尾部添加或移除元素的效率非常高,但是往头部或者中部插入元素或移除元素则比较耗时。为了降低空间配置时的速度成本,vector 实际配置的大小可能比客户端需求大一些,以应付将来可能的扩充,这里是容量的概念。换句话说,一个 vector 的容量永远大于或等于其大小,一旦容量等于大小,便是满载,下次再需要新增元素时,整个 vector 容器就得另觅居所(即扩容)。值得一提的是,所谓动态增加大小,并不是在原空间之后续接新空间(因为无法保证原空间之后尚有可配置的连续空间),而是申请一块更大的内存空间,然后将原数据拷贝到新空间,并释放原空间。因此,对 vector 的任何操作,一旦引起空间的重新配置,指向原 vector 的所有迭代器就都失效了,这是程序最容易出错的地方,务必小心。特别注意,vector 一旦需要执行扩容操作,那么每次都会以原来空间大小的 2 倍进行扩容。
vector 的初始化方式
1 2 3 4 5 6 7 8
| vector<int> v1 = {1, 2, 3};
vector<int> v2{1, 2, 3};
vector<int> v{};
|
1 2 3 4 5 6 7
| vector<int> v1 = {1, 2, 3};
vector<int> v2(v1);
vector<int> v3 = v1;
|
1 2 3 4
| vector<int> v1 = {1, 2, 3};
vector<int> v2(move(v1));
|
1 2
| int arr[] = {1, 2, 3, 4}; vector<int> v(arr, arr + 4);
|
1 2
| list<int> lst = {1, 2, 3}; vector<int> v(lst.begin(), lst.end());
|
1 2
| int arr[] = {1, 2, 3}; vector<int> v(begin(arr), end(arr));
|
1
| auto v = vector{1, 2, 3};
|
vector 容易混淆的初始化写法
- vector 初始化时,使用括号与花括号,语义完全不同,比如:
vector<int> v(3, 2); 使用圆括号 () 初始化,调用的是 vector(size_type count, const T& value) 构造函数,表示创建一个包含 3 个元素 的 vector,并将每个元素都初始化为 2,因此最终结果为 [2, 2, 2]。vector<int> v{3, 2}; 使用花括号 {} 初始化,优先匹配 vector(initializer_list<T>) 构造函数,表示直接使用初始化列表中的元素进行构造,初始化列表中包含两个元素 3 和 2,因此最终结果为 [3, 2]。
vector 与范围 for 使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| vector<string> v1 = {"a", "b", "c"};
for (const string& item : v1) { cout << item << " "; } cout << endl;
for (string& item : v1) { item = item + "1"; cout << item << " "; } cout << endl;
|
程序运行输出的结果如下:
特别注意
- 在 C++ 中使用范围
for 遍历 vector 时,不应在遍历过程中对容器执行插入或删除元素的操作,否则会导致未定义行为,可能表现为程序崩溃、数据异常或难以复现的内存错误。 - 这是因为范围
for 遍历 vector 本质上依赖迭代器,而 vector 在插入或删除元素时可能发生内存重分配(比如扩容)或元素整体移动,导致迭代器失效;遍历过程中继续使用失效的迭代器会产生未定义行为,从而引发崩溃或各种不可预期的内存问题。 - 不只是范围
for,任何使用迭代器遍历 vector 的场景,在对 vector 进行结构性修改(插入 / 删除)时都需要格外小心。
vector 与迭代器的使用
迭代器的概述
vector 提供了多种迭代器类型,用于以不同方式遍历和访问容器元素。其迭代器属于随机访问迭代器(Random Access Iterator),支持 ++、--、+n、-n、[] 下标访问等操作,效率与指针相当。
迭代器的使用注意事项
vector 的迭代器,本质上类似指针,内部通常就是原始指针或指针封装。- 往
vector 插入、删除元素(尤其是触发扩容时)会导致迭代器失效。 - 范围
for、STL 算法(如 std::find()、std::sort())底层都依赖迭代器来实现。 - 使用范围
for 遍历 vector 时,不应在遍历过程中对容器执行插入或删除元素的操作,否则会导致未定义行为,从而引发崩溃或各种不可预期的内存问题。
迭代器的类型
vector 容器的迭代器有四种类型:
iterator:普通正向迭代器const_iterator:只读正向迭代器reverse_iterator:普通反向迭代器(逆序)const_reverse_iterator:只读反向迭代器(逆序)
![]()
![]()
迭代器的操作
vector 的迭代器属于随机访问迭代器(Random Access Iterator),支持 ++、--、+n、-n、[] 下标访问等操作,效率与指针相当。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| vector<string> v1 = {"a", "b", "c", "d", "e"}; vector<string>::iterator it = v1.begin();
cout << *it << endl;
cout << it[1] << endl;
++it; cout << *it << endl;
--it; cout << *it << endl;
|
程序运行输出的结果如下:
迭代器的失效
迭代器失效是指:在使用容器迭代器的过程中,由于对容器进行了结构性修改(如调用容器的 erase()、insert()、push_back()、resize()、clear() 等成员函数),导致原本指向容器元素的迭代器不再指向有效元素或合法位置,此时继续使用该迭代器会产生未定义行为。因此,修改容器后必须确认迭代器是否仍然有效,必要时重新获取或使用返回的新迭代器。
- 错误示例一:范围
for 中删除 vector 的元素
1 2 3 4 5 6
| for (int x : v) { if (x == 3) { v.erase(v.begin()); } }
|
- 错误示例二:遍历 vector 时删除元素(迭代器失效)
1 2 3 4 5 6 7 8
| vector<int> v1 = {1, 2, 3, 4, 5};
for (vector<int>::iterator it = v1.begin(); it != v1.end(); ++it) { if (*it % 2 == 0) { v1.erase(it); } }
|
- 错误示例三:遍历 vector 时插入元素(迭代器失效)
1 2 3 4 5 6 7 8
| vector<int> v = {1, 2, 3};
for (auto it = v.begin(); it != v.end(); ++it) { if (*it == 2) { v.insert(it, 100); } }
|
- 正确示例一:使用
erase() 的返回值继续遍历
1 2 3 4 5 6 7 8 9 10
| vector<int> v1 = {1, 2, 3, 4, 5};
for (vector<int>::iterator it = v1.begin(); it != v1.end();) { if (*it % 2 == 0) { it = v1.erase(it); } else { ++it; } }
|
- 正确示例二:使用
erase() + remove_if(),避免遍历时删除
1 2 3 4 5 6 7 8 9
| vector<int> v1 = {1, 2, 3, 4, 5};
v1.erase( remove_if(v1.begin(), v1.end(), [](int x) { return x % 2 == 0; }), v1.end() );
|
- 正确示例三:使用
insert() 的返回值继续遍历
1 2 3 4 5 6 7 8 9 10 11
| vector<int> v1 = {1, 2, 3};
for (auto it = v1.begin(); it != v1.end();) { if (*it == 2) { it = v1.insert(it, 100); it += 2; } else { ++it; } }
|
1 2 3 4 5 6 7 8 9 10 11
| vector<int> v = {1, 2, 3}; vector<int> result;
for (int x : v) { if (x == 2) { result.push_back(100); } result.push_back(x); }
v.swap(result);
|
总结
- C++ 中迭代器失效是指在容器发生结构性修改后,原有迭代器不再指向有效元素,继续使用失效的迭代器会导致未定义行为,正确做法是使用
erase()、insert() 的返回值或避免在遍历过程中修改(插入 / 删除)容器。 - 由于范围
for 遍历 vector 本质上依赖迭代器,因此使用范围 for 遍历 vector 时,不应在遍历过程中对容器执行插入或删除元素的操作,否则会导致未定义行为,从而引发崩溃或各种不可预期的内存问题。
| 容器类型 | 内存结构 | insert() 导致的迭代器失效 | erase() 导致的迭代器失效 | 推荐写法 | 说明 |
|---|
| vector | 连续内存 | ⚠️ 发生扩容时:所有迭代器、指针、引用全部失效 ⚠️ 未扩容时:插入点及其后的迭代器失效 | ⚠️ 被删位置及其后的迭代器全部失效 | it = v.erase(it)
erase() + remove_if() | 连续内存,插入 / 删除需要移动元素,是迭代器最容易失效的容器。 |
| deque | 分段连续 | ⚠️ 可能导致所有迭代器失效 ⚠️ C++ 标准未保证稳定性,尤其是中间插入 | ⚠️ 可能导致所有迭代器失效 | 尽量只在头尾两端操作 | 分段连续结构,迭代器稳定性差,不要依赖 deque 的迭代器长期有效。 |
| list | 双向链表 | ✅ 插入不使任何已有迭代器失效 | ✅ 仅被删除元素的迭代器失效 | list::insert()
list::remove_if() | 节点独立,插入删除只改指针,迭代器最稳定。 |
| map / set | 红黑树 | ✅ 插入不使任何已有迭代器失效 | ✅ 仅被删除元素的迭代器失效 | it = m.erase(it) | 树结构保持节点稳定,删除时必须接收返回迭代器继续遍历。 |