
C++标准库中的
vector和
map是两种基石级的容器,它们分别提供了动态数组和键值对存储的功能,几乎是每个C++开发者日常工作中不可或缺的工具。简单来说,
vector就像一个可以自动扩容的数组,适合存储同类型元素的序列;而
map则是一个基于键(key)来查找值(value)的字典,并且它会自动保持键的有序性。理解并熟练运用它们,是编写高效、可维护C++代码的关键一步。
std::vector和
std::map使用示例
在我看来,C++标准库的容器设计得非常精妙,尤其是
vector和
map,它们几乎覆盖了我们日常编程中大部分数据组织的需求。
vector提供了一种连续存储的方式,这对于性能敏感的场景非常有利;而
map则以其键值对的直观性,让数据管理变得异常清晰。
std::vector:动态数组的瑞士军刀
std::vector本质上就是一个动态数组,它最吸引人的地方在于可以根据需要自动调整大小。当你需要一个元素序列,并且不确定最终会有多少个元素时,
vector几乎是第一选择。
#include <vector>
#include <string>
#include <iostream>
#include <map> // 即使是vector的例子,也可能需要其他头文件
int main() {
// 声明一个存储整数的vector
std::vector<int> numbers;
// 添加元素到vector的末尾
numbers.push_back(10);
numbers.push_back(20);
numbers.push_back(30);
numbers.push_back(40);
// 访问元素
std::cout << "第一个元素: " << numbers[0] << std::endl; // 可能会越界,不检查边界
std::cout << "第二个元素 (安全访问): " << numbers.at(1) << std::endl; // at()会进行边界检查,越界抛出异常
// 遍历vector
std::cout << "所有元素 (范围for循环): ";
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;
// 使用迭代器遍历
std::cout << "所有元素 (迭代器): ";
for (std::vector<int>::iterator it = numbers.begin(); it != numbers.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;
// 获取vector大小和容量
std::cout << "Vector大小: " << numbers.size() << std::endl;
std::cout << "Vector容量: " << numbers.capacity() << std::endl; // 容量通常 >= 大小
// 插入元素到指定位置 (效率较低,因为可能需要移动后续所有元素)
numbers.insert(numbers.begin() + 2, 25); // 在索引2处插入25
std::cout << "插入25后的Vector: ";
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;
// 删除元素 (同样效率较低)
numbers.erase(numbers.begin() + 0); // 删除第一个元素
std::cout << "删除第一个元素后的Vector: ";
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;
// 清空vector
numbers.clear();
std::cout << "清空后Vector大小: " << numbers.size() << std::endl;
// 预留空间,避免频繁重新分配
std::vector<std::string> names;
names.reserve(100); // 预留100个string的空间
for (int i = 0; i < 50; ++i) {
names.push_back("Name_" + std::to_string(i));
}
std::cout << "预留空间后Vector容量: " << names.capacity() << std::endl;
return 0;
} std::map:有序键值对的典范
std::map提供了一种将键(Key)映射到值(Value)的机制,而且它最大的特点就是所有元素都会根据键自动排序。这意味着当你遍历
map时,你会得到一个按键升序排列的序列。这在需要快速查找、并且保持数据有序的场景下非常有用。
#include <map>
#include <string>
#include <iostream>
// vector的例子已经包含了iostream和string
// main函数可以拆分或合并,这里为了演示方便,放在同一个main函数里
// 实际使用中,通常会封装在不同的函数或类中
// int main() { // 如果是单独的map例子,可以这样开始
// 声明一个存储string键和int值的map
std::map<std::string, int> ages;
// 插入元素
ages["Alice"] = 30; // 如果键不存在,则插入;如果存在,则更新值
ages["Bob"] = 25;
ages.insert({"Charlie", 35}); // 另一种插入方式
ages.insert(std::make_pair("David", 28)); // 也可以用std::make_pair
// 访问元素
std::cout << "Alice的年龄: " << ages["Alice"] << std::endl;
// 注意:使用[]访问一个不存在的键会默认插入一个新元素,其值会被默认初始化
// std::cout << "Eve的年龄: " << ages["Eve"] << std::endl; // 此时map中会多一个键为"Eve",值为0的元素
// 安全访问元素,检查键是否存在
auto it_bob = ages.find("Bob");
if (it_bob != ages.end()) {
std::cout << "Bob的年龄 (find): " << it_bob->second << std::endl;
} else {
std::cout << "Bob不存在于map中。" << std::endl;
}
auto it_frank = ages.find("Frank");
if (it_frank != ages.end()) {
std::cout << "Frank的年龄 (find): " << it_frank->second << std::endl;
} else {
std::cout << "Frank不存在于map中。" << std::endl;
}
// 遍历map (按键的升序)
std::cout << "Map中的所有元素: " << std::endl;
for (const auto& pair : ages) { // 推荐使用const auto&
std::cout << " 键: " << pair.first << ", 值: " << pair.second << std::endl;
}
// 删除元素
ages.erase("Bob");
std::cout << "删除Bob后Map中的所有元素: " << std::endl;
for (const auto& pair : ages) {
std::cout << " 键: " << pair.first << ", 值: " << pair.second << std::endl;
}
// 获取map大小
std::cout << "Map大小: " << ages.size() << std::endl;
return 0;
// } // 如果是单独的map例子,这里结束
为什么std::vector是C++中动态数组的首选?
在我多年的C++开发经验里,
std::vector几乎是我每次需要一个动态大小的同类型元素集合时的首选。它不仅仅是“能用”,更是因为它的设计哲学和底层实现,使其在大多数场景下都表现出色。
首先,
vector最核心的优势在于它的连续内存存储。这意味着
vector中的所有元素在内存中是紧挨着存放的。这种布局对现代CPU的缓存机制极其友好。当你访问
vector中的一个元素时,CPU很可能会把这个元素以及它周围的几个元素一起加载到缓存中,这样你接下来访问相邻元素时,就能以极快的速度从缓存中获取,而不是从慢速的主内存中读取。这对于遍历操作或者需要频繁访问相邻元素的算法来说,性能提升是巨大的。相比之下,像
std::list这种链表结构,元素在内存中是分散的,每次访问都可能导致缓存失效,从而性能下降。
其次,
vector提供了O(1)的随机访问能力。因为是连续存储,给定一个索引,
vector可以直接通过简单的指针算术计算出元素的内存地址,这和原生数组的访问速度是一样的。无论是通过
[]操作符还是
at()方法,都能实现常数时间访问。
再者,
vector的自动内存管理极大地简化了开发。你不再需要手动
new和
delete内存,也不用担心内存泄漏。当你
push_back一个元素时,如果当前容量不足,
vector会自动重新分配一块更大的内存,并将现有元素拷贝(或移动)过去,然后释放旧内存。虽然重新分配是一个相对昂贵的操作,但
vector通常会以指数级增长容量(比如每次翻倍),这使得均摊下来,
push_back操作的复杂度依然是O(1)。
当然,
vector并非没有它的“脾气”。比如,频繁在中间插入或删除元素效率会比较低,因为这需要移动大量后续元素。当存储的元素是复杂对象时,拷贝或移动的成本也需要考虑。但总的来说,对于大多数需要动态数组的场景,
vector的优点远超其缺点,是名副其实的“瑞士军刀”。
// 更多vector操作示例
#include <vector>
#include <iostream>
#include <algorithm> // 用于std::sort
struct MyObject {
int id;
std::string name;
MyObject(int i, const std::string& n) : id(i), name(n) {
// std::cout << "MyObject(" << id << ") 构造" << std::endl;
}
MyObject(const MyObject& other) : id(other.id), name(other.name) {
// std::cout << "MyObject(" << id << ") 拷贝构造" << std::endl;
}
MyObject(MyObject&& other) noexcept : id(other.id), name(std::move(other.name)) {
// std::cout << "MyObject(" << id << ") 移动构造" << std::endl;
}
MyObject& operator=(const MyObject& other) {
if (this != &other) {
id = other.id;
name = other.name;
// std::cout << "MyObject(" << id << ") 拷贝赋值" << std::endl;
}
return *this;
}
MyObject& operator=(MyObject&& other) noexcept {
if (this != &other) {
id = other.id;
name = std::move(other.name);
// std::cout << "MyObject(" << id << ") 移动赋值" << std::endl;
}
return *this;
}
~MyObject() {
// std::cout << "MyObject(" << id << ") 析构" << std::endl;
}
void print() const {
std::cout << "{ID: " << id << ", Name: " << name << "} ";
}
};
void print_vector_objects(const std::vector<MyObject>& vec) {
for (const auto& obj : vec) {
obj.print();
}
std::cout << std::endl;
}
// int main() { // 假设在之前的main函数之后继续
std::vector<MyObject> objects;
objects.reserve(5); // 预留空间,避免频繁重新分配
std::cout << "--- Emplace_back vs Push_back ---" << std::endl;
// emplace_back 直接在vector内部构造对象,避免一次拷贝或移动
objects.emplace_back(1, "Alpha"); // 直接构造
objects.push_back(MyObject(2, "Beta")); // 构造临时对象,然后移动到vector中 (C++11后通常是移动)
objects.emplace_back(3, "Gamma");
print_vector_objects(objects);
std::cout << "\n--- 调整大小 (resize) ---" << std::endl;
objects.resize(5, MyObject(0, "Default")); // 调整大小到5,新元素用默认值填充
print_vector_objects(objects);
objects.resize(2); // 缩小大小,多余的元素被析构
print_vector_objects(objects);
std::cout << "\n--- 排序 ---" << std::endl;
objects.emplace_back(5, "Epsilon");
objects.emplace_back(4, "Delta");
std::sort(objects.begin(), objects.end(), [](const MyObject& a, const MyObject& b) {
return a.id < b.id;
});
print_vector_objects(objects);
std::cout << "\n--- 迭代器失效示例 ---" << std::endl;
std::vector<int> nums = {1, 2, 3, 4, 5};
auto it = nums.begin() + 2; // 指向3
std::cout << "原始Vector: ";
for(int n : nums) std::cout << n << " ";
std::cout << std::endl;
// 插入操作可能导致迭代器失效
nums.insert(nums.begin(), 0); // 在开头插入,所有迭代器都可能失效
// std::cout << "失效的迭代器指向: " << *it << std::endl; // 此时it可能指向错误位置或野指针
// 正确的做法是重新获取迭代器
it = nums.begin() + 3; // 重新指向新的3 (原先的2)
std::cout << "插入后Vector: ";
for(int n : nums) std::cout << n << " ";
std::cout << std::endl;
std::cout << "重新获取的迭代器指向: " << *it << std::endl;
// 清空并释放内存
std::cout << "\n--- 清空并释放内存 (shrink_to_fit) ---" << std::endl;
std::vector<int> big_vec;
big_vec.reserve(100);
for(int i = 0; i < 10; ++i) big_vec.push_back(i);
std::cout << "初始容量: " << big_vec.capacity() << std::endl;
big_vec.clear();
std::cout << "清空后容量: " << big_vec.capacity() << std::endl; // 容量通常不变
big_vec.shrink_to_fit(); // 请求将容量缩小到与大小匹配
std::cout << "shrink_to_fit后容量: " << big_vec.capacity() << std::endl; // 容量现在可能为0
// return 0; // 如果是单独的main函数
// } std::map与
std::unordered_map:如何选择合适的键值对容器?
这是个很经典的问题,也是我在设计系统时经常需要权衡的。
std::map和
std::unordered_map都提供了键值对的存储功能,但它们底层的实现机制截然不同,这直接导致了它们在性能特性和适用场景上的巨大差异。
std::map:有序的魅力与对数时间复杂度
std::map的底层通常是一个红黑树(一种自平衡二叉搜索树)。这种数据结构保证了
map中的元素总是按照键的严格弱序进行排列。
-
优点:
-
自动排序: 这是
map
最显著的特点。当你需要按键顺序遍历元素,或者需要获取某个键范围内的元素时,map
是理想选择。 -
稳定的性能: 插入、查找和删除操作的时间复杂度都是O(log N),其中N是
map
中元素的数量。这个性能是稳定的,不会出现像哈希表那样最坏情况下的性能退化。 -
无需哈希函数: 键类型只需要支持比较操作(
operator<
),不需要提供哈希函数。
-
自动排序: 这是
-
缺点:
-
相对较慢: 相比于
unordered_map
平均O(1)的性能,O(log N)在N很大时会显得慢一些。 -
内存开销: 每个节点都需要存储键、值以及指向父节点和子节点的指针,这会比
unordered_map
有更大的内存开销。 - 非连续内存: 节点在内存中是分散的,可能导致缓存效率不高。
-
相对较慢: 相比于
std::unordered_map:速度的追求与平均常数时间复杂度
std::unordered_map的底层实现是哈希表。它通过一个哈希函数将键映射到表中的一个桶(bucket),然后在这个桶中查找值。
-
优点:
-
极快的平均性能: 插入、查找和删除操作的平均时间复杂度是O(1)。在数据量大、对查找速度要求极高的场景下,
unordered_map
通常表现更优。 -
更低的内存开销(相对而言): 虽然也有一些内部结构,但通常比
map
的每个节点开销小。
-
极快的平均性能: 插入、查找和删除操作的平均时间复杂度是O(1)。在数据量大、对查找速度要求极高的场景下,
-
缺点:
-
无序性: 元素在
unordered_map
中没有固定的顺序,遍历结果是不确定的。 - 最坏情况: 如果哈希函数设计不当,或者遇到大量哈希冲突,所有元素都映射到同一个桶,那么操作的复杂度可能退化到O(N),性能会急剧下降。
-
需要哈希函数: 键类型必须提供一个可用的哈希函数(
std::hash
特化或自定义)。对于自定义类型,你需要自己提供一个哈希函数。
-
无序性: 元素在
如何选择?
我的经验是,选择哪个容器,主要看你的核心需求:
-
是否需要键的有序性? 如果你需要按键排序遍历,或者需要查找某个范围内的键,那么
std::map
是你的不二之选。比如,你需要按日期顺序存储事件,或者按字母顺序存储用户名字典。 -
是否对查找/插入速度有极致要求,且不关心顺序? 如果你的主要操作是快速查找、插入和删除,并且顺序无关紧要,那么
std::unordered_map
通常会提供更好的平均性能。比如,统计词频、缓存数据等。 -
键的类型是否复杂? 对于自定义类型作为键,
std::unordered_map
需要你提供一个合适的哈希函数,这可能需要额外的工作。而std::map
只需要重载operator<
。
通常情况下,如果对性能没有特别严苛的要求,并且数据量不是天文数字,
std::map的O(log N)性能已经足够好,而且其有序性常常能带来便利。但如果你真的在追求极致的查找速度,并且能够很好地管理哈希冲突,
std::unordered_map无疑是更强大的工具。
// std::map 和 std::unordered_map 示例
#include <map>
#include <unordered_map>
#include <string>
#include <iostream>
struct Person {
int id;
std::string name;
// 对于std::map,需要定义 operator<
bool operator<(const Person& other) const {
return id < other.id;
}
// 对于std::unordered_map,需要定义 operator==
bool operator==(const Person& other) const {
return id == other.id;
}
};
// 为std::unordered_map 定义 Person 的哈希函数
namespace std {
template <>
struct 以上就是C++标准库容器 vector map使用示例的详细内容,更多请关注知识资源分享宝库其它相关文章!







发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。