
关注我,学习c++不迷路:
专栏如下:
后续会更新更多有趣的小知识,关注我带你遨游知识世界

期待你的关注。

文章目录
- 第一章:老王的"古代"代码
- 第二章:小李登场
- 第三章:智能指针:内存管理的革命
- [第四章:容器新成员:array 和 forward_list](#第四章:容器新成员:array 和 forward_list)
- 第五章:unordered_map:哈希表的逆袭
- 第六章:emplace革命:零拷贝构造
- 第七章:Lambda表达式:算法的灵魂
- 第八章:范围for和新算法
- 第九章:右值引用和移动语义在STL中的应用
- 第十章:老王的实践与顿悟
- [第十一章:C++11 STL 核心变化总结](#第十一章:C++11 STL 核心变化总结)
-
- [1. **内存管理革命**](#1. 内存管理革命)
- [2. **容器性能飞跃**](#2. 容器性能飞跃)
- [3. **构造效率革命**](#3. 构造效率革命)
- [4. **泛型编程简化**](#4. 泛型编程简化)
- [5. **遍历方式进化**](#5. 遍历方式进化)
- [6. **移动语义支持**](#6. 移动语义支持)
- 最终的感悟
第一章:老王的"古代"代码
老王是个2003年就开始写C++的老兵,最近接了个紧急任务:解析100万条日志,统计错误码出现次数,找出Top 10。
他打开尘封的VC++6.0,开始敲代码:
cpp
// 老王的C++98代码(2003年版本)
#include <vector>
#include <map>
#include <algorithm>
#include <functional>
#include <iostream>
struct LogEntry {
int code;
int line;
LogEntry(int c, int l) : code(c), line(l) {}
};
void old_wang_process() {
// 1. 存储日志条目
std::vector<LogEntry*> logs;
for (int i = 0; i < 1000000; ++i) {
LogEntry* pEntry = new LogEntry(500, i); // 每次都要new
logs.push_back(pEntry); // 存储指针,避免拷贝构造
}
// 2. 统计错误码
std::map<int, int> stats; // 红黑树,O(log n)
for (size_t i = 0; i < logs.size(); ++i) {
stats[logs[i]->code]++; // 每次插入都要树节点分配
}
// 3. 转成vector排序
std::vector<std::pair<int, int>> vec(stats.begin(), stats.end());
// 复杂的函数对象适配器...
std::sort(vec.begin(), vec.end(),
std::bind2nd(std::greater<std::pair<int, int>>(),
std::select2nd<std::map<int, int>::value_type>()));
// 4. 取Top 10
std::vector<std::pair<int, int>> top10(vec.begin(), vec.begin() + 10);
// 5. 清理内存
for (size_t i = 0; i < logs.size(); ++i) {
delete logs[i]; // 容易漏!
}
}
老王的烦恼:
- 代码200行,内存管理占50行
- 调试3天,发现一个
delete漏了导致内存泄漏 - 程序跑一次要15秒,老板嫌慢
第二章:小李登场
小李是个90后,接过代码扫了一眼:"老王,你这代码可以简化10倍,速度还能快3倍。"
老王瞪眼:"不可能!STL就那些东西,还能变出花来?"
小李笑了笑,打开Visual Studio 2019,开始重构:
cpp
// 小李的C++11代码(2024年版本)
#include <vector>
#include <unordered_map>
#include <algorithm>
#include <memory>
#include <string>
#include <iostream>
struct LogEntry {
int code;
int line;
LogEntry(int c, int l) : code(c), line(l) {}
};
void xiao_li_process() {
// 1. 使用智能指针,自动管理内存
std::vector<std::unique_ptr<LogEntry>> logs;
logs.reserve(1000000); // 预分配,避免重复扩容
for (int i = 0; i < 1000000; ++i) {
// emplace_back 直接在容器内构造,零拷贝
logs.emplace_back(std::make_unique<LogEntry>(500, i));
}
// 2. 使用 unordered_map,O(1)插入
std::unordered_map<int, int> stats;
for (const auto& entry : logs) { // 范围for,简洁
stats[entry->code]++; // 哈希表,比map快3-5倍
}
// 3. 使用 lambda 表达式,一行搞定排序
std::vector<std::pair<int, int>> vec(stats.begin(), stats.end());
std::sort(vec.begin(), vec.end(),
[](const auto& a, const auto& b) { return a.second > b.second; });
// 4. 只取前10个
vec.resize(10);
// 5. 不需要手动delete!unique_ptr 自动清理
}
老王看着代码,半信半疑:"这能跑?不会崩?"
第三章:智能指针:内存管理的革命
老王的疑问
"小李,你这些 unique_ptr 是什么?C++98里的 auto_ptr 不是很难用吗?"
小李的解答
"老王,auto_ptr 确实是个坑,转移所有权后原指针就废了。C++11的 unique_ptr 才是真正的独占指针。"
代码对比:
cpp
// C++98的 auto_ptr(已被废弃)
std::auto_ptr<LogEntry> p1(new LogEntry(500, 1));
std::auto_ptr<LogEntry> p2 = p1; // p1 变成了 nullptr!
// 后续使用 p1 会崩溃!
// C++11的 unique_ptr(独占所有权)
std::unique_ptr<LogEntry> u1(new LogEntry(500, 1));
// std::unique_ptr<LogEntry> u2 = u1; // 编译错误!不能拷贝
std::unique_ptr<LogEntry> u2 = std::move(u1); // 必须显式移动
// 移动后,u1 变为 nullptr,u2 拥有对象
// 作用:强制开发者明确所有权转移,避免意外拷贝
shared_ptr:共享所有权
cpp
// C++98:手动引用计数,容易出错
class RefCounted {
int* data;
int* ref_count;
public:
RefCounted(const RefCounted& other) {
data = other.data;
ref_count = other.ref_count;
(*ref_count)++; // 必须记得增加计数
}
~RefCounted() {
(*ref_count)--;
if (*ref_count == 0) delete data; // 必须记得检查
}
};
// C++11:shared_ptr 自动管理
std::shared_ptr<LogEntry> s1 = std::make_shared<LogEntry>(500, 1);
std::shared_ptr<LogEntry> s2 = s1; // 自动增加引用计数
// s1 和 s2 销毁时,自动释放对象
weak_ptr:解决循环引用
cpp
struct Node {
std::shared_ptr<Node> next; // 强引用
// std::shared_ptr<Node> prev; // 这样会形成循环引用,内存泄漏!
std::weak_ptr<Node> prev; // 弱引用,不增加计数
};
// C++98:需要手动打破循环,极难维护
// C++11:weak_ptr 自动识别对象是否存活
第四章:容器新成员:array 和 forward_list
老王的疑问
"array 不就是C数组吗?forward_list 又是什么?"
小李的解答
"array 是C数组的STL升级版,forward_list 是为性能而生的单向链表。"
std::array:栈上的安全数组
cpp
// C++98
int c_arr[10]; // 无size信息,越界不报错
std::vector<int> vec(10); // 堆分配,有开销
// C++11
std::array<int, 10> arr; // 栈分配,有size,支持迭代器
arr.size(); // 编译期已知,效率高
arr.at(10); // 运行时检查,抛异常
arr[10]; // 不检查,但调试模式可用assert
std::forward_list:省内存的单向链表
cpp
// C++98:std::list 是双向链表
struct ListNode {
ListNode* prev;
ListNode* next;
int data;
}; // 每个节点2个指针,64位系统占16字节开销
// C++11:forward_list 单向链表
struct ForwardNode {
ForwardNode* next;
int data;
}; // 只有1个指针,8字节开销,省50%
// 适用场景:只需要单向遍历,不需要随机访问
std::forward_list<int> flist;
flist.push_front(1); // 只能头部插入
flist.insert_after(flist.begin(), 2); // 只能在指定位置后插入
第五章:unordered_map:哈希表的逆袭
老王的疑问
"map 是红黑树,unordered_map 是什么?为什么快?"
小李的解答
"unordered_map 是哈希表,平均O(1)插入,比O(log n)的map快得多。"
性能实测:
cpp
// 插入100万条数据测试
std::map<int, int> m;
std::unordered_map<int, int> um;
um.reserve(1000000); // 预分配桶,避免rehash
// 测试结果(Release模式):
// map插入:约 800ms
// unordered_map插入:约 200ms
// 快4倍!
哈希原理:
cpp
// map的红黑树结构(有序)
// 5
// / \
// 3 8
// / \ / \
// 1 4 6 9
// unordered_map的哈希表(无序)
// 桶0:
// 桶1: -> 1 -> 11 -> 21
// 桶2:
// 桶3: -> 3
// 桶4: -> 4 -> 14
// 桶5: -> 5
// ...
// 插入时计算 hash(key) % 桶数量,直接定位到桶
使用陷阱:
cpp
// 错误:自定义类型需要哈希函数
struct MyKey {
int a, b;
};
std::unordered_map<MyKey, int> um; // 编译错误!
// 解决方案1:特化std::hash
namespace std {
template<> struct hash<MyKey> {
size_t operator()(const MyKey& k) const {
return hash<int>()(k.a) ^ hash<int>()(k.b);
}
};
}
// 解决方案2:C++11的用户定义字面量(更优雅)
// 或者使用 boost::hash_combine
第六章:emplace革命:零拷贝构造
老王的疑问
"emplace_back 比 push_back 好在哪?"
小李的解答
"push_back 必须先构造临时对象,再拷贝到容器;emplace_back 直接在容器内构造。"
代码剖析:
cpp
class Heavy {
std::string data;
public:
Heavy(const std::string& s) : data(s) {
std::cout << "构造函数\n";
}
Heavy(const Heavy& other) : data(other.data) {
std::cout << "拷贝构造函数\n";
}
Heavy(Heavy&& other) noexcept : data(std::move(other.data)) {
std::cout << "移动构造函数\n";
}
};
std::vector<Heavy> vec;
vec.reserve(3);
// push_back 的过程
vec.push_back(Heavy("hello"));
// 1. 调用 Heavy("hello") 构造临时对象
// 2. 调用 Heavy(Heavy&&) 移动构造到容器
// 3. 销毁临时对象
// 输出:构造函数 -> 移动构造函数 -> 析构函数
// emplace_back 的过程
vec.emplace_back("hello");
// 1. 直接在容器内存上调用 Heavy("hello")
// 2. 无临时对象,无额外拷贝/移动
// 输出:构造函数
性能差异:
cpp
// 插入100万个字符串
std::vector<std::string> v;
v.reserve(1000000);
// push_back
for (int i = 0; i < 1000000; ++i) {
v.push_back(std::to_string(i)); // 临时string构造 + 移动
}
// 耗时:约 450ms
// emplace_back
for (int i = 0; i < 1000000; ++i) {
v.emplace_back(std::to_string(i)); // 直接构造
}
// 耗时:约 280ms
// 快38%!
第七章:Lambda表达式:算法的灵魂
老王的疑问
"lambda 是什么?不就是匿名函数吗?"
小李的解答
"lambda 是C++11的函数对象生成器,让算法变得直观。"
C++98的痛苦:
cpp
// 需要找第一个大于100的数
struct GreaterThan100 {
bool operator()(int x) const {
return x > 100;
}
};
auto it = std::find_if(v.begin(), v.end(), GreaterThan100());
// 或者用bind2nd(更难懂)
auto it = std::find_if(v.begin(), v.end(),
std::bind2nd(std::greater<int>(), 100));
C++11的优雅:
cpp
// lambda 一目了然
auto it = std::find_if(v.begin(), v.end(),
[](int x) { return x > 100; });
// 捕获外部变量
int threshold = 100;
auto it = std::find_if(v.begin(), v.end(),
[threshold](int x) { return x > threshold; });
// 捕获并修改
int count = 0;
std::for_each(v.begin(), v.end(),
[&count](int x) { if (x > 100) count++; });
lambda 捕获模式详解:
cpp
int a = 1, b = 2;
[a, b]() {} // 按值捕获a,b,const
[a, &b]() {} // 按值捕获a,按引用捕获b
[=]() {} // 按值捕获所有外部变量
[&]() {} // 按引用捕获所有外部变量
[=, &b]() {} // 按值捕获所有,但b按引用
[a]() mutable { a++; } // mutable允许修改按值捕获的变量
第八章:范围for和新算法
老王的疑问
"range-based for 不就是语法糖吗?"
小李的解答
"它是语法糖,但让代码更安全、更简洁。"
范围for的真相:
cpp
std::vector<int> v = {1, 2, 3, 4, 5};
// C++98
for (std::vector<int>::iterator it = v.begin(); it != v.end(); ++it) {
std::cout << *it;
}
// C++11
for (auto x : v) { // 等价于 for (auto x = v.begin(); ... )
std::cout << x;
}
// 底层展开
{
auto&& __range = v;
auto __begin = __range.begin();
auto __end = __range.end();
for (; __begin != __end; ++__begin) {
auto x = *__begin;
std::cout << x;
}
}
新算法实战:
cpp
std::vector<int> v = {1, -2, 3, -4, 5};
// all_of/any_of/none_of
bool all_positive = std::all_of(v.begin(), v.end(),
[](int x) { return x > 0; }); // false
bool any_negative = std::any_of(v.begin(), v.end(),
[](int x) { return x < 0; }); // true
bool none_zero = std::none_of(v.begin(), v.end(),
[](int x) { return x == 0; }); // true
// iota:填充序列
std::vector<int> v2(10);
std::iota(v2.begin(), v2.end(), 0); // 0,1,2,3,4,5,6,7,8,9
// move:高效转移
std::vector<std::string> src = {"a", "b", "c"};
std::vector<std::string> dst;
std::move(src.begin(), src.end(), std::back_inserter(dst));
// src 现在全为空,dst拥有数据,零拷贝
第九章:右值引用和移动语义在STL中的应用
老王的疑问
"右值引用不是自己用的吗?STL怎么用的?"
小李的解答
"STL容器已经全面支持移动语义,你的对象只要实现移动构造,就能自动受益。"
移动构造的威力:
cpp
class BigData {
std::vector<int> data;
public:
BigData(size_t n) : data(n) {}
// C++98:只有拷贝构造
BigData(const BigData& other) : data(other.data) {} // 深拷贝,O(n)
// C++11:增加移动构造
BigData(BigData&& other) noexcept : data(std::move(other.data)) {} // 转移,O(1)
};
std::vector<BigData> v;
v.reserve(100);
BigData bd(1000000); // 100万个int
v.push_back(bd); // 拷贝构造,慢!
v.push_back(std::move(bd)); // 移动构造,快!bd被掏空
容器的移动操作:
cpp
std::vector<std::string> v1 = {"a", "b", "c"};
// C++98:swap 是O(1)
std::vector<std::string> v2;
v2.swap(v1); // 只交换指针,O(1)
// C++11:move 赋值也是O(1)
std::vector<std::string> v3 = std::move(v1); // 移动构造,O(1)
v1 = std::move(v2); // 移动赋值,O(1)
第十章:老王的实践与顿悟
老王决定自己改写代码,但遇到了问题:
cpp
std::vector<std::unique_ptr<LogEntry>> logs;
logs.emplace_back(std::make_unique<LogEntry>(500, 1));
// 错误:不能拷贝unique_ptr
// auto it = logs.begin(); // 这可以
// auto copy = *it; // 错误!不能拷贝
小李指导:"unique_ptr 不能拷贝,但可以移动。如果需要遍历并保留指针,用 shared_ptr。"
cpp
// 方案1:只读遍历
for (const auto& entry : logs) { // const&,不拷贝
std::cout << entry->code;
}
// 方案2:需要共享所有权
std::vector<std::shared_ptr<LogEntry>> shared_logs;
// ... 填充数据
for (auto entry : shared_logs) { // 拷贝shared_ptr,增加计数
// entry 是独立的shared_ptr,但指向同一对象
}
老王的最终代码:
cpp
void old_wang_final() {
// 1. 使用shared_ptr(需要共享时)
std::vector<std::shared_ptr<LogEntry>> logs;
logs.reserve(1000000);
for (int i = 0; i < 1000000; ++i) {
logs.emplace_back(std::make_shared<LogEntry>(500, i));
}
// 2. unordered_map统计
std::unordered_map<int, int> stats;
for (const auto& entry : logs) {
stats[entry->code]++;
}
// 3. lambda排序
std::vector<std::pair<int, int>> vec(stats.begin(), stats.end());
std::sort(vec.begin(), vec.end(),
[](const auto& a, const auto& b) { return a.second > b.second; });
vec.resize(10);
// 4. 输出结果
for (const auto& [code, count] : vec) { // C++17结构化绑定
std::cout << "Code " << code << ": " << count << " times\n";
}
// 5. 自动清理,无需手动delete!
}
性能对比:
- 代码行数:200行 → 50行
- 运行时间:15秒 → 3秒
- 内存泄漏:有 → 无
- 可读性:地狱 → 天堂
第十一章:C++11 STL 核心变化总结
1. 内存管理革命
auto_ptr→unique_ptr(独占) +shared_ptr(共享) +weak_ptr(弱引用)- 核心:从"人管理内存"到"编译器管理内存"
2. 容器性能飞跃
map→unordered_map(哈希表,O(1))- 新增
array(栈数组)和forward_list(单向链表) - 核心:提供更多选择,按需优化
3. 构造效率革命
push_back→emplace_back(原地构造)- 核心:减少临时对象,零拷贝
4. 泛型编程简化
- 函数对象 → lambda表达式
- 复杂的
bind2nd→ 简洁的[](int x) { return x > 100; } - 核心:让代码意图一目了然
5. 遍历方式进化
- 迭代器 → 范围for
- 核心:减少样板代码,降低错误率
6. 移动语义支持
- 容器支持移动构造/赋值
- 算法支持
std::move - 核心:高效转移资源,避免不必要拷贝
最终的感悟
老王看着自己改完的代码,感慨万千:
"C++11不是简单的语法升级,而是编程范式的转变 。它让我从'如何正确实现'解放出来,专注于'要做什么'。智能指针让内存管理不可能出错 ,unordered_map让性能不需要优化 ,lambda让代码不再难懂。这不是升级,这是进化!"
小李笑着补充:"而且C++11只是开始,C++14、17、20还有更多惊喜。但掌握这些基础,你已经能写出现代C++了。"
老王合上电脑,站起身:"走,请你吃饭!顺便教教我C++17的结构化绑定。"
现代C++编程箴言:
- 能用智能指针就不用裸指针
- 能用emplace就不用push
- 能用lambda就不用函数对象
- 能用unordered_map就不用map
- 能用范围for就不用迭代器
从今天开始,让你的C++代码也进化吧!