从零到一学习c++(基础篇--筑基期七-vector与迭代器)

发布于:2025-02-12 ⋅ 阅读:(256) ⋅ 点赞:(0)

  从零到一学习C++(基础篇) 作者:羡鱼肘子

温馨提示1:本篇是记录我的学习经历,会有不少片面的认知,万分期待您的指正。

 温馨提示2:本篇会尽量用更加通俗的语言介绍c++的基础,用通俗的语言去解释术语。

 温馨提示3:看本篇前可以先了解前篇的内容,知识体系会更加完整哦。

从零到一学习c++(基础篇--筑基期六-string)-CSDN博客

标准库类型vector 

1. 什么是vector?

vector 的基本概念

定义
  • vector 是一个 动态数组(dynamic array),属于C++标准库中的 顺序容器(sequential container)。

  • 头文件#include <vector>

  • 命名空间std::vector<T>

核心特性
  1. 动态大小:元素数量可以动态增长或缩减。

  2. 连续存储:元素在内存中连续存放,支持快速随机访问(通过下标)。

  3. 类型安全:所有元素必须是相同类型 T

  4. 自动内存管理:内存由 vector 自动分配和释放。

2. 基本用法

声明与初始化
#include <vector>
using namespace std;

// 初始化方式
vector<int> v1;             // 默认初始化,空vector
vector<int> v2(v1);         // 拷贝v1的元素(v1和v2类型必须相同)
vector<int> v3 = v1;        // 等价于v2(v1)
vector<int> v4(5, 10);      // 5个元素,每个初始化为10
vector<int> v5(5);          // 5个元素,默认值初始化(int为0)
vector<int> v6{1, 2, 3};    // 列表初始化(C++11)
vector<int> v7 = {1, 2, 3}; // 等价于v6
添加元素
  • push_back():在尾部插入元素。

  • emplace_back()(C++11):直接在容器尾部构造元素(更高效)。

  • insert():在指定位置插入元素(效率较低,需移动后续元素)。

vector<int> 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 时:

vector<Person> people;
people.push_back(Person("Alice", 25));

发生了什么呐?

  1. 构造临时对象:先调用构造函数 Person("Alice", 25),创建一个临时对象。

  2. 拷贝到容器:再调用拷贝构造函数,将这个临时对象复制到 vector 的内存中。

  3. 销毁临时对象:临时对象被销毁。

多了一次额外的拷贝操作呢!

2. emplace_back 的工作原理

改用 emplace_back

people.emplace_back("Alice", 25);

发生了什么?

  1. 直接构造:直接在 vector 的内存空间中调用构造函数 Person("Alice", 25)

  2. 没有临时对象不创建临时对象,因此没有拷贝操作

3. 为什么更高效?

  • push_back:需要先构造对象,再拷贝到容器(如果对象很大,拷贝成本高)。

  • emplace_back:直接在容器内存中构造对象,跳过了临时对象和拷贝步骤

  • 性能提升:尤其对复杂对象(如包含大量数据的类)效果显著。

4. 技术原理

  • 可变参数模板emplace_back 使用 C++11 的可变参数模板,可以接受任意数量和类型的参数。

  • 完美转发:将这些参数直接传递给对象的构造函数,实现“原地构造”(in-place construction)。

5. 对比示例

