STL 与 Java 集合框架:不同的设计哲学
Java 开发者对 ArrayList、HashMap 等集合框架非常熟悉。C++ 的 STL(Standard Template Library)提供类似的功能,但设计理念完全不同——STL 是泛型 + 算法分离的:容器只管存储数据,算法通过迭代器操作的容器的元素,容器和算法之间通过迭代器解耦。
核心哲学:Java 将操作封装在对象内部(
list.sort()),C++ 则分离关注——容器存储、算法操作、迭代器连接两者。
// C++ 风格:算法自由组合
std::vector<int> vec = {3, 1, 4, 1, 5};
// 排序不必是 vec 的方法
std::sort(vec.begin(), vec.end());
// 查找不必绑定特定容器
auto it = std::find(vec.begin(), vec.end(), 4);
// 对任何容器都能用统一算法
std::list<int> lst = {3, 1, 4, 1, 5};
auto it2 = std::find(lst.begin(), lst.end(), 4);
std::cout << "找到了?" << (it2 != lst.end()) << std::endl;
// Java 风格:操作封装在对象内部
List<Integer> list = Arrays.asList(3, 1, 4, 1, 5);
list.sort(null);
boolean found = list.contains(4);
容器对照总览
| Java | C++ | 数据结构 | 性能特征 |
|---|---|---|---|
ArrayList<E> |
std::vector<T> |
动态数组 | 尾部 O(1),中间 O(n) |
LinkedList<E> |
std::list<T> |
双向链表 | 头尾 O(1),随机访问 O(n) |
| — | std::forward_list<T>(C++11) |
单向链表 | 更省内存,仅顺序遍历 |
| — | std::array<T, N>(C++11) |
固定大小数组 | 栈上分配,零开销 |
HashMap<K,V> |
std::unordered_map<K,V>(C++11) |
哈希表 | O(1) 平均,无序 |
TreeMap<K,V> |
std::map<K,V> |
红黑树 | O(log n),有序 |
HashSet<E> |
std::unordered_set<E>(C++11) |
哈希集合 | O(1) 平均 |
TreeSet<E> |
std::set<E> |
红黑树 | O(log n),有序 |
PriorityQueue<E> |
std::priority_queue<T> |
堆 | 插入 O(log n) |
ArrayDeque<E> |
std::deque<T> |
双端队列 | 头尾 O(1) |
| — | std::stack<T>(适配器) |
栈 | LIFO |
| — | std::queue<T>(适配器) |
队列 | FIFO |
| — | std::tuple(C++11) |
异质元组 | 固定大小,多类型 |
| — | std::pair<T1,T2> |
双元素元组 | 两个值 |
std::array(C++11)——固定大小数组
C++ 的传统 C 风格数组(int arr[10])存在问题:不会自动管理大小、不能作为函数参数传递大小信息、没有 begin()/end() 接口。std::array 解决了这些问题。
#include <array>
#include <iostream>
#include <algorithm>
int main() {
// std::array:栈上分配,大小是类型的一部分
std::array<int, 5> arr = {1, 2, 3, 4, 5};
// 访问元素
std::cout << arr[0] << std::endl; // 越界不检查(UB)
std::cout << arr.at(0) << std::endl; // 越界抛 std::out_of_range
std::cout << arr.front() << std::endl; // 第一个元素
std::cout << arr.back() << std::endl; // 最后一个元素
// 大小信息
std::cout << "Size: " << arr.size() << std::endl; // 5
std::cout << "Max Size: " << arr.max_size() << std::endl;
// STL 算法兼容
std::sort(arr.begin(), arr.end());
// 范围 for 遍历
for (const auto& elem : arr) {
std::cout << elem << " ";
}
// 填充和交换
std::array<int, 5> other;
other.fill(0); // all zeros
arr.swap(other); // 交换内容
}
std::array vs 传统数组
| 特性 | std::array<int, 5> |
int arr[5] |
|---|---|---|
| 大小作为类型 | ✅ sizeof(std::array<int,5>) 编译期可知 |
❌ 退化后丢失大小 |
边界检查 .at() |
✅ 有 | ❌ 无 |
| STL 算法兼容 | ✅ 有 begin()/end() |
❌ 需 std::begin()/std::end() |
| 赋值 | ✅ arr2 = arr1; |
❌ 必须逐元素拷贝或 memcpy |
| 开销 | 零开销(等同 C 数组) | 原生数组 |
| 传参 | 传 std::array<int, 5>&(保留大小) |
退化为指针,大小丢失 |
// 传统数组的问题
void process(int arr[]) { // 退化为 int*!
// sizeof(arr) == sizeof(int*),不是数组大小
}
// std::array 保留大小
void process(std::array<int, 5>& arr) {
// arr.size() == 5
}
std::vector(对应 ArrayList)
std::vector 是 C++ 中最常用的容器——相当于 Java 的 ArrayList。
#include <vector>
#include <algorithm>
#include <iostream>
int main() {
// 构造
std::vector<int> vec; // 空 vector
std::vector<int> vec2(10, 0); // 10 个 0
std::vector<int> vec3 = {1, 2, 3, 4, 5}; // 初始化列表(C++11)
std::vector<int> vec4(vec3); // 拷贝构造
// 大小 vs 容量
std::cout << "size: " << vec3.size() // 5,实际元素数
<< " capacity: " << vec3.capacity(); // 通常 >= size
// 修改
vec3.push_back(6); // 尾部追加(类似 add)
vec3.pop_back(); // 尾部删除
vec3.insert(vec3.begin(), 0); // 头部插入(O(n))
vec3.erase(vec3.begin()); // 删除第一个
vec3.clear(); // 清空
vec3.shrink_to_fit(); // 释放多余容量(C++11)
// 访问
int x = vec3[0]; // 下标访问(越界 UB!)
int y = vec3.at(1); // 带边界检查(越界抛异常)
int z = vec3.front(); // 第一个
int w = vec3.back(); // 最后一个
int* data = vec3.data(); // 获取底层数组指针
// 预分配(提升性能的关键)
vec.reserve(1000); // 预留 1000 个元素空间
// 这避免了多次扩容拷贝
// 遍历
for (size_t i = 0; i < vec.size(); ++i) {
std::cout << vec[i] << " ";
}
for (auto it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " ";
}
for (const auto& elem : vec) { // ✅ 推荐:范围 for
std::cout << elem << " ";
}
}
✅ 好习惯:用 emplace_back 代替 push_back
struct Person {
std::string name;
int age;
Person(std::string n, int a) : name(std::move(n)), age(a) {}
};
std::vector<Person> people;
// ❌ push_back:先构造临时对象,再拷贝/移动到 vector
people.push_back(Person("Alice", 30));
// ✅ emplace_back:直接在 vector 内存中构造,省去临时对象
people.emplace_back("Alice", 30);
// 对于内置类型,两者性能相同
vec.push_back(42); // 没问题
vec.emplace_back(42); // 也可以,但没必要
✅ 好习惯:用 reserve 预分配避免多次扩容
std::vector<int> vec;
// 如果不 reserve,push_back 可能触发多次内存分配和元素拷贝
// ✅ 预分配,一次到位
vec.reserve(1000);
for (int i = 0; i < 1000; ++i)
vec.push_back(i);
✅ 好习惯:用 auto 简化迭代器
// ❌ 冗长
for (std::vector<std::pair<std::string, int>>::const_iterator it = vec.begin(); it != vec.end(); ++it) { }
// ✅ auto 简化
for (auto it = vec.cbegin(); it != vec.cend(); ++it) { }
// ✅ 范围 for 最简洁
for (const auto& [key, value] : map) { } // C++17 结构化绑定
std::forward_list(C++11)——单向链表
与 std::list(双向链表)相比,forward_list 更省内存,但只能单向遍历。
#include <forward_list>
#include <iostream>
int main() {
std::forward_list<int> flist = {1, 2, 3, 4, 5};
// 只能在头部快速插入/删除
flist.push_front(0); // {0, 1, 2, 3, 4, 5}
flist.pop_front(); // {1, 2, 3, 4, 5}
// 没有 size() 方法(O(1) 不可实现)
// 需要逐个遍历计数
// 没有 end() 之前的插入——只有 insert_after
auto it = flist.before_begin();
flist.insert_after(it, 99); // {99, 1, 2, 3, 4, 5}
// 排序(比 list::sort 更省内存)
flist.sort();
// 去重
flist.unique();
// 合并
std::forward_list<int> other = {10, 20};
flist.merge(other); // other 变空
for (const auto& x : flist)
std::cout << x << " ";
}
何时使用:内存极其受限的嵌入式场景,或只需要单向遍历的链表操作。
std::map(对应 TreeMap)
std::map 基于红黑树,元素按键排序存储。遍历时按键的升序遍历。
#include <map>
#include <iostream>
int main() {
// 基本用法
std::map<std::string, int> scores;
scores["Alice"] = 95; // 插入
scores.insert({"Bob", 87}); // C++11 初始化列表插入
scores.emplace("Charlie", 92); // C++11 就地构造
// 读取
int alice = scores["Alice"]; // ✅ 已存在则返回;不存在则插入默认值!
int bob = scores.at("Bob"); // ✅ 不存在抛 std::out_of_range
// int david = scores["David"]; // ❌ 副作用:如果 David 不存在,插入默认值
// 查找——正确的做法
auto it = scores.find("David");
if (it != scores.end()) {
std::cout << it->first << ": " << it->second;
} else {
std::cout << "未找到\n";
}
// 遍历——按键升序
for (const auto& pair : scores) {
std::cout << pair.first << ": " << pair.second << "\n";
}
// C++17 结构化绑定
for (const auto& [key, value] : scores) {
std::cout << key << ": " << value << "\n";
}
// multiset:有序但允许重复
std::multiset<int> ms = {1, 2, 2, 3, 3, 3};
std::cout << ms.count(3); // 3
}
std::unordered_map(对应 HashMap)
C++11 引入的哈希表容器,查找 O(1) 平均,但元素无序。
#include <unordered_map>
#include <iostream>
int main() {
std::unordered_map<std::string, int> hash;
hash["apple"] = 5;
hash["banana"] = 3;
hash["cherry"] = 7;
// 接口和 map 基本一致
auto it = hash.find("apple");
if (it != hash.end())
std::cout << it->first << ": " << it->second;
// 遍历——注意:无序!
for (const auto& [k, v] : hash) {
std::cout << k << ": " << v << "\n";
// 每次运行输出顺序可能不同
}
// 自定义 key 类型需要哈希函数和相等比较
struct Point {
int x, y;
bool operator==(const Point& other) const {
return x == other.x && y == other.y;
}
};
// 自定义哈希函数
struct PointHash {
std::size_t operator()(const Point& p) const {
return std::hash<int>()(p.x) ^ (std::hash<int>()(p.y) << 1);
}
};
std::unordered_map<Point, std::string, PointHash> points;
points[{1, 2}] = "home";
points[{3, 4}] = "office";
}
map vs unordered_map 遍历顺序差异
#include <iostream>
#include <map>
#include <unordered_map>
int main() {
// map:按键升序遍历
std::map<int, std::string> ordered;
ordered[5] = "five";
ordered[1] = "one";
ordered[3] = "three";
for (const auto& [k, v] : ordered) {
std::cout << k << " "; // 总是输出: 1 3 5
}
// unordered_map:顺序不确定
std::unordered_map<int, std::string> unordered;
unordered[5] = "five";
unordered[1] = "one";
unordered[3] = "three";
for (const auto& [k, v] : unordered) {
std::cout << k << " "; // 输出顺序不确定(可能 5 1 3)
}
}
选择建议:需要有序遍历用 map;只需要快速查找用 unordered_map。
std::tuple(C++11)——异质元组
std::tuple 可以容纳多个不同类型的值,功能类似于 Python 的元组或 Java 没有的(Java 没有原生元组)。
#include <tuple>
#include <iostream>
#include <string>
int main() {
// 构造
std::tuple<int, double, std::string> t1(42, 3.14, "hello");
// C++17 类模板参数推导
std::tuple t2(42, 3.14, "hello"); // 自动推导类型
// make_tuple
auto t3 = std::make_tuple(42, 3.14, "hello");
// 访问元素——std::get
std::cout << std::get<0>(t1) << std::endl; // 42(编译期索引)
std::cout << std::get<1>(t1) << std::endl; // 3.14
std::cout << std::get<std::string>(t1); // "hello"(按类型索引)
// 修改
std::get<0>(t1) = 100;
// std::tie:解包元组到已有变量
int a;
double b;
std::string c;
std::tie(a, b, c) = t1;
std::cout << a << " " << b << " " << c; // 100 3.14 hello
// 忽略不需要的元素
std::tie(a, std::ignore, c) = t1; // 忽略第二个元素
// C++17 结构化绑定
auto [x, y, z] = t1;
std::cout << x << " " << y << " " << z;
// 连接元组
auto t4 = std::tuple_cat(t1, std::make_tuple(1, 2));
}
为什么 pair 不够?
// pair 只能装两个
std::pair<int, std::string> p = {1, "one"};
// tuple 可以装任意多个
auto employee = std::make_tuple(1001, "Alice", "Engineering", 75000.0);
std::string——远强于 Java String
C++ 的 std::string 是可变对象,支持直接拼接、查找、替换等。
#include <string>
#include <iostream>
int main() {
std::string s1 = "hello";
std::string s2 = "world";
std::string s3 = s1 + " " + s2; // 拼接——Java 也能做到
// 但 C++ string 是可变的!
s1 += " world"; // 直接修改(Java 会创建新对象)
// 比较——比较内容,不是引用
if (s1 == s2) { } // 比较内容(Java 用 .equals())
// 查找
size_t pos = s1.find("world"); // 返回位置或 npos
if (pos != std::string::npos) {
std::cout << "从位置 " << pos << " 找到\n";
}
// 子串
std::string sub = s1.substr(0, 5); // "hello"
// 转换
std::string num = std::to_string(12345); // 数字 → 字符串
int val = std::stoi("42"); // 字符串 → int
double d = std::stod("3.14");
// 遍历
for (char ch : s1) { }
for (auto& ch : s1) ch = toupper(ch); // 修改每个字符
}
迭代器——连接容器和算法的桥梁
迭代器是指针的一层抽象,让算法不依赖具体容器。
#include <vector>
#include <list>
#include <iostream>
int main() {
std::vector<int> vec = {10, 20, 30, 40, 50};
// 正向迭代器
for (auto it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " ";
}
// 反向迭代器
for (auto it = vec.rbegin(); it != vec.rend(); ++it) {
std::cout << *it << " "; // 50 40 30 20 10
}
// const 迭代器(只读)
for (auto it = vec.cbegin(); it != vec.cend(); ++it) { }
// 迭代器分类
std::list<int> lst = {1, 2, 3};
auto lit = lst.begin();
++lit; // ✅ 前向迭代器
// lit += 2; // ❌ list 不支持随机访问!
// auto d = lit - lst.begin(); // ❌ list 没有 operator-
}
迭代器类别
| 类别 | 能力 | 容器示例 |
|---|---|---|
| 前向迭代器 | ++ |
forward_list、unordered_map |
| 双向迭代器 | ++, -- |
list、map、set |
| 随机访问迭代器 | ++, --, +=, -=, [], - |
vector、deque、array |
| 连续迭代器 | 同随机访问 + 内存连续 | vector、array |
STL 算法
STL 算法位于 <algorithm> 和 <numeric> 等头文件中,是 C++ 泛型编程的核心。
#include <algorithm>
#include <numeric>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6, 5};
// 排序
std::sort(v.begin(), v.end()); // 升序
std::sort(v.begin(), v.end(), std::greater<int>()); // 降序
// 查找
auto it = std::find(v.begin(), v.end(), 5);
if (it != v.end()) std::cout << "找到: " << *it;
// 二分查找(需要在有序序列上操作)
if (std::binary_search(v.begin(), v.end(), 5)) { }
// 计数
int cnt = std::count(v.begin(), v.end(), 1);
// 求和(<numeric>)
int sum = std::accumulate(v.begin(), v.end(), 0);
// 反转
std::reverse(v.begin(), v.end());
// 变换(类似 Java 的 stream().map())
std::vector<int> squared(v.size());
std::transform(v.begin(), v.end(), squared.begin(),
[](int x) { return x * x; });
// 筛选(类似 Java 的 stream().filter())
std::vector<int> evens;
std::copy_if(v.begin(), v.end(),
std::back_inserter(evens),
[](int x) { return x % 2 == 0; });
// 最小/最大元素
auto [min_it, max_it] = std::minmax_element(v.begin(), v.end());
// 全部满足/任意满足/不满足
bool all_positive = std::all_of(v.begin(), v.end(),
[](int x) { return x > 0; });
bool any_zero = std::any_of(v.begin(), v.end(),
[](int x) { return x == 0; });
}
编码好习惯与坏味道
✅ 好习惯
① 用 emplace_back 代替 push_back 插入复杂对象
// ✅ emplace_back:直接在容器内存中构造,省去一次拷贝/移动
vec.emplace_back("Alice", 30);
// 但对 int 等简单类型,push_back 更清晰
vec.push_back(42);
② 用 reserve 预分配容量
std::vector<int> vec;
vec.reserve(1000); // ✅ 一次分配
for (int i = 0; i < 1000; ++i)
vec.push_back(i);
// ❌ 无 reserve:可能多次分配和拷贝
std::vector<int> bad_vec;
for (int i = 0; i < 1000; ++i)
bad_vec.push_back(i); // 可能触发多次 reallocation
③ 用 auto 简化迭代器声明
// ❌ 冗长
std::map<std::string, std::vector<int>>::const_iterator it = m.begin();
// ✅ 简洁
auto it = m.cbegin();
④ 遍历大对象用 const auto&
// ❌ 拷贝整个元素
for (auto elem : vec_of_large_objects) { }
// ✅ 只读引用,避免拷贝
for (const auto& elem : vec_of_large_objects) { }
// 对 int 等小类型,按值遍历也没问题
for (int x : vec_of_ints) { }
⑤ 优先用 at() 而非 [] 做边界保护
// ❌ vec[i] 越界 = 未定义行为(可能崩溃,可能静默损坏数据)
vec[100] = 42;
// ✅ vec.at(i) 越界抛 std::out_of_range
try {
vec.at(100) = 42;
} catch (const std::out_of_range& e) {
std::cerr << "越界错误: " << e.what();
}
⑥ 用 try_emplace(C++17)避免 map 重复构造
std::map<std::string, ExpensiveObject> m;
// ❌ 即使 key 已存在,也会构造临时对象
m.emplace("key", ExpensiveObject(params...));
// ✅ try_emplace:key 已存在时不构造 value
m.try_emplace("key", params...); // C++17
❌ 坏味道
① 用传统 C 风格数组代替 std::array 或 std::vector
// ❌ C 风格数组:丢失大小信息、不能赋值、不能直接传 STL 算法
int arr[5] = {1, 2, 3, 4, 5};
void func(int arr[]); // 退化成 int*!
// ✅ std::array(固定大小)或 std::vector(动态大小)
std::array<int, 5> arr = {1, 2, 3, 4, 5};
std::vector<int> vec = {1, 2, 3, 4, 5};
② vector<bool> 的特殊性——它不是存 bool 的数组!
// ⚠️ vector<bool> 是特化版本,不是"bool 的 vector"
// 它用位压缩存储,导致:
std::vector<bool> vb = {true, false, true};
// ❌ 不能取引用!
// auto& ref = vb[0]; // 编译错误!返回的是代理对象
// ❌ 不是真正的容器
// bool* ptr = vb.data(); // 没有 data() 方法
// ✅ 替代方案
std::vector<char> vc = {1, 0, 1}; // 每个元素 1 字节
std::deque<bool> dq = {true, false, true}; // deque 没有特化
std::bitset<3> bs = 0b101; // 编译期固定大小
③ 用 [] 访问 map 不检查存在性(会默认插入!)
std::map<std::string, int> scores;
scores["Alice"] = 95;
// ❌ 坏味道:不经意插入新元素
if (scores["Bob"] > 90) { // "Bob" 不存在,被插入为 0!
// 这个分支永远不会执行
}
// ✅ 正确做法:用 find 检查
auto it = scores.find("Bob");
if (it != scores.end() && it->second > 90) { }
// ✅ 或者用 at():不存在时抛异常
try {
int bob = scores.at("Bob");
} catch (const std::out_of_range&) {
// Bob 不存在
}
④ 手动管理内存代替容器
// ❌ 手动 new/delete(容易内存泄漏、异常不安全)
int* arr = new int[100];
// ... 使用 ...
delete[] arr;
// ✅ 用 std::vector(自动管理)
std::vector<int> arr(100);
⑤ 在循环中频繁调用 size()
// ❌ 每次迭代重新求 size(对 list 是 O(n) 操作!)
for (size_t i = 0; i < vec.size(); ++i) { }
// ✅ 缓存 size,或使用范围 for
const size_t n = vec.size();
for (size_t i = 0; i < n; ++i) { }
// ✅ 范围 for 最好
for (const auto& elem : vec) { }
⑥ 在 unordered_map 中使用未提供哈希函数的自定义类型
struct Point { int x, y; };
// ❌ 编译错误:没有哈希函数
std::unordered_map<Point, int> m;
// ✅ 需要提供哈希和相等比较
struct PointHash {
std::size_t operator()(const Point& p) const {
return std::hash<int>()(p.x) ^ (std::hash<int>()(p.y) << 1);
}
};
std::unordered_map<Point, int, PointHash> m;
实用对照速查
常用操作对比
| 操作 | Java | C++ |
|---|---|---|
| 创建列表 | new ArrayList<>() |
std::vector<int> v |
| 追加元素 | list.add(x) |
v.push_back(x) 或 v.emplace_back(args...) |
| 获取大小 | list.size() |
v.size() |
| 访问元素 | list.get(i) |
v[i] 或 v.at(i) |
| 排序 | list.sort(null) |
std::sort(v.begin(), v.end()) |
| 查找 | list.contains(x) |
std::find(v.begin(), v.end(), x) != v.end() |
| 清空 | list.clear() |
v.clear() |
| 创建映射 | new HashMap<>() |
std::unordered_map<K,V> m |
| 放入值 | map.put(k, v) |
m[k] = v 或 m.emplace(k, v) |
| 获取值 | map.get(k) |
m[k](可能插入默认值)或 m.at(k)(抛异常) |
| 遍历映射 | for (Map.Entry e : map) |
for (auto& [k,v] : m) (C++17) |
总结
- 默认用
vector——动态数组是最通用的选择,绝大多数场景首选 - 需要快速查找用
unordered_map(O(1)),需要有序用map(O(log n)) - 固定大小用
array——栈上分配,零开销,保留大小信息 - 用
emplace_back替代push_back——避免不必要的临时对象 - 用
reserve预分配——减少 vector 扩容复制的开销 - 熟悉常用算法——很多操作不用自己写循环
- 用
const auto&遍历大对象——避免不必要的拷贝 - 迭代器 + 算法 = 函数式编程风格——
std::find_if、std::transform、std::copy_if等
下一篇文章我们将学习现代 C++ 的并发编程。