【C++】 vector 全面解析:从使用到底层实现

🔥铅笔小新z:个人主页

🎬博客专栏:C++学习

💫滴水不绝,可穿石;步履不休,能至渊。

引言

在C++标准模板库(STL)中,vector是最重要、最常用的容器之一。它提供了动态数组的功能,能够自动管理内存,同时保持了数组的随机访问特性。本文将深入探讨vector的各个方面,从基本使用到高级特性,再到底层实现原理。


1. vector的基本介绍

1.1 vector是什么?

vector是一个序列容器,表示可以改变大小的数组。与普通数组不同,vector能够动态增长和收缩,自动处理内存管理。它支持快速随机访问,在尾部插入和删除元素效率高,但在中间或头部插入删除效率相对较低。

1.2 学习vector的三个境界

  1. 能用:掌握基本接口,能够在实际编程中使用vector
  2. 明理:理解vector的工作原理和内部机制
  3. 能扩展:能够根据需要自定义或扩展vector的功能

1.3 vector 相关接口


2. vector的基本使用

2.1 vector的构造函数

cpp 复制代码
// 1. 默认构造函数 - 创建空vector
vector<int> v1;

// 2. 构造并初始化n个val
vector<int> v2(5, 10); // 5个元素,每个都是10

// 3. 拷贝构造
vector<int> v3(v2);

// 4. 使用迭代器范围构造
int arr[] = {1, 2, 3, 4, 5};
vector<int> v4(arr, arr + 5);
2.1.1 默认构造函数
cpp 复制代码
vector() 
	: _start(nullptr)
	, _finish(nullptr)
	, _end_of_storage(nullptr) 
	{}
2.1.2 构造并初始化n个val
cpp 复制代码
vector(size_t n, const T& val = T())
{
	reserve(n);
	for (size_t i = 0; i < n; i++)
	{
		push_back(val);
	}
}
2.1.3 拷贝构造
cpp 复制代码
vector(const vector<T>& v)
{
	reserve(v.size());
	for (auto& e : v)
	{
		push_back(e);
	}
}

2.2 vector的迭代器

迭代器提供了访问容器元素的统一方式:

cpp 复制代码
vector<int> v = {1, 2, 3, 4, 5};

// 正向迭代器
for(auto it = v.begin(); it != v.end(); ++it) 
{
    cout << *it << " ";
}

// 反向迭代器
for(auto rit = v.rbegin(); rit != v.rend(); ++rit) 
{
    cout << *rit << " ";
}

// 范围for循环(C++11)
for(auto& elem : v) 
{
    cout << elem << " ";
}

2.3 vector的空间管理

vector有两个重要的容量概念:

  • size: 当前容器中实际元素的数量
  • capacity: 容器在不重新分配内存的情况下可以容纳的元素数量
cpp 复制代码
vector<int> v;

cout << "size: " << v.size() << endl;      // 0
cout << "capacity: " << v.capacity() << endl; // 0
cout << "empty: " << v.empty() << endl;    // true

v.reserve(100);  // 预分配100个元素的空间
cout << "capacity after reserve: " << v.capacity() << endl; // 100

v.resize(50);    // 改变size为50,多出的位置用0填充
cout << "size after resize: " << v.size() << endl; // 50

2.4 vector的容量增长策略

vector的容量增长策略在不同实现中有所不同:

cpp 复制代码
void TestVectorExpand() 
{
    vector<int> v;
    size_t sz = v.capacity();
    
    cout << "capacity growth:\n";
    for(int i = 0; i < 100; ++i) 
    {
        v.push_back(i);
        if(sz != v.capacity()) 
        {
            sz = v.capacity();
            cout << "capacity changed: " << sz << endl;
        }
    }
}

不同编译器的实现差异

  • VS系列编译器:按大约1.5倍增长
  • g++编译器:按2倍增长

这种差异是因为不同版本的STL实现不同,不要固化认为vector总是2倍增长。

2.5 vector的增删查改操作

cpp 复制代码
vector<int> v = {1, 2, 3};

// 1. 添加元素
v.push_back(4);      // 尾部插入
v.insert(v.begin() + 1, 5); // 在指定位置插入

// 2. 删除元素
v.pop_back();        // 尾部删除
v.erase(v.begin());  // 删除指定位置元素
v.clear();           // 清空所有元素

// 3. 访问元素
cout << v[0] << endl;     // 使用operator[],不检查边界
cout << v.at(0) << endl;  // 使用at(),会检查边界

// 4. 查找元素(需要包含<algorithm>)
auto it = find(v.begin(), v.end(), 3);
if(it != v.end()) 
{
    cout << "Found: " << *it << endl;
}

