C/C++ 入门:拓展选学

入门主线走完,最后补五块常用的进阶拼图:CMake、模板、异常、shared_ptrweak_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
2
3
4
5
6
7
8
9
# CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(MyApp)

set(CMAKE_CXX_STANDARD 17) # 指定 C++17

add_executable(app main.cpp math.cpp) # 源文件组织

target_compile_options(app PRIVATE -Wall -Wextra) # 编译选项

add_executable 的第一个参数是 target 名,后面是组成它的源文件。target 是 CMake 的核心概念:可执行程序是一个 target,库也是一个 target。

构建流程

1
2
3
4
mkdir build && cd build
cmake .. # 根据 CMakeLists.txt 生成构建文件
make # 按生成的构建文件编译链接
./app # 运行

也可以不区分平台地写:

1
2
cmake -S . -B build
cmake --build 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
using namespace std;

template <typename T>
T myMax(T a, T b)
{
return a > b ? a : b;
}

int main()
{
cout << myMax(3, 5) << endl; // 5,T 推断为 int
cout << myMax(3.1, 2.9) << endl; // 3.1,T 推断为 double
cout << myMax('a', 'c') << endl; // c,T 推断为 char
return 0;
}

template <typename T> 表示”下面这个函数对任意类型 T 都适用”。编译器会在编译期根据调用时的实际类型生成对应代码,不是运行时判断类型。

类模板

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
#include <iostream>
using namespace std;

template <typename T>
class Box {
private:
T value;

public:
Box(T v) : value(v) {}

T get() const
{
return value;
}
};

int main()
{
Box<int> b1(42);
Box<string> b2("hello");

cout << b1.get() << endl; // 42
cout << b2.get() << endl; // hello
return 0;
}
对比 语法 例子
函数模板 template <typename T> + 函数 myMax(3, 5)
类模板 template <typename T> + 类 Box<int>vector<int>

泛型编程的核心思想:把”类型”也变成参数,一份逻辑服务多种类型。这是 STL 容器和算法能通吃各种类型的原因。

🥧 lambda:就地定义的匿名函数

第 9 章 sort 的自定义比较函数其实就用了 lambda。lambda 是”就地定义、用完即走”的函数,语法:

1
[捕获列表](参数列表) -> 返回类型 { 函数体 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using namespace std;

int main()
{
// 捕获外部变量 x,返回两数之和
int x = 10;
auto addX = [x](int a) -> int {
return a + x;
};

cout << addX(5) << endl; // 15

// 捕获方式:[] 不捕获,[=] 值捕获,[&] 引用捕获
int base = 100;
auto addBase = [&](int a) { // 引用捕获,可修改 base
return a + base;
};
cout << addBase(1) << endl; // 101

return 0;
}
捕获写法 含义
[] 不捕获外部变量
[x] 值捕获 x
[&x] 引用捕获 x
[=] 全部值捕获
[&] 全部引用捕获

配合 STL 算法非常顺手(第 9 章的排序示例):

1
2
3
sort(v.begin(), v.end(), [](const Student& a, const Student& b) {
return a.score > b.score;
});

🔄 移动语义:std::move 与右值引用

为什么需要移动

拷贝构造(第 7 章)复制内容;但对临时对象来说,复制是浪费——它的资源马上就不要了,不如直接”搬走”。

1
2
3
vector<int> big(1000000);
vector<int> v2 = big; // 拷贝:复制 100 万个元素
vector<int> v3 = move(big); // 移动:把 big 的内部指针搬给 v3,big 变空

std::move 把对象”标记”成可移动的,配合移动构造/移动赋值,资源直接转移而不是复制。vectorstring 等标准库类型都自带移动构造。

右值引用

右值引用(T&&)绑定临时对象,是移动语义的载体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <string>
using namespace std;

class Buffer {
private:
string data;

public:
Buffer(string s) : data(move(s)) {} // 把 s 的内容搬进 data

// 移动构造:把 other 的资源"偷"过来
Buffer(Buffer&& other) noexcept
: data(move(other.data))
{
}
};

要点:

对比 拷贝构造 Buffer(const Buffer&) 移动构造 Buffer(Buffer&&)
参数 左值引用,只读 右值引用,可修改
行为 复制资源 转移资源
开销 与资源大小相关 通常只是指针/句柄搬运

unique_ptr 不能拷贝只能移动(第 8 章),就是移动语义的典型应用。写自己的资源类时,提供移动构造能显著提升性能。

📌 函数指针

函数也有地址,可以存进指针、作为参数传递,实现”把函数当数据用”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using namespace std;

int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }

