C++ vector 深度解析:从原理到实战的全方位指南

一、引言

在 C++ 编程中,我们经常需要处理一组数据。比如,你想存储一个班级所有学生的成绩,或者保存用户输入的一组数字。最容易想到的方法是使用数组:

cpp 复制代码
int scores[100]; // 定义一个能存储100个成绩的数组

但数组有两个明显的缺点:

  1. 大小固定:一旦定义,无法动态调整。如果实际学生超过 100 人,数组就不够用了。
  2. 内存管理麻烦:需要手动分配和释放内存(在使用动态数组时)。

vector 就是为解决这些问题而生的! 它是 C++ 标准库提供的动态数组,可以自动管理内存,还支持各种方便的操作

二、vector 基础:快速上手

2.1 如何使用 vector?

要使用 vector,首先需要包含头文件:

cpp 复制代码
#include <vector>
using namespace std; // 为了简化代码,使用标准命名空间

2.2 创建 vector 对象

vector 的使用非常灵活,可以根据需要创建不同类型和初始值的 vector:

cpp 复制代码
// 1. 创建空的 vector(最常用)
vector<int> v1; // 存储整数的 vector

// 2. 创建包含 n 个元素的 vector
vector<double> v2(5); // 创建包含 5 个 double 的 vector,初始值为 0.0

// 3. 创建包含 n 个指定值的 vector
vector<string> v3(3, "hello"); // 创建包含 3 个 "hello" 的 vector

// 4. 使用现有数组或其他 vector 初始化
int arr[] = {1, 2, 3, 4};
vector<int> v4(arr, arr + 4); // 使用数组初始化

vector<int> v5(v4); // 使用另一个 vector 初始化

2.3 常用操作:增删查改

下面是 vector 最常用的操作,新手掌握这些就可以应对大部分场景:

cpp 复制代码
vector<int> v; // 创建空的 vector

// 1. 添加元素(最常用)
v.push_back(10); // 在尾部添加元素 10
v.push_back(20); // 在尾部添加元素 20
v.push_back(30); // 在尾部添加元素 30
// 此时 v 中的元素是:[10, 20, 30]

// 2. 访问元素
cout << v[0] << endl; // 输出第 0 个元素:10
cout << v.at(1) << endl; // 输出第 1 个元素:20(更安全,会检查越界)

// 3. 修改元素
v[0] = 100; // 将第 0 个元素修改为 100
// 此时 v 中的元素是:[100, 20, 30]

// 4. 删除元素
v.pop_back(); // 删除最后一个元素
// 此时 v 中的元素是:[100, 20]

// 5. 获取 vector 大小
cout << v.size() << endl; // 输出 2(当前有 2 个元素)

// 6. 判断 vector 是否为空
if (v.empty()) {
    cout << "vector 为空" << endl;
} else {
    cout << "vector 不为空" << endl;
}

// 7. 清空 vector
v.clear(); // 删除所有元素
cout << v.size() << endl; // 输出 0(vector 现在为空)

三、如何遍历 vector

遍历 vector 中的元素是常见需求,有多种方法可以实现:

3.1 使用下标遍历(类似数组)

cpp 复制代码
vector<int> v = {1, 2, 3, 4, 5};
for (int i = 0; i < v.size(); i++) {
    cout << v[i] << " ";
}
// 输出:1 2 3 4 5

3.2 使用迭代器遍历(更通用)

迭代器是一种类似指针的对象,用于访问容器中的元素。所有标准库容器都支持迭代器:

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

// 正向迭代器
for (auto it = v.begin(); it != v.end(); ++it) {
    cout << *it << " "; // 使用 *it 访问当前元素
}
// 输出:1 2 3 4 5

// 反向迭代器(从后往前遍历)
for (auto rit = v.rbegin(); rit != v.rend(); ++rit) {
    cout << *rit << " ";
}
// 输出:5 4 3 2 1

3.3 使用范围 for 循环(C++11 及以后,最简单)

cpp 复制代码
vector<int> v = {1, 2, 3, 4, 5};
for (int num : v) {
    cout << num << " ";
}
// 输出:1 2 3 4 5

// 如果需要修改元素,可以使用引用
for (int& num : v) {
    num *= 2; // 将每个元素乘以 2
}
// 此时 v 中的元素是:[2, 4, 6, 8, 10]

四、vector 的高级用法

4.1 存储自定义类型

vector 可以存储任何类型,包括自定义的类或结构体:

cpp 复制代码
struct Student {
    string name;
    int age;
};

vector<Student> students;

// 添加元素
students.push_back({"Alice", 20});
students.push_back({"Bob", 21});

// 访问元素
for (const auto& s : students) {
    cout << s.name << " " << s.age << endl;
}
// 输出:
// Alice 20
// Bob 21

