从 0 到 1 吃透 C++ vector:这可能是最详细的保姆级教程,模拟实现vector

引入

本文深入剖析 C++ 中的vector容器,从其本质、使用、迭代器失效、底层实现到实际应用,进行了全面且细致的讲解。vector作为动态数组,具备连续存储、动态扩容等特性,在使用上有多种构造方式、丰富的操作接口,但其迭代器在某些操作下会失效,需要开发者谨慎处理。文章详细阐述了vector的模拟实现,揭示了如memcpy拷贝自定义类型的陷阱,以及动态二维数组的相关知识,并结合 OJ 实战,展示了vector在解决经典问题中的应用。此外,还指出常见误区并给出最佳实践建议,帮助读者深入理解并熟练运用vector

一、vector 的本质:会 “长大” 的数组

1.1 为什么需要 vector?

想象你要做一个班级成绩管理系统,一开始不知道有多少学生,用普通数组的话:

int scores[100]; // 假设最多100人,不够用就崩溃

问题:静态数组大小固定,浪费空间或不够用。
vector 解决方案:动态数组,自动管理内存,想存多少存多少,还能像数组一样快速访问。

1.2 vector 的三个核心属性(必懂!)

  • start:指向内存起始位置(像数组首地址)
  • finish:指向最后一个有效元素的下一个位置(类似数组end
  • end_of_storage:指向内存空间的末尾(总容量边界)

举例

vector<int> v = {1,2,3}; 
// start指向1,finish指向4(3的下一个位置),end_of_storage可能指向6(假设初始容量4,预留空间)

1.3 与其他容器的区别(选对工具很重要)

容器内存布局随机访问尾部操作中间插入适用场景
array静态连续O(1)低效不支持已知固定大小的数据
vector动态连续O(1)O(1)O(n)尾部频繁操作 + 随机访问
list动态链表O(n)O(1)O(1)频繁中间插入 / 删除
deque分段连续O(1)O(1)O(n)双端频繁操作(如栈 / 队列)

二、vector 的使用:从定义到操作全解析

2.1 构造函数:5 种初始化方式(附代码示例)

构造函数列表

序号函数形式功能说明
1vector()无参构造
2vector(size_type n, const value_type& val = value_type())构造并初始化 n 个 val
3vector(const vector& x)拷贝构造
4vector(InputIterator first, InputIterator last)使用迭代器进行初始化构造

各构造函数详细信息

1. vector();(无参构造)
  • 参数:无。
  • 详细说明:此构造函数用于创建一个空的 std::vector 对象,容器中没有任何元素,容量为 0。适用于需要一个初始为空的容器,后续再动态添加元素的场景。
  • 示例
#include <vector>
#include <iostream>
int main() {
    std::vector<int> v1;
    std::cout << "v1 的大小: " << v1.size() << std::endl; 
    return 0;
}
  • 示例讲解:在这个示例中,首先包含了 <vector> 和 <iostream> 头文件,前者用于使用 std::vector 容器,后者用于输入输出操作。在 main 函数里,通过 std::vector<int> v1; 调用无参构造函数创建了一个空的 std::vector<int> 对象 v1。接着使用 v1.size() 获取 v1 的元素数量,由于它是通过无参构造创建的,所以元素数量为 0,并将结果输出。
2. vector(size_type n, const value_type& val = value_type());
  • 参数n 为元素个数,val 为每个元素的初始值(默认值为 value_type())。
  • 详细说明:构造一个 std::vector,包含 n 个元素,每个元素都是 val 的副本。若 n = 0,容器为空。适用于初始化固定数量且值相同的元素。
  • 示例
#include <vector>
#include <iostream>
int main() {
    std::vector<int> v2(5, 10); // 5 个元素,每个元素为 10
    std::cout << "v2 的大小: " << v2.size() << std::endl; 
    for (int i : v2) {
        std::cout << i << " ";
    }
    return 0;
}
  • 示例讲解:此示例同样包含了必要的头文件。在 main 函数中,使用 std::vector<int> v2(5, 10); 调用带参数的构造函数,创建了一个包含 5 个元素,每个元素值都为 10 的 std::vector<int> 对象 v2。通过 v2.size() 输出 v2 的元素数量,结果为 5。然后使用范围 for 循环遍历 v2 中的每个元素并输出,因此会依次输出 5 个 10。
3. vector(const vector& x);(拷贝构造)
  • 参数x 为要拷贝的 vector 对象。
  • 详细说明:通过拷贝另一个 vector 对象 x 创建新 vector,新容器与 x 元素、大小、容量完全相同。
  • 示例
#include <vector>
#include <iostream>
int main() {
    std::vector<int> v3(3, 5); // v3 有 3 个元素,值为 5
    std::vector<int> v4(v3); // 拷贝 v3
    std::cout << "v4 的大小: " << v4.size() << std::endl; 
    for (int i : v4) {
        std::cout << i << " ";
    }
    return 0;
}
  • 示例讲解:代码开始包含了相关头文件。在 main 函数中,先使用 std::vector<int> v3(3, 5); 创建了一个包含 3 个元素,每个元素值为 5 的 std::vector<int> 对象 v3。接着通过 std::vector<int> v4(v3); 调用拷贝构造函数,创建了 v4,它是 v3 的一个副本,元素和 v3 完全相同。然后使用 v4.size() 输出 v4 的元素数量,结果为 3,再使用范围 for 循环遍历 v4 并输出每个元素,输出结果为 3 个 5。
4. vector(InputIterator first, InputIterator last);
  • 参数first 和 last 为输入迭代器,指向复制元素的范围 [first, last)
  • 详细说明:构造一个 vector,将 [first, last) 范围内的元素复制到新容器。可用于从其他容器(如 list)或迭代器范围初始化 vector
  • 示例
#include <vector>
#include <list>
#include <iostream>
int main() {
    std::list<int> lst = {1, 2, 3};
    std::vector<int> v5(lst.begin(), lst.end()); // 用 list 的迭代器初始化 vector
    std::cout << "v5 的大小: " << v5.size() << std::endl; 
    for (int i : v5) {
        std::cout << i << " ";
    }
    return 0;
}
  • 示例讲解:该示例包含了 <vector><list> 和 <iostream> 头文件。在 main 函数中,首先创建了一个 std::list<int> 对象 lst,并初始化为包含元素 1、2、3。然后使用 std::vector<int> v5(lst.begin(), lst.end()); 调用使用迭代器的构造函数,将 lst 中从 lst.begin() 到 lst.end() 范围内的元素复制到 v5 中。接着通过 v5.size() 输出 v5 的元素数量,结果为 3。最后使用范围 for 循环遍历 v5 并输出每个元素,依次输出 1、2、3。

2.2 迭代器:玩转 vector 的 “指针”

2.2.1 六大迭代器函数

迭代器函数列表

序号函数形式功能说明
1begin() + end()获取第一个数据位置的 iterator/const_iterator,获取最后一个数据的下一个位置的 iterator/const_iterator
2rbegin() + rend()获取最后一个数据位置的 reverse_iterator,获取第一个数据前一个位置的 reverse_iterator
3cbegin() + cend()获取第一个数据位置的 const_iterator,获取最后一个数据的下一个位置的 const_iterator

各迭代器函数详细信息

1. begin() + end()
  • 参数:无(作为 vector 成员函数,直接通过容器对象调用,如 v.begin()v.end())。
  • 详细说明begin() 函数用于获取指向容器首个元素的迭代器(iterator)或常量迭代器(const_iterator),这取决于调用该函数的对象是否为常量对象。end() 函数用于获取指向容器最后一个元素下一个位置的迭代器(iterator)或常量迭代器(const_iterator)。二者共同定义了一个左闭右开区间 [begin(), end()),该区间覆盖了容器中的所有元素,常用于对容器进行正向遍历操作。
  • 示例
#include <vector>
#include <iostream>
int main() {
    std::vector<int> v = {1, 2, 3, 4, 5};
    for (auto it = v.begin(); it != v.end(); ++it) {
        std::cout << *it << " ";
    }
    return 0;
}
  • 示例讲解:在这个示例中,v.begin() 指向向量 v 中的第一个元素 1v.end() 指向向量 v 中最后一个元素 5 的下一个位置。通过 for 循环,使用迭代器 it 从 begin() 开始,不断向后移动(++it),直到 it 等于 end() 为止,从而遍历整个向量并输出每个元素,输出结果为 1 2 3 4 5
2. rbegin() + rend()
  • 参数:无(作为 vector 成员函数,直接通过容器对象调用,如 v.rbegin()v.rend())。
  • 详细说明rbegin() 函数用于获取指向容器最后一个元素的反向迭代器(reverse_iterator),rend() 函数用于获取指向容器第一个元素前一个位置的反向迭代器(reverse_iterator)。它们共同定义了一个左闭右开区间 [rbegin(), rend()),该区间可用于对容器进行反向遍历操作。
  • 示例
#include <vector>
#include <iostream>
int main() {
    std::vector<int> v = {1, 2, 3, 4, 5};
    for (auto it = v.rbegin(); it != v.rend(); ++it) {
        std::cout << *it << " ";
    }
    return 0;
}
  • 示例讲解:在此示例中,v.rbegin() 指向向量 v 中的最后一个元素 5v.rend() 指向向量 v 中第一个元素 1 的前一个位置。通过 for 循环,使用反向迭代器 it 从 rbegin() 开始,不断向后移动(++it,对于反向迭代器,向后移动实际上是向前遍历元素),直到 it 等于 rend() 为止,从而实现对向量的反向遍历并输出每个元素,输出结果为 5 4 3 2 1
3. cbegin() + cend()
  • 参数:无(作为 vector 成员函数,直接通过容器对象调用,如 v.cbegin()v.cend())。
  • 详细说明cbegin() 函数用于获取指向容器第一个元素的常量迭代器(const_iterator),cend() 函数用于获取指向容器最后一个元素下一个位置的常量迭代器(const_iterator)。无论调用该函数的对象是否为常量对象,返回的都是常量迭代器,这意味着通过这些迭代器不能修改容器中的元素,主要用于在不需要修改元素的情况下对容器进行遍历,以保证数据的安全性。
  • 示例
#include <vector>
#include <iostream>
int main() {
    std::vector<int> v = {1, 2, 3, 4, 5};
    for (auto it = v.cbegin(); it != v.cend(); ++it) {
        // *it = 10; // 编译错误,不能通过 const_iterator 修改元素
        std::cout << *it << " ";
    }
    return 0;
}
  • 示例讲解:在这个示例中,v.cbegin() 指向向量 v 中的第一个元素 1v.cend() 指向向量 v 中最后一个元素 5 的下一个位置。通过 for 循环,使用常量迭代器 it 从 cbegin() 开始,不断向后移动(++it),直到 it 等于 cend() 为止,从而遍历整个向量并输出每个元素。如果尝试通过常量迭代器修改元素(如注释中的 *it = 10;),会导致编译错误,因为常量迭代器不允许修改其所指向的元素,输出结果为 1 2 3 4 5
2.2.2 迭代器失效:最容易踩的坑(附解决方案)

场景 1:扩容导致迭代器失效(如 push_back/insert 触发扩容)

  • 失效本质与野指针形成
    vector 依赖连续内存存储元素。当通过 push_back 或 insert 操作添加元素触发扩容时,容器会申请一块新的更大内存空间(例如原容量为 3,添加元素后需扩容至 6),将旧内存中的元素逐一复制到新内存,随后释放旧内存。此时,指向旧内存的迭代器(如 it)未被更新,它所存储的地址指向的内存已被释放,成为 “野指针”(指向已释放或无效内存区域的指针 / 迭代器)。解引用野指针(如 *it)时,程序会访问到无效内存,引发未定义行为(可能表现为程序崩溃、读取到随机值等)。
  • 错误代码深度分析
    vector<int> v = {1,2,3};  
    auto it = v.begin(); // it 指向旧内存中元素 1 的位置  
    v.push_back(4); // 触发扩容:旧内存释放,新内存分配并复制元素 1,2,3,再存入 4。此时 it 仍指向旧内存地址,成为野指针。  
    cout << *it; // 访问野指针指向的区域,该区域已非 `vector` 有效内存,行为不可预测。  
    
  • 正确做法原理重新获取迭代器,扩容后,所有迭代器需重新赋值。
    v.push_back(4);
    it = v.begin();  // 重新指向新内存的首元素

场景 2:删除元素导致迭代器失效(erase 操作)

  • 失效本质与野指针形成
    当使用 erase 删除元素时,被删除元素后方的所有元素会向前移动填补空缺。例如,对于 [1,2,3,4],若 pos 指向元素 3,执行 v.erase(pos) 后,元素 4 会前移到 3 的位置。此时,未更新的 pos 仍指向旧的 3 的存储位置,而该位置要么已被后续元素覆盖,要么处于无效状态,使 pos 沦为野指针。解引用这样的野指针(如 *pos),程序会访问到无意义或已被修改的内存区域,导致崩溃或逻辑错误。
  • 错误代码深度分析
    auto pos = find(v.begin(), v.end(), 3); // 假设找到元素 3 的位置,pos 指向它。  
    v.erase(pos); // 删除元素 3,后续元素前移。pos 未更新,仍指向旧的 3 的位置,成为野指针。  
    cout << *pos; // 访问野指针,该位置已非有效元素存储区,操作非法。  
    
  • 正确做法原理
    erase 函数设计上会返回一个迭代器,该迭代器指向删除操作后下一个元素的位置(如删除 3 后,返回指向 4 的迭代器)。通过 pos = v.erase(pos); 更新迭代器,使其指向有效元素(如 4),彻底消除野指针风险,保证后续操作基于合法内存地址进行。
  • pos = v.erase(pos);  // pos 现在指向元素 4

综上,vector 的迭代器失效本质是迭代器指向的内存不再有效(被释放或元素布局改变),形成野指针。理解这一过程,在涉及容器修改操作(扩容、删除等)时,通过更新迭代器来规避野指针,是编写正确、健壮代码的关键。

2.3 空间管理:size vs capacity(彻底搞懂内存分配)

2.3.1 核心接口对比(表格 + 例子)
接口作用是否改变 size是否初始化新元素示例(v 初始 size=0, capacity=0)
size()获取有效元素个数v.size() → 0
capacity()获取已分配内存可容纳的元素个数v.capacity () → 0(初始)
resize(n)调整 size 为 n,若 n>capacity 则扩容是(默认值初始化)v.resize (5) → size=5,元素为 0
resize(n, val)调整 size 为 n,用 val 初始化新元素v.resize(3, 10) → [10,10,10]
reserve(n)保证 capacity 至少为 n,若 n>current 则扩容否(不初始化)v.reserve(100) → capacity≥100

1. size()

  • 作用:用于查询 vector 容器中当前有效元素的实际个数,就如同数清篮子里现有的苹果数量,仅为统计功能,不涉及对篮子(容器)本身容量或苹果(元素)的增删改操作。
  • 是否改变 size:否。它只是一个 “查看” 操作,不会对容器中有效元素的数量进行任何修改,就像只看不拿放,篮子里苹果数不变。
  • 是否初始化新元素:否。该操作与新元素的创建或初始化毫无关系,它不关心篮子外面有没有苹果,只看篮子里现有多少。
  • 示例
    #include <vector>  
    #include <iostream>  
    int main() {  
        std::vector<int> v;  
        std::cout << "v 的有效元素个数: " << v.size() << std::endl; // v 初始 size=0,输出 0  
        return 0;  
    }  
    
  • 在此示例中,v 是一个初始为空的 vectorv.size() 仅仅是去 “数” 容器里有多少元素,由于此时容器中没有任何元素,所以返回 0,整个过程没有对容器做任何改动。

2. capacity()

  • 作用:查询 vector 容器已分配内存能够容纳的元素个数,这类似于知道篮子目前最多能装多少苹果(即使现在没装满),是对容器内存 “潜力” 的一种查询。
  • 是否改变 size:否。它关注的是容器的内存容量,与容器中实际装了多少元素(size)没有关系,不会因为查询容量而改变元素个数。
  • 是否初始化新元素:否。该操作仅聚焦于内存空间的信息,不涉及新元素的产生或初始化,就像只问篮子能装多少,不管有没有苹果、苹果怎么来的。
  • 示例
    #include <vector>  
    #include <iostream>  
    int main() {  
        std::vector<int> v;  
        std::cout << "v 已分配内存可容纳的元素个数: " << v.capacity() << std::endl; // 初始 capacity=0,输出 0  
        return 0;  
    }  
    
  • 这里 v 初始时没有进行任何扩容操作,就像一个刚拿出来还没调整大小的篮子,它能容纳的苹果数(capacity)为 0v.capacity() 只是把这个信息反馈出来,不涉及其他变化。

3. resize(n)

  • 作用:调整 vector 的 size(有效元素个数)为 n。如果 n 大于当前 capacity(已分配内存可容纳的元素个数),则容器会进行扩容,以便能容纳 n 个元素;如果 n 小于当前 size,则容器尾部的元素会被 “丢弃”,只保留前 n 个元素;如果 n 大于当前 size,新增的元素会以默认值初始化(如 int 类型默认初始化为 0)。这好比调整篮子里苹果的数量,不够就换大篮子(扩容),多了就拿走一些,新增位置的苹果先默认放 0 个。
  • 是否改变 size:是。该操作直接目标就是修改容器中有效元素的个数,将其明确调整为 n,就像主动增加或减少篮子里的苹果数。
  • 是否初始化新元素:是。当 n 大于当前 size 时,新增的元素位置需要 “放东西”,这些新位置会以默认值初始化,如同在新扩大的篮子区域先默认放 0 个苹果。
  • 示例
    #include <vector>  
    #include <iostream>  
    int main() {  
        std::vector<int> v;  
        v.resize(5); // 调整 size 为 5,若 capacity 不足则扩容,新元素默认初始化为 0  
        std::cout << "v 的 size: " << v.size() << std::endl; // 输出 5  
        for (int i : v) {  
            std::cout << i << " "; // 输出 0 0 0 0 0  
        }  
        return 0;  
    }  
    
  • 此示例中,v 初始 size 和 capacity 均为 0v.resize(5) 要把 size 改成 5。因为原来容量不够,所以容器会扩容,然后在这 5 个位置上放入默认值 0,就像给一个小篮子换成大篮子,再在新的五个位置都先放 0 个苹果,最终 size 变为 5,元素都是 0

4. resize(n, val)

  • 作用:调整 vector 的 size 为 n,与 resize(n) 类似,但更 “明确”。若 n > capacity,触发扩容;若 n < size,删除尾部元素;若 n > size,新增元素用 val 初始化。这就像调整篮子里苹果数,同时明确新位置的苹果要放成 val 这个值。
  • 是否改变 size:是。直接将容器有效元素个数改成 n,主动控制篮子里苹果的数量。
  • 是否初始化新元素:是。当 n > size 时,新增元素用 val 来初始化,不像 resize(n) 用默认值,这里是自定义值,如同给新篮子位置放特定数量的苹果。
  • 示例
    #include <vector>  
    #include <iostream>  
    int main() {  
        std::vector<int> v;  
        v.resize(3, 10); // 调整 size 为 3,若 capacity 不足则扩容,新元素初始化为 10  
        std::cout << "v 的 size: " << v.size() << std::endl; // 输出 3  
        for (int i : v) {  
            std::cout << i << " "; // 输出 10 10 10  
        }  
        return 0;  
    }  
    
  • 这里 v 初始 size 和 capacity 为 0v.resize(3, 10) 要把 size 变为 3。容器先扩容(如果需要),然后在这 3 个位置都放入 10,就像准备一个能放 3 个苹果的篮子,每个位置都放 10 个苹果,最终 size 是 3,元素都是 10

5. reserve(n)

  • 作用:保证 vector 的 capacity(已分配内存可容纳的元素个数)至少为 n。如果 n 大于当前 capacity,容器会进行扩容,使 capacity 不小于 n;如果 n 小于或等于当前 capacity,则不做任何操作。该操作不改变容器中有效元素的个数(size),也不对新的内存空间进行初始化,就像给篮子准备足够的空间,但不关心现在篮子里有几个苹果,新空间也不放苹果。
  • 是否改变 size:否。它只关注容器的内存容量,不涉及对现有元素个数的增加或减少,篮子里苹果数不变。
  • 是否初始化新元素:否。仅对内存进行扩容(如果需要),但不会往新的内存空间放任何元素,新空间就像空着的位置,没苹果。
  • 示例
    #include <vector>  
    #include <iostream>  
    int main() {  
        std::vector<int> v;  
        v.reserve(100); // 保证 capacity 至少为 100,若初始 capacity 为 0 则扩容  
        std::cout << "v 的 capacity: " << v.capacity() << std::endl; // 输出 >=100 的值(如 100,具体取决于实现)  
        std::cout << "v 的 size: " << v.size() << std::endl; // 输出 0,size 未改变  
        return 0;  
    }  
    
    此示例中,v.reserve(100) 是给 v 这个容器 “准备” 至少能放 100 个元素的内存空间(如果初始不够就扩容)。但容器里实际的元素个数(size)还是 0,就像把篮子做得很大,但里面还没开始放苹果,新的篮子空间也没放任何东西,只是有了能放 100 个苹果的潜力。
2.3.2 扩容策略:VS vs GCC(面试常考!)

VS(PJ STL):按 1.5 倍扩容

  • 扩容过程分析

    • 初始状态:当我们创建一个空的 vector 时,初始 capacity 为 0。这意味着此时 vector 还没有分配任何用于存储元素的内存空间。
    • 插入元素 1:当插入第一个元素时,vector 需要为这个元素分配内存,所以 capacity 变为 1。
    • 插入元素 2:插入第二个元素时,当前 capacity 刚好可以容纳,所以 capacity 保持为 2。
    • 插入元素 3:同理,当前 capacity 足够,capacity 仍为 3。
    • 插入元素 4:还是能容纳,capacity 为 4。
    • 插入元素 5:此时当前 capacity 为 4,不足以容纳新元素,需要进行扩容。按照 VS 的 1.5 倍扩容策略,新的 capacity 为 4 * 1.5 = 6
  • 设计原因:VS 的 1.5 倍扩容策略是为了平衡空间利用率。每次扩容为原来的 1.5 倍,不会像 2 倍扩容那样每次都分配过多的额外空间,避免了大量内存的浪费。例如,如果只需要存储少量额外元素,2 倍扩容可能会导致分配的空间远远超过实际需求,而 1.5 倍扩容相对更 “节制”,能在一定程度上提高空间的使用效率。

GCC(SGI STL):按 2 倍扩容
  • 扩容过程分析

    • 初始状态:同样,创建空 vector 时,初始 capacity 为 0。
    • 插入元素 1:为第一个元素分配内存,capacity 变为 1。
    • 插入元素 2capacity 变为 2。
    • 插入元素 3:当前 capacity 为 2,无法容纳新元素,进行扩容。按照 2 倍扩容策略,新的 capacity 为 2 * 2 = 4
    • 插入元素 5:此时 capacity 为 4,再次插入元素时需要扩容,新的 capacity 变为 4 * 2 = 8
  • 设计原因:GCC 的 2 倍扩容策略追求简单高效。每次扩容为原来的 2 倍,计算简单,并且能快速增加可用空间,减少扩容的次数。因为扩容操作涉及到内存的重新分配和元素的拷贝,频繁扩容会带来较大的性能开销,所以 2 倍扩容可以在一定程度上减少这种开销,提高插入元素的效率。

  • reserve(1000) 的作用reserve 函数用于预先分配指定数量的内存空间。在这个例子中,v.reserve(1000) 会让 vector 提前分配足够存储 1000 个元素的内存空间,将 capacity 至少设置为 1000。这样在后续的 for 循环中,使用 push_back 插入 1000 个元素时,由于 capacity 已经足够,不会触发扩容操作,也就避免了元素拷贝的开销,从而显著提升了性能。

2.4 增删查改:常用接口详解

序号函数形式功能说明
1push_back尾插
2pop_back尾删
3find查找。(注意这个是算法模块实现,不是 vector 的成员接口)
4insert在 position 之前插入 val
5erase删除 position 位置的数据
6swap交换两个 vector 的数据空间
7operator[]像数组一样访问
8at访问指定位置的元素,并进行边界检查
各接口详细信息
1. push_back
  • 函数形式void push_back(const T& val)(假设 T 是 vector 存储的元素类型)
  • 参数val,要插入到尾部的元素值。
  • 详细说明:此函数用于在 vector 的尾部插入一个元素。若插入后元素个数超过当前 capacity,会触发扩容。均摊时间复杂度为 O (1)(考虑扩容影响)。
  • 示例
#include <vector>  
#include <iostream>  
int main() {  
    std::vector<int> v; // 创建空的 vector  
    v.push_back(1); // 尾部插入 1,此时 v 为 [1]  
    v.push_back(2); // 尾部插入 2,此时 v 为 [1, 2]  
    for (int i : v) { // 遍历输出元素  
        std::cout << i << " ";  
    }  
    return 0;  
}  
  • 示例讲解:首先创建一个空的 std::vector<int> 容器 v。第一次调用 v.push_back(1),在容器尾部插入元素 1,此时容器内元素为 [1]。第二次调用 v.push_back(2),在尾部插入元素 2,容器内元素变为 [1, 2]。最后通过范围 for 循环遍历容器,依次输出每个元素,结果为 1 2
2. pop_back
  • 函数形式void pop_back()
  • 参数:无。
  • 详细说明:用于删除 vector 尾部的元素。若 vector 为空,调用 pop_back 会导致未定义行为(如 Debug 模式下触发断言)。
  • 示例
#include <vector>  
#include <iostream>  
int main() {  
    std::vector<int> v = {1, 2, 3}; // v 初始为 [1, 2, 3]  
    v.pop_back(); // 删除尾部元素 3,此时 v 为 [1, 2]  
    for (int i : v) { // 遍历输出元素  
        std::cout << i << " ";  
    }  
    return 0;  
}  
  • 示例讲解:先创建一个包含 1、2、3 三个元素的 std::vector<int> 容器 v。调用 v.pop_back() 时,会删除容器尾部的元素 3,容器内元素变为 [1, 2]。最后通过范围 for 循环遍历容器,输出结果为 1 2
3. find
  • 函数形式Iterator find(Iterator first, Iterator last, const T& val)(需包含 <algorithm> 头文件)
  • 参数first 和 last 是迭代器,指定查找范围 [first, last)val 是要查找的值。
  • 详细说明:在指定范围内查找等于 val 的元素,找到返回指向该元素的迭代器,否则返回 last。是标准算法,非 vector 成员函数。
  • 示例
#include <vector>  
#include <iostream>  
#include <algorithm> // 包含 find 所在的算法头文件  
int main() {  
    std::vector<int> v = {1, 2, 3}; // v 初始为 [1, 2, 3]  
    auto it = std::find(v.begin(), v.end(), 2); // 在 [1, 2, 3] 中找 2  
    if (it != v.end()) { // 若找到(it 不指向 end)  
        std::cout << "找到元素 2" << std::endl;  
    }  
    return 0;  
}  
  • 示例讲解:首先创建一个包含 1、2、3 的 std::vector<int> 容器 v。通过 std::find(v.begin(), v.end(), 2) 在容器 v 的 [begin(), end()) 范围内查找值为 2 的元素。由于 v 中存在 2find 会返回指向 2 的迭代器 it。通过判断 it != v.end()(若没找到,it 会等于 v.end()),确认找到元素后输出提示信息 找到元素 2
4. insert
  • 函数形式Iterator insert(Iterator position, const T& val)
  • 参数position 是迭代器,指定插入位置;val 是要插入的元素。
  • 详细说明:在 position 之前插入 val,返回指向新插入元素的迭代器。若插入导致扩容,迭代器可能失效。
  • 示例
#include <vector>  
#include <iostream>  
int main() {  
    std::vector<int> v = {1, 2, 3}; // v 初始为 [1, 2, 3]  
    auto it = v.begin() + 1; // 迭代器指向位置 1(元素 2)  
    v.insert(it, 4); // 在该位置前插入 4,v 变为 [1, 4, 2, 3]  
    for (int i : v) { // 遍历输出元素  
        std::cout << i << " ";  
    }  
    return 0;  
}  
  • 示例讲解:先创建一个包含 1、2、3 的 std::vector<int> 容器 vauto it = v.begin() + 1 获取指向第二个元素(2)的迭代器 it。调用 v.insert(it, 4) 时,会在 it 指向的元素(2)之前插入 4,容器内元素变为 [1, 4, 2, 3]。最后通过范围 for 循环遍历容器,输出结果为 1 4 2 3
5. erase
  • 函数形式Iterator erase(Iterator position)
  • 参数position 是迭代器,指定要删除的位置。
  • 详细说明:删除 position 位置的元素,后面的元素向前移动,返回指向删除位置下一个元素的迭代器。迭代器在删除后会失效,需用返回值更新。
  • 示例
#include <vector>  
#include <iostream>  
int main() {  
    std::vector<int> v = {1, 2, 3}; // v 初始为 [1, 2, 3]  
    auto it = v.begin() + 1; // 迭代器指向位置 1(元素 2)  
    v.erase(it); // 删除该位置元素,v 变为 [1, 3]  
    for (int i : v) { // 遍历输出元素  
        std::cout << i << " ";  
    }  
    return 0;  
}  
  • 示例讲解:创建包含 1、2、3 的 std::vector<int> 容器 vauto it = v.begin() + 1 获取指向第二个元素(2)的迭代器 it。调用 v.erase(it) 时,会删除 it 指向的元素(2),后面的元素 3 向前移动,容器内元素变为 [1, 3]。最后通过范围 for 循环遍历容器,输出结果为 1 3
6. swap
  • 函数形式void swap(vector& x)
  • 参数x 是另一个 vector 对象。
  • 详细说明:交换当前 vector 和 x 的数据空间,包括元素、sizecapacity 等。操作效率高,时间复杂度 O (1)。
  • 示例
#include <vector>  
#include <iostream>  
int main() {  
    std::vector<int> v1 = {1, 2}; // v1 初始为 [1, 2]  
    std::vector<int> v2 = {3, 4}; // v2 初始为 [3, 4]  
    v1.swap(v2); // 交换 v1 和 v2 的数据空间  
    for (int i : v1) { // 遍历输出 v1 元素  
        std::cout << i << " ";  
    }  
    for (int i : v2) { // 遍历输出 v2 元素  
        std::cout << i << " ";  
    }  
    return 0;  
}  
  • 示例讲解:先创建 v1[1, 2])和 v2[3, 4])两个 std::vector<int> 容器。调用 v1.swap(v2) 时,会交换它们的数据空间,包括元素、大小和容量等信息。此时 v1 变为 [3, 4]v2 变为 [1, 2]。通过两个范围 for 循环分别遍历 v1 和 v2,输出结果为 3 4 和 1 2
