C++ 容器类 :动态数组的高效实现
在 C++ 的世界里,数组是基础数据结构,但它的“固定大小”特性常常让人束手无策。当你需要存储用户输入的学生成绩、处理从文件读取的配置项,或者动态管理一组数据时,静态数组就显得力不从心了。这时,C++ 标准库中的 vector 容器类便成为解决这类问题的首选方案。它既保留了数组的高效访问特性,又具备动态扩容的能力,是现代 C++ 编程中不可或缺的工具。
vector 是 C++ 标准模板库(STL)中最常用的容器之一,它本质上是一个动态数组。你可以把它想象成一个“会自己长大的数组”——你不需要事先知道它要多大,它会根据你添加的数据自动调整容量。这种灵活性让开发效率大幅提升,尤其适合处理不确定长度的数据集合。
本文将带你从零开始,深入理解 vector 的核心机制、常用操作和实际应用,帮助你在项目中更自信地使用它。
创建数组与初始化
在 C++ 中,传统数组的大小必须在编译时确定,例如 int arr[10];。而 vector 则允许你在运行时动态决定大小。这为处理不确定数据量的场景提供了极大的便利。
#include <vector>
#include <iostream>
int main() {
// 方法1:创建一个空的 vector,类型为 int
std::vector<int> vec1;
// 方法2:创建一个大小为 5 的 vector,所有元素初始化为 0
std::vector<int> vec2(5);
// 方法3:创建一个大小为 3 的 vector,所有元素初始化为 10
std::vector<int> vec3(3, 10);
// 方法4:使用初始化列表创建 vector(C++ 11 起支持)
std::vector<int> vec4 = {1, 2, 3, 4, 5};
// 输出验证
std::cout << "vec1 size: " << vec1.size() << std::endl; // 输出 0
std::cout << "vec2 size: " << vec2.size() << std::endl; // 输出 5
std::cout << "vec3 size: " << vec3.size() << std::endl; // 输出 3
std::cout << "vec4 size: " << vec4.size() << std::endl; // 输出 5
return 0;
}
注意:
std::vector是模板类,必须指定元素类型。例如std::vector<int>表示存储整数的向量。
在初始化时,你可以选择不传参数(创建空容器)、传入大小(自动填充默认值)、传入大小和初始值,或者直接用 {} 初始化列表。这种方式让 vector 在创建阶段就表现出极强的适应性。
基本操作:添加、访问与遍历
vector 提供了丰富的方法来管理数据。最常用的操作包括添加元素、访问元素、获取大小和遍历。
添加元素
使用 push_back() 方法可以将元素添加到容器末尾。这是最常用的插入方式。
#include <vector>
#include <iostream>
int main() {
std::vector<int> numbers;
// 添加元素到末尾
numbers.push_back(10);
numbers.push_back(20);
numbers.push_back(30);
std::cout << "当前元素个数: " << numbers.size() << std::endl; // 输出 3
return 0;
}
push_back() 是线程安全的吗?—— 不是。在多线程环境下,多个线程同时调用 push_back() 可能导致数据竞争。如果需要线程安全,应使用互斥锁保护。
访问元素
你可以通过下标访问或使用 at() 方法访问元素。
std::vector<int> scores = {85, 92, 78, 96};
// 使用下标访问(不检查越界)
std::cout << "第一科成绩: " << scores[0] << std::endl; // 输出 85
// 使用 at() 方法访问(会检查越界,越界时抛出异常)
try {
std::cout << "第四科成绩: " << scores.at(3) << std::endl; // 输出 96
std::cout << "第五科成绩: " << scores.at(4) << std::endl; // 抛出 out_of_range 异常
} catch (const std::out_of_range& e) {
std::cout << "访问越界了!" << std::endl;
}
建议:在调试阶段使用
at()方法,能帮你尽早发现越界错误。发布版本中可改用[]以获得更好性能。
遍历元素
vector 支持多种遍历方式:
- 传统 for 循环(适合需要索引的场景)
for (size_t i = 0; i < numbers.size(); ++i) {
std::cout << "第 " << i + 1 << " 个元素: " << numbers[i] << std::endl;
}
- 范围 for 循环(推荐,简洁且安全)
for (const int& value : numbers) {
std::cout << value << " ";
}
std::cout << std::endl;
- 使用迭代器(适合复杂操作,如删除元素)
for (auto it = numbers.begin(); it != numbers.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;
元素访问对比表
| 访问方式 | 是否检查越界 | 性能 | 是否推荐 |
|---|---|---|---|
[] |
否 | 高 | 一般场景推荐 |
at() |
是 | 较低 | 调试阶段推荐 |
| 迭代器 | 否(需手动判断) | 高 | 复杂操作推荐 |
容量与大小管理
vector 有两个关键概念:大小(size) 和 容量(capacity)。
size():当前实际存储的元素个数。capacity():当前分配的内存空间能容纳的元素个数。
当 size() 达到 capacity() 时,vector 会自动扩容(通常是原容量的 1.5 到 2 倍),并复制所有元素。这个过程会带来性能开销,因此在已知数据量时,可提前分配容量。
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec;
std::cout << "初始大小: " << vec.size() << std::endl;
std::cout << "初始容量: " << vec.capacity() << std::endl;
// 添加 10 个元素
for (int i = 0; i < 10; ++i) {
vec.push_back(i);
}
std::cout << "添加后大小: " << vec.size() << std::endl;
std::cout << "添加后容量: " << vec.capacity() << std::endl;
// 预分配容量,避免频繁扩容
vec.reserve(100); // 提前分配 100 个元素的空间
std::cout << "reserve 后容量: " << vec.capacity() << std::endl;
return 0;
}
小技巧:如果你知道要存储 1000 个整数,建议在开始时调用
reserve(1000),这样可以避免中间多次扩容带来的性能损耗。
常用方法与实用案例
vector 提供了大量实用方法,掌握它们能极大提升编码效率。
常用方法列表
| 方法 | 说明 |
|---|---|
clear() |
清空所有元素 |
empty() |
判断是否为空 |
resize(n) |
调整大小为 n,不足补默认值 |
shrink_to_fit() |
释放多余内存(建议使用) |
insert(pos, value) |
在指定位置插入元素 |
erase(pos) |
删除指定位置元素 |
swap(other) |
与另一个 vector 交换内容 |
实际案例:学生成绩管理系统
假设你需要管理一个班级的学生成绩,支持添加、删除、查找和排序。
#include <vector>
#include <string>
#include <algorithm>
#include <iostream>
struct Student {
std::string name;
double score;
};
void addStudent(std::vector<Student>& students, const std::string& name, double score) {
students.push_back({name, score});
}
void removeStudent(std::vector<Student>& students, const std::string& name) {
auto it = std::find_if(students.begin(), students.end(),
[&name](const Student& s) { return s.name == name; });
if (it != students.end()) {
students.erase(it);
std::cout << "已删除学生: " << name << std::endl;
} else {
std::cout << "未找到学生: " << name << std::endl;
}
}
void displayStudents(const std::vector<Student>& students) {
std::cout << "\n当前学生列表:\n";
for (const auto& s : students) {
std::cout << s.name << ": " << s.score << std::endl;
}
}
void sortStudentsByScore(std::vector<Student>& students) {
std::sort(students.begin(), students.end(),
[](const Student& a, const Student& b) {
return a.score > b.score; // 降序排列
});
}
int main() {
std::vector<Student> classA;
addStudent(classA, "张三", 88.5);
addStudent(classA, "李四", 92.0);
addStudent(classA, "王五", 76.5);
displayStudents(classA);
sortStudentsByScore(classA);
std::cout << "\n按成绩排序后:\n";
displayStudents(classA);
removeStudent(classA, "王五");
displayStudents(classA);
return 0;
}
这个例子展示了 vector 在实际项目中的强大能力:支持自定义类型、结合算法库进行排序、动态增删元素。
总结与进阶建议
C++ 容器类 <vector> 是现代 C++ 编程中最为基础且核心的工具之一。它不仅解决了静态数组的大小限制问题,还提供了丰富的接口和高效的内存管理机制。无论是简单的数据存储,还是复杂的算法实现,vector 都能胜任。
通过本文的学习,你应该已经掌握了:
- 如何创建和初始化
vector - 如何添加、访问和遍历元素
- 容量与大小的区别及优化技巧
- 常用方法的实际应用
- 如何结合 STL 算法进行高效编程
在后续学习中,建议深入研究 vector 的内存布局、拷贝语义(深拷贝 vs 浅拷贝)、移动语义(move semantics)以及与 std::move 的配合使用。这些知识将帮助你写出更高效、更安全的 C++ 代码。
记住:vector 虽强大,但并非万能。对于频繁插入/删除中间元素的场景,std::list 或 std::deque 可能更合适。选择合适的容器,是写出高质量 C++ 代码的关键一步。