Table of contents
Open Table of contents
TL;DR
C++11/14 的核心升级不是加了几个语法糖,而是把值语义 + RAII + 移动 + lambda 组合成新范式:资源用智能指针自动管理、返回大对象用移动不用拷贝、行为用 lambda 就地编写——日常 C++ 的认知负载一次性大幅下降,Bjarne 本人说”像一门新语言”不是营销话术。
1. C++03 的痛点:为什么需要一次”重新定义”
C++03 时代的 idiomatic code 与 C++11 后几乎是两种语言。典型痛点:
- 手动资源管理无处不在:
new/delete手写成对,异常路径极易泄漏;唯一的智能指针std::auto_ptr拷贝即转移所有权,放进容器会内存腐败,实际不可用 - 值语义拷贝开销无法绕开:返回大对象只能依赖 RVO,
vector<string>的 push_back 要深拷贝两次 - 函数对象冗长:给
std::sort传一个比较器必须写一个命名 struct 重载operator(),代码远离使用点 - 类型名冗长:
std::map<std::string, std::vector<int>>::const_iterator这种嵌套写到手酸 - 并发完全缺失:标准库没有线程、没有 mutex、没有内存模型
>嵌套歧义:vector<vector<int>>被解析成右移,必须写vector<vector<int> >- NULL 是 0:
void f(int); void f(char*); f(NULL);走 int 重载
C++11 把这些痛点一次性扫掉。核心是四块:RAII + 智能指针、移动语义、lambda、auto/nullptr/统一初始化。
2. RAII:C++ 的灵魂资源管理范式
RAII = Resource Acquisition Is Initialization。把资源生命周期绑定到对象生命周期——构造时获取,析构时释放。C++ 保证对象离开作用域一定调用析构(异常展开也不例外),所以资源一定被释放。
vs GC:
- GC 只管内存。文件句柄、锁、socket、DB 连接 GC 管不了,Java 必须 try-with-resources
- RAII 是确定性析构,释放点在代码上可预测,不会因 GC 延迟导致句柄耗尽
- 零运行时代价,没有 STW、没有写屏障
vs try-finally:try-finally 把释放逻辑散在每个调用点。RAII 把释放封装进类型本身,使用者只写声明。语言级别的关注点分离。
异常安全三种保证(Abrahams guarantees,来源:Herb Sutter《Exceptional C++》):
- basic:异常后状态一致,无泄漏
- strong:异常后回滚到操作前(copy-and-swap 模式)
- nothrow:保证不抛(析构、swap、移动构造应做到)
RAII 是实现这三种保证的基石。没有 RAII,写异常安全代码几乎不可能。
3. 智能指针体系
3.1 std::unique_ptr:独占所有权,零开销
// g++ -std=c++14
#include <memory>
struct Widget { void hello(); };
std::unique_ptr<Widget> make() {
return std::make_unique<Widget>(); // C++14 引入
}
auto p = make();
// auto q = p; // 编译错误:不可拷贝
auto q = std::move(p); // 显式转移所有权,p 变为 nullptr
sizeof(unique_ptr<T>) 等于裸指针(无自定义 deleter 时),编译后与手写 T* + delete 完全等价。彻底替代 auto_ptr。
为什么 make_unique 是 C++14 才有:委员会疏忽(Herb Sutter N3656 提案原话)。用它的理由不只是简洁,更是异常安全——C++17 之前 func(unique_ptr<A>(new A), unique_ptr<B>(new B)) 的参数求值顺序未指定,若 new A 成功后 new B 抛异常,A 就泄漏了。make_unique 把构造和包装绑在一起堵住这个裂缝。
3.2 std::shared_ptr:引用计数共享
控制块(control block)结构:shared_ptr 不是一个指针,是两个指针——一个指向 managed object,一个指向控制块。控制块至少含:strong_count、weak_count、deleter、allocator。
两种构造方式的内存布局差异(深入点):
std::shared_ptr<T> p1(new T); // 方式 A:两次分配
// 布局: [T object] [control block + 裸指针指向 T]
auto p2 = std::make_shared<T>(); // 方式 B:一次分配
// 布局: [control block + T object 连续]
make_shared 的好处:一次 malloc,省分配开销 + 减少堆碎片 + 改善 cache 局部性;异常安全同前。
make_shared 的隐藏代价(面试加分点):因为 T 对象和控制块共享同一块内存,只有 weak_count 也归零时整块内存才能释放。如果有长期存活的 weak_ptr,对象那块内存(可能很大)会延迟释放,即使 strong_count 早已归零。shared_ptr<T>(new T) 不存在这个问题——对象内存和控制块分开,strong_count 归零立即释放对象。
引用计数的原子操作代价:strong_count 和 weak_count 的增减必须原子(支持跨线程拷贝)。x86 上一次原子自增约 10–20 个周期,比非原子慢 10 倍以上。热路径上频繁拷贝 shared_ptr 是常见性能 bug。正确做法是传 const shared_ptr<T>& 或直接传 T&。
注意:引用计数的线程安全 不等于 被指向对象的线程安全。两个线程同时写同一个 T 对象仍需加锁。
3.3 std::weak_ptr:打破循环引用
struct Node {
std::shared_ptr<Node> child;
std::weak_ptr<Node> parent; // 必须 weak,否则循环引用
};
// 访问 parent 必须 lock
if (auto p = c->parent.lock()) {
// lock() 是原子操作:检查 strong_count 非 0 并原子自增
// 返回的 shared_ptr 期间保证对象存活
}
lock() 的原子性解决了”检查指针有效”和”使用指针”之间的 TOCTOU 竞态——这是 weak_ptr 存在的核心理由。
3.4 enable_shared_from_this 陷阱
struct Bad : std::enable_shared_from_this<Bad> {
std::shared_ptr<Bad> get() { return shared_from_this(); }
};
Bad b;
auto p = b.get(); // UB:栈对象没有控制块
auto s = std::make_shared<Bad>();
auto q = s->get(); // OK
原理:enable_shared_from_this 内部存一个 weak_ptr<T>,只有当对象由 shared_ptr 管理时才被初始化。栈对象和构造函数内(控制块尚未绑定)都无法使用。
4. 移动语义与右值引用
4.1 值类别(Value Categories)
C++11 重新定义了值类别,形成 5 分类:
expression
/ \
glvalue rvalue
/ \ / \
lvalue xvalue prvalue
- lvalue:有身份、不可移动。变量名
x、*p - prvalue(纯右值):无身份、可移动。字面量
42、a + b、返回对象 - xvalue(将亡值):有身份、可移动。
std::move(x)、返回T&&的函数 - rvalue = prvalue ∪ xvalue:可绑定到
T&&
判断窍门:能取地址的是 lvalue,不能取地址的是 prvalue,std::move(x) 主动转成可移动的是 xvalue。
4.2 std::move 本质是 cast
// libstdc++ 实现(简化)
template <typename T>
constexpr typename std::remove_reference<T>::type&&
move(T&& t) noexcept {
return static_cast<typename std::remove_reference<T>::type&&>(t);
}
std::move 不移动任何东西。 它只是一个 static_cast<T&&>。真正的移动在移动构造/移动赋值里完成。
4.3 移动构造为什么必须 noexcept(面试核心)
std::vector 扩容要提供 strong exception guarantee:扩容失败必须回滚到原状态。扩容过程:分配新内存 → 把旧元素搬过去 → 释放旧内存。第二步用拷贝还是移动决定了正确性:
- 拷贝:任何一个抛异常都能回滚(旧内存完好无损,清掉新内存即可)
- 移动:第 5 个元素移动时抛异常,前 4 个已被”偷走”,旧 vector 已经烂了,回滚不可能
STL 的规则:vector::push_back 扩容时用 std::move_if_noexcept——只有移动构造是 noexcept 才移动,否则退化为拷贝。
class Widget {
public:
Widget(Widget&&) noexcept; // 关键:noexcept
Widget& operator=(Widget&&) noexcept;
};
漏写 noexcept 的后果是 vector 扩容悄无声息地退化为拷贝,性能暴跌。Scott Meyers《Effective Modern C++》Item 14 专门强调。
4.4 完美转发与引用折叠
template <typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
引用折叠规则(C++11 引入):
T& &→T&T& &&→T&T&& &→T&T&& &&→T&&
当模板参数 T 遇到 T&&(forwarding reference,只有在类型推导中才是万能引用):
- 传 lvalue
int x→T推导为int&,T&&=int& &&=int& - 传 rvalue
42→T推导为int,T&&=int&&
std::forward<T> 为什么需要显式模板参数:forward 的签名是
template <typename T>
T&& forward(typename std::remove_reference<T>::type& t) noexcept;
参数类型里 T 在 :: 左边,是 non-deduced context,不参与推导。如果让它自动推导,输入永远是有名字的变量(lvalue),T 会被推成 lvalue-reference,forward 就废了。显式传 T 是把”原始值类别信息”从调用点带进来的唯一方式。
记忆法:std::move 无条件转 rvalue;std::forward<T> 按 T 有条件转 rvalue。
5. Lambda 表达式
5.1 基础与编译器视角
int x = 10;
auto f1 = [](int a, int b) { return a + b; }; // 无捕获
auto f2 = [x](int a) { return a + x; }; // 按值捕获
auto f3 = [&x](int a) { x += a; return x; }; // 按引用捕获
auto f4 = [=](int a) { return a + x; }; // 按值捕获所有
auto f5 = [&](int a) { x += a; return x; }; // 按引用捕获所有
底层实现:lambda 等价于编译器生成的匿名 class,捕获变量成为成员,operator() 就是函数体:
// [x](int a) { return a + x; } 大致等价于:
struct __lambda_1 {
int x;
int operator()(int a) const { return a + x; }
};
无捕获的 lambda 可隐式转函数指针——能传给 C 风格回调 API。
5.2 C++14 两个关键升级
// 1. 泛型 lambda(参数用 auto)
auto add = [](auto a, auto b) { return a + b; };
// 编译器生成的 class 有 template <A, B> operator()(A, B)
// 2. 初始化捕获(generalized capture)
auto p = std::make_unique<int>(42);
auto lam = [ptr = std::move(p)]() { return *ptr; };
// 解决了 C++11 无法把 move-only 类型放进 lambda 的硬伤
5.3 三个致命陷阱
陷阱 1:按引用捕获局部变量 → 悬空
std::function<int()> make_counter() {
int count = 0;
return [&count]() { return ++count; }; // 返回后 count 销毁,UB
}
陷阱 2:异步场景捕获 this → 悬空(asio/gRPC 生产高频坑)
class Session {
int id_;
public:
void start() {
submit_task([this]() {
std::cout << id_; // Session 销毁后访问 → UB
});
}
};
标准做法是 self-owning callback:
class Session : public std::enable_shared_from_this<Session> {
public:
void start() {
auto self = shared_from_this();
submit_task([self, this]() { std::cout << id_; });
}
};
这是 asio / gRPC-cpp 里到处可见的模式。
陷阱 3:[=] 不捕获 this——但会隐式捕获 this
class Foo {
int x_ = 10;
void bar() {
auto lam = [=]() { return x_; };
// C++11/14: [=] 隐式捕获 this(不是 x_ 的拷贝!)
// 访问 x_ 实际是 this->x_,this 悬空一样 UB
}
};
C++20 deprecated 了 [=] 对 this 的隐式捕获,要求显式 [=, this] 或 [=, *this]。C++11/14 时代这是大坑。
6. auto 与 decltype
6.1 auto 的推导规则
auto 的规则与模板参数推导一致(一个例外:initializer_list)。
int x = 42;
const int cx = x;
const int& rx = cx;
auto a1 = x; // int(丢 const/ref)
auto a2 = cx; // int(顶层 const 剥掉)
auto a3 = rx; // int(丢 const 和 ref)
auto& a4 = cx; // const int&(显式加 & 保留 const)
const auto& a5 = x; // const int&
auto a6 = { 1, 2, 3 }; // std::initializer_list<int>(auto 的特殊规则)
陷阱:template<typename T> void f(T) 调 f({1,2,3}) 会推导失败——auto 与模板推导唯一的区别。
6.2 decltype 与 decltype(auto)
decltype(expr) 规则:
- 无括号的变量名/成员 → 返回声明类型
- lvalue 表达式 →
T& - xvalue →
T&& - prvalue →
T
int x = 0;
decltype(x) d1 = 0; // int(变量名规则)
decltype((x)) d2 = x; // int&(加了括号,成 lvalue 表达式)—— 著名陷阱
decltype(auto) 是 C++14 引入:像 auto 一样从初始化器推导,但用 decltype 规则(保留引用和 const)。完美转发返回类型必用:
template <typename Container, typename Index>
decltype(auto) at(Container&& c, Index i) {
return std::forward<Container>(c)[i]; // 保留引用
}
写 auto 会丢引用,at(v, 0) = 42 就不能用。
7. 统一初始化、nullptr、enum class
7.1 {} 与 most vexing parse
Widget w(); // 陷阱:这是函数声明
Widget w{}; // 正确:默认构造
int j{3.14}; // 编译错误:narrowing conversion
// initializer_list 优先级陷阱
std::vector<int> v1(10, 5); // 10 个 5
std::vector<int> v2{10, 5}; // 2 个元素 10 和 5(优先 initializer_list 构造函数)
Scott Meyers EMC++ Item 7 专门警告:{} 和 () 不是完全等价,当一个类同时有 initializer_list 构造函数时,{} 会优先走它。
7.2 nullptr
void f(int); void f(char*);
f(0); // f(int)
f(NULL); // 通常走 f(int)(NULL 往往是 0)
f(nullptr); // f(char*),意图明确
nullptr 类型是 std::nullptr_t,可隐式转任何指针,不能转整型。彻底消除 NULL 的重载歧义。
7.3 enum class
enum class Color : uint8_t { Red, Green, Blue };
Color c = Color::Red;
// int x = c; // 编译错误
int x = static_cast<int>(c);
三个好处:作用域隔离(Color::Red 不污染命名空间)、禁止隐式转 int、可指定底层类型、可前向声明(老 enum 不行因为大小未定)。
8. constexpr:从运行期搬到编译期
// C++11:函数体必须一个 return
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
// C++14:放宽,可含循环
constexpr int factorial_14(int n) {
int result = 1;
for (int i = 2; i <= n; ++i) result *= i;
return result;
}
constexpr int f = factorial_14(5);
static_assert(f == 120, "");
陷阱:constexpr 函数不是必须编译期求值,只是可以。用运行期参数调就是普通函数。只有在需要常量表达式的上下文(模板参数、数组大小、static_assert、constexpr 变量初始化)才强制编译期求值。
9. 变参模板与 C++14 小改进
// C++11 变参模板:递归展开
void print() {}
template <typename T, typename... Rest>
void print(const T& first, const Rest&... rest) {
std::cout << first << " ";
print(rest...);
}
// C++14 其他
int mask = 0b1010'1100; // 二进制字面量 + 数字分隔符
int million = 1'000'000;
auto compute(int x) { return x * 2; } // 返回类型推导
template <typename T> constexpr T pi = T(3.14159); // 变量模板
int old = std::exchange(val, new_val); // 原子替换返回旧值(常用于移动构造)
10. 常见陷阱总表
| 陷阱 | 为什么是坑 | 如何避免 |
|---|---|---|
| 按引用捕获局部变量 | lambda 出作用域后悬空 | 按值捕获,或返回值拷贝 |
异步捕获 this | 对象销毁后回调访问悬空 | shared_from_this + self-owning callback |
[=] 隐式捕获 this | 对象销毁一样 UB | 显式 [=, *this](C++17)或 shared_from_this |
| 移动构造没写 noexcept | vector 扩容退化为拷贝 | 总是写 noexcept |
shared_ptr 热路径拷贝 | 原子自增开销大 | 传 const shared_ptr& 或 T& |
make_shared + 大对象 + 长存 weak | 内存延迟释放 | 用 shared_ptr<T>(new T) |
shared_from_this 栈对象 / 构造函数 | 控制块未绑定 | 必须由 make_shared 创建 |
Widget w(); | most vexing parse 声明函数 | Widget w{}; |
vector<int> v{10, 5} | 被 initializer_list 抢走 | 明确意图用 () |
decltype((x)) 多一对括号 | 变 lvalue 表达式 → T& | 记住规则,测试推导结果 |
11. 生产 Checklist
- 资源管理一律 RAII,不手写
new/delete(放进unique_ptr/shared_ptr) - 构造
unique_ptr/shared_ptr用make_unique/make_shared,不用 rawnew - 所有类型的移动构造/移动赋值都写
noexcept(除非真的可能抛) - 热路径不要随便拷贝
shared_ptr - 异步回调持有对象用
shared_from_this模式 - 默认用
auto声明局部变量,用auto&避免拷贝 - 用
enum class替代老 enum - 字符串常量传参用
const std::string&(C++17 后改std::string_view) T&&在模板里记住它是 forwarding reference,不是 rvalue reference- 工厂函数返回按值(RVO 会消除拷贝)
关键信息来源
- ISO 标准草案:N3337(C++11 final)、N4140(C++14 final)
- Scott Meyers,《Effective Modern C++》 O’Reilly 2014——智能指针、移动语义、lambda 捕获的陷阱几乎都出自这本
- Herb Sutter,《Exceptional C++》——异常安全三种保证的经典论述
- cppreference.com——API 级权威参考
- 提案文档:N3656 (make_unique)、N2118 (rvalue references)、N2927 (lambdas)、N3648 (generalized lambda capture)
置信度说明:上述 C++11/14 语义都是 ISO 标准稳定内容,可信度高。具体标准条款号需查 N3337/N4140 对应章节(需验证)。