int main()
{
// 声明函数指针:返回类型 (*名字)(参数列表)
int (*fp)(int, int);

fp = add; // 指向 add
cout << fp(2, 3) << endl; // 5,通过指针调用

fp = mul; // 改指向 mul
cout << fp(2, 3) << endl; // 6

return 0;
}

函数指针最常见的用途是回调:把”要执行的操作”作为参数传给别人,由对方在合适的时机调用:

1
2
3
4
5
6
void forEach(int arr[], int size, int (*action)(int))
{
for (int i = 0; i < size; i++) {
arr[i] = action(arr[i]);
}
}

现代 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
2
3
4
5
double d = 3.7;
int i = static_cast<int>(d); // 3,截断小数

const int n = 10;
int* p = const_cast<int*>(&n); // 去掉 const(慎用,修改可能未定义)
对比 C 风格 (int)x static_cast<int>(x)
意图 模糊,什么都能转 明确是”常规转换”
编译器检查 基本不检查 类型不兼容时编译报错
可读性 看不出要转成什么类型 一目了然

优先用 C++ 风格转换:代码意图清晰,编译器能帮你抓住错误。reinterpret_castconst_cast 属于”知道自己在干什么”才用的工具。

🚨 异常

基本语法

异常用于把错误从”发生的地方”传递到”能处理的地方”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <stdexcept>
using namespace std;

double divide(double a, double b)
{
if (b == 0) {
throw runtime_error("除以零"); // 抛出异常
}
return a / b;
}

int main()
{
try {
double r = divide(10, 0);
cout << r << endl;
}
catch (const exception& e) {
cout << "出错:" << e.what() << endl; // 出错:除以零
}
return 0;
}

throw 把异常对象抛出去,try 块里出现异常后,程序沿调用栈向上找匹配的 catch,找到就进入处理,找不到程序会终止。

资源安全

异常和 RAII(第 9 章)是天生一对:抛出异常时会”栈展开”,栈上所有局部对象都会被自动析构。所以资源只要交给 RAII 对象(vectorunique_ptr、文件流等),即使中途抛异常也会被正确释放。

1
2
3
4
5
void process()
{
vector<int> v(100); // RAII:异常时自动释放
// 如果这里抛异常,v 的析构函数照常执行
}

反过来,如果靠手动 new/delete 管理资源,异常路径上的 delete 很容易被跳过。这就是为什么现代 C++ 强调”RAII + 异常”组合。

🤝 shared_ptr:共享所有权

第 9 章的 unique_ptr 是独占所有权:一块内存只属于一个指针。shared_ptr 则允许多个指针共享同一块内存,靠引用计数管理:每多一个持有者计数加一,每少一个减一,计数归零才真正释放内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <memory>
using namespace std;

int main()
{
shared_ptr<int> a = make_shared<int>(42);
shared_ptr<int> b = a; // b 和 a 共享,计数变成 2

cout << *a << endl; // 42
cout << a.use_count() << endl; // 2

b.reset(); // b 放弃持有,计数回到 1
// a 离开作用域时计数归零,内存才被释放
return 0;
}
对比 unique_ptr shared_ptr
所有权 独占 共享
复制 禁止,只能移动 可以复制,计数加一
释放时机 析构即释放 最后一个持有者析构才释放
适用场景 默认首选 确实需要共享时

原则:默认用 unique_ptr,只有当多个地方确实需要共同持有同一对象时才换 shared_ptr

👀 weak_ptr:打破循环引用

循环引用问题

shared_ptr 的一个陷阱:两个对象互相持有对方的 shared_ptr,双方的引用计数永远不会归零,内存永远不释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <memory>
using namespace std;

struct Node {
shared_ptr<Node> next;
};

int main()
{
auto a = make_shared<Node>();
auto b = make_shared<Node>();
a->next = b;
b->next = a; // 互相持有:a 和 b 的计数都至少是 2,永远释放不了
return 0;
}


flowchart LR
    A[a] -->|持有 b| B[b]
    B -->|持有 a| A

weak_ptr 的解法

weak_ptr 是”旁观者”:它可以指向对象,但不增加引用计数,也不拥有对象。要访问对象时,先 lock() 临时得到一个 shared_ptr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <memory>
using namespace std;

struct Node {
weak_ptr<Node> next; // 改用 weak_ptr:不增加计数
};

int main()
{
auto a = make_shared<Node>();
auto b = make_shared<Node>();
a->next = b;
b->next = a; // 计数各为 1,离开作用域正常释放
return 0;
}

观察用法:

1
2
3
4
5
6
7
8
weak_ptr<int> w = a;

if (auto sp = w.lock()) {
// lock 成功:对象还活着,sp 是临时 shared_ptr
cout << *sp << endl;
} else {
// 对象已被释放
}
对比 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! 🎉