// 5. 交换两个vector
vector<int> v2 = {10, 20, 30};
v.swap(v2);  // 交换v和v2的内容

3. vector迭代器失效问题(重点)

迭代器失效是使用vector时需要特别注意的问题。迭代器失效意味着迭代器指向的内存空间已经无效,继续使用会导致未定义行为(通常程序崩溃)。

3.1 vector的底层存储结构

vector在内存中是一段连续的存储空间:

cpp 复制代码
template<class T>
class vector {
private:
    T* _start;          // 指向数据起始位置
    T* _finish;         // 指向最后一个元素的下一个位置
    T* _end_of_storage; // 指向存储空间末尾
    // ...
};

当你获取一个迭代器时,它本质上是一个指针(或指针的封装):

cpp 复制代码
vector<int> v = {1, 2, 3, 4, 5};
auto it = v.begin();  // it本质上是一个 int*,指向第一个元素

内存布局如下:

复制代码
地址:0x1000  0x1004  0x1008  0x100C  0x1010  0x1014...
数据:  1       2       3       4       5      ...
       ↑
       it (指向0x1000)

3.2 导致迭代器失效的操作

情况1:空间重新分配
cpp 复制代码
vector<int> v = {1, 2, 3, 4, 5};
auto it = v.begin();

// 以下操作都可能导致迭代器失效
v.resize(100);      // 扩容
v.reserve(100);     // 扩容
v.push_back(6);     // 可能导致扩容
v.insert(v.begin(), 0); // 可能导致扩容
v.assign(100, 8);   // 重新赋值,可能改变容量

// 错误:it可能已经失效
while(it != v.end()) 
{  // 可能访问已释放的内存
    cout << *it << " ";
    ++it;
}

扩容过程:

  1. 在堆上申请新的、更大的连续内存空间
  2. 将旧内存中的数据拷贝到新内存
  3. 释放旧内存空间
  4. 更新vector的内部指针到新内存

关键问题: 迭代器it仍然指向旧的、已被释放的内存地址:

复制代码
旧内存(已被释放):0x1000  0x1004  0x1008...
                    ↑
                    it (仍然指向这里,但内存已无效)

新内存(当前有效):0x2000  0x2004  0x2008...
                    1       2       3       ...

解决方案:在可能导致扩容的操作后,重新获取迭代器。

情况2:元素删除
cpp 复制代码
vector<int> v = {1, 2, 3, 4, 5};
auto it = v.begin() + 2;  // it指向元素3

v.erase(v.begin() + 1);  // 删除元素2

删除过程:

复制代码
删除前: [1, 2, 3, 4, 5]
               ↑
               it指向3

删除元素2:
1. 删除位置1的元素: [1, _, 3, 4, 5]
2. 向前移动元素3-5: [1, 3, 4, 5]

删除后: [1, 3, 4, 5]
               ↑
               it指向4(不是原来的3了)

解决方案:使用erase的返回值更新迭代器。

cpp 复制代码
// 正确删除vector中所有偶数的示例
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;
    }
}
情况3:插入元素导致内存移动

即使不扩容,插入操作也可能导致迭代器失效:

cpp 复制代码
vector<int> v = {1, 2, 3, 4, 5};
auto it = v.begin() + 2;  // it指向元素3

v.insert(v.begin() + 1, 99);  // 在位置1插入99

插入操作需要移动元素:

复制代码
插入前: [1, 2, 3, 4, 5]
               ↑
               it指向3

插入过程:
1. 向后移动元素2-5: [1, _, 2, 3, 4, 5]
2. 在位置1插入99:   [1, 99, 2, 3, 4, 5]

插入后: [1, 99, 2, 3, 4, 5]
                ↑
                it仍然指向原来的内存位置,但现在是元素2

虽然it指向的内存地址仍然有效,但元素已经改变了位置,原来的迭代器不再指向期望的元素。

情况4:编译器的差异处理

不同编译器对迭代器失效的处理严格程度不同:

  • VS编译器:检测严格,失效后立即报错
  • g++编译器:检测较宽松,可能继续运行但结果错误
cpp 复制代码
// 在g++下可能不崩溃,但结果错误
vector<int> v = {1, 2, 3, 4, 5};
auto it = v.begin();
v.reserve(100);  // 扩容,迭代器失效

// 可能不崩溃,但输出错误
while(it != v.end()) 
{
    cout << *it << " ";  // 未定义行为
    ++it;
}

3.3 string的迭代器失效问题

vector类似,string也有迭代器失效问题:

cpp 复制代码
string s = "hello";
auto it = s.begin();