7. operator[]
  • 函数形式T& operator[](size_type pos)(非 const vector);const T& operator[](size_type pos)const vector
  • 参数pos 是要访问元素的位置索引。
  • 详细说明:像数组一样通过索引访问 vector 中的元素,不进行越界检查(越界行为未定义)。
  • 示例
#include <vector>  
#include <iostream>  
int main() {  
    std::vector<int> v = {1, 2, 3}; // v 初始为 [1, 2, 3]  
    std::cout << v[1] << std::endl; // 访问索引为 1 的元素(值为 2)  
    return 0;  
}  
  • 示例讲解:创建包含 1、2、3 的 std::vector<int> 容器 v。通过 v[1] 访问索引为 1 的元素(第二个元素),由于 operator[] 不检查越界(此处索引合法),直接返回该位置的元素 2,并输出 2
8. at
  • 函数形式T& at(size_type pos)(非 const vector);const T& at(size_type pos)const vector
  • 参数pos 是要访问元素的位置索引。
  • 详细说明:用于访问 vector 中指定位置的元素,与 operator[] 不同的是,at 会进行边界检查。如果 pos 超出了 vector 的有效范围(即 pos >= size()),会抛出 std::out_of_range 异常,这样可以避免因越界访问导致的未定义行为,增强了程序的健壮性。
  • 示例
