【c++进阶】从C++98到C++11的奇妙旅程(故事科普版)

关注我,学习c++不迷路:

个人主页:爱装代码的小瓶子

专栏如下:

  1. c++学习
  2. Linux学习

后续会更新更多有趣的小知识,关注我带你遨游知识世界

期待你的关注。


文章目录


第一章:老王的"古代"代码

老王是个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_ptrunique_ptr(独占) + shared_ptr(共享) + weak_ptr(弱引用)
  • 核心:从"人管理内存"到"编译器管理内存"

2. 容器性能飞跃

  • mapunordered_map(哈希表,O(1))
  • 新增 array(栈数组)和 forward_list(单向链表)
  • 核心:提供更多选择,按需优化

3. 构造效率革命

  • push_backemplace_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++编程箴言

  1. 能用智能指针就不用裸指针
  2. 能用emplace就不用push
  3. 能用lambda就不用函数对象
  4. 能用unordered_map就不用map
  5. 能用范围for就不用迭代器

从今天开始,让你的C++代码也进化吧!

相关推荐
智航GIS2 小时前
2.3 运算符详解
开发语言·python
web3.08889992 小时前
接入API-自动化批量获取淘宝商品详情数据
开发语言·python
世转神风-2 小时前
qt-在字符串中指定位置插入字符串
开发语言·qt
时光呀时光慢慢走2 小时前
C# WinForms 实战:MQTTS 客户端开发(与 STM32 设备通信)
开发语言·c#
superman超哥3 小时前
仓颉类型别名的使用方法深度解析
c语言·开发语言·c++·python·仓颉
LFly_ice3 小时前
Next-4-路由导航
开发语言·前端·javascript
3824278273 小时前
python :__call__方法
开发语言·python
是Yu欸3 小时前
从Ascend C算子开发视角看CANN的“软硬协同”
c语言·开发语言·云原生·昇腾·ascend·cann·开放社区
黎雁·泠崖3 小时前
C 语言字符串进阶:strcpy/strcat/strcmp 精讲
c语言·开发语言