4.2 二维 vector

可以创建多维 vector,最常见的是二维 vector(类似二维数组):

cpp 复制代码
// 创建一个 3x4 的矩阵,初始值为 0
vector<vector<int>> matrix(3, vector<int>(4, 0));

// 赋值
matrix[0][0] = 1;
matrix[1][2] = 5;

// 遍历
for (const auto& row : matrix) {
    for (int num : row) {
        cout << num << " ";
    }
    cout << endl;
}
// 输出:
// 1 0 0 0
// 0 0 5 0
// 0 0 0 0

4.3 排序和查找

vector没有排序和查找的函数,但是可以结合标准库算法进行排序和查找:

cpp 复制代码
#include <algorithm> // 需要包含算法库

vector<int> v = {3, 1, 4, 1, 5, 9};

// 排序
sort(v.begin(), v.end()); // 升序排序
// v 现在是:[1, 1, 3, 4, 5, 9]

// 查找
auto it = find(v.begin(), v.end(), 4);
if (it != v.end()) {
    cout << "找到元素 4,位置是:" << (it - v.begin()) << endl;
} else {
    cout << "未找到元素 4" << endl;
}

五、vector 底层原理与内存管理

5.1 核心数据结构

vector 的底层通过三个指针实现动态数组的管理(不同平台实现不同):

  • T* _start:指向数据存储区的起始位置。
  • T* _finish:指向最后一个有效元素的下一个位置。
  • T* _end_of_storage:指向已分配内存空间的末尾位置。

这三个指针构成了 vector 的内存模型:

cpp 复制代码
template<typename T>
class vector {
private:
    T* _start;
    T* _finish;
    T* _end_of_storage;
};

5.2 扩容机制详解

vector 的最大优势之一是可以动态扩容,不需要我们手动管理内存。但它是如何做到的呢?

5.2.1 容量 vs 大小
  • 大小(size):当前实际存储的元素个数。
  • 容量(capacity):当前分配的内存能够容纳的元素个数。

vector 空间不足时,会触发扩容:

  1. 计算新容量:不同编译器采用不同策略:

    • GCC(SGI STL):以 2 倍扩容(如容量为 4 时,扩容后为 8)。
    • MSVC(VS):以 1.5 倍扩容(如容量为 4 时,扩容后为 6)。
    • CLANG:与 MSVC 类似,采用 1.5 倍扩容。
  2. 分配新内存 :使用 operator new 分配更大的内存块。

  3. 数据迁移 :通过 uninitialized_copy 将旧数据迁移到新空间。

  4. 释放旧内存 :调用 operator delete 释放旧内存块。

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v;
    size_t cap = v.capacity();
    cout << "初始容量:" << cap << endl; // 0
    for (int i = 0; i < 10; ++i) {
        v.push_back(i);
        if (cap != v.capacity()) {
            cap = v.capacity();
            cout << "扩容后容量:" << cap << endl;
            // GCC 输出:1, 2, 4, 8, 16...;VS 输出:1, 2, 3, 4, 6...
        }
    }
    return 0;
}
5.2.2 如何避免不必要的扩容?

如果事先知道需要存储多少元素,可以使用 reserve() 方法预分配内存:

cpp 复制代码
vector<int> v;
v.reserve(100); // 预分配 100 个元素的空间

for (int i = 0; i < 100; i++) {
    v.push_back(i); // 不会触发扩容,效率更高
}

扩容策略的数学分析

  • 若扩容因子为 m,插入 n 个元素的均摊时间复杂度为 O(n),因为每次扩容复制的元素数量呈几何级数增长,总和为 O(n)

代码示例:

观察扩容行为

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v;
    size_t cap = v.capacity();
    cout << "初始容量:" << cap << endl; // 0
    for (int i = 0; i < 10; ++i) {
        v.push_back(i);
        if (cap != v.capacity()) {
            cap = v.capacity();
            cout << "扩容后容量:" << cap << endl;
            // GCC 输出:1, 2, 4, 8, 16...;VS 输出:1, 2, 3, 4, 6...
        }
    }
    return 0;
}

5.3 模拟实现关键函数

5.3.1 拷贝构造函数
cpp 复制代码
template<typename InputIterator>
vector(InputIterator first, InputIterator last)
{
	while (first != last)
	{
		push_back(*first);
		first++;
	}
}
vector(const vector<T>& v)
{
    vector<T> tmp(v.begin(), v.end()); // 利用迭代器区间构造临时对象
    swap(tmp); // 交换资源,实现深拷贝
}
vector(const vector<T>& v)		
{
    reserve(v.capacity());
	for (auto& e : v)
	{
		push_back(e);
	}
}
5.3.2 赋值运算符重载
cpp 复制代码
vector<T>& operator=(vector<T> tmp) { // 参数为值传递,自动调用拷贝构造
    swap(tmp); // 交换资源,实现异常安全
    return *this;
}