#include <vector>  
#include <iostream>  
#include <stdexcept> // 包含异常头文件  

int main() {  
    std::vector<int> v = {1, 2, 3}; // v 初始为 [1, 2, 3]  
    try {  
        std::cout << v.at(1) << std::endl; // 正常访问索引 1 的元素(值为 2)  
        std::cout << v.at(3) << std::endl; // 越界访问(有效索引 0-2),触发异常  
    } catch (const std::out_of_range& e) {  
        std::cerr << "Out of range error: " << e.what() << std::endl; // 捕获异常并输出信息  
    }  
    return 0;  
}  
  • 示例讲解:创建包含 1、2、3 的 std::vector<int> 容器 v。在 try 块中,首先通过 v.at(1) 访问索引为 1 的元素(合法访问),返回值 2 并输出。接着尝试通过 v.at(3) 访问索引为 3 的元素,由于 v 的有效索引范围是 0 到 2,这是一次越界访问。此时 at 会抛出 std::out_of_range 异常,程序跳转到 catch 块,捕获异常并输出错误信息 Out of range error: vector::_M_range_check: __n (which is 3) >= this->size() (which is 3)(不同编译器输出细节可能有差异,但都表明越界),避免了程序因越界访问导致的崩溃或未定义行为。

三、迭代器失效:从原理到实战(避坑指南)

