C/C++ 入门:拓展选学
入门主线走完,最后补五块常用的进阶拼图:CMake、模板、异常、shared_ptr
和 weak_ptr,每块都值得单独深入,这里先打开一扇门。
📚 基本概念速读
| 名称 | 定义 | 省流 |
|---|---|---|
| CMake | 跨平台构建系统,自动生成编译命令 | 构建工具 |
| target | CMake 里的构建目标(可执行程序/库) | 要构建的东西 |
| 模板(template) | 以类型为参数的代码生成机制 | 泛型 |
| 函数模板 | 一套函数逻辑适配多种类型 | 泛型函数 |
| 类模板 | 一套类逻辑适配多种类型 | 泛型类 |
| 异常(exception) | 程序错误传递机制:throw 抛出、catch
捕获 |
错误上报 |
| lambda | 就地定义的匿名函数,可捕获外部变量 | 迷你函数 |
| 右值引用(rvalue reference) | 绑定临时对象的引用,T&& |
移动语义的载体 |
| 函数指针 | 保存函数地址的指针,可调用 | 把函数当数据 |
static_cast 等 |
C++ 风格的显式类型转换 | 带类型检查的转换 |
shared_ptr |
共享所有权的智能指针,靠引用计数管理 | 多人共享 |
weak_ptr |
不增加引用计数的观察指针 | 旁观者 |
🔧 CMake 入门
为什么需要 CMake
第 13 章里,两个源文件要手写
g++ main.cpp math.cpp -o app。文件一多,编译命令就难以维护,还容易漏文件。CMake
把”有哪些源文件、用什么编译选项”写进一份
CMakeLists.txt,自动生成构建命令。
最小示例:
1 | # CMakeLists.txt |
add_executable 的第一个参数是 target
名,后面是组成它的源文件。target 是 CMake 的核心概念:可执行程序是一个
target,库也是一个 target。
构建流程
1 | mkdir build && cd build |
也可以不区分平台地写:
1 | cmake -S . -B build |
CMake 本身不是编译器,它负责生成编译命令,真正编译的还是
g++/clang等。
| 概念 | 说明 |
|---|---|
add_executable |
生成可执行程序 target |
add_library |
生成库 target |
target_compile_options |
给某个 target 加编译选项 |
set(CMAKE_CXX_STANDARD 17) |
全局指定 C++ 标准 |
🧩 模板:泛型编程入门
函数模板
第 10 章的
vector<int>、vector<string>
已经用到了模板——vector 是类模板,<int>
是模板参数。自己写函数模板也很简单:
1 |
|
template <typename T> 表示”下面这个函数对任意类型
T
都适用”。编译器会在编译期根据调用时的实际类型生成对应代码,不是运行时判断类型。
类模板
1 |
|
| 对比 | 语法 | 例子 |
|---|---|---|
| 函数模板 | template <typename T> + 函数 |
myMax(3, 5) |
| 类模板 | template <typename T> + 类 |
Box<int>、vector<int> |
泛型编程的核心思想:把”类型”也变成参数,一份逻辑服务多种类型。这是 STL 容器和算法能通吃各种类型的原因。
🥧 lambda:就地定义的匿名函数
第 9 章 sort 的自定义比较函数其实就用了 lambda。lambda
是”就地定义、用完即走”的函数,语法:
1 | [捕获列表](参数列表) -> 返回类型 { 函数体 } |
1 |
|
| 捕获写法 | 含义 |
|---|---|
[] |
不捕获外部变量 |
[x] |
值捕获 x |
[&x] |
引用捕获 x |
[=] |
全部值捕获 |
[&] |
全部引用捕获 |
配合 STL 算法非常顺手(第 9 章的排序示例):
1 | sort(v.begin(), v.end(), [](const Student& a, const Student& b) { |
🔄 移动语义:std::move 与右值引用
为什么需要移动
拷贝构造(第 7 章)复制内容;但对临时对象来说,复制是浪费——它的资源马上就不要了,不如直接”搬走”。
1 | vector<int> big(1000000); |
std::move
把对象”标记”成可移动的,配合移动构造/移动赋值,资源直接转移而不是复制。vector、string
等标准库类型都自带移动构造。
右值引用
右值引用(T&&)绑定临时对象,是移动语义的载体:
1 |
|
要点:
| 对比 | 拷贝构造 Buffer(const Buffer&) |
移动构造 Buffer(Buffer&&) |
|---|---|---|
| 参数 | 左值引用,只读 | 右值引用,可修改 |
| 行为 | 复制资源 | 转移资源 |
| 开销 | 与资源大小相关 | 通常只是指针/句柄搬运 |
unique_ptr不能拷贝只能移动(第 8 章),就是移动语义的典型应用。写自己的资源类时,提供移动构造能显著提升性能。
📌 函数指针
函数也有地址,可以存进指针、作为参数传递,实现”把函数当数据用”:
1 |
|
函数指针最常见的用途是回调:把”要执行的操作”作为参数传给别人,由对方在合适的时机调用:
1 | void forEach(int arr[], int size, int (*action)(int)) |
现代 C++ 里,很多”传函数”的场景可以用
std::function或 lambda(更灵活、更安全),但理解函数指针是理解”函数也是一等公民”的基础。
🔀 C++ 风格类型转换
第 2 章讲过 static_cast。C++ 提供四种风格化转换,比 C
风格 (int)x 更明确、更安全:
| 转换 | 用途 | 例子 |
|---|---|---|
static_cast |
常规类型转换(数值、父子类指针) | static_cast<int>(3.7) |
const_cast |
去掉或加上 const |
const_cast<char*>(s) |
dynamic_cast |
多态运行时安全检查(沿继承层级转) | dynamic_cast<Derived*>(p) |
reinterpret_cast |
底层重新解释字节(危险,慎用) | reinterpret_cast<char*>(&x) |
1 | double d = 3.7; |
| 对比 | C 风格 (int)x |
static_cast<int>(x) |
|---|---|---|
| 意图 | 模糊,什么都能转 | 明确是”常规转换” |
| 编译器检查 | 基本不检查 | 类型不兼容时编译报错 |
| 可读性 | 看不出要转成什么类型 | 一目了然 |
优先用 C++ 风格转换:代码意图清晰,编译器能帮你抓住错误。
reinterpret_cast和const_cast属于”知道自己在干什么”才用的工具。
🚨 异常
基本语法
异常用于把错误从”发生的地方”传递到”能处理的地方”:
1 |
|
throw 把异常对象抛出去,try
块里出现异常后,程序沿调用栈向上找匹配的
catch,找到就进入处理,找不到程序会终止。
资源安全
异常和 RAII(第 9
章)是天生一对:抛出异常时会”栈展开”,栈上所有局部对象都会被自动析构。所以资源只要交给
RAII
对象(vector、unique_ptr、文件流等),即使中途抛异常也会被正确释放。
1 | void process() |
反过来,如果靠手动
new/delete管理资源,异常路径上的delete很容易被跳过。这就是为什么现代 C++ 强调”RAII + 异常”组合。
🤝 shared_ptr:共享所有权
第 9 章的 unique_ptr
是独占所有权:一块内存只属于一个指针。shared_ptr
则允许多个指针共享同一块内存,靠引用计数管理:每多一个持有者计数加一,每少一个减一,计数归零才真正释放内存。
1 |
|
| 对比 | unique_ptr |
shared_ptr |
|---|---|---|
| 所有权 | 独占 | 共享 |
| 复制 | 禁止,只能移动 | 可以复制,计数加一 |
| 释放时机 | 析构即释放 | 最后一个持有者析构才释放 |
| 适用场景 | 默认首选 | 确实需要共享时 |
原则:默认用
unique_ptr,只有当多个地方确实需要共同持有同一对象时才换shared_ptr。
👀 weak_ptr:打破循环引用
循环引用问题
shared_ptr 的一个陷阱:两个对象互相持有对方的
shared_ptr,双方的引用计数永远不会归零,内存永远不释放。
1 |
|
flowchart LR
A[a] -->|持有 b| B[b]
B -->|持有 a| A
weak_ptr 的解法
weak_ptr
是”旁观者”:它可以指向对象,但不增加引用计数,也不拥有对象。要访问对象时,先
lock() 临时得到一个 shared_ptr:
1 |
|
观察用法:
1 | weak_ptr<int> w = a; |
| 对比 | shared_ptr |
weak_ptr |
|---|---|---|
| 所有权 | 有 | 无 |
| 引用计数 | 增加 | 不增加 |
| 访问对象 | 直接 | 先 lock() |
| 用途 | 持有共享资源 | 打破循环引用、缓存观察 |
⚠️ 常见误区
| 误区 | 正解 |
|---|---|
| CMake 是编译器 | CMake 生成构建文件,编译仍由 g++/clang
完成 |
| 模板在运行时判断类型 | 模板在编译期实例化,每种类型生成一份代码 |
| 异常会破坏资源安全 | 配合 RAII,栈展开时自动析构,异常反而安全 |
shared_ptr 越多越好 |
共享所有权容易循环引用,能用 unique_ptr 就用它 |
weak_ptr 可以直接访问对象 |
weak_ptr 不拥有对象,必须先 lock()
再访问 |
| 循环引用只是浪费一点内存 | 计数永不归零,内存永久泄漏,必须用 weak_ptr 打破 |
✅ 总结
拓展选学是一组常用拼图:CMake 管构建,模板写泛型,lambda 就地定义函数,移动语义搬运资源,函数指针把函数当数据,类型转换用 C++ 风格,异常配 RAII 保证资源安全,
shared_ptr共享所有权,weak_ptr打破循环引用。
到这里,C/C++ 入门系列的主线就收尾了:从编译环境、基础语法、函数、指针、引用,到结构体、类与对象、动态内存、STL、面向对象进阶、文件 IO、多文件工程,最后用这五块选学内容补齐常用拼图。接下来无论是深入某个专题,还是直接上手项目,都已有足够的地基。
Happy Hacking! 🎉