STL 容器是 C++ 标准库里最常用的部分。不同容器背后是不同的数据结构,选错了轻则性能差,重则在嵌入式上把内存吃光。这篇重点讲三个最常用的:vector、map、string,以及在嵌入式里该怎么用。
一、vector:动态数组
vector 是最常用的容器,本质是一块连续内存的动态数组,支持随机访问,尾部插入摊销 O(1)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #include <vector>
std::vector<int> v; v.push_back(1); v.push_back(2); v.push_back(3);
v[0]; v.at(0); v.front(); v.back();
v.size(); v.capacity(); v.empty();
v.pop_back(); v.erase(v.begin() + 1); v.clear();
|
扩容机制
vector 容量不够时自动扩容,通常是翻倍。扩容时分配新内存、移动所有元素、释放旧内存。这意味着:
- 扩容时所有迭代器和指针失效
- 频繁
push_back 如果触发多次扩容,会有内存碎片和拷贝开销
知道大小时提前 reserve:
1 2 3 4
| std::vector<SensorData> samples; samples.reserve(100); for (int i = 0; i < 100; ++i) samples.push_back(read_sensor());
|
初始化方式
1 2 3 4
| std::vector<int> a(10); std::vector<int> b(10, 255); std::vector<int> c = {1, 2, 3}; std::vector<int> d(c);
|
遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| for (const auto &item : v) { process(item); }
for (size_t i = 0; i < v.size(); ++i) { process(v[i]); }
for (auto it = v.begin(); it != v.end(); ++it) { process(*it); }
|
emplace_back vs push_back
push_back 先构造临时对象再拷贝/移动进容器,emplace_back 直接在容器内部原地构造,少一次构造:
1 2 3 4
| struct Point { int x, y; };
v.push_back(Point{1, 2}); v.emplace_back(1, 2);
|
二、map 和 unordered_map
map:红黑树实现,有序,查找/插入/删除 O(log n)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #include <map>
std::map<std::string, float> sensor_values;
sensor_values["temp"] = 25.6f; sensor_values["humi"] = 60.0f; sensor_values.insert({"pressure", 1013.0f}); sensor_values.emplace("light", 500.0f);
auto it = sensor_values.find("temp"); if (it != sensor_values.end()) { float val = it->second; }
float v = sensor_values["unknown"];
if (sensor_values.count("temp")) { }
if (sensor_values.contains("temp")) { }
for (const auto &[key, val] : sensor_values) { printf("%s: %.2f\n", key.c_str(), val); }
sensor_values.erase("temp");
|
unordered_map:哈希表实现,无序,平均查找/插入/删除 O(1),最坏 O(n)。
1 2 3 4
| #include <unordered_map>
std::unordered_map<uint32_t, DeviceInfo> devices; devices[0x1234] = {};
|
选哪个:需要有序遍历用 map,只需要快速查找用 unordered_map。unordered_map 平均性能更好,但哈希碰撞时会退化,且内存占用通常比 map 多。
三、string
std::string 是字符串的标准容器,本质是 vector<char> 加上字符串专用操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #include <string>
std::string s = "hello"; s += " world"; s.append("!"); s.size(); s.length(); s.empty(); s.c_str(); s[0]; s.at(0);
size_t pos = s.find("world"); if (pos != std::string::npos) { }
std::string sub = s.substr(6, 5);
s == "hello world!"; s.compare("abc");
std::string num = std::to_string(42); std::string flt = std::to_string(3.14f);
int i = std::stoi("42"); float f = std::stof("3.14");
|
SSO(Small String Optimization)
大多数标准库实现对短字符串(通常 15 字节以内)会直接存在对象内部,不做堆分配。所以短字符串的 string 没有动态内存开销。
四、其他常用容器速查
array:固定大小数组,编译期确定,栈上分配,比裸数组多了 size()、迭代器等接口:
1 2 3
| #include <array> std::array<uint8_t, 8> mac_addr = {0x00, 0x1A, 0x2B, 0x3C, 0x4D, 0x5E}; mac_addr.size();
|
deque:双端队列,两端插入删除 O(1),中间插入 O(n),内存不连续:
1 2 3 4 5
| #include <deque> std::deque<int> dq; dq.push_front(1); dq.push_back(2); dq.pop_front();
|
list:双向链表,任意位置插入删除 O(1),不支持随机访问,内存不连续,cache 不友好:
1 2 3 4
| #include <list> std::list<int> lst = {1, 2, 3}; lst.push_front(0); lst.sort();
|
set / unordered_set:不重复元素的有序/无序集合:
1 2 3 4
| #include <set> std::set<int> s = {3, 1, 2, 1}; s.insert(4); s.count(1);
|
queue / stack / priority_queue:适配器容器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #include <queue> std::queue<SensorData> q; q.push(data); q.front(); q.pop();
std::stack<int> stk; stk.push(1); stk.top(); stk.pop();
std::priority_queue<int> pq; pq.push(3); pq.push(1); pq.top();
|
五、容器选型速查
| 需求 |
容器 |
| 随机访问、尾部增删 |
vector |
| 固定大小数组 |
array |
| 两端增删 |
deque |
| 任意位置增删频繁 |
list |
| key-value,有序 |
map |
| key-value,快速查找 |
unordered_map |
| 唯一元素集合 |
set / unordered_set |
| FIFO |
queue |
| LIFO |
stack |
| 优先级队列 |
priority_queue |
六、嵌入式里的注意事项
STL 容器几乎都依赖动态内存,在裸机 MCU 上使用需要谨慎:
堆内存问题:vector、map、string 都会调用 new/delete,裸机上堆空间有限,且标准 malloc 实现可能有碎片问题。如果 MCU 的堆只有几 KB,随意使用 STL 容器很容易 OOM。
array 是例外:std::array 不做动态分配,完全可以在裸机上放心用,是裸数组的升级替代品。
string 的 SSO:短字符串不堆分配,临时拼接日志、状态名等短字符串问题不大。长字符串谨慎。
Linux 嵌入式:有 MMU 和完整的内存管理,STL 容器开箱即用,和 PC 开发没有区别。
替代方案:对内存敏感的场合,可以用第五篇模板章节里实现的 RingBuffer,或者 ETL(Embedded Template Library)—— 专门为嵌入式设计的 STL 替代品,所有容器都使用固定大小、静态分配。
总结
vector:最常用,动态数组,随机访问 O(1),扩容时用 reserve 避免多次重分配
map:红黑树,有序 key-value,O(log n);unordered_map 哈希表,平均 O(1)
string:字符串容器,短字符串 SSO 无堆分配,和 C 接口用 c_str() 互通
array:固定大小,栈上分配,嵌入式裸机上最安全的容器
- 裸机 MCU 上 STL 容器用之前先想清楚堆内存够不够,
array 是最安全的选择