3.1 失效的本质:指针指向无效内存

vector 的迭代器本质是原始指针T*,当底层内存被释放或元素位置改变时,指针就失效了,就像你记住了朋友家的地址,但朋友搬家了,你再按旧地址去找就找不到了。

3.2 触发失效的操作(全列表)

改变底层空间的操作(导致旧内存释放)

这类操作会对 vector 的底层内存布局进行调整,通常会涉及到内存的重新分配,旧的内存空间会被释放,这就使得原本指向旧内存的迭代器变得无效。

  • push_back/insert(可能扩容)

    • push_back:用于在 vector 的尾部插入一个新元素。当插入元素后,vector 的元素数量超过了当前的 capacity(容量)时,就会触发扩容操作。扩容时,vector 会重新分配一块更大的内存空间,将旧内存中的元素复制到新内存中,然后释放旧内存。例如,初始 capacity 为 2 的 vector,插入第 3 个元素时就可能触发扩容。
    • insert:可以在指定位置插入一个或多个元素。如果插入元素后导致元素数量超过当前 capacity,同样会触发扩容。例如,在 vector 的中间位置插入元素,可能会使得原本的 capacity 不够用,从而引发扩容。
  • resize(可能扩容,且改变 size

    • resize 函数用于调整 vector 的 size(元素数量)。如果指定的新 size 大于当前的 capacity,就会触发扩容。同时,它会改变 vector 的 size。如果新 size 小于当前 size,则会删除尾部的元素;如果新 size 大于当前 size,则会在尾部添加新元素(可以指定初始值)。例如,resize(10) 可能会让 vector 扩容以容纳 10 个元素。
  • reserve(直接改变 capacity,可能扩容)

    • reserve 函数的作用是预先分配一定的内存空间,确保 vector 的 capacity 至少达到指定的值。如果指定的值大于当前的 capacity,就会触发扩容。例如,reserve(20) 会让 vector 的 capacity 至少变为 20,如果原本 capacity 小于 20,就会进行扩容操作。
  • assign(重新赋值,可能扩容)

    • assign 函数用于将新的元素赋值给 vector,它会替换掉 vector 中原来的所有元素。如果新元素的数量超过了当前的 capacity,就会触发扩容。例如,assign({1, 2, 3, 4, 5}) 可能会因为新元素数量过多而导致扩容。
改变元素位置的操作(不释放内存,但元素移动)

这类操作虽然不会释放底层内存,但会改变元素在内存中的相对位置,这会使得原本指向某些元素的迭代器不再指向正确的元素。

  • erase(删除位置后的元素前移)

    • erase 函数用于删除 vector 中指定位置的元素。当删除一个元素后,该元素后面的所有元素都会向前移动一个位置,以填补删除元素后留下的空缺。例如,删除 vector 中间的一个元素,后面的元素会依次向前移动。这样一来,原本指向删除位置之后元素的迭代器就会失效,因为它们指向的元素已经发生了变化。
  • swap(交换两个 vector 的数据,迭代器指向对方的内存)

    • swap 函数用于交换两个 vector 的数据。交换后,两个 vector 的元素、size 和 capacity 都会相互交换。此时,原本指向一个 vector 元素的迭代器会指向另一个 vector 的对应位置,导致迭代器失效。例如,vector1 和 vector2 交换后,vector1 的迭代器会指向 vector2 的元素,这显然不是我们所期望的。

3.3 经典错误案例(附调试方法)

案例 1:扩容后继续使用旧迭代器(VS 直接崩溃,GCC 可能输出乱码)
#include <iostream>
#include <vector>

int main() {
    std::vector<int> v;
    auto it = v.begin(); // 初始it是end()(size=0)
    v.push_back(1); // 扩容,capacity=1,it失效(原end()指向旧内存,已释放)
    std::cout << *it; // VS报错“访问违规”,GCC可能输出随机值(未定义行为)
    return 0;
}
  • 详细分析

    • 首先创建了一个空的 vector v,此时 v 的 size 为 0,it 初始化为 v.begin(),实际上它指向的是 v 的 end() 位置。
    • 调用 v.push_back(1) 时,由于 v 原本为空,插入元素 1 会触发扩容操作。vector 会重新分配一块新的内存空间来存储元素 1,同时释放旧的内存空间。
    • 此时,it 仍然指向旧的内存地址,而这块内存已经被释放,成为了无效的内存。当尝试使用 std::cout << *it; 解引用 it 时,就会出现问题。在 VS 编译器中,会直接报错 “访问违规”,因为程序试图访问已经释放的内存;在 GCC 编译器中,可能会输出随机值,这是一种未定义行为,程序的表现是不可预测的。
  • 调试方法

    • 可以在关键操作前后输出 vector 的 size 和 capacity,观察内存的变化情况。例如,在 push_back 前后分别输出:
std::cout << "Before push_back: size = " << v.size() << ", capacity = " << v.capacity() << std::endl;
v.push_back(1);
std::cout << "After push_back: size = " << v.size() << ", capacity = " << v.capacity() << std::endl;
  • 使用调试器(如 Visual Studio 的调试工具或 GDB),在 push_back 之后设置断点,检查 it 的值和指向的内存地址,确认其是否失效。
案例 2:erase 后未更新迭代器(GCC 允许运行,但结果错误)
#include <iostream>
#include <vector>

int main() {
    std::vector<int> v = {1, 2, 3, 4};
    auto it = v.begin() + 1; // 指向2
    v.erase(it); // 删除2,后面的3、4前移
    std::cout << *it; // GCC可能输出3(it现在指向原3的位置),但逻辑上it已失效,结果不可靠
    return 0;
}
  • 详细分析

    • 首先创建了一个包含 4 个元素的 vector v,并将迭代器 it 指向元素 2 的位置。
    • 调用 v.erase(it) 时,会删除 it 指向的元素 2。删除后,元素 3 和 4 会向前移动一个位置,填补元素 2 的空缺。
    • 此时,it 仍然指向原来元素 2 的位置,但这个位置现在存储的是元素 3。虽然在 GCC 编译器中,程序可能会允许运行并输出 3,但从逻辑上来说,it 已经失效,因为它不再指向我们原本期望的元素。如果继续使用这个失效的迭代器进行操作,可能会导致程序出现错误的结果。
  • 调试方法

    • 在 erase 前后输出 vector 的元素,观察元素的变化情况。例如:
std::cout << "Before erase: ";
for (auto num : v) {
    std::cout << num << " ";
}
std::cout << std::endl;

v.erase(it);

std::cout << "After erase: ";
for (auto num : v) {
    std::cout << num << " ";
}
std::cout << std::endl;
  • 使用调试器,在 erase 之后设置断点,检查 it 的值和指向的元素,确认其是否失效。
正确做法:每次可能失效的操作后,重新获取迭代器或使用返回值
#include <iostream>
#include <vector>

int main() {
    std::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;
        }
    }
    for (auto num : v) {
        std::cout << num << " ";
    }
    std::cout << std::endl;
    return 0;
}
  • 详细分析
    • 这段代码的目的是删除 vector 中的所有偶数元素。
    • 首先,将迭代器 it 初始化为 v.begin(),指向 vector 的第一个元素。
    • 进入 while 循环,只要 it 不等于 v.end(),就继续循环。
    • 在循环中,检查 it 指向的元素是否为偶数。如果是偶数,调用 v.erase(it) 删除该元素,并将返回的迭代器赋值给 iterase 函数会返回指向删除元素下一个元素的迭代器,这样就保证了 it 始终是一个有效的迭代器,避免了漏判元素的情况。
    • 如果 it 指向的元素不是偶数,则将 it 向后移动一个位置(++it)。
    • 最后,遍历 vector 并输出剩余的元素。