s.resize(20, '!');  // 可能导致扩容,迭代器失效

// 错误:使用可能已失效的迭代器
while(it != s.end()) 
{
    cout << *it;  // 可能崩溃
    ++it;
}

3.4 具体操作分析

(1) resize(n, val) 可能扩容
cpp 复制代码
vector<int> v = {1, 2, 3, 4, 5};  // 容量5
auto it = v.begin();

v.resize(100, 0);  // 需要扩容到至少100
// 旧内存不够 → 申请新内存 → 拷贝数据 → 释放旧内存
// it失效!
(2) reserve(n) 可能扩容
cpp 复制代码
vector<int> v = {1, 2, 3, 4, 5};  // 容量5
auto it = v.begin();

v.reserve(100);  // 容量从5扩大到100
// 必须重新分配内存 → it失效!
(3) push_back(val) 可能触发扩容
cpp 复制代码
vector<int> v = {1, 2, 3, 4, 5};  // 容量5,已满
auto it = v.begin();

v.push_back(6);  // 需要扩容
// 容量不足 → 重新分配内存 → it失效!
(4) insert(pos, val) 可能触发扩容或移动
cpp 复制代码
vector<int> v = {1, 2, 3, 4, 5};  // 容量5
auto it = v.begin() + 2;

v.insert(v.begin(), 0);  // 在开头插入
// 两种情况:
// 1. 如果容量不足:扩容 → it完全失效(指向释放的内存)
// 2. 如果容量足够:元素向后移动 → it指向的元素改变
(5) assign(n, val) 完全重新分配
cpp 复制代码
vector<int> v = {1, 2, 3, 4, 5};
auto it = v.begin();

v.assign(100, 8);  // 替换所有内容
// 清空现有元素 → 可能需要重新分配内存 → it失效!

3.5 为什么访问失效迭代器会导致问题?

对于已释放的内存(情况1)
cpp 复制代码
vector<int> v = {1, 2, 3};
auto it = v.begin();
v.reserve(100);  // 重新分配内存,释放旧内存

cout << *it;  // 访问已释放的内存!

可能的后果:

  1. 程序崩溃:访问无效内存地址(段错误)
  2. 读取垃圾值:内存已被其他数据覆盖
  3. 未定义行为:任何事情都可能发生

对于元素位置改变(情况2和3)

cpp 复制代码
vector<int> v = {1, 2, 3};
auto it = v.begin() + 1;  // 指向2
v.insert(v.begin(), 0);   // 插入元素

cout << *it;  // 输出2?不,可能是其他值!

结果是逻辑错误:迭代器不指向期望的元素。

3.6 正确的做法

方案1:重新获取迭代器
cpp 复制代码
vector<int> v = {1, 2, 3, 4, 5};
auto it = v.begin();

// 执行可能使迭代器失效的操作
v.resize(100);

// 重新获取迭代器
it = v.begin();  // 重要:重新赋值

// 现在可以安全使用
while(it != v.end()) 
{
    cout << *it << " ";
    ++it;
}
方案2:使用索引替代迭代器
cpp 复制代码
vector<int> v = {1, 2, 3, 4, 5};
size_t index = 0;  // 使用索引而不是迭代器

v.resize(100);  // 扩容

// 索引不受影响(只要不超过size)
if(index < v.size()) 
{
    cout << v[index];  // 安全
}
方案3:使用返回值更新迭代器
cpp 复制代码
vector<int> v = {1, 2, 3, 4, 5};
auto it = v.begin();

// insert返回新插入元素的位置
it = v.insert(it, 0);  // 在开头插入0,it更新为指向新元素

// erase返回被删除元素的下一个位置
it = v.erase(it);  // 删除it指向的元素,it更新为下一个元素

3.7 总结:迭代器失效的核心原因

操作 失效原因 影响范围
resize/reserve 内存重新分配 所有迭代器、指针、引用失效
push_back 可能触发内存重新分配 如果扩容,所有迭代器失效
insert 1. 可能扩容 2. 元素移动 1. 所有迭代器失效 2. 部分迭代器指向错误元素
erase 元素向前移动 被删除及之后位置的迭代器失效
assign/clear 完全重新分配或清空 所有迭代器失效

根本原因:vector保证元素在内存中连续存储。为了维持这种连续性,当需要更多空间或插入删除元素时,必须移动元素或重新分配内存,这会导致原有的地址引用失效。

重要原则:在修改vector容量的操作后,永远假设所有现有的迭代器、指针和引用都已失效,除非操作文档明确说明它们保持有效。


4. vector在算法题中的应用

4.1 只出现一次的数字