六、迭代器失效场景与解决方案

6.1 导致迭代器失效的操作

  1. 空间重新分配resizereserveinsertpush_backassign 等操作可能触发扩容,导致原有迭代器失效。
  2. 元素删除erase 操作会使被删除位置之后的迭代器失效。
  3. 容器交换swap 操作会交换两个 vector 的内容,导致原迭代器指向其他容器。
  4. 容器清空clear 操作会删除所有元素,迭代器失效。

6.2 典型错误与修正

6.2.1 错误示例:未处理扩容导致的迭代器失效
cpp 复制代码
vector<int> v = {1, 2, 3, 4};
auto it = v.begin();
v.insert(it, 0); // 可能触发扩容
cout << *it << endl; // 未定义行为,it 已失效
6.2.2 正确做法:重新获取迭代器
复制代码
vector<int> v = {1, 2, 3, 4};
auto it = v.begin();
it = v.insert(it, 0); // insert 返回新元素的迭代器
cout << *it << endl; // 正确输出 0

6.3 安全遍历与删除

6.3.1 错误示例:未更新迭代器
cpp 复制代码
vector<int> v = {1, 2, 3, 4, 5};
for (auto it = v.begin(); it != v.end(); ++it) {
    if (*it % 2 == 0) {
        v.erase(it); // erase 后 it 失效
    }
}
6.3.2 正确做法:使用 erase 的返回值
cpp 复制代码
vector<int> v = {1, 2, 3, 4, 5};
for (auto it = v.begin(); it != v.end(); ) {
    if (*it % 2 == 0) {
        it = v.erase(it); // 更新 it 到下一个有效位置
    } else {
        ++it;
    }
}

七、常见问题与陷阱

7.1 浅拷贝问题

  • 场景 :当 vector 存储包含动态资源的自定义类型时,默认拷贝构造函数会导致浅拷贝。
  • 解决方案
    1. 为自定义类型实现深拷贝构造函数和赋值运算符。
    2. 使用 vector 的默认深拷贝机制(基于元素的拷贝构造函数)。

7.2 迭代器失效与编译器差异

  • VS 编译器:对迭代器失效检测严格,访问失效迭代器可能直接崩溃。
  • GCC 编译器:检测较宽松,但仍可能导致未定义行为。
  • 建议:避免依赖编译器行为,严格处理迭代器失效。

7.3 内存泄漏

  • 场景 :使用 reserve 预分配空间后,直接通过下标访问未初始化的元素。

  • 示例

    cpp 复制代码
    vector<int> v;
    v.reserve(10);
    v[5] = 42; // 未定义行为,未初始化的内存
  • 修正 :使用 resize 初始化元素:

    cpp 复制代码
    vector<int> v;
    v.resize(10);
    v[5] = 42; // 合法

八、实战案例:高效算法实现

8.1 杨辉三角

cpp 复制代码
vector<vector<int>> generate(int numRows) {
    vector<vector<int>> vv(numRows);
    for(int i=0;i<numRows;i++)
    {
        vv[i].resize(i+1,1);
    }
    for(int i=2;i<numRows;i++)
    {
        for(int j=1;j<i;j++)
        {
            vv[i][j]=vv[i-1][j]+vv[i-1][j-1];
        }
    }
    return vv;
}

8.2 只出现一次的数字

cpp 复制代码
int singleNumber(vector<int>& nums) {
    int result = 0;
    for (int num : nums) {
        result ^= num; // 异或相同数得 0,0 异或任意数得原数
    }
    return result;
}

九、总结与最佳实践

7.1 核心优势

  • 随机访问高效:支持 O (1) 时间复杂度的下标访问。
  • 动态扩容:自动管理内存,避免手动分配 / 释放。
  • 丰富接口 :提供 push_backinserterase 等便捷操作。

7.2 最佳实践

  1. 预分配空间 :使用 reserve 避免频繁扩容。
  2. 优先使用 emplace_back:减少拷贝 / 移动开销。
  3. 处理迭代器失效 :在 insert/erase 后更新迭代器。

通过深入理解 vector 的底层原理和正确使用其接口,开发者可以高效地处理动态数据,避免常见错误,并在实际项目中充分发挥其性能优势。建议结合模拟实现和算法练习(如 LeetCode 题目)进一步巩固知识。

附录

模拟实现(代码)