通过这种方式,我们可以正确地处理可能导致迭代器失效的操作,避免程序出现错误。

四、深度剖析:模拟实现 vector(理解底层原理)

每个成员变量功能

  • _start:指向存储元素的内存起始处。空 vector 时为 nullptr,有元素后指向分配内存起点,是元素访问和迭代器起始的依据。
  • _finish:指向最后一个有效元素的下一个位置。界定有效元素范围 [_start, _finish),用于计算元素数量和作为迭代器遍历终点。
  • _endofstorage:指向已分配内存的末尾。界定容量范围 [_start, _endofstorage),用于计算容量和判断是否需要扩容。

协作机制

  • 插入元素:检查 _finish 与 _endofstorage,若相等则扩容,更新三个指针,插入元素并后移 _finish
  • 删除元素:移除元素后元素前移,前移 _finish 更新有效范围。
  • 扩容操作:分配新内存,_start 指向新起点,复制元素,调整 _finish 和 _endofstorage

1. 默认构造函数

vector() : _start(nullptr), _finish(nullptr), _endofstorage(nullptr) {}
  • 实现目的:初始化 vector 对象,使其处于空状态,为后续元素插入等操作做好准备。
  • 方法选择原因:直接将三个指针成员(_start 指向起始位置,_finish 指向最后一个元素的下一个位置,_endofstorage 指向分配内存的末尾)置为 nullptr,清晰表明容器初始无任何有效内存分配和元素存储。
  • 注意事项:这是容器的初始状态,后续对容器的操作(如 push_back)会基于此状态进行内存分配和元素管理。

2. 带参数的构造函数

vector(int n, const T& val = T()) { resize(n, val); }
  • 实现目的:创建一个包含 n 个元素的 vector,每个元素初始化为 val
  • 方法选择原因:复用 resize 函数,因为 resize 本身就负责调整容器大小并初始化元素。这样可以减少代码重复,提高代码复用性。
  • 注意事项:若 T 类型没有默认构造函数,使用默认值 T() 会报错,此时需用户显式提供 val

3. 迭代器范围构造函数

template<class InputIterator>
vector(InputIterator first, InputIterator last) {
    while (first != last) {
        push_back(*first);
        ++first;
    }
}
  • 实现目的:通过给定的迭代器范围 [first, last) 初始化 vector,将范围内的元素逐个插入。
  • 方法选择原因:利用 push_back 的尾部插入功能,逐个添加元素,适用于各种支持输入迭代器的容器(如 listarray 等),增强了 vector 初始化的灵活性。
  • 注意事项:确保传入的迭代器范围有效(first 不超过 last),否则会陷入死循环。

4. 拷贝构造函数

vector(const vector<T>& v) {
    _start = new T[v.capacity()];
    for (size_t i = 0; i < v.size(); i++) {
        _start[i] = v._start[i];
    }
    _finish = _start + v.size();
    _endofstorage = _start + v.capacity();
}
  • 实现目的:深拷贝一个 vector,使新 vector 与源 vector 有独立的内存空间,避免浅拷贝导致的资源重复释放问题。
  • 方法选择原因:手动分配内存(_start = new T[v.capacity()])并逐个元素复制(_start[i] = v._start[i]),而不用 memcpy。因为 memcpy 是按字节复制,若 T 是自定义类型(如包含指针成员),会导致浅拷贝(两个对象的指针成员指向同一内存),后续析构时会重复释放内存。逐个复制能确保每个元素都正确复制,实现深拷贝。
  • 注意事项:确保源 vector 的 capacity 和 size 准确获取,避免内存分配不足或浪费。

5. 析构函数

~vector() {
    if (_start) {
        delete[] _start;
        _start = _finish = _endofstorage = nullptr;
    }
}
  • 实现目的:释放 vector 动态分配的内存,防止内存泄漏。
  • 方法选择原因:检查 _start 是否为 nullptr,若不为空则释放数组内存(delete[] _start),并将三个指针置为 nullptr,确保对象不再持有无效内存指针。
  • 注意事项:若 vector 中有指针成员指向其他资源,需在析构函数中额外处理这些资源的释放,这里 vector 仅管理元素内存,故只需释放 _start

6. 迭代器获取函数

iterator begin() { return _start; }
iterator end() { return _finish; }
const_iterator begin()const { return _start; }
const_iterator end()const { return _finish; }
  • 实现目的:为 vector 提供迭代器,方便遍历容器元素。
  • 方法选择原因:直接返回内部指针 _start 和 _finishconst 版本返回 const_iterator 确保常量对象不能通过迭代器修改元素,符合常量对象的语义。
  • 注意事项:迭代器的有效性依赖于 vector 的内部状态(_start 和 _finish),若 vector 进行了可能改变内存布局的操作(如 inserterase 导致扩容或元素移动),迭代器可能失效,需重新获取。

7. reserve(预留空间)