cpp 复制代码
class Solution 
{
public:
    int singleNumber(vector<int>& nums) 
    {
        int result = 0;
        for(int num : nums) 
        {
            result ^= num;  // 利用异或性质
        }
        return result;
    }
};

4.2 杨辉三角

cpp 复制代码
class Solution 
{
public:
    vector<vector<int>> generate(int numRows)
    {
        vector<vector<int>> triangle(numRows);
        
        for(int i = 0; i < numRows; ++i) 
        {
            triangle[i].resize(i + 1, 1);  // 每行有i+1个元素,初始化为1
            
            // 计算中间元素
            for(int j = 1; j < i; ++j) 
            {
                triangle[i][j] = triangle[i-1][j-1] + triangle[i-1][j];
            }
        }
        
        return triangle;
    }
};

4.3 删除排序数组中的重复项

cpp 复制代码
class Solution 
{
public:
    int removeDuplicates(vector<int>& nums) 
    {
        if(nums.empty()) return 0;
        
        int k = 0;
        for(int i = 1; i < nums.size(); ++i) 
        {
            if(nums[i] != nums[k]) 
            {
                nums[++k] = nums[i];
            }
        }
        
        return k + 1;
    }
};

5. vector的模拟实现

5.1 vector的基本框架

cpp 复制代码
namespace my 
{
    template<class T>
    class vector 
    {
    private:
        T* _start;          // 指向数据起始位置
        T* _finish;         // 指向最后一个元素的下一个位置
        T* _end_of_storage; // 指向存储空间末尾
        
    public:
        // 构造函数
        vector() : _start(nullptr), _finish(nullptr), _end_of_storage(nullptr) {}
        
        // 析构函数
        ~vector() 
        {
            delete[] _start;
            _start = _finish = _end_of_storage = nullptr;
        }
        
        // 容量相关
        size_t size() const { return _finish - _start; }
        size_t capacity() const { return _end_of_storage - _start; }
        bool empty() const { return _start == _finish; }
        
        // 元素访问
        T& operator[](size_t pos) { return _start[pos]; }
        const T& operator[](size_t pos) const { return _start[pos]; }
        
        // 迭代器
        T* begin() { return _start; }
        T* end() { return _finish; }
        const T* begin() const { return _start; }
        const T* end() const { return _finish; }
        
        // 预留空间
        void reserve(size_t n) 
        {
            if(n > capacity()) 
            {
                T* new_start = new T[n];
                size_t sz = size();
                
                // 深拷贝元素
                for(size_t i = 0; i < sz; ++i) 
                {
                    new_start[i] = _start[i];
                }
                
                delete[] _start;
                _start = new_start;
                _finish = _start + sz;
                _end_of_storage = _start + n;
            }
        }
        
        // 添加元素
        void push_back(const T& val) 
        {
            if(_finish == _end_of_storage) 
            {
                reserve(capacity() == 0 ? 4 : capacity() * 2);
            }
            *_finish = val;
            ++_finish;
        }
        
        // 交换数据
        void swap(vector<T>& v)
		{
			std::swap(_start, v._start);
			std::swap(_finish, v._finish);
			std::swap(_end_of_storage, v._end_of_storage);
		}
		
		// 赋值运算符重载
		vector<T>& operator=(vector<T> v)
		{
			swap(v);
			return *this;
		}

		// 插入数据
		iterator insert(iterator pos, const T& x)
		{
			assert(pos >= _start);
			assert(pos <= _finish);

			// 扩容
			if (_finish == _end_of_storage)
			{
				size_t len = pos - _start;
				reserve(capacity() == 0 ? 4 : capacity() * 2);
				pos = _start + len;
			}

			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				--end;
			}
			*pos = x;

			++_finish;

			return pos;
		}

		// 删除数据
		iteraror erase(iterator pos)
		{
			assert(pos >= _start);
			assert(pos < _finish);

			iterator it = pos + 1;
			while (it != end())
			{
				*(it - 1) = *it;
				++it;
			}
			--_finish;
			return pos;
		}
    };
}

5.2 使用memcpy的问题

在实现vector的扩容时,不能简单地使用memcpy进行内存拷贝:

cpp 复制代码
// 错误示例:使用memcpy拷贝自定义类型
void reserve(size_t n) 
{
    if(n > capacity()) 
    {
        T* new_start = new T[n];
        
        // 错误:对于管理资源的自定义类型,memcpy是浅拷贝
        memcpy(new_start, _start, size() * sizeof(T));
        
        delete[] _start;  // 释放旧空间,可能导致资源重复释放
        _start = new_start;
        // ... 其他更新
    }
}

