在C++开发中,vector是最常用的序列容器之一,它封装了动态数组的能力,让开发者无需关心内存的申请与释放,就能安全地存储和管理一组相同类型的元素。无论是读取文件中的多行文本,还是缓存传感器上报的数值,vector都能以简洁的接口完成数据暂存与后续处理。

一、vector的基本初始化方式
使用vector前需要包含头文件<vector>,并通过std命名空间访问。最基础的空向量声明方式为std::vector<int> v;,此时容器内没有任何元素,容量也为0。当第一次调用push_back时,vector会向堆申请一小块连续内存。
除了空向量,还可以用指定长度和初始值的方式构造。例如std::vector<int> v(10, 0);会创建包含10个整数的向量,每个元素都被初始化为0。也可以用初始化列表直接赋值,如std::vector<int> v{1, 2, 3};,这种方式在C++11及之后非常直观。下面给出几种常见初始化代码:
#include <vector>
#include <iostream>
int main() {
// 空向量
std::vector<int> a;
// 长度为5,初值均为100
std::vector<int> b(5, 100);
// 初始化列表
std::vector<int> c{2, 4, 6, 8};
std::cout << "b size: " << b.size() << std::endl;
std::cout << "c[0]: " << c[0] << std::endl;
return 0;
}
从上面的代码可以看出,vector的构造非常灵活。需要注意的是,使用圆括号(5, 100)和花括号{5, 100}含义完全不同,前者表示五个元素都是100,后者表示两个元素分别为5和100,这是初学者容易混淆的地方。
二、向vector中存入数据
最常用的写入方法是push_back,它把元素追加到向量尾部。如果当前容量不足,vector会将原有数据拷贝到更大的新内存块中,并释放旧内存。这种自动扩容机制让用户不必预先估算准确大小,但频繁扩容会带来性能开销。
若已经知道大概要存多少数据,可以提前调用reserve预留容量,避免多次重新分配。C++11还提供了emplace_back,它直接在容器尾部构造对象,减少一次临时对象拷贝,对自定义类型尤其有用。以下示例展示不同写入方式:
#include <vector>
#include <string>
struct User {
int id;
std::string name;
User(int i, std::string n) : id(i), name(n) {}
};
int main() {
std::vector<User> users;
users.reserve(3); // 预留空间,减少扩容
users.push_back(User(1, "Tom"));
users.emplace_back(2, "Lucy"); // 直接构造,效率更高
return 0;
}
在上面的代码中,reserve只改变容量不改变大小,所以不能用下标访问未实际存入的元素。push_back和emplace_back都使size增加1,但emplace_back通过参数包直接转发给构造函数,避免了User临时对象的生成与拷贝。
三、访问与遍历存储的数据
vector支持随机访问,使用下标运算符[]或成员函数at都能获取元素。区别在于,[]不做边界检查,越界时行为未定义;at会抛出std::out_of_range异常,更安全。遍历时可用传统for循环、范围for或迭代器。
范围for语法最简洁,适合只读或顺序修改;若需要在遍历中删除元素,则必须使用迭代器并小心处理失效问题。下面的例子演示三种遍历:
#include <vector>
#include <iostream>
int main() {
std::vector<int> v{10, 20, 30};
// 下标遍历
for (size_t i = 0; i < v.size(); ++i) {
std::cout << v[i] << " ";
}
std::cout << std::endl;
// 范围for
for (int val : v) {
std::cout << val << " ";
}
std::cout << std::endl;
// 迭代器
for (auto it = v.begin(); it != v.end(); ++it) {
std::cout << *it << " ";
}
return 0;
}
实际项目中,如果只需要读取数据,优先用范围for,代码可读性最好。当容器可能为空时,先用empty判断再访问,可以防止at抛出异常或[]越界。
四、删除元素与迭代器失效
vector提供pop_back删除尾部元素,也可以用erase删除中间或指定区间元素。但必须注意,除尾部外任何位置的删除都会使被删位置之后的迭代器、引用和指针失效,因为后续元素会向前移动填补空位。
在循环中删除元素时,正确做法是让erase返回新的有效迭代器。下面代码展示如何删除所有偶数,同时避免迭代器失效导致的未定义行为:
#include <vector>
#include <iostream>
int main() {
std::vector<int> v{1, 2, 3, 4, 5, 6};
for (auto it = v.begin(); it != v.end(); ) {
if (*it % 2 == 0) {
it = v.erase(it); // erase返回下一个有效迭代器
} else {
++it;
}
}
for (int x : v) std::cout << x << " ";
return 0;
}
如果不接收erase的返回值而继续使用旧迭代器,程序可能崩溃或产生错误结果。此外,调用clear只会清空元素不改变容量,若想释放多余内存,可借助swap技巧或与空向量交换来真正回收空间。
五、vector存储自定义类型的注意事项
当vector存放的是自定义类对象时,该类最好具备可拷贝或可移动语义,因为扩容时容器会拷贝或移动原有元素。如果类中含有不能安全拷贝的资源(如文件句柄),应显式定义移动构造和移动赋值,或禁止拷贝改用指针容器。
对于多态对象,不要直接存基类实例,而应使用std::vector<std::unique_ptr<Base>>这类智能指针向量,既保证内存自动释放,又支持多态调用。如下示例简要说明:
#include <vector>
#include <memory>
class Shape {
public:
virtual void draw() const = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
public:
void draw() const override {}
};
int main() {
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>());
return 0;
}
使用智能指针向量能有效规避手动delete带来的泄漏风险,同时unique_ptr不可拷贝只能移动,迫使开发者明确资源所有权,是现代C++推荐的存储方式。
六、性能与使用建议总结
vector在尾部插入删除的时间复杂度为均摊O(1),随机访问为O(1),但中间插入删除为O(n)。因此它适合读多写少、主要在尾部追加的场景。如果频繁在头部或中间插入,应考虑list或deque。
合理运用reserve、emplace_back和移动语义,可以显著提升vector处理大量数据时的效率。理解迭代器失效规则,则能帮助写出健壮的增删逻辑,让vector真正成为可靠的数据存储工具。