情况1:简单类型(如 int

vector<int> v;
v.push_back(20);      // 构造临时int(20),然后复制(或移动)
v.emplace_back(20);   // 直接构造,没有区别(但对int来说,优化不明显)
  • 对简单类型,性能差异不大,但 emplace_back 仍更优。

情况2:复杂对象

class BigData {
public:
    BigData(int size) { /* 分配大量内存 */ }
    BigData(const BigData& other) { /* 深拷贝,成本高! */ }
};

vector<BigData> vec;
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<int> v;
v.reserve(100); // 预分配100个元素的内存
for (int i = 0; i < 100; ++i) {
    v.push_back(i); // 不会触发扩容
}

3. 遍历vector

方法1:下标遍历
for (int i = 0; i < v.size(); i++) {
    cout << v[i] << " ";
}
方法2:迭代器遍历

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(减少拷贝开销)。

  • vector<int> v = {1, 2, 3};
    auto it = v.begin();
    v.push_back(4); // 可能导致扩容,it失效!
    cout << *it;    // 未定义行为!

方法3:范围循环(C++11+)
for (auto num : v) {
    cout << num << " ";
}

 对比其他容器

容器 特点
vector 动态数组,尾部操作高效,支持随机访问,内存连续
list 双向链表,任意位置插入/删除高效,不支持随机访问
deque 双端队列,头尾插入/删除高效,支持随机访问
array 固定大小数组,不支持动态扩容

4. 重要特性小结

动态扩容
  • 当插入元素超过当前容量时,vector会自动分配更大的内存(通常是当前容量的2倍)。

  • 频繁扩容会影响性能,可以用 reserve() 预先分配足够空间。

元素连续存储
  • 所有元素在内存中是连续存放的,类似数组。

  • 支持指针算术操作(例如 &v[0] 是首元素地址)。

5. 注意事项

  1. 越界访问v[i]不会检查越界,但v.at(i)会抛出std::out_of_range异常。

  2. 迭代器失效

    • 在修改vector(如插入、删除)后,旧的迭代器可能失效。

    • 例如,push_back可能导致内存重新分配,之前的迭代器指向无效地址。

  3. 性能

    • 尾部插入/删除(push_back/pop_back)高效(O(1)时间)。

    • 中间插入/删除需要移动元素,效率较低(O(n)时间)。

6. 示例代码

#include <vector>
#include <iostream>
using namespace std;

int main() {
    vector<int> 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;
}

7. 什么时候用vector?

  • 需要频繁在尾部添加/删除元素。

  • 需要随机访问元素(通过下标)。

  • 不确定元素数量,需要动态调整大小。

通过以上的学习,我们接触到了一个新的词:迭代器 ,那么什么是迭代器呢?

迭代器 (强化概念)

1. 迭代器的基本概念

什么是迭代器?
  • 迭代器是访问和操作容器(如 vectorlistmap 等)元素的通用机制。

  • 行为类似指针:可以解引用(*it)访问元素,用箭头操作符(->)访问成员,支持移动(++it--it)。

  • 迭代器是容器和算法之间的桥梁,使得算法可以独立于容器类型工作。

为什么需要迭代器?
  • 提供统一的元素访问方式,避免直接操作容器内部结构。

  • 支持泛型编程(例如 sort() 函数可以对所有支持随机访问迭代器的容器排序)。

2. 迭代器的基本操作

获取迭代器
vector<int> 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

3. 迭代器类型

迭代器类别

C++ 标准库定义了 5 类迭代器(从功能弱到强排序):

  1. 输入迭代器(Input Iterator):只读,单遍扫描(如 istream_iterator)。

  2. 输出迭代器(Output Iterator):只写,单遍扫描(如 ostream_iterator)。

  3. 前向迭代器(Forward Iterator):可读写,多遍扫描(如 forward_list 的迭代器)。

  4. 双向迭代器(Bidirectional Iterator):可前向和后向移动(如 list 的迭代器)。

  5. 随机访问迭代器(Random Access Iterator):支持所有指针算术操作(如 vectordeque 的迭代器)。

不同容器的迭代器类型
容器 迭代器类型
vector 随机访问迭代器
deque 随机访问迭代器
list 双向迭代器
forward_list 前向迭代器
map/set 双向迭代器

4. 迭代器的使用

遍历容器
vector<int> v = {1, 2, 3};
// 使用迭代器遍历
for (auto it = v.begin(); it != v.end(); ++it) {
    cout << *it << " ";
}
// 使用范围for循环(底层依赖迭代器)
for (int num : v) {
    cout << num << " ";
}
修改元素
vector<int> v = {1, 2, 3};
auto it = v.begin();
*it = 100; // 将第一个元素改为100

5. 迭代器失效问题

何时失效?
  • 向容器中添加或删除元素可能导致迭代器失效(尤其是 vector 和 string)。

  • 具体场景

    1. 添加元素

      • 如果引发扩容(push_back 导致 size > capacity),所有迭代器失效。

    2. 删除元素

      • 被删除元素之后的迭代器失效。

示例
vector<int> v = {1, 2, 3};
auto it = v.begin();
v.push_back(4);      // 可能导致扩容,it 失效!
cout << *it << endl; // 未定义行为!
如何避免?
  • 在修改容器后,重新获取迭代器。

  • 使用返回值更新迭代器(例如 erase() 返回删除后的下一个有效迭代器)。

vector<int> v = {1, 2, 3, 4};
auto it = v.begin();
while (it != v.end()) {
    if (*it % 2 == 0) {
        it = v.erase(it); // erase 返回下一个有效迭代器
    } else {
        ++it;
    }
}

6. 特殊迭代器

1. const_iterator
  • 用于只读访问容器元素,不能修改元素。

  • 通过 cbegin() 和 cend() 获取。

vector<int> v = {1, 2, 3};
vector<int>::const_iterator cit = v.cbegin();
// *cit = 10; // 错误:不能修改元素
2. 反向迭代器(reverse_iterator)
  • 从后向前遍历容器,通过 rbegin() 和 rend() 获取。

  • 仅双向或随机访问迭代器支持。

vector<int> v = {1, 2, 3};
for (auto rit = v.rbegin(); rit != v.rend(); ++rit) {
    cout << *rit << " "; // 输出 3 2 1
}
3. 插入迭代器(insert_iterator)
  • 用于向容器中插入元素(如 back_inserterfront_inserter)。

  • 示例:

vector<int> v = {1, 2, 3};
auto back_it = back_inserter(v); // 插入到尾部
*back_it = 4; // v变为 [1, 2, 3, 4]

7. 迭代器与算法

标准库算法(如 sortfind)通过迭代器操作容器:

vector<int> v = {3, 1, 4, 2};
sort(v.begin(), v.end()); // 排序 [1, 2, 3, 4]
auto it = find(v.begin(), v.end(), 3); // 查找元素3的位置

8. 迭代器辅助函数

advance 和 distance
  • advance(it, n):将迭代器移动 n 步。

  • distance(it1, it2):计算两个迭代器之间的距离。

  • 注意:distance 的时间复杂度取决于迭代器类型(随机访问迭代器为 O(1),其他为 O(n))。

vector<int> 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

9. 注意事项

  1. 不要解引用 end() 迭代器:它指向尾后元素,不可解引用。

  2. 迭代器类型必须匹配:不同容器的迭代器类型不同,不能混用。

  3. 谨慎处理迭代器失效:修改容器后,迭代器可能失效。


网站公告

今日签到

点亮在社区的每一天
去签到