问题分析

  1. memcpy是二进制内存拷贝,执行的是浅拷贝
  2. 对于管理资源的自定义类型(如string),浅拷贝会导致多个对象共享同一资源
  3. 释放旧空间时,资源被释放,新空间中的对象指向已释放的资源

正确做法:使用深拷贝

cpp 复制代码
// 正确做法:使用深拷贝
void reserve(size_t n) 
{
    if(n > capacity()) 
    {
        T* new_start = new T[n];
        size_t sz = size();
        
        // 深拷贝:调用元素的拷贝构造函数或赋值运算符
        for(size_t i = 0; i < sz; ++i) 
        {
            new_start[i] = _start[i];  // 调用T的赋值运算符或拷贝构造函数
        }
        
        // 析构旧元素
        for(size_t i = 0; i < sz; ++i) 
        {
            _start[i].~T();  // 显式调用析构函数
        }
        
        delete[] _start;
        _start = new_start;
        _finish = _start + sz;
        _end_of_storage = _start + n;
    }
}

6. 动态二维数组的实现

vector可以方便地实现动态二维数组:

cpp 复制代码
// 创建n行的二维数组
vector<vector<int>> createMatrix(int n) 
{
    vector<vector<int>> matrix(n);
    
    for(int i = 0; i < n; ++i) 
    {
        matrix[i].resize(i + 1, 1);  // 第i行有i+1个元素
    }
    
    return matrix;
}

// 访问二维数组
void printMatrix(const vector<vector<int>>& matrix) 
{
    for(int i = 0; i < matrix.size(); ++i) 
    {
        for(int j = 0; j < matrix[i].size(); ++j) 
        {
            cout << matrix[i][j] << " ";
        }
        cout << endl;
    }
}

7. 使用vector的最佳实践

  1. 预分配空间 :如果知道大概需要多少元素,使用reserve()预分配空间,避免频繁扩容。

  2. 谨慎使用迭代器:在可能修改容量的操作后,不要使用旧的迭代器。

  3. 选择合适的访问方式

    • 随机访问:使用operator[]at()
    • 遍历:使用范围for循环或迭代器
    • 性能敏感:考虑使用指针访问
  4. 注意元素的拷贝成本:存储大对象时,考虑存储指针或使用移动语义。

  5. 善用swap :使用swap()快速清空vector或交换两个vector的内容。

cpp 复制代码
// 快速清空vector(释放内存)
vector<int> v(1000000);
vector<int>().swap(v);  // 清空v并释放内存

// C++11更简洁的方式
v.clear();
v.shrink_to_fit();  // 请求释放未使用的内存

总结

vector是C++中最重要、最常用的容器之一。掌握vector不仅需要了解其基本用法,还需要深入理解其内部工作原理,特别是迭代器失效、内存管理和性能特征。通过合理使用vector,可以编写出高效、安全的C++代码。

在实际开发中,应根据具体需求选择合适的容器。vector适合需要频繁随机访问、尾部插入删除的场景。如果需要频繁在中间插入删除,可能需要考虑listdeque;如果需要快速查找,可能需要考虑setmap

通过深入理解vector,我们不仅能够更好地使用这个容器,还能够学习到C++内存管理、模板编程、异常安全等高级主题,为成为更优秀的C++程序员打下坚实基础。


希望这篇长文对你有帮助!如果觉得不错,欢迎点赞、收藏、分享~


作者:铅笔小新z

日期:2025 年 12 月 16 日

目标:一篇讲透 C++ vector,从使用到底层实现。

相关推荐
好好沉淀5 小时前
开发过程中动态 SQL 中where 1=1的作用是什么
java·服务器·开发语言·数据库·sql
froginwe115 小时前
Bootstrap4 输入框组
开发语言
listhi5205 小时前
matlab大规模L1范数优化问题
开发语言·matlab
傅里叶的耶5 小时前
C++ Primer Plus(第6版):第二章 开始学习C++
开发语言·c++·学习
雾岛听蓝5 小时前
C++ 类和对象(二):默认成员函数详解
开发语言·c++
爱吃大芒果5 小时前
Flutter 动画实战:隐式动画、显式动画与自定义动画控制器
开发语言·javascript·flutter·ecmascript·gitcode
郝学胜-神的一滴5 小时前
OpenGL中的glDrawArrays函数详解:从基础到实践
开发语言·c++·程序人生·算法·游戏程序·图形渲染
李白你好5 小时前
Bypass_Webshell webshell编码工具 支持 jsp net php asp编码免杀
开发语言·php
feifeigo1235 小时前
C#中实现控件拖动功能
开发语言·c#