void reserve(size_t n) {
    if (n > capacity()) {
        size_t sz = size();
        T* tmp = new T[n];
        if (_start) {
            for (size_t i = 0; i < sz; i++) {
                tmp[i] = _start[i];
            }
            delete[] _start;
        }
        _start = tmp;
        _finish = _start + sz;
        _endofstorage = _start + n;
    }
}
  • 实现目的:确保 vector 至少有 n 个元素的存储容量,若不足则扩容。
  • 方法选择原因:先分配新内存(T* tmp = new T[n]),若原 _start 有内存则逐个复制元素(避免 memcpy 的浅拷贝问题),再释放旧内存(delete[] _start),更新指针。这种方式保证了元素的深拷贝和内存的正确管理。
  • 注意事项:扩容后,原来的迭代器会失效(因为内存地址改变),需重新获取迭代器。

8. resize(调整大小)

void resize(size_t n, const T& val = T()) {
    if (n < size()) {
        _finish = _start + n;
    } else {
        reserve(n);
        while (_finish != _start + n) {
            *_finish = val;
            ++_finish;
        }
    }
}
  • 实现目的:调整容器的有效元素个数为 n,若 n 大于当前容量则扩容。
  • 方法选择原因:当 n 小于当前 size 时,直接截断(_finish = _start + n);当 n 大于当前 size 时,先调用 reserve 扩容,再初始化新元素(*_finish = val)。这样分情况处理,既保证了内存的合理分配,又实现了元素的初始化。
  • 注意事项:若 val 是复杂对象(如自定义类),其赋值操作(*_finish = val)可能涉及拷贝构造等操作,需确保 T 类型的赋值操作正确。

9. push_back(尾部插入)

void push_back(const T& x) { insert(end(), x); }
  • 实现目的:在 vector 尾部插入一个元素。
  • 方法选择原因:复用 insert 函数,在 end()(最后一个元素的下一个位置)前插入元素 x,代码简洁且复用性高。
  • 注意事项:若插入导致扩容,所有指向 vector 的迭代器都会失效。

10. pop_back(尾部删除)

void pop_back() { erase(--end()); }
  • 实现目的:删除 vector 尾部的元素。
  • 方法选择原因:先将 end() 指针前移(--end(),指向最后一个元素),再调用 erase 函数删除该位置元素,复用 erase 函数实现尾部删除。
  • 注意事项:若 vector 为空,调用 pop_back 会导致未定义行为(erase 中的 assert 检查会报错),使用前需确保 vector 非空。

11. insert(指定位置插入)

iterator insert(iterator pos, const T& x) {
    assert(pos >= _start && pos <= _finish);
    if (_finish == _endofstorage) {
        size_t len = pos - _start;
        size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
        reserve(newcapacity);
        pos = _start + len;
    }
    iterator end = _finish - 1;
    while (end >= pos) {
        *(end + 1) = *end;
        --end;
    }
    *pos = x;
    ++_finish;
    return pos;
}
  • 实现目的:在指定位置 pos 前插入元素 x
  • 方法选择原因:先检查 pos 合法性(assert(pos >= _start && pos <= _finish)),若空间不足则扩容(计算偏移量 len,调用 reserve 扩容后重新计算 pos),然后从 _finish - 1 向前移动元素(*(end + 1) = *end),为插入元素腾出位置,最后插入元素并更新 _finish。这种方式保证了插入操作的正确性和对迭代器失效的处理(扩容时迭代器失效,重新获取 pos 位置)。
  • 注意事项:插入操作可能导致迭代器失效(扩容或元素移动),调用后需确保使用返回的迭代器或重新获取迭代器。

12. erase(指定位置删除)

iterator erase(iterator pos) {
    assert(pos >= _start && pos < _finish);
    iterator it = pos + 1;
    while (it != _finish) {
        *(it - 1) = *it;
        ++it;
    }
    --_finish;
    return pos;
}
  • 实现目的:删除指定位置 pos 的元素。
  • 方法选择原因:检查 pos 合法性(assert(pos >= _start && pos < _finish)),从 pos + 1 开始向前移动元素(*(it - 1) = *it)覆盖被删除元素,更新 _finish 指针(--_finish)。这种方式实现了元素的删除和后续元素的前移。
  • 注意事项:被删除位置的迭代器 pos 在删除后指向已被覆盖的位置,需更新迭代器(通常使用返回的 pos 或重新获取迭代器),否则会导致迭代器失效。

13. 获取容器状态函数

  • capacity(获取容量)
size_t capacity()const { return _endofstorage - _start; }
  • 实现目的:返回 vector 已分配内存可容纳的元素个数。

  • 方法选择原因:通过计算 _endofstorage(分配内存的末尾)与 _start(起始位置)的差值,直接得到容量,简单直观。

  • 注意事项:容量是已分配内存的大小,与实际元素个数(size)不同。

  • size(获取元素个数)

size_t size()const { return _finish - _start; }
  • 实现目的:返回 vector 中当前有效元素的个数。
  • 方法选择原因:计算 _finish(最后一个元素的下一个位置)与 _start(起始位置)的差值,得到元素个数,清晰明了。
  • 注意事项size 始终小于或等于 capacity

14. 下标访问函数

T& operator[](size_t pos) {
    assert(pos < size());
    return _start[pos];
}
const T& operator[](size_t pos)const {
    assert(pos < size());
    return _start[pos];
}
  • 实现目的:提供类似数组的下标访问方式,访问 vector 中的元素。
  • 方法选择原因:通过 assert 检查 pos 越界(pos < size()),确保安全访问,然后返回 _start[pos]const 版本保证常量对象只能读取不能修改元素。
  • 注意事项operator[] 不检查越界(仅通过 assert 在调试时检查),使用时需确保 pos 合法,否则会导致未定义行为。

15. 交换函数与赋值运算符重载

  • swap(交换两个 vector 的成员)
void swap(vector<T>& v) {
    std::swap(_start, v._start);
    std::swap(_finish, v._finish);
    std::swap(_endofstorage, v._endofstorage);
}
  • 实现目的:快速交换两个 vector 的数据空间、大小和容量等信息。

  • 方法选择原因:利用 std::swap 交换三个指针成员,实现高效交换。这种方式时间复杂度为 O (1),因为只是交换指针,不涉及元素的复制。

  • 注意事项:交换后,两个 vector 的迭代器、引用等都指向对方原来的内存,需注意迭代器的有效性。

  • operator=(赋值运算符重载)

vector<T>& operator=(vector<T> v) {
    swap(v);
    return *this;
}
  • 实现目的:将一个 vector 的内容赋值给另一个 vector
  • 方法选择原因:通过调用 swap 函数,将当前 vector 与传入的 vector v 交换数据空间等信息。由于 v 是拷贝构造的临时对象,函数结束时 v 会析构,释放其原来的资源,从而实现正确的赋值操作。这种实现方式保证了异常安全(若 swap 过程中无异常,原对象资源已交换,不会造成泄漏)。
  • 注意事项:赋值后,原来的 vector 内容被交换出去,其资源会在临时对象 v 析构时释放,无需手动管理。

4.4 为什么不能用 memcpy?(自定义类型的噩梦)

在 vector 拷贝构造函数里,使用 memcpy 对包含指针成员的自定义类型复制时会导致浅拷贝问题。浅拷贝仅复制指针的值(内存地址),而非指针指向的数据,多个对象的指针成员会指向同一块内存。当其中一个对象释放该内存,其他对象的指针就会变成野指针,再次访问会引发未定义行为。为实现深拷贝,需逐元素赋值,调用自定义类型的赋值运算符,确保指针成员指向不同内存区域。

_start = new T[v.capacity()]; 开辟空间后仍可能浅拷贝的原因

_start = new T[v.capacity()]; 虽为新 vector 对象开辟了内存空间,但 memcpy 按字节复制对象的二进制内容,对于包含指针成员的自定义类型,仅复制指针的值,不复制指针指向的数据,仍会导致浅拷贝问题。

对于指针成员,memcpy 是否解引用复制

memcpy 对指针成员不进行解引用复制,只是将指针所占据的字节原样复制到目标对象的对应位置,即复制了指针的值(内存地址)。若要进行解引用复制,需手动编写代码,如在类中提供自定义的拷贝构造函数和赋值运算符,对指针指向的数据进行复制

不同类型使用 memcpy 的情况

  • 包含指针成员的自定义类型:使用 memcpy 会导致浅拷贝问题,不能使用,应采用逐元素赋值或自定义拷贝构造函数和赋值运算符实现深拷贝。
  • 不包含指针成员且成员均为基本数据类型的自定义类型:使用 memcpy 通常是安全的,因为这些类型的数据可直接按字节复制。
  • 包含标准库容器或其他复杂类型的自定义类型:不能使用 memcpy,因为这些类型内部可能使用了动态内存分配,使用 memcpy 会破坏其内部状态。

结论:对自定义类型,必须用拷贝构造函数逐个元素深拷贝,不能用 memcpy 这种浅拷贝。

以下就是模拟实现的vector的全部代码

 vector.h代码:

#define _CRT_SECURE_NO_WARNINGS 1
#include<assert.h>
#include<iostream>
#include<string>
#include<algorithm>
using namespace std;

namespace NJ {

    template<class T>
    class vector {

    public:
        // 定义迭代器类型,方便后续使用
        typedef T* iterator;
        typedef const T* const_iterator;

        // 获取普通迭代器,指向容器起始位置
        iterator begin() {
            return _start;
        }
        // 获取普通迭代器,指向容器末尾(最后一个元素的下一个位置)
        iterator end() {
            return _finish;
        }

