菜单

Administrator
发布于 2026-05-22 / 0 阅读
0
0

Java 开发者学 C++(五):STL 容器与算法速通

STL 与 Java 集合框架:不同的设计哲学

Java 开发者对 ArrayListHashMap 等集合框架非常熟悉。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_listunordered_map
双向迭代器 ++, -- listmapset
随机访问迭代器 ++, --, +=, -=, [], - vectordequearray
连续迭代器 同随机访问 + 内存连续 vectorarray

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::arraystd::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] = vm.emplace(k, v)
获取值 map.get(k) m[k](可能插入默认值)或 m.at(k)(抛异常)
遍历映射 for (Map.Entry e : map) for (auto& [k,v] : m) (C++17)

总结

  1. 默认用 vector——动态数组是最通用的选择,绝大多数场景首选
  2. 需要快速查找用 unordered_map(O(1)),需要有序用 map(O(log n))
  3. 固定大小用 array——栈上分配,零开销,保留大小信息
  4. emplace_back 替代 push_back——避免不必要的临时对象
  5. reserve 预分配——减少 vector 扩容复制的开销
  6. 熟悉常用算法——很多操作不用自己写循环
  7. const auto& 遍历大对象——避免不必要的拷贝
  8. 迭代器 + 算法 = 函数式编程风格——std::find_ifstd::transformstd::copy_if

下一篇文章我们将学习现代 C++ 的并发编程。


评论