C++ 语法速查表 - C++ 常用语法与 STL 容器大全
这套速查给写应用、游戏或库的 C++ 开发者,围绕现代 C++ 组织——智能指针、STL 和 Lambda 正是把 C++ 那著名的"手动生命周期易出错"压下去的关键。内容覆盖类与对象及其访问控制、各场景该选哪个 STL 容器、智能指针对比裸 new/delete、Lambda 捕获、函数与类模板、异常。与平铺的 C++ 特性表不同,条目按你在写哪类结构来分,并点出 RAII 与手动所有权怎么选。读完应能优先用 std::vector/std::map 而非裸数组、用 unique_ptr 而不是裸 new,并写出不泄漏的模板函数或 Lambda。
典型使用场景
面向写 C++ 时想在安全性和性能之间取得平衡的开发者,尤其是从「会 new/delete」过渡到现代 C++ 工程写法的阶段。本表沿着类与对象、STL 容器、智能指针、Lambda、模板、异常处理、现代特性七条线展开:把手动内存管理换成 unique_ptr / shared_ptr / weak_ptr 三件套,明确什么时候用独占、什么时候需要引用计数、为什么观察者场景要用弱引用避免循环;容器部分给出 vector / list / map / unordered_map / set / stack / queue 的选型对照;最后落到 auto、decltype、constexpr、optional、variant、结构化绑定这些能立刻减少样板代码的特性上。
类与对象 8
class MyClass { public: MyClass(); ~MyClass(); };struct MyStruct { int x; int y; };MyClass obj;MyClass *obj = new MyClass();delete obj;class Derived : public Base { };virtual void func() override;class Abstract { virtual void func() = 0; };STL 容器 8
std::vector<int> v = {1, 2, 3};v.push_back(4); v.size(); v[0];std::list<int> l;std::map<std::string, int> m;std::unordered_map<std::string, int> m;std::set<int> s;std::stack<int> st; std::queue<int> q;std::pair<int, std::string> p = {1, "one"};智能指针 5
std::unique_ptr<int> p = std::make_unique<int>(10);std::shared_ptr<int> p = std::make_shared<int>(10);std::weak_ptr<int> wp = sp;if (auto locked = wp.lock()) { }std::unique_ptr<int> p2 = std::move(p);Lambda 表达式 6
auto fn = []() { return 42; };int x = 10; auto fn = [x]() { return x; };auto fn = [&x]() { x++; };auto fn = [=]() { return x; };auto fn = [&]() { x++; };auto fn = [](int a, int b) { return a + b; };模板 4
template <typename T> T add(T a, T b) { return a + b; }template <typename T> class Stack { };template <> void func<int>(int x) { }std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; });异常处理 5
try { } catch (const std::exception &e) { }throw std::runtime_error("error");catch (...) { }noexceptstd::terminate();现代 C++ 特性 8
auto x = 42;decltype(x) y = 10;constexpr int MAX = 100;std::optional<int> opt = 42;std::variant<int, std::string> v = 42;std::any a = 42;for (auto &[key, value] : map) { }std::string_view sv = "hello";参数矩阵
| 参数 | 作用 | 示例 |
|---|---|---|
class MyClass | 类定义,默认成员私有 | class MyClass { public: MyClass(); ~MyClass(); }; |
struct MyStruct | 结构体,成员默认公开 | struct MyStruct { int x; int y; }; |
MyClass obj; | 在栈上创建对象,作用域结束自动析构 | MyClass obj; |
new MyClass() | 在堆上创建对象,必须配对释放 | MyClass *obj = new MyClass(); |
delete obj; | 释放堆上对象,指针置空避免悬垂 | delete obj; |
class Derived : public Base | 公有继承 | class Derived : public Base { }; |
virtual void func() override; | 重写基类虚函数并标注校验 | virtual void func() override; |
virtual void func() = 0; | 纯虚函数,使类成为抽象基类 | class Abstract { virtual void func() = 0; }; |
std::vector<int> | 连续内存动态数组,随机访问 O(1) | std::vector<int> v = {1, 2, 3}; |
v.push_back(4) / v.size() / v[0] | vector 的尾部追加、长度与下标访问 | v.push_back(4); v.size(); v[0]; |
std::list<int> | 双向链表,中间插删不移动元素 | std::list<int> l; |
std::map<K, V> | 有序映射(红黑树),键按序排列 | std::map<std::string, int> m; |
std::unordered_map<K, V> | 无序映射(哈希表),查找平均更快但无序 | std::unordered_map<std::string, int> m; |
std::set<T> | 有序去重集合 | std::set<int> s; |
std::stack<T> / std::queue<T> | 栈与队列的适配器 | std::stack<int> st; std::queue<int> q; |
std::pair<A, B> | 键值对,常与结构化绑定搭配 | std::pair<int, std::string> p = {1, "one"}; |
std::make_unique<T>(v) | 构造独占指针,是推荐的创建方式 | std::unique_ptr<int> p = std::make_unique<int>(10); |
std::make_shared<T>(v) | 构造共享指针,单次分配提升效率 | std::shared_ptr<int> p = std::make_shared<int>(10); |
std::weak_ptr<T> wp = sp; | 弱引用,不增加引用计数 | std::weak_ptr<int> wp = sp; |
if (auto locked = wp.lock()) | 把弱引用临时提升为强引用,失败说明对象已销毁 | if (auto locked = wp.lock()) { } |
std::move(p) | 转移所有权,让源指针置空 | std::unique_ptr<int> p2 = std::move(p); |
[]() { return 42; } | 无捕获 Lambda | auto fn = []() { return 42; }; |
[x] / [&x] | 按值捕获 / 按引用捕获指定变量 | auto fn = [&x]() { x++; }; |
[=] / [&] | 按值 / 按引用捕获所有外层变量 | auto fn = [&]() { x++; }; |
template <typename T> T add(T, T) | 函数模板,泛型求值 | template <typename T> T add(T a, T b) { return a + b; } |
template <typename T> class Stack | 类模板 | template <typename T> class Stack { }; |
template <> ... 特化 | 为特定类型提供专门实现 | template <> void func<int>(int x) { } |
std::sort(begin, end, cmp) | 排序并可传自定义比较函数 | std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; }); |
try / catch (const std::exception &e) | 捕获标准异常并读取错误信息 | try { } catch (const std::exception &e) { } |
throw std::runtime_error("error") | 抛出带信息的异常 | throw std::runtime_error("error"); |
catch (...) | 捕获所有异常,作为兜底分支 | catch (...) { } |
noexcept | 声明函数不会抛异常 | noexcept |
auto x = 42; | 类型推导,省去显式类型 | auto x = 42; |
decltype(x) y = 10; | 按表达式类型声明新变量 | decltype(x) y = 10; |
constexpr int MAX = 100; | 编译期常量 | constexpr int MAX = 100; |
std::optional<int> | 可含可不含的值,替代裸指针表达「可能没有」 | std::optional<int> opt = 42; |
std::variant<int, std::string> | 编译期确定的多类型联合体 | std::variant<int, std::string> v = 42; |
std::any a = 42; | 可存放任意类型,代价是类型检查在运行时 | std::any a = 42; |
for (auto &[key, value] : map) | 结构化绑定遍历映射,key/value 直接可用 | for (auto &[key, value] : map) { } |
std::string_view sv = "hello" | 字符串视图,不持有内存,零拷贝 | std::string_view sv = "hello"; |
易错点与避坑指南
现象delete 之后又访问指针,程序偶发崩溃或读出乱码。
原因释放堆对象后没有把指针置空,形成了悬垂指针;更隐蔽的是两个指针指向同一块内存被重复释放。
处置优先改用 std::unique_ptr 接管所有权;若必须手动管理,释放后立即置空,并保证每个 new 恰好对应一次 delete。
现象两个对象互相持有 shared_ptr,程序结束时泄漏且两者都还在。
原因shared_ptr 的循环引用会让引用计数永远归不了零,对象无法被销毁。
处置把其中一侧改成 std::weak_ptr,观察者持有弱引用;访问时用 if (auto locked = wp.lock()) 临时提升。
现象按基类指针删除派生类对象时,析构函数没有被调用,出现资源泄漏。
原因基类析构函数不是虚函数,delete 基类指针不会走派生类的析构逻辑。
处置给打算以基类多态方式管理的那个类把析构函数声明为 virtual。
现象循环里频繁 push_back 导致性能突然下降。
原因vector 扩容需要重新分配并拷贝/移动全部元素,缺少预留容量时每次增长都有额外开销。
处置在已知规模时先 reserve 好容量,或改用 std::list 这类中间插删不移动元素的容器。
现象对同一个字符串做大量拼接,程序越来越慢。
原因每次 + 都会构造新字符串并拷贝已有内容,复杂度随长度增长累积。
处置改用 std::string_view 传递只读片段不复制,需要拼接时集中构造而不是循环内反复相加。
现象Lambda 里修改外部变量的修改没生效,或者捕获后调用时变量已析构。
原因按值捕获拿到的是当时的副本,之后外层变化不可见;按引用捕获则可能在对象销毁后留下悬垂引用。
处置确认语义需求:要快照用 [x],要联动用 [&x] 并保证被捕获对象的生命周期长于 Lambda。
现象多态调用时发现子类方法没被调到,退回了基类实现。
原因虚函数被隐藏而非重写(派生类里同名函数没有 override 且签名不同),或通过非多态方式(值传参、模板推导)调用。
处置在重写处加 override 让编译器校验签名;调用处用基类引用或指针以触发动态派发。
排障路径
1确认异常究竟从哪里抛出
try { } catch (const std::exception &e) { }e.what() 会给出异常携带的信息,可据此定位是哪种标准异常。
2观察弱引用指向的对象是否已被销毁
if (auto locked = wp.lock()) { }lock() 成功说明对象还活着,返回空说明引用计数已归零,不要再解引用。
3验证共享指针的所有权是否已转移
std::unique_ptr<int> p2 = std::move(p);转移后 p 处于空状态,再使用 p 会得到空指针解引用。
4打印容器内容与长度确认数据正确性
v.push_back(4); v.size(); v[0]先确认写入与读取路径无误,再怀疑排序或比较逻辑。
5用自定义比较函数验证排序行为
std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; });比较函数必须构成严格弱序,返回 a > b 得到降序结果。
6确认被捕获的变量在 Lambda 生命周期内有效
auto fn = [&x]() { x++; };若 Lambda 被交给异步任务或长期保存,引用捕获容易在作用域结束后失效。
命令示例
RAII:make_unique 管理生命周期,move 转移所有权
auto p = std::make_unique<int>(10);
std::unique_ptr<int> p2 = std::move(p);
if (!p) {
// p 已经为空所有权,析构不会二次释放
}unique_ptr 离开作用域自动 delete,异常路径也不会漏;std::move 之后原指针为空。函数返回 unique_ptr 时不能写拷贝,必须靠移动或直接 return(现代编译器做了 NRVO)。
shared_ptr / weak_ptr 打破循环引用
std::shared_ptr<int> sp = std::make_shared<int>(10);
std::weak_ptr<int> wp = sp;
if (auto locked = wp.lock()) {
// locked 是有效的 shared_ptr,作用域内 sp 不会归零
}shared_ptr 之间互相引用会让计数永远降不到 0,节点与它持有的数据一起泄漏;weak_ptr 不参与计数,lock() 在对象已析构时返回空指针。
Lambda 值捕获避免悬垂,引用捕获改动原变量
int x = 10;
auto byValue = [x]() { return x; };
auto byRef = [&x]() { x++; };
byRef();
int y = byValue();值捕获把当时的 x 拷进闭包,之后改 x 不影响它;引用捕获存的是地址,若被捕获的局部变量先出作用域,闭包再执行就是悬垂引用。
结构化绑定遍历 map,配合 optional 处理缺失
std::map<std::string, int> m = {{"a", 1}};
for (auto &[key, value] : m) {
value += 1;
}
std::optional<int> opt = find(3);
if (opt.has_value()) {
use(*opt);
}auto & 让循环直接改 map 里的值;auto 按值会拷贝一份,改动丢失。optional 用 * 解包,直接参与算术会编译失败,避免了「忘记判空」的静默错误。
异常:按类型捕获并保留 message
try {
throw std::runtime_error("error");
} catch (const std::exception &e) {
log(e.what());
} catch (...) {
log("unknown");
}按 const 引用捕获基类才能拿到 what() 里的内容;漏写 catch 块时异常会一路传播到 main 并触发 std::terminate,程序直接退出且没有可读堆栈。
常见坑与注意事项
- 悬垂指针最常见的来源是引用捕获与临时对象:函数返回 [x](按值捕获才安全),或把 vector 元素的引用/指针存到外部后继续 push_back 触发扩容,旧内存立即失效。
- 迭代器与引用会在容器变更后失效:for 循环里 erase 当前元素后迭代器已经无效,继续 ++ 是未定义行为;range-for 中 push_back 会让遍历的区间与实际存储错位。
- 裸 new 遇到异常就泄漏:初始化中途抛出时对应的 delete 永远不会执行;类成员优先用 unique_ptr,只有确实需要共享所有权时才上 shared_ptr。
- 两个 shared_ptr 互相持有会形成引用环,引用计数永远回不到 0,valgrind 里表现为 "still reachable" 且内存持续上涨;环上必须有一个是 weak_ptr。
- 通过基类指针 delete 派生对象,若基类析构函数不是 virtual 就是未定义行为:派生成员不析构、资源泄漏,部分运行时会直接崩。
- delete 与 delete[] 混用、重复 delete、以及对栈对象用 delete 都会造成堆破坏:症状是程序在毫不相关的位置崩,valgrind 的 invalid free 是唯一线索。
提示
- 优先使用智能指针,避免手动 new/delete。
- STL 容器选择:频繁随机访问用 vector,频繁插入删除用 list,键值查找用 unordered_map。
- Lambda 表达式适合短小的回调函数,复杂逻辑建议用普通函数或函数对象。
常见问题
shared_ptr 循环引用为什么会导致内存泄漏?
shared_ptr 靠引用计数管理生命周期,对象销毁时计数归零即释放。问题在于计数是成对增减的:A 持有一个指向 B 的 shared_ptr,B 又持有一个指向 A 的 shared_ptr,于是两者互相把对方的计数加一,谁都不会先归零,即便外部所有 shared_ptr 都被销毁,这两个对象仍然存活,对应的堆内存永远回收不了。解法是打断环:其中一侧改用 std::weak_ptr<int> wp = sp,它不增加计数,再通过 if (auto locked = wp.lock()) { } 在真正要用时临时提升为强引用,从而在环断开后正常析构。
按引用捕获和按值捕获,在 Lambda 里改 x 有什么区别?
[x] 是值捕获:Lambda 内部拿到的是 x 在定义那一刻的副本,改这个副本不会影响外层的 x。 [&x] 是引用捕获:内部和外层操作同一个对象,写 [&x]() { x++ } 会真的把外层 x 加一;代价是如果 Lambda 活得比 x 还久(比如被异步保存、存进容器再返回),就会引用到失效对象。实践上的取舍是:短小的同步回调用引用捕获省拷贝,逃逸出去或生命周期不可控的回调优先值捕获,需要写外层状态时用 [&],只读就用 [=]。
C++17 的结构化绑定替代了哪些手写循环?
过去遍历 std::map 要写 for (const auto &p : m) { auto key = p.first; auto value = p.second; },C++17 之后可以写成 for (auto &[key, value] : map) { },直接得到键和值两个变量,且引用形式可以就地修改。pair 同样适用:for (const auto &[id, name] : users)。它不只是少写两行,而是让「解构」这件事在语法上成立,意图更明确。注意 auto & 是引用绑定,写入会影响容器元素;只读遍历用 const auto &。
什么时候该用 unordered_map,什么时候该用 map?
核心差别是底层结构与代价。std::map 是红黑树,键有序,查找、插入、删除都是 O(log n),而且天然支持按序遍历——需要「取最大键」「输出有序结果」时 map 是唯一选择。std::unordered_map 是哈希表,平均 O(1),没有顺序保证,遍历顺序由哈希分布决定,纯查找场景更快。实际写代码时先问一句:这个映射需不需要有序遍历或范围查询?需要就 map,不需要且键数量大、查找密集就 unordered_map。此外键是对象时 hash 成本高,收益会被削弱。
纯虚函数、override 和 noexcept 分别解决什么问题?
virtual void func() = 0 是纯虚函数声明,它让类成为抽象类、无法实例化,但派生类必须实现它,于是「接口约束」被编译器强制执行;配套的虚析构函数保证通过基类指针删除派生对象时不泄漏。override 是给重写用的标记:函数签名一旦和基类不一致,编译器立刻报错,避免「拼错参数类型导致悄悄变成重载」的经典 bug。noexcept 声明函数不会抛异常,配合 std::vector 等容器在扩容时选择更快的移动路径;一旦违反承诺会触发 std::terminate() 直接终止程序。
由 巧匠 维护
公开更新于 2026年7月21日,内容持续校对官方文档。