        // 获取常量迭代器,指向容器起始位置,用于常量对象
        const_iterator begin()const {
            return _start;
        }
        // 获取常量迭代器,指向容器末尾(最后一个元素的下一个位置),用于常量对象
        const_iterator end()const {
            return _finish;
        }

        // 构造函数,创建包含n个元素的vector,元素值为val(默认值为T())
        vector(int n, const T& val = T()) {
            resize(n, val);
        }
        // 默认构造函数,初始化三个指针成员为空
        vector()
            : _start(nullptr)
            , _finish(nullptr)
            , _endofstorage(nullptr)
        {}
        // 迭代器范围构造函数,将[first, last)范围内的元素插入到vector中
        template<class InputIterator>
        vector(InputIterator first, InputIterator last) {
            while (first != last) {
                push_back(*first); // 逐个将范围内元素插入
                ++first;
            }
        }

        // 拷贝构造函数,深拷贝另一个vector
        vector(const vector<T>& v) {
            // 分配与v相同容量的内存
            _start = new T[v.capacity()];
            // 逐个元素复制(不用memcpy,因为memcpy是按字节复制,对于自定义类型(如包含指针的类)会导致浅拷贝,这里逐元素赋值确保深拷贝,避免资源重复释放等问题)
            for (size_t i = 0; i < v.size(); i++) {
                _start[i] = v._start[i];
            }
            _finish = _start + v.size(); // 更新finish指针
            _endofstorage = _start + v.capacity(); // 更新endofstorage指针
        }

        // 交换两个vector的成员(指针、大小、容量),高效实现赋值等操作
        void swap(vector<T>& v) {
            std::swap(_start, v._start);
            std::swap(_finish, v._finish);
            std::swap(_endofstorage, v._endofstorage);
        }

        // 赋值运算符重载,利用swap实现,确保异常安全(先交换,原资源会在swap的临时对象析构时释放)
        vector<T>& operator=(vector<T> v) {
            swap(v);
            return *this;
        }

        // 析构函数,释放动态分配的内存
        ~vector() {
            if (_start) {
                delete[] _start;
                _start = _finish = _endofstorage = nullptr; // 指针置空,避免野指针
            }
        }

        // 预留至少n个元素的空间,若n大于当前容量则扩容
        void reserve(size_t n) {
            if (n > capacity()) {
                size_t sz = size(); // 记录当前元素个数
                T* tmp = new T[n]; // 分配新内存
                if (_start) {
                    // 逐个元素复制(不用memcpy原因同上,避免浅拷贝)
                    for (size_t i = 0; i < sz; i++) {
                        tmp[i] = _start[i];
                    }
                    delete[] _start; // 释放旧内存
                }
                _start = tmp; // 更新指针
                _finish = _start + sz; // 更新finish(元素个数不变)
                _endofstorage = _start + n; // 更新endofstorage
            }
        }

        // 调整容器大小为n,若n小于当前size则截断,若n大于当前size则扩容并初始化新元素为val
        void resize(size_t n, const T& val = T()) {
            if (n < size()) {
                _finish = _start + n; // 截断
            }
            else {
                reserve(n); // 先预留空间
                while (_finish != _start + n) { // 初始化新元素
                    *_finish = val;
                    ++_finish;
                }
            }
        }

        // 尾部插入元素,调用insert实现
        void push_back(const T& x) {
            insert(end(), x);
        }

        // 尾部删除元素,调用erase实现
        void pop_back() {
            erase(--end());
        }

        // 获取容器容量(总分配内存可容纳元素个数)
        size_t capacity()const {
            return _endofstorage - _start;
        }

        // 获取容器有效元素个数
        size_t size()const {
            return _finish - _start;
        }

        // 下标访问,非const对象,带断言检查越界
        T& operator[](size_t pos) {
            assert(pos < size());
            return _start[pos];
        }

        // 下标访问,const对象,带断言检查越界
        const T& operator[](size_t pos)const {
            assert(pos < size());
            return _start[pos];
        }

        // 在pos位置前插入元素x,返回插入位置的迭代器
        iterator insert(iterator pos, const T& x) {
            assert(pos >= _start && pos <= _finish); // 检查pos合法性
            if (_finish == _endofstorage) { // 空间不足时扩容
                size_t len = pos - _start; // 记录pos相对起始位置的偏移
                size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2; // 扩容策略(初始4,之后双倍)
                reserve(newcapacity); // 扩容
                pos = _start + len; // 更新pos(因为内存重新分配,需重新计算位置)
            }
            iterator end = _finish - 1; // 从末尾开始向前移动元素
            while (end >= pos) {
                *(end + 1) = *end; // 元素后移
                --end;
            }
            *pos = x; // 插入元素
            ++_finish; // finish后移
            return pos; // 返回插入位置迭代器
        }

        // 删除pos位置的元素,返回删除位置下一个元素的迭代器
        iterator erase(iterator pos) {
            assert(pos >= _start && pos < _finish); // 检查pos合法性
            iterator it = pos + 1; // 从pos下一个元素开始
            while (it != _finish) {
                *(it - 1) = *it; // 元素前移覆盖
                ++it;
            }
            --_finish; // finish前移
            return pos; // 返回删除位置迭代器(指向已删除元素位置,一般后续会更新迭代器)
        }

    private:
        iterator _start = nullptr; // 指向容器起始位置
        iterator _finish = nullptr; // 指向容器末尾(最后一个元素的下一个位置)
        iterator _endofstorage = nullptr; // 指向容器内存末尾(总分配内存的最后一个位置)
    };
}

Test.cpp代码:

#define _CRT_SECURE_NO_WARNINGS 1
#include "vector.h"
#include <iostream>

// 测试默认构造函数和push_back
void TestVector1() {
    NJ::vector<int> v; // 默认构造空vector
    v.push_back(1); // 尾部插入1
    v.push_back(2); // 尾部插入2
    v.push_back(3); // 尾部插入3
    v.push_back(4); // 尾部插入4
    // 遍历输出元素
    for (size_t i = 0; i < v.size(); ++i) {
        std::cout << v[i] << " ";
    }
    std::cout << std::endl;
}

// 测试带初始值的构造函数
void TestVector2() {
    // 构造包含5个元素,每个元素值为10的vector
    NJ::vector<int> v(5, 10);
    // 遍历输出元素
    for (size_t i = 0; i < v.size(); ++i) {
        std::cout << v[i] << " ";
    }
    std::cout << std::endl;
}

// 测试拷贝构造函数
void TestVector3() {
    NJ::vector<int> v1(3, 5); // 构造v1,包含3个元素,值为5
    NJ::vector<int> v2(v1); // 拷贝构造v2
    // 遍历输出v2元素
    for (size_t i = 0; i < v2.size(); ++i) {
        std::cout << v2[i] << " ";
    }
    std::cout << std::endl;
}

// 测试赋值运算符重载
void TestVector4() {
    NJ::vector<int> v1(3, 5); // 构造v1
    NJ::vector<int> v2; // 构造空v2
    v2 = v1; // 赋值操作
    // 遍历输出v2元素
    for (size_t i = 0; i < v2.size(); ++i) {
        std::cout << v2[i] << " ";
    }
    std::cout << std::endl;
}

// 测试insert和erase
void TestVector5() {
    NJ::vector<int> v; // 空vector
    v.push_back(1); // 插入1
    v.push_back(2); // 插入2
    v.push_back(3); // 插入3
    v.push_back(4); // 插入4
    auto it = v.begin() + 1; // 迭代器指向1之后的位置(即2)
    v.insert(it, 5); // 在该位置插入5
    // 遍历输出插入后的元素
    for (size_t i = 0; i < v.size(); ++i) {
        std::cout << v[i] << " ";
    }
    std::cout << std::endl;
    it = v.begin() + 2; // 迭代器指向插入5后的第三个位置(即3原来的位置,现在是5之后的元素)
    v.erase(it); // 删除该位置元素
    // 遍历输出删除后的元素
    for (size_t i = 0; i < v.size(); ++i) {
        std::cout << v[i] << " ";
    }
    std::cout << std::endl;
}

int main() {
    TestVector1(); // 执行测试1
    TestVector2(); // 执行测试2
    TestVector3(); // 执行测试3
    TestVector4(); // 执行测试4
    TestVector5(); // 执行测试5
    return 0;
}

五、动态二维数组:vector<vector<int>> 的正确打开方式

5.1 结构解析(以杨辉三角前 5 行为例)

vector<vector<int>> 是一个 二维动态数组,本质是 “容器的容器”:外层 vector 的每个元素本身又是一个 vector,用于存储一行数据。以杨辉三角前 5 行为例:

初始化与内存结构
vector<vector<int>> vv(5); // 初始化外层vector,包含5个空的内层vector(每行是一个空的vector)  
  • 初始状态
    外层 vector(记为 vv)有 5 个元素,每个元素是一个空的 vector<int>。每个内层 vector 的三个核心指针(_start_finish_endofstorage)均为 nullptr,相当于 5 个 “空盒子”,尚未分配任何内存。

  • 填充后状态(以第 0、1、2 行为例)

    • 第 0 行[1]
      • 内层 vector 的 size=1_finish - _start = 1),capacity≥1(具体容量由编译器决定,通常初始为 1)。
    • 第 1 行[1, 1]
      • size=2capacity≥2(可能扩容为 2 或更大,如 GCC 按 2 倍扩容)。
    • 第 2 行[1, 2, 1]
      • size=3capacity≥3(若之前容量为 2,插入第 3 个元素时触发扩容,容量变为 4 等)。