cpp 复制代码
//vector.h
#include<iostream>
#include<assert.h>
namespace My_vector
{
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;
		typedef const T* const_iterator;
		vector()
		{ }
		vector(size_t n, const T& val = T())
		{
			reserve(n);
			for (size_t i = 0; i < n; i++)
			{
				push_back(val);
			}
		}
		vector(int n, const T& val = T())
		{
			reserve(n);
			for (size_t i = 0; i < n; i++)
			{
				push_back(val);
			}
		}
		vector(const vector<T>& v)
		{
			reserve(v.capacity());
			for (auto& e : v)
			{
				push_back(e);
			}
		}
		template<typename InputIterator>
		vector(InputIterator first, InputIterator last)
		{
			while (first != last)
			{
				push_back(*first);
				first++;
			}
		}
		vector(std::initializer_list<T> il)
		{
			reserve(il.size());
			for (auto& e : il)
			{
				push_back(e);
			}
		}
		~vector()
		{
			if (_start)
			{
				delete[] _start;
				_start = _finish = _end_of_storage = nullptr;
			}
		}
		iterator begin()
		{
			return _start;
		}
		iterator end()
		{
			return _finish;
		}
		const_iterator begin()const
		{
			return _start;
		}
		const_iterator end()const
		{
			return _finish;
		}
		size_t capacity()const
		{
			return _end_of_storage - _start;
		}
		size_t size()const
		{
			return _finish - _start;
		}
		void reserve(size_t n)
		{
			if (n > capacity())
			{
				size_t old_size = size();
				iterator tmp = new T[n];
				if (_start)
				{
					for (size_t i = 0; i < old_size ;i++)
					{
						tmp[i] = _start[i];
					}
					delete[] _start;
				}
				_start = tmp;
				_finish = tmp + old_size;
				_end_of_storage = tmp + n;
			}

		}
		void push_back(const T& val)
		{
			if (_finish == _end_of_storage)
			{
				size_t newcapacity = 0 == capacity() ? 4 : 2 * capacity();
				reserve(newcapacity);
			}
			*_finish = val;
			_finish++;
		}
		void pop_back()
		{
			assert(_finish > _start);
			--_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);
		}
		void resize(size_t n, const T& val=T())
		{
			if (n > size())
			{
				if (n > capacity())
				{
					reserve(n);
				}
				while (_finish != _end_of_storage)
				{
					push_back(val);
				}
			}
			else
			{
				_finish = _start + n;
			}
		}
		iterator insert(iterator pos,const T& val)
		{
			assert(pos >= _start);
			assert(pos <= _finish);
			if (_finish == _end_of_storage)
			{
				size_t len = pos - _start;
				size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newcapacity);
				pos = _start + len;
			}
			iterator it = _finish - 1;
			while (it >= pos)
			{
				*(it + 1) = *it;
				it--;
			}
			*pos = val;
			_finish++;
			return pos;
		}
		iterator erase(iterator pos)
		{
			assert(pos >= _start);
			assert(pos <= _finish);
			iterator it = pos;
			while (it < _finish)
			{
				*it = *(it + 1);
				it++;
			}
			_finish--;
			return pos;
		}
		T& operator[](size_t n)
		{
			assert(n < size());
			return _start[n];
		}
		vector<T>& operator=(vector<T> tmp)
		{
			swap(tmp);
			return *this;
		}
	private:
		iterator _start = nullptr;
		iterator _finish = nullptr;
		iterator _end_of_storage = nullptr;
	};
}
相关推荐
zhengddzz几秒前
解锁C++编辑距离:文本相似度的度量密码
c++
coding随想18 分钟前
JavaScript的三大核心组成:ECMAScript、DOM与BOM
开发语言·javascript·ecmascript
0xCC说逆向28 分钟前
Windows逆向工程提升之IMAGE_EXPORT_DIRECTORY
开发语言·数据结构·windows·安全·网络安全·pe结构·逆向工程
带电的小王28 分钟前
C++:动态刷新打印内容
开发语言·c++
贺函不是涵30 分钟前
【沉浸式求职学习day47】【JSP详解】
java·开发语言·学习
满怀10151 小时前
【Python正则表达式终极指南】从零到工程级实战
开发语言·python·正则表达式·自动化·文本处理·数据清晰
好学且牛逼的马1 小时前
#6 百日计划第六天 java全栈学习
算法
草莓熊Lotso1 小时前
【自定义类型-结构体】--结构体类型,结构体变量的创建和初始化,结构体内存对齐,结构体传参,结构体实现位段
c语言·开发语言·经验分享·笔记·其他
旋风菠萝1 小时前
八股--SSM(2)
java·开发语言·数据库·八股·八股文·复习
Christo31 小时前
LNCS-2009《Adaptive Sampling for $k$-Means Clustering》
人工智能·算法·机器学习·kmeans