开篇
如果用过C语言,就会知道C语言里处理字符串要手动管内存、strcpy容易越界;C++的string帮你把这些脏活累活都干了。vector更是日常开发用得最多的容器,没有之一。
这一篇讲《C++ Primer》第3章的核心:string的常用操作、vector的动态增长、数组与指针的关系、迭代器的正确使用。
一、标准库string
string表示可变长的字符序列,使用string类型必须首先包含string头文件
#include <string>初始化方式:
常用操作:
避坑1:size()返回无符号数。如果for (int i = 0; i <= s.size() - 1; i++),当s为空时,s.size() - 1是无符号下溢,变成一个巨大的数,循环失控。正确的做法是先判断empty(),或者直接使用i < s.size(),或者使用i + 1 < s.size()(如果确实需要遍历到倒数第二个元素),或者用std::ssize得到有符号大小。
避坑2:字符串字面值和string相加时,“+”号两边至少有一个是string。"hello" + "world"是错误的(两个const char*不能相加),但s1 + "world"是对的。
处理string对象中的字符(下图为cctype头文件中的主要函数):
二、vector动态数组
vector是标准库最常用的容器,定义在<vector>中。它是一个能自动增长的动态数组。
初始化:
常用操作:
v[n]:下标访问(不检查越界)
v.at(n):下标访问(越界抛异常)
v.pop_back():删除尾部元素
vector扩容机制(面试高频):
vector在底层是连续内存。当size达到capacity时,再push_back会触发扩容:
1. 分配一块更大的内存(通常是原来的1.5倍或2倍,因编译器而异);
2. 把旧元素拷贝/移动到新内存;
3. 销毁旧元素,释放旧内存。
关键后果:扩容后,所有指向旧内存的迭代器、指针、引用全部失效!
这就是为什么在循环中push_back要特别小心:
// 危险写法 for (auto it = v.begin(); it != v.end(); ++it) { v.push_back(*it); // 可能触发扩容,it失效! }性能优化:如果知道大概需要多少元素,用v.reserve(n)预先分配容量,避免多次扩容拷贝。
三、迭代器
迭代器是STL的核心概念,是容器和算法之间的桥梁。所有标准库容器都支持迭代器。
auto it = v.begin(); // 指向第一个元素 auto end = v.end(); // 指向最后一个元素的下一个位置(尾后迭代器) *it; // 解引用,获取元素 ++it; // 移动到下一个元素标准容器迭代器运算符:
迭代器类型:
1. begin/end:普通迭代器,可读写;
2. cbegin/cend:const迭代器,只读;
3. rbegin/rend:反向迭代器,从尾向头遍历。
迭代器失效场景总结:
操作 | 哪些迭代器失效 |
vector插入导致扩容 | 全部失效 |
vector中间插入 | 插入点之后全部失效 |
vector删除元素 | 删除点之后全部失效 |
list插入/删除 | 仅被删元素的迭代器失效 |
vector和string迭代器支持的运算:
四、数组与指针
C++保留了C风格数组,但《C++ Primer》明确建议:优先使用vector和string,数组只在性能极端敏感或与C接口交互时使用。
数组的特点:
1. 大小固定,编译时确定;
2. 数组名在大多数表达式中会退化为指向首元素的指针;
3. 数组不能拷贝或赋值;
4. 数组越界是未定义行为,编译器不检查;
5. 默认情况下,数组的元素被默认初始化;
6. 不存在引用的数组
7. 当使用字符串字面值对数组进行初始化时,一定要注意字符串字面值的结尾处还有一个空字符'\0'。
unsigned cnt = 42; // 不是常量表达式 constexpr unsigned sz = 42; // 常量表达式 int arr[10]; // 含有10个整数的数组 int *parr[sz]; // 含有42个整型指针的数组 int &refs[10] = /*?*/; // 错误,不存在引用的数组 int (*parray)[10] = &arr; // parray指向一个含有10个整数的数组 int (&arrRef)[10] = arr; // arrRef引用一个含有10个整数的数组 string bad[cnt]; // 错误:cnt不是常量表达式 string strs[get_size()]; // 当get_size是constexpr时正确;否则错误 int arr[5] = {1, 2, 3, 4, 5}; int *p = arr; // arr退化为&arr[0] *(p + 2); // 等价于arr[2],值为3指针和数组的关系:下标操作本质上是指针运算。arr[i]等价于*(arr + i)。但指针是独立变量,可以改指向;数组名是常量地址,不能改。
多维数组:C++的多维数组本质是“数组的数组”。int a[3][4]是一个包含3个元素的数组,每个元素是一个包含4个int的数组。
标准库函数begin和end(定义在iterator头文件中):
为了让指针的使用更简单安全,C++11引入了begin和end。
int ia[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; int *beg = begin(ia); // 指向ia的首元素 int *last = end(ia); // 指向ia尾元素的下一位置的指针多维数组:
使用范围for语句处理多维数组:
size_t cnt = 0; for (auto &row : ia) { for (auto &col : row) { col = cnt; ++cnt; } }注意:要使用范围for语句处理多维数组,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型(为了避免数组被自动转换成指针导致编译错误)。
指针和多维数组:
因为多维数组实际上是数组的数组,所以由多维数组名转换得来的指针实际上是指向第一个内层数组的指针。
int ia[3][4]; // 大小为3的数组,每个元素是含有4个整数的数组 int (*p)[4] = ia; // p指向含有4个整数的数组 p = &ia[2]; // p指向ia的尾元素C++11通过auto或者decltype就能尽可能地避免在数组前面加上一个指针类型了
for (auto p = ia; p != ia + 3; ++p) for (auto q = *p; q != *p + 4; ++q) cout << *q << ' '; cout << endl;或者
for (auto p = begin(ia); p != end(ia); ++p) for (auto q = begin(*p); q != end(*p); ++q) cout << *q << ' '; cout << endl;本期小结
1. string自动管理内存,注意size()无符号下溢和字符串拼接规则;
2. vector扩容会使所有迭代器失效,预知大小时用reserve优化;
3. 迭代器是容器和算法的桥梁,不同容器的失效规则不同;
4. 数组名退化为指针,数组大小固定,优化使用vector替代。
下一期预告:第3期——类基础:构造、析构、访问控制、const成员函数、友元等。