关键特性
  • 动态性:每行的长度可独立变化,无需预先指定二维数组的列数,适合处理行列数不确定的数据(如杨辉三角每行长度递增)。
  • 内存非连续:外层 vector 的每个内层 vector 的内存是独立分配的,即每行的元素在内存中连续,但行与行之间的内存地址不一定连续。
5.2 初始化技巧(避免越界)

直接使用 vv[i][j] 访问元素时,若内层 vector 尚未初始化(即第 i 行还没有分配内存),会导致 越界访问(未定义行为)。正确做法是先初始化每行的大小:

for (int i = 0; i < numRows; i++) {  
    vv[i].resize(i + 1, 1); // 第 i 行设置为 i+1 个元素,初始值为 1  
}  
细节解析
  • resize(i + 1, 1) 的作用
    • 对第 i 行的内层 vector 调用 resize,将其大小设为 i + 1,每个元素初始化为 1(杨辉三角每行首尾为 1,中间后续计算)。
    • 若内层 vector 原本为空,resize 会触发内存分配,避免直接访问 vv[i][j] 时因该行无内存导致的越界。
  • 直接访问的风险
    若未初始化第 i 行,vv[i] 是一个空的 vector,此时 vv[i][j] 会访问不存在的元素,导致程序崩溃或未定义行为(如访问野指针)。
5.3 访问方式(下标 vs 迭代器)
1. 下标访问(vv[i][j]
  • 适用场景:已知行号和列号的场景(如杨辉三角计算中间元素),直观高效。
  • 示例(计算杨辉三角中间元素)
    for (int i = 2; i < numRows; i++) {  
        for (int j = 1; j < i; j++) {  
            vv[i][j] = vv[i-1][j-1] + vv[i-1][j]; // 核心逻辑:当前元素等于上一行相邻元素之和  
        }  
    }  
    
    • 逻辑解析
      • 第 i 行(从第 0 行开始)的第 j 个元素(j=1 到 j=i-1,首尾为 1 已初始化),等于上一行第 j-1 和 j 个元素之和。
      • 下标访问直接定位到具体元素,时间复杂度为 O (1)(通过指针偏移直接访问)。
2. 迭代器访问
  • 适用场景:通用算法(如遍历所有元素、使用 STL 算法如 for_each)。
  • 示例(遍历所有元素)
    for (auto& row : vv) { // 遍历外层vector,row是内层vector的引用  
        for (auto num : row) { // 遍历内层vector的元素  
            cout << num << " ";  
        }  
        cout << endl;  
    }  
    
    • 细节
      • auto& row : vv:使用引用避免拷贝整个内层 vector,提高效率。
      • 迭代器访问通过 begin() 和 end() 获取首尾迭代器,适合未知下标或通用逻辑。

六、OJ 实战:用 vector 解决经典问题

6.1 只出现一次的数字(异或法)
问题描述

数组中只有一个数字出现一次,其他数字出现两次,找出该数字。

思路解析
  • 异或运算性质
    • 交换律:a ^ b = b ^ a
    • 结合律:(a ^ b) ^ c = a ^ (b ^ c)
    • 自反性:a ^ a = 00 ^ a = a
  • 核心逻辑:将数组中所有元素依次异或,相同元素异或为 0,最终结果即为只出现一次的数字。
代码详解
class Solution {  
public:  
    int singleNumber(vector<int>& nums) {  
        int res = 0;  
        for (int num : nums) { // 范围for遍历,自动获取nums的begin()和end()  
            res ^= num; // 等价于 res = res ^ num  
        }  
        return res;  
    }  
};  
  • 关键点
    • 范围 for 循环:无需手动管理迭代器,直接遍历 vector 中的每个元素,代码简洁。
    • 异或累积:初始 res=0,每次异或一个数,最终 res 即为唯一出现的数字(如数组 [2,2,1],异或后 0^2^2^1 = 1)。
6.2 杨辉三角(动态二维数组)
问题描述

生成杨辉三角的前 numRows 行,每行首尾为 1,中间元素为上一行相邻元素之和。

关键点解析
  • 二维数组初始化
    • 外层 vector 初始化为 numRows 行(vector<vector<int>> triangle(numRows)),每行是一个空的内层 vector
    • 对每行调用 resize(i+1, 1),初始化每行的大小为 i+1,元素初始化为 1(首尾元素固定为 1)。
  • 中间元素计算
    • 第 i 行第 j 个元素(1 ≤ j ≤ i-1)等于第 i-1 行第 j-1 和 j 个元素之和(利用二维数组的下标访问直接计算)。
代码详解
class Solution {  
public:  
    vector<vector<int>> generate(int numRows) {  
        vector<vector<int>> triangle(numRows); // 外层vector包含numRows个内层vector  
        for (int i = 0; i < numRows; i++) {  
            triangle[i].resize(i + 1, 1); // 第i行有i+1个元素,初始化为1(首尾元素)  
            for (int j = 1; j < i; j++) { // j从1到i-1,计算中间元素  
                triangle[i][j] = triangle[i-1][j-1] + triangle[i-1][j];  
            }  
        }  
        return triangle;  
    }  
};  
  • 细节注意
    • 当 i=0 或 i=1 时,内层循环(j < i)不执行(如 i=0 时,j 从 1 开始,不满足 j < 0),避免越界(第 0 行和第 1 行只有首尾元素,无需计算中间元素)。
6.3 删除排序数组中的重复项(双指针法)
问题描述

给定排序数组,原地删除重复元素,返回有效元素个数,不使用额外空间。

思路解析
  • 双指针法
    • 慢指针 slow:记录当前有效元素的最后位置(初始为 0)。
    • 快指针 fast:遍历数组,当遇到与 nums[slow] 不同的元素时,将该元素赋值给 nums[slow+1],并移动 slow
  • 核心逻辑:利用排序数组的特性,重复元素必相邻,通过快慢指针将不重复的元素 “前移”,最终 slow + 1 即为有效元素个数。
代码详解
class Solution {  
public:  
    int removeDuplicates(vector<int>& nums) {  
        if (nums.empty()) return 0; // 处理空数组  
        int slow = 0; // 慢指针,指向当前有效元素的最后一个位置  
        for (int fast = 1; fast < nums.size(); fast++) { // 快指针从第二个元素开始遍历  
            if (nums[fast] != nums[slow]) { // 发现不同元素  
                nums[++slow] = nums[fast]; // 慢指针先移动,再赋值(等价于尾插不重复元素)  
            }  
        }  
        return slow + 1; // 有效元素个数为slow+1(索引从0开始)  
    }  
};  
  • 关键步骤解析
    • 初始化:若数组为空,直接返回 0。
    • 快慢指针移动
      • 当 nums[fast] 与 nums[slow] 相等时,快指针继续移动(跳过重复元素)。
      • 当不相等时,慢指针先自增(指向新的有效位置),再将快指针当前元素赋值给慢指针位置(相当于在有效区域 “添加” 新元素)。
    • 返回结果:慢指针最终指向最后一个有效元素的位置,有效元素个数为 slow + 1(如数组 [1,1,2],处理后 slow=1,有效个数为 2)。

总结

  • 二维 vector 的核心:每行是独立的 vector,需先初始化每行的大小(如 resize)避免越界。
  • 异或法:利用异或的数学性质,在 O (n) 时间和 O (1) 空间内解决唯一元素问题。
  • 双指针法:通过快慢指针分工,原地处理排序数组,高效解决重复元素删除问题。
  • 下标 vs 迭代器:下标访问适合已知位置的场景,迭代器适合通用遍历和算法,根据需求选择合适方式。

七、常见误区与最佳实践

7.1 误区排雷

  1. 认为 vector 增容一定是 2 倍

    • 真相:VS 是 1.5 倍,GCC 是 2 倍,具体看编译器实现,不要固化思维。
  2. 用 memcpy 拷贝自定义类型

    • 后果:浅拷贝导致资源泄漏,必须用拷贝构造或 STL 算法(如copy)。
  3. 删除元素后不更新迭代器

    • 错误:v.erase(pos); ++it;(可能跳过元素或访问失效位置),正确写法it = v.erase(pos);(自动指向下一个有效元素)。

7.2 最佳实践

  1. 预分配空间:用reserve(n)避免多次扩容,尤其在大量尾插时(如读取文件数据到 vector)。
  2. 优先使用operator[]:下标访问比迭代器解引用更简洁,且现代编译器会优化到等价效率。
  3. 善用emplace_back(C++11+):原地构造元素,避免临时对象拷贝,提高效率。
    v.emplace_back(10); // 等价于push_back(10),但更高效
    

八、总结:vector 的核心价值

  • 连续存储:保证随机访问高效,适合需要快速定位元素的场景。
  • 动态扩容:自动管理内存,无需手动处理数组越界,减少代码复杂度。
  • 迭代器抽象:让算法(如 find、sort)通用化,解耦数据结构与算法。

掌握 vector 的关键在于理解其底层实现(三指针模型、扩容策略、迭代器本质),并在实践中注意迭代器失效、空间管理等细节。建议通过模拟实现 vector 的核心接口(如构造函数、push_back、reserve)来加深理解,再结合 OJ 题目巩固应用,最终达到 “能用、明理、能扩展” 的 STL 使用境界。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南玖yy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值