从零到一学习C++(基础篇) 作者:羡鱼肘子
温馨提示1:本篇是记录我的学习经历,会有不少片面的认知,万分期待您的指正。
温馨提示2:本篇会尽量用更加通俗的语言介绍c++的基础,用通俗的语言去解释术语。
温馨提示3:看本篇前可以先了解前篇的内容,知识体系会更加完整哦。
从零到一学习c++(基础篇--筑基期六-string)-CSDN博客
标准库类型vector
vector 的基本概念vector 是一个 动态数组(dynamic array),属于C++标准库中的 顺序容器(sequential container)。
头文件:#include
命名空间:std::vector
动态大小:元素数量可以动态增长或缩减。
连续存储:元素在内存中连续存放,支持快速随机访问(通过下标)。
类型安全:所有元素必须是相同类型 T。
自动内存管理:内存由 vector 自动分配和释放。
#include
using namespace std;
// 初始化方式
vector v1; // 默认初始化,空vector
vector v2(v1); // 拷贝v1的元素(v1和v2类型必须相同)
vector v3 = v1; // 等价于v2(v1)
vector v4(5, 10); // 5个元素,每个初始化为10
vector v5(5); // 5个元素,默认值初始化(int为0)
vector v6{1, 2, 3}; // 列表初始化(C++11)
vector v7 = {1, 2, 3}; // 等价于v6
push_back():在尾部插入元素。
emplace_back()(C++11):直接在容器尾部构造元素(更高效)。
insert():在指定位置插入元素(效率较低,需移动后续元素)。
vector v;
v.push_back(42); // 插入42
v.emplace_back(42); // 直接构造42(避免复制)
v.insert(v.begin(), 100); // 在头部插入100
温馨小贴士:
为什么emplace_back()更高效?
1.
push_back的工作原理假设你有一个类 Person: class Person { public: Person(string name, int age) { cout << "构造函数被调用" << endl; } Person(const Person& other) { cout << "拷贝构造函数被调用" << endl; } };当你使用
push_back时:vectorpeople; people.push_back(Person("Alice", 25)); 发生了什么呐?
构造临时对象:先调用构造函数
Person("Alice", 25),创建一个临时对象。拷贝到容器:再调用拷贝构造函数,将这个临时对象复制到
vector的内存中。销毁临时对象:临时对象被销毁。
多了一次额外的拷贝操作呢!
2.
emplace_back的工作原理改用
emplace_back:people.emplace_back("Alice", 25);发生了什么?
直接构造:直接在
vector的内存空间中调用构造函数Person("Alice", 25)。没有临时对象:不创建临时对象,因此没有拷贝操作!
3. 为什么更高效?
push_back:需要先构造对象,再拷贝到容器(如果对象很大,拷贝成本高)。
emplace_back:直接在容器内存中构造对象,跳过了临时对象和拷贝步骤。性能提升:尤其对复杂对象(如包含大量数据的类)效果显著。
4. 技术原理
可变参数模板:
emplace_back使用 C++11 的可变参数模板,可以接受任意数量和类型的参数。完美转发:将这些参数直接传递给对象的构造函数,实现“原地构造”(in-place construction)。
5. 对比示例
情况1:简单类型(如
int)vectorv; v.push_back(20); // 构造临时int(20),然后复制(或移动) v.emplace_back(20); // 直接构造,没有区别(但对int来说,优化不明显)
对简单类型,性能差异不大,但
emplace_back仍更优。情况2:复杂对象
class BigData { public: BigData(int size) { /* 分配大量内存 */ } BigData(const BigData& other) { /* 深拷贝,成本高! */ } }; vectorvec; vec.push_back(BigData(1000)); // 1次构造 + 1次拷贝(代价高) vec.emplace_back(1000); // 仅1次构造(无拷贝)
对复杂对象,
emplace_back节省了深拷贝的时间。6. 什么时候用
emplace_back?
当对象的构造函数需要参数时,优先用
emplace_back。需要插入临时对象或直接传递参数时,用它更高效。
简单类型(如
int)可以用,但性能提升不明显。总结:
emplace_back通过直接在容器内存中构造对象,避免了临时对象的创建和拷贝操作,是 C++11 后更高效的插入方式!
v[n]:通过下标访问,不检查越界(类似数组)。
v.at(n):通过下标访问,越界抛出 std::out_of_range 异常。
v.front():返回第一个元素。
v.back():返回最后一个元素。
cout << v[0]; // 访问第0个元素(不检查越界)
cout << v.at(1); // 访问第1个元素(越界会抛出异常)(推荐)
cout << v.front(); // 第一个元素(即v[0])
cout << v.back(); // 最后一个元素(即v[v.size()-1])
pop_back():删除尾部元素。
erase():删除指定位置的元素(返回指向下一个元素的迭代器)。
clear():清空所有元素。
v.pop_back(); // 删除最后一个元素
v.clear(); // 清空所有元素
v.erase(v.begin() + 1); // 删除第2个元素(迭代器位置)
vector 的内存管理size():当前元素个数。
capacity():当前分配的存储空间能容纳的元素数量。
resize(n):调整 size 为 n,多出的元素默认初始化。
reserve(n):预分配至少能容纳 n 个元素的内存(避免频繁扩容)。
当插入元素超过当前 capacity 时,vector 会重新分配更大的内存(通常是翻倍)。
扩容成本:需要拷贝所有元素到新内存,并释放旧内存。
优化策略:如果预先知道元素数量,使用 reserve() 减少扩容次数。
int size = v.size(); // 当前元素个数
bool isEmpty = v.empty(); // 是否为空
v.resize(10); // 调整大小为10(多出的元素默认初始化)
int cap = v.capacity(); // 当前分配的内存能容纳的元素数量
v.reserve(100); // 预分配内存(避免频繁扩容)
vector v;
v.reserve(100); // 预分配100个元素的内存
for (int i = 0; i < 100; ++i) {
v.push_back(i); // 不会触发扩容
}
for (int i = 0; i < v.size(); i++) {
cout << v[i] << " ";
}
vector 的迭代器迭代器提供了统一的访问容器元素的方式。
begin() 和 end() 返回指向首元素和尾后元素的迭代器。
范围for循环(C++11)底层依赖迭代器。
for (auto it = v.begin(); it != v.end(); it++) {
cout << *it << " ";
}
温馨小贴士:
vector的迭代器是 随机访问迭代器(支持it + n操作)。修改
vector(如push_back)可能导致迭代器失效。
以下操作会导致
vector的迭代器、指针或引用失效:
插入元素:如果引发扩容,所有迭代器失效。
删除元素:被删除元素之后的迭代器失效。
性能建议
避免在中间位置频繁插入或删除(时间复杂度 O(n))。
优先使用
emplace_back代替push_back(减少拷贝开销)。vectorv = {1, 2, 3}; auto it = v.begin(); v.push_back(4); // 可能导致扩容,it失效! cout << *it; // 未定义行为!
for (auto num : v) {
cout << num << " ";
}
| 容器 | 特点 |
|---|---|
vector |
动态数组,尾部操作高效,支持随机访问,内存连续 |
list |
双向链表,任意位置插入/删除高效,不支持随机访问 |
deque |
双端队列,头尾插入/删除高效,支持随机访问 |
array |
固定大小数组,不支持动态扩容 |
当插入元素超过当前容量时,vector会自动分配更大的内存(通常是当前容量的2倍)。
频繁扩容会影响性能,可以用 reserve() 预先分配足够空间。
所有元素在内存中是连续存放的,类似数组。
支持指针算术操作(例如 &v[0] 是首元素地址)。
越界访问:v[i]不会检查越界,但v.at(i)会抛出std::out_of_range异常。
迭代器失效:
在修改vector(如插入、删除)后,旧的迭代器可能失效。
例如,push_back可能导致内存重新分配,之前的迭代器指向无效地址。
性能:
尾部插入/删除(push_back/pop_back)高效(O(1)时间)。
中间插入/删除需要移动元素,效率较低(O(n)时间)。
#include
#include
using namespace std;
int main() {
vector v; // 空vector
v.reserve(10); // 预分配内存
for (int i = 0; i < 10; ++i) {
v.emplace_back(i); // 插入0~9
}
// 修改元素
v[0] = 100;
// 删除偶数
for (auto it = v.begin(); it != v.end(); ) {
if (*it % 2 == 0) {
it = v.erase(it); // erase返回下一个有效迭代器
} else {
++it;
}
}
// 输出结果
for (int num : v) {
cout << num << " ";
}
// 输出:100 1 3 5 7 9
return 0;
}
需要频繁在尾部添加/删除元素。
需要随机访问元素(通过下标)。
不确定元素数量,需要动态调整大小。
通过以上的学习,我们接触到了一个新的词:迭代器 ,那么什么是迭代器呢?
迭代器 (强化概念)
迭代器是访问和操作容器(如 vector、list、map 等)元素的通用机制。
行为类似指针:可以解引用(*it)访问元素,用箭头操作符(->)访问成员,支持移动(++it、--it)。
迭代器是容器和算法之间的桥梁,使得算法可以独立于容器类型工作。
提供统一的元素访问方式,避免直接操作容器内部结构。
支持泛型编程(例如 sort() 函数可以对所有支持随机访问迭代器的容器排序)。
vector v = {1, 2, 3};
auto it_begin = v.begin(); // 指向第一个元素的迭代器
auto it_end = v.end(); // 指向尾后元素(最后一个元素的下一个位置)
| 操作 | 说明 |
|---|---|
*it |
解引用,获取迭代器指向的元素 |
it->mem |
访问元素的成员(等价于 (*it).mem) |
++it / it++ |
移动到下一个元素 |
--it / it-- |
移动到上一个元素(仅双向或随机访问迭代器支持) |
it1 == it2 |
判断两个迭代器是否指向同一位置 |
it + n / it - n |
随机访问迭代器支持跳跃(如 vector) |
C++ 标准库定义了 5 类迭代器(从功能弱到强排序):
输入迭代器(Input Iterator):只读,单遍扫描(如 istream_iterator)。
输出迭代器(Output Iterator):只写,单遍扫描(如 ostream_iterator)。
前向迭代器(Forward Iterator):可读写,多遍扫描(如 forward_list 的迭代器)。
双向迭代器(Bidirectional Iterator):可前向和后向移动(如 list 的迭代器)。
随机访问迭代器(Random Access Iterator):支持所有指针算术操作(如 vector、deque 的迭代器)。
| 容器 | 迭代器类型 |
|---|---|
vector |
随机访问迭代器 |
deque |
随机访问迭代器 |
list |
双向迭代器 |
forward_list |
前向迭代器 |
map/set |
双向迭代器 |
vector v = {1, 2, 3};
// 使用迭代器遍历
for (auto it = v.begin(); it != v.end(); ++it) {
cout << *it << " ";
}
// 使用范围for循环(底层依赖迭代器)
for (int num : v) {
cout << num << " ";
}
vector v = {1, 2, 3};
auto it = v.begin();
*it = 100; // 将第一个元素改为100
向容器中添加或删除元素可能导致迭代器失效(尤其是 vector 和 string)。
具体场景:
添加元素:
如果引发扩容(push_back 导致 size > capacity),所有迭代器失效。
删除元素:
被删除元素之后的迭代器失效。
vector v = {1, 2, 3};
auto it = v.begin();
v.push_back(4); // 可能导致扩容,it 失效!
cout << *it << endl; // 未定义行为!
在修改容器后,重新获取迭代器。
使用返回值更新迭代器(例如 erase() 返回删除后的下一个有效迭代器)。
vector v = {1, 2, 3, 4};
auto it = v.begin();
while (it != v.end()) {
if (*it % 2 == 0) {
it = v.erase(it); // erase 返回下一个有效迭代器
} else {
++it;
}
}
const_iterator用于只读访问容器元素,不能修改元素。
通过 cbegin() 和 cend() 获取。
vector v = {1, 2, 3};
vector::const_iterator cit = v.cbegin();
// *cit = 10; // 错误:不能修改元素
从后向前遍历容器,通过 rbegin() 和 rend() 获取。
仅双向或随机访问迭代器支持。
vector v = {1, 2, 3};
for (auto rit = v.rbegin(); rit != v.rend(); ++rit) {
cout << *rit << " "; // 输出 3 2 1
}
用于向容器中插入元素(如 back_inserter、front_inserter)。
示例:
vector v = {1, 2, 3};
auto back_it = back_inserter(v); // 插入到尾部
*back_it = 4; // v变为 [1, 2, 3, 4]
标准库算法(如 sort、find)通过迭代器操作容器:
vector v = {3, 1, 4, 2};
sort(v.begin(), v.end()); // 排序 [1, 2, 3, 4]
auto it = find(v.begin(), v.end(), 3); // 查找元素3的位置
advance 和 distanceadvance(it, n):将迭代器移动 n 步。
distance(it1, it2):计算两个迭代器之间的距离。
注意:distance 的时间复杂度取决于迭代器类型(随机访问迭代器为 O(1),其他为 O(n))。
vector v = {1, 2, 3, 4, 5};
auto it = v.begin();
advance(it, 3); // it 指向4
cout << *it << endl; // 输出4
cout << distance(v.begin(), it) << endl; // 输出3
不要解引用 end() 迭代器:它指向尾后元素,不可解引用。
迭代器类型必须匹配:不同容器的迭代器类型不同,不能混用。
谨慎处理迭代器失效:修改容器后,迭代器可能失效。