C++11新特性

AI-assisted


C++11 新特性详解

C++11(ISO/IEC 14882:2011)是继 C++98/03 之后的第一次重大标准修订。它在不破坏兼容性的前提下,向语言和标准库引入了大量新特性,目标是让 C++ 代码更简洁、更安全、更高效。

本文按照「语言核心 → 函数与类 → 模板 → 并发 → 智能指针 → 标准库」的顺序,对 C++11 的新特性进行系统梳理,每个特性均配有示例代码与注意事项。

目录

  1. 类型推导:auto 与 decltype
  2. 统一初始化(列表初始化)
  3. nullptr 空指针
  4. 强类型枚举(enum class)
  5. constexpr 常量表达式
  6. 范围 for 循环
  7. Lambda 表达式
  8. 右值引用与移动语义
  9. 函数的改进
  10. 类与对象的其他改进
  11. 模板的增强
  12. 字符串与字面量
  13. 其他语言特性
  14. 并发编程支持
  15. 智能指针
  16. 标准库(STL)新增内容
  17. 常见陷阱与迁移建议
  18. 参考资料

1. 类型推导:auto 与 decltype

C++11 引入 auto 和 decltype,让编译器在编译期推导类型,简化冗长的类型书写,也是尾置返回类型、泛型编程的基础。

1.1 auto

auto 让编译器根据初始化表达式自动推导变量类型:

auto x = 42;             // int
auto pi = 3.14;          // double
auto s = std::string("hello");  // std::string,无需再写一遍类型名
auto v = std::vector<int>{1, 2, 3};

// 迭代器类型尤其受益
for (auto it = v.begin(); it != v.end(); ++it) { /* ... */ }

推导规则:

  • auto 会忽略初始化表达式的顶层 const、volatile 和引用:
const int ci = 10;
auto a = ci;        // int(const 被丢弃)
auto& b = ci;       // const int&(显式写 & 则保留引用与 cv 限定)
const auto c = ci;  // const int(显式写 const 则保留)
  • 一行声明多个变量时,推导结果必须一致,否则报错:
auto m = 1, n = 2;      // ok,都是 int
auto p = 1, q = 1.0;    // error:类型不一致,有二义性

使用限制(C++11):

场景 是否允许
必须立即初始化 是,否则报错
函数形参 void f(auto x) 不允许(C++20 的缩写模板才支持)
类的非静态成员变量 不允许
模板实参 std::vector<auto> 不允许
定义数组 auto arr[10] 不允许(但 auto 可推导数组为指针)
int arr[3] = {1, 2, 3};
auto p = arr;           // int*,数组退化为指针
auto& r = arr;          // int(&)[3],显式引用则保留数组类型

与动态内存配合:

auto p1 = new auto(x);   // p1 的类型为 decltype(x)*
// auto p2 = new auto{1, 2, 3};  // error:new auto 只允许单个初始化器
// auto 不能用于直接分配数组

什么时候用 auto: 类型复杂、显而易见或根本不关心具体类型时(迭代器、lambda、std::function、模板返回值)优先使用 auto;对 int、double 这类一目了然的简单类型,写不写均可,以可读性为准。

auto func = [](int i) { return i * 2; };              // lambda 类型无法手写
auto fut = std::async(std::launch::async, func, 21);  // future 类型冗长

1.2 decltype

decltype(exp) 返回表达式 exp 的类型,只分析不求值:

int x = 0;
decltype(x) y = 1;         // int
decltype(x + 1) z;         // int(x + 1 不求值)
decltype(&x) p = &x;       // int*

std::vector<int> vec;
typedef decltype(vec.begin()) IterType;  // 与 using/typedef 配合很方便

推导规则(四条):

  1. exp 是不带括号的标识符表达式(变量名)或类成员访问表达式 → 就是该变量/成员的类型;
  2. exp 是函数调用 → 函数的返回值类型;
  3. exp 是类型为 T 的将亡值(右值引用语义的值)→ T&&;
  4. exp 是类型为 T 的其他左值 → T&;
  5. 其余情况 → T。
int i = 4;
int arr[5] = {0};
int* ptr = arr;

decltype(arr) var1;       // int[5],规则1:标识符表达式
decltype(ptr) var2;       // int*,  规则1
decltype(arr[0]) var3;    // int&,  规则4:下标运算返回左值
decltype(*ptr) var4;      // int&,  规则4:解引用返回左值
decltype(i++) var5;       // int,   规则5:i++ 返回右值
decltype(++i) var6 = i;   // int&,  规则4:++i 返回左值
decltype(10) var7;        // int,   规则5:字面量

// 注意括号的影响:
decltype(i)  a = 0;       // int,i 是标识符表达式
decltype((i)) b = i;      // int&,(i) 是左值表达式,加括号就不是标识符表达式了

与 auto 的区别:

auto decltype
推导来源 初始化表达式的类型 任意表达式的类型
引用 / cv 限定 默认忽略 保留
典型用途 变量声明 表达式类型分析、尾置返回

配合尾置返回类型推导函数返回值(decltype 最大的用途之一):

// 错误写法:t、u 在 decltype 处尚未声明
template <typename T, typename U>
decltype(t + u) add(T t, U u) { return t + u; }   // C++11 编译失败

// 正确写法:尾置返回类型
template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u) { return t + u; }

注:decltype(auto) 是 C++14 引入的;函数返回类型直接推导(省略 -> ...)也是 C++14。


2. 统一初始化(列表初始化)

C++11 提出用花括号 {} 作为统一的初始化语法,适用于变量、数组、对象、容器、new 表达式和函数返回值。

int a{0};                    // 变量
int b = {0};
int arr[]{1, 2, 3};          // 数组
std::string s{"hello"};
std::vector<int> v{1, 2, 3};                       // 容器
std::map<int, std::string> m{{1, "one"}, {2, "two"}};
Foo* p = new Foo{1, 2};                            // new 表达式(动态分配对象的列表初始化)
int* pa = new int[4]{0, 1, 2, 3};                  // 动态分配数组的列表初始化(不足的元素值初始化)
std::vector<int>* pv = new std::vector<int>{0, 1, 2, 3};

std::vector<int> make() { return {1, 2, 3}; }      // 函数返回

std::vector<int> vi;
vi = {4, 5, 6};                                    // 赋值(右侧列表)

2.1 防止类型窄化(narrowing conversion)

列表初始化禁止有精度损失的隐式转换,编译期直接报错,这是它相比 () 和 = 初始化的最大优势:

int i = 2.5;        // ok(编译器最多警告),隐式截断
int j = {2.5};      // error:double → int 窄化

float f = {1e70};   // error:溢出
char c1 = 1000;     // ok(警告)
char c2 = {1000};   // error:int → char 窄化

禁止的转换包括:浮点 → 整数、大范围浮点 → 小范围浮点、整数 → 无法完整表示它的浮点/整数类型(常量表达式且值安全的除外)。

2.2 聚合类型与 std::initializer_list

聚合类型(普通数组,或无用户定义构造函数、无基类、无虚函数、无私有非静态成员、无类内初始值的类)可以直接用列表逐成员初始化:

struct Point { int x; int y; };
Point p{1, 2};      // 聚合初始化:x=1, y=2

struct NonAgg {     // 有自定义构造函数,不是聚合类型
    int a;
    NonAgg(int v) : a(v) {}
};
// NonAgg n{1};     // error,只能走构造函数

非聚合类型的列表初始化会匹配构造函数。若类有 std::initializer_list<T> 参数的构造函数,花括号初始化会优先匹配它:

class CustomVec {
public:
    CustomVec(std::initializer_list<int> list) {
        for (auto it = list.begin(); it != list.end(); ++it)
            data_.push_back(*it);
    }
private:
    std::vector<int> data_;
};

CustomVec cv{1, 2, 3, 4, 5};   // 任意长度,元素类型必须能转换为 T

STL 容器(vector、map 等)能接受任意长度初始化列表,底层正是 std::initializer_list。

经典陷阱: std::vector<int> v{10, 20}; 得到 2 个元素(10 和 20),因为 {} 优先匹配 initializer_list 构造函数;而 std::vector<int> v(10, 20); 得到 10 个值为 20 的元素。见 第 17 节。

2.3 类内成员初始值

C++11 允许在类定义中直接给非静态数据成员初始值:

class Widget {
    int id = 0;              // 等号形式
    std::string name{"N/A"}; // 花括号形式
    std::vector<int> cache;  // 无初始值 → 默认初始化
};

类内初始值会被构造函数使用:构造函数初始化列表中显式初始化的成员以初始化列表为准,其余成员用类内初始值。


3. nullptr 空指针

C++98 中 NULL 通常是宏 0(整型),重载决议时会出现二义性:

void foo(int);
void foo(void*);

foo(0);         // 调用 foo(int)!0 被当作整数
foo(NULL);      // 二义性或调用 foo(int),取决于 NULL 的定义
foo(nullptr);   // 明确调用 foo(void*)

nullptr 是 std::nullptr_t 类型的字面量,可以隐式转换为任何指针类型,但不会转换为整型:

std::nullptr_t np = nullptr;
int* p = nullptr;
bool b = (p == nullptr);
// int i = nullptr;   // error:不能转换为整型

建议: C++11 之后一律使用 nullptr,淘汰 0 和 NULL 表示空指针的写法。


4. 强类型枚举(enum class)

传统 enum 有三个问题:枚举值泄漏到外层作用域(同名冲突)、隐式转换为 int、不能指定底层类型/不能前置声明。C++11 引入有作用域枚举解决:

enum class Color { Red, Green, Blue };

Color c = Color::Red;        // 必须带作用域
// Color c2 = Red;           // error:Red 不再泄漏到外层
// int i = c;                // error:不允许隐式转整型
int i = static_cast<int>(c); // 可以显式转换

if (c == Color::Green) { /* ... */ }

可以指定底层类型、前置声明:

enum class Pixel : uint8_t;           // 前置声明(有作用域枚举默认底层类型为 int)
enum class Pixel : uint8_t { A, B };

enum Status : int;                    // 普通枚举也可指定底层类型并前置声明(C++11)
enum Status : int { OK, ERROR };
特性 传统 enum enum class
枚举值作用域 外层作用域 枚举作用域内
隐式转整型 是 否
指定底层类型 C++11 起可以 可以(默认 int)
前置声明 需指定底层类型 可以

5. constexpr 常量表达式

constexpr 声明一个值/函数可以在编译期求值,使变量能用于数组长度、模板参数等需要常量表达式的场合:

constexpr int square(int x) { return x * x; }
constexpr int size = square(5);   // 编译期 = 25
int arr[size];                    // ok,arr 大小为编译期常量

constexpr int bad(int x) { return x + 1; }
int n;
std::cin >> n;
int m = bad(n);                   // 也可以运行期调用,此时无编译期要求

const 与 constexpr 的区别: const 表示「值不可修改」,不保证编译期可知;constexpr 表示「可编译期求值」。

const int c1 = 10;           // 编译期常量
int x; std::cin >> x;
const int c2 = x;            // 运行期才确定,不是常量表达式
// constexpr int c3 = x;     // error

constexpr 构造函数(使类的对象能用于常量表达式):

class Point {
public:
    constexpr Point(int x, int y) : x_(x), y_(y) {}
    constexpr int x() const { return x_; }
private:
    int x_, y_;
};
constexpr Point p(1, 2);
constexpr int px = p.x();    // 编译期求值

C++11 对 constexpr 函数的限制: 函数体只能有单条 return 语句(可含 typedef、static_assert),不能包含循环、分支、局部变量等——这些在 C++14 中放宽。


6. 范围 for 循环

对容器/数组的每个元素执行操作,不再需要手写迭代器:

std::vector<int> v{1, 2, 3, 4};

for (int x : v)            { std::cout << x; }          // 拷贝每个元素
for (auto& x : v)          { x *= 2; }                  // 引用:可修改元素
for (const auto& x : v)    { std::cout << x; }          // const 引用:避免拷贝
for (auto x : {1, 2, 3})   { /* 直接遍历初始化列表 */ }

int arr[] = {1, 2, 3};
for (auto x : arr) { /* 原生数组也可以 */ }

底层展开为基于 begin()/end() 的迭代器循环,因此:

  • 任何提供 begin/end 的类型(含原生数组)都能用;
  • 循环体内不要增删容器元素(迭代器失效);
  • 元素较大时用 const auto& 避免拷贝。

7. Lambda 表达式

Lambda 是 C++11 最重要的特性之一:就地定义匿名函数,并可捕获作用域内的变量。

7.1 语法

[capture](params) mutable noexcept -> ret { body }
  • capture:捕获列表
  • params:参数表(无参数时可省略括号)
  • mutable:允许修改按值捕获的变量副本(且不再要求 operator() 为 const)
  • -> ret:尾置返回类型(可省略,由编译器推导)
auto add = [](int a, int b) { return a + b; };          // 返回值自动推导
auto mod = [](int a, int b) -> int { return a % b; };   // 显式返回类型
std::cout << add(1, 2) << '\n';

// 最典型用途:配合算法
std::vector<int> v{3, 1, 2};
std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; });

7.2 捕获列表

写法 含义
[] 不捕获任何变量
[x] 按值捕获 x
[&x] 按引用捕获 x
[=] 按值捕获外层作用域所有变量
[&] 按引用捕获外层作用域所有变量
[=, &x] 默认按值捕获,x 按引用捕获
[&, x] 默认按引用捕获,x 按值捕获
[this] 捕获当前对象的 this 指针
int a = 1, b = 2, count = 0;

auto sum  = [a, b]() { return a + b; };
auto incr = [&count]() { ++count; };        // 通过引用修改外部变量
auto all  = [=]() { return a + b + count; };

int x = 10;
auto by_val = [x]() mutable { x += 1; return x; };  // 修改的是捕获的副本
by_val();   // 11
by_val();   // 12,副本保留状态;外部 x 仍是 10

7.3 Lambda 的本质

编译器为每个 lambda 生成一个匿名类(闭包类型),捕获的变量成为其成员:

  • 按值捕获 → 成员变量,默认 operator() 是 const 的,因此不能修改;加 mutable 后可修改;
  • 按引用捕获 → 引用成员,不拷贝;
  • 每个 lambda 的闭包类型互不相同,所以无法手写类型,通常用 auto 或 std::function 存储。

7.4 使用注意

  • 按引用捕获时注意生命周期:lambda 被存储/异步执行时,被引用的局部变量可能已销毁(悬垂引用);
  • 捕获 this 的 lambda 若在对象销毁后执行,同样危险;
  • 泛型 lambda([](auto x) {...})是 C++14 特性。

8. 右值引用与移动语义

右值引用(T&&)与移动语义是 C++11 性能提升的核心,支撑了移动构造、完美转发、emplace 等大量特性。

8.1 左值与右值

  • 左值:有名字、可取地址、生命周期较长的对象(变量、解引用结果、++i、字符串字面量等);
  • 右值:没有名字、不能取地址的临时对象,又细分为:
    • 纯右值(prvalue):字面量(除字符串)、算术表达式结果、返回非引用的函数调用、lambda 表达式等;
    • 将亡值(xvalue):即将销毁但仍可被「抢救」的对象,如 std::move() 的返回值、T&& 函数的返回值。
int a = 4;        // a 是左值,4 是纯右值
int b = a + 1;    // a + 1 是纯右值
int&& r = a + 1;  // 右值引用绑定右值,r 本身是有名字的左值

8.2 右值引用

type&  name = exp;   // 左值引用
type&& name = exp;   // 右值引用,只能绑定右值
int a = 1;
int& lr = a;            // ok
int&& rr = 10;          // ok,绑定纯右值
// int&& rr2 = a;       // error:a 是左值
int&& rr3 = std::move(a);   // ok:move 把左值转为将亡值
const int& cr = 10;     // ok:const 左值引用可以绑定右值(经典写法)

右值引用的价值:延长临时对象的生命周期,并标识「这个对象的资源可以被夺走」,从而让移动构造/赋值接管其资源,避免深拷贝。

8.3 std::move

std::move 不移动任何东西,它只是一个到 T&& 的强制转换,表示「我承诺不再使用该对象的值」:

std::vector<std::string> names = get_names();
std::vector<std::string> cache = std::move(names);  // 转移内部缓冲区,O(1)
// 此后 names 处于「有效但未指定」状态,只应销毁或重新赋值

注意:

  • 被移动后的对象仍然有效(可安全析构、赋值),但值未指定,不要依赖;
  • 不要对之后还要使用的对象、函数返回值、const 对象执行 move(对 const 对象 move 实际退化为拷贝)。

8.4 移动构造函数与移动赋值运算符

class Buffer {
public:
    explicit Buffer(std::size_t n) : size_(n), data_(new int[n]()) {}
    ~Buffer() { delete[] data_; }

    // 拷贝:申请新内存并复制 —— O(n)
    Buffer(const Buffer& other)
        : size_(other.size_), data_(new int[other.size_]) {
        std::copy(other.data_, other.data_ + size_, data_);
    }
    Buffer& operator=(const Buffer& other) {
        if (this != &other) {
            int* p = new int[other.size_];
            std::copy(other.data_, other.data_ + other.size_, p);
            delete[] data_;
            data_ = p;
            size_ = other.size_;
        }
        return *this;
    }

    // 移动:窃取资源,把源对象置空 —— O(1)
    Buffer(Buffer&& other) noexcept
        : size_(other.size_), data_(other.data_) {
        other.size_ = 0;
        other.data_ = nullptr;
    }
    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data_;
            size_ = other.size_;
            data_ = other.data_;
            other.size_ = 0;
            other.data_ = nullptr;
        }
        return *this;
    }

private:
    std::size_t size_;
    int* data_;
};

Buffer a(1024);
Buffer b = std::move(a);   // 移动构造,不复制 1024 个 int

Rule of Five / Rule of Zero:

  • 自定义了析构、拷贝构造、拷贝赋值、移动构造、移动赋值中任何一个,通常五个都要考虑(Rule of Five);
  • 尽量让类只通过成员(如 std::vector、std::string)管理资源,五个特殊函数都不用写(Rule of Zero)。

移动构造函数应声明为 noexcept:标准容器扩容时,只有移动构造是 noexcept 的才会使用移动,否则退回拷贝以保证强异常安全(见 17.1)。

8.5 完美转发与 std::forward

命名变量会「丢失」右值性——T&& t 传入函数后,t 本身是有名字的左值。std::forward<T> 可按 T 的推导结果恢复值类别:

void process(int& x)  { std::cout << "lvalue\n"; }
void process(int&& x) { std::cout << "rvalue\n"; }

template <typename T>
void wrapper(T&& arg) {           // T&& 在这里是「万能引用」(转发引用)
    process(arg);                 // arg 有名字,永远是左值 → 总是 lvalue
    process(std::forward<T>(arg));// 保持原来的值类别 → 完美转发
}

int n = 1;
wrapper(n);   // lvalue, lvalue
wrapper(2);   // lvalue, rvalue

引用折叠规则(forward 的原理):

组合 折叠结果
T& & T&
T& && T&
T&& & T&
T&& && T&&

当 T 推导为 X&(传入左值)时,T&& 折叠为 X&;推导为 X(传入右值)时,T&& 为 X&&。

8.6 返回值优化(RVO/NRVO)与 std::move 的误用

函数按值返回局部对象时,编译器通常直接省略拷贝/移动(RVO:返回值优化):

std::vector<int> make() {
    std::vector<int> v{1, 2, 3};
    return v;               // 触发 RVO:不拷贝、不移动
}

std::vector<int> make_bad() {
    std::vector<int> v{1, 2, 3};
    return std::move(v);    // 画蛇添足:抑制了 RVO!
}

结论:按值返回局部对象时直接 return local;,不要加 std::move。(对返回值的进一步保证在 C++17 的强制拷贝消除中完成。)

8.7 移动迭代器

std::make_move_iterator 把普通迭代器适配为「解引用得到右值引用」,配合算法可以把一段范围的资源移动走:

std::vector<std::string> src{"a", "b", "c"};
std::vector<std::string> dst(std::make_move_iterator(src.begin()),
                             std::make_move_iterator(src.end()));
// src 中的 string 内容已被移动到 dst

9. 函数的改进

9.1 默认函数 = default 与删除函数 = delete

class NonCopyable {
public:
    NonCopyable() = default;                       // 显式要求编译器生成默认版本
    NonCopyable(const NonCopyable&) = delete;      // 禁止拷贝
    NonCopyable& operator=(const NonCopyable&) = delete;
};

要点:

  • = default 只能用于编译器本来就能自动生成的特殊成员函数(默认/拷贝/移动构造、拷贝/移动赋值、析构);类内写 = default 与不写效果等价(但可保持特殊函数的「平凡」属性);
  • = delete 可用于任意函数,常用于禁用某些重载,阻止不期望的隐式转换:
void feed(double);
void feed(int) = delete;   // 传 int 时直接报错,而不是隐式转 double

feed(1);      // error: use of deleted function
feed(1.0);    // ok

class OnlyInt {
public:
    void set(int);
    template <typename T>
    void set(T) = delete;  // 除 int 外的一切类型都被禁止
};

9.2 final 与 override

struct Base {
    virtual void foo();
    virtual void bar() final;         // bar 不允许被派生类重写
};

struct Derived : Base {
    void foo() override;              // 显式声明重写,签名不匹配会报错
    // void bar() override;           // error:bar 已被 final
};

struct MoreDerived : Derived { };
struct Locked final { };              // final 类:不允许被继承
// struct X : Locked { };            // error

好处:把「本应重写却因签名笔误变成新函数」的隐蔽 bug 变成编译错误;final 还给编译器去虚化优化的机会。建议: 所有重写函数都加 override。

9.3 尾置返回类型

auto func(int i) -> int(*)[10];        // 返回「指向 10 个 int 的数组」的指针
auto ratio(double a, double b) -> double { return a / b; }

适用于返回类型依赖参数(模板)、返回数组/函数指针等复杂类型的场景;lambda 需要显式指定返回类型时也必须用它。

9.4 委托构造函数

一个构造函数可以调用同类的另一个构造函数,消除重复初始化代码:

class Widget {
public:
    Widget()               : Widget(0, "default") {}
    Widget(int id)         : Widget(id, "default") {}
    Widget(int id, std::string name) : id_(id), name_(std::move(name)) {}  // 目标构造
private:
    int id_;
    std::string name_;
};

注意:委托构造中,被委托的构造函数执行完后,委托方不能再通过初始化列表初始化其他成员(只能在函数体内赋值)。

9.5 继承构造函数

派生类可以用 using Base::Base; 直接继承基类的全部构造函数:

struct Base {
    Base(int) {}
    Base(double, std::string) {}
};

struct Derived : Base {
    using Base::Base;      // 继承所有基类构造函数
    int extra = 0;         // 其他成员用类内初始值
};

Derived d1(1);
Derived d2(2.0, "x");

若派生类自己定义了同签名构造函数,则覆盖继承来的版本;默认/拷贝/移动构造不会被继承,仍由编译器生成。

多重继承注意: 若从多个基类继承构造函数,而不同基类存在形参列表相同的构造函数,会产生二义性导致编译错误,此时必须在派生类中自行定义该签名的构造函数来解决冲突:

struct Base1 { Base1(const std::string&); };
struct Base2 { Base2(const std::string&); };
struct D : public Base1, public Base2 {
    using Base1::Base1;
    using Base2::Base2;
    // Base1 与 Base2 都有 (const string&) 构造,D 必须自定义
    D(const std::string& s) : Base1(s), Base2(s) {}
};

9.6 引用限定成员函数

成员函数可以用 & 或 && 限定「只能被左值/右值对象调用」,从而对临时对象做特殊处理:

class Widget {
public:
    Widget& operator=(const Widget&) &;       // 只允许左值对象赋值
    std::vector<int> data() && {              // 只允许对右值(临时对象)调用
        return std::move(data_);              // 可以安全地把数据移走
    }
};

9.7 noexcept

void f() noexcept;                 // 承诺不抛异常;若抛出则直接 std::terminate
void g() noexcept(noexcept(f())); // noexcept 运算符:编译期判断表达式是否不抛

用途:

  • 移动构造/赋值声明 noexcept,容器扩容才会用移动(重要);
  • 析构函数默认是 noexcept(C++11 起);
  • 作为接口契约与优化提示。

10. 类与对象的其他改进

10.1 非受限联合体

C++11 允许 union 拥有非 POD 成员(如 std::string),但需要程序员手动管理其构造/析构(placement new / 显式调用析构函数):

union Value {
    int i;
    std::string s;      // C++98 不允许
    Value() : i(0) {}
    ~Value() {}
};

典型应用是实现 variant 类容器(C++17 有了 std::variant)。

10.2 内存对齐:alignas 与 alignof

alignof(double);                 // 查询 double 的对齐要求(通常为 8)
alignas(16) float simd[4];       // 强制 16 字节对齐(SIMD、缓存行优化常用)

struct alignas(8) Padded { char c; };   // 整个结构体至少 8 字节对齐

alignas 只能增大对齐(不能小于类型自然对齐);相比编译器扩展(__attribute__((aligned)))是可移植的标准写法。

10.3 属性说明符(attributes)

C++11 统一了属性语法 [[...]],首批引入两个:

[[noreturn]] void fatal_error(const std::string& msg);  // 该函数永不返回
// [[carries_dependency]] 用于原子操作的内存模型优化,实践中很少直接用

([[deprecated]] 是 C++14,[[nodiscard]]、[[maybe_unused]] 是 C++17。)

10.4 explicit:显式构造函数与类型转换运算符

explicit 用于阻止非预期的隐式转换,可修饰单参数构造函数和类型转换运算符。

显式构造函数: 单参数构造函数可以被隐式调用来完成类型转换,explicit 禁用这种隐式转换:

struct A {
    A(int value) {}               // 未加 explicit
};
A a1 = 1;                          // ok:发生隐式转换(可能并非本意)

struct B {
    explicit B(int value) {}
};
// B b1 = 1;                       // error:不允许隐式转换
B b2(2);                           // ok:显式构造
B b3{3};                           // ok

A a = 1 这种拷贝初始化会触发隐式构造;加上 explicit 后必须直接初始化,避免了意料之外的隐式转换(典型例子:std::vector 的大小构造、std::string(const char*) 等)。

显式类型转换运算符:「安全 bool」惯用法成为标准做法:

class Optional {
public:
    explicit operator bool() const { return has_value_; }
private:
    bool has_value_ = false;
};

Optional opt;
if (opt) { /* ... */ }            // 上下文需要 bool 时仍可用
// int i = opt;                   // error:不能隐式转成 int

11. 模板的增强

11.1 右尖括号 >> 的修复

C++98 中嵌套模板必须写 > >(否则被解析为右移运算符),C++11 修复:

std::vector<std::vector<int>> v;   // C++11 合法;C++98 必须写 > >

11.2 类型别名声明:using

C++11 提供了别名声明(alias declaration)来定义类型别名,语法比 typedef 更直观,并且支持给模板起别名:

基础类型别名:

using SI = Sales_item;              // SI 是 Sales_item 的别名(同义词)
using uint = unsigned int;
using func_t = void (*)(int, int);  // 函数指针别名,从左往右读,比 typedef 清晰

typedef unsigned int UINT;          // 等价的旧式写法

类型别名与类型名等价——凡是类型名能出现的地方,都能使用类型别名。

模板别名(typedef 无法做到):

template <typename T>
using Vec = std::vector<T, std::allocator<T>>;

Vec<int> v;                    // 等价于 std::vector<int>

template <typename T>
using StrMap = std::map<std::string, T>;
StrMap<double> m;              // typedef 做不到这一点

11.3 函数模板的默认模板参数

template <typename T = int, typename U = double>
T convert(U u) { return static_cast<T>(u); }

convert(3.14);            // T=int, U=double 推导
convert<float>(3.14);     // 显式指定 T

11.4 可变参数模板(variadic templates)

模板参数包 typename... Args 可以接受任意数量、任意类型的模板实参,是实现 tuple、make_shared、完美转发工厂的基础:

// sizeof... 查询参数包中元素个数
template <typename... Args>
std::size_t count_args() { return sizeof...(Args); }

// 递归展开
void print() {}
template <typename T, typename... Rest>
void print(T first, Rest... rest) {
    std::cout << first << ' ';
    print(rest...);
}
print(1, 2.5, "three");    // 1 2.5 three

// 非递归展开技巧:利用初始化列表按序求值
template <typename... Args>
void print2(Args... args) {
    (void)std::initializer_list<int>{(std::cout << args << ' ', 0)...};
}

参数包展开 args... 会出现在模式右侧,例如 std::forward<Args>(args)...。

完美转发工厂(可变参数 + 完美转发的经典应用):

template <typename T, typename... Args>
std::unique_ptr<T> make_unique_(Args&&... args) {   // 注:std::make_unique 是 C++14
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

11.5 显式实例化控制

// big.cpp 中
template class BigClass<int>;                 // 显式实例化定义:本翻译单元生成代码

// 其他文件
extern template class BigClass<int>;          // 显式实例化声明:本文件不生成,链接时使用
extern template void bigFunc(int);

用于减少编译时间与目标文件体积(常用于大型模板库)。

11.6 模板类型参数作友元

template <typename T>
class Handle {
    friend T;    // C++11:模板参数可以直接作为友元
};

11.7 static_assert 静态断言

编译期断言,失败时编译中止并输出自定义信息:

static_assert(sizeof(int) == 4, "需要 32 位 int");
static_assert(std::is_integral<T>::value, "T 必须是整数类型");

(C++11 中消息参数必填,C++17 起可省略。)


12. 字符串与字面量

12.1 原始字符串字面量(raw string literal)

所见即所得,引号与反斜杠无需转义,写正则、路径、JSON 非常方便:

std::string path = R"(C:\Users\name\file.txt)";
std::string re   = R"(\d{4}-\d{2}-\d{2})";          // 正则不用双写 \\

// 自定义定界符,处理内容中含 )" 的情况
std::string tricky = R"delim(内容里可以出现 )" 也不怕)delim";

12.2 用户定义字面量(UDL)

可以为自定义后缀定义「字面量运算符」,实现带单位的常量等:

struct Distance {
    double meters;
    constexpr Distance(long double m) : meters(static_cast<double>(m)) {}
};
constexpr Distance operator"" _km(long double v) { return Distance(v * 1000); }
constexpr Distance operator"" _m(long double v)  { return Distance(v); }

Distance d = 1.5_km;      // 1500 米

规则:

  • 数值字面量的参数只能是 unsigned long long(整数)或 long double(浮点);
  • 字符串字面量参数为 const char* + 长度;
  • 后缀以下划线开头是保留给标准库/用户的约定;
  • 标准库的 10ms、"abc"s 等字面量是 C++14 引入的(std::literals)。

12.3 Unicode 支持

新增 char16_t、char32_t 类型与对应字面量前缀:

char16_t u16[] = u"你好";      // UTF-16
char32_t u32[] = U"你好";      // UTF-32
char utf8[]    = u8"hello";    // 保证以 UTF-8 编码存储

12.4 long long

long long 成为标准类型,至少 64 位(unsigned long long 同理)。


13. 其他语言特性

13.1 thread_local 线程局部存储

thread_local 声明的变量每个线程拥有独立副本:

thread_local int tls_counter = 0;   // 每个线程独立的计数器

void worker() {
    ++tls_counter;                  // 只影响本线程
}

可与 static 组合(static thread_local),用于线程内缓存、避免锁竞争等场景。

13.2 内联命名空间

内联命名空间内的成员对外层命名空间「可见」,主要用于库的 ABI 版本管理:

namespace lib {
    inline namespace v2 {      // 当前默认版本
        void api();
    }
    namespace v1 {             // 旧版本保留
        void api();
    }
}
lib::api();          // 找到 v2::api
lib::v1::api();      // 显式使用旧版

关键字 inline 必须出现在命名空间第一次定义处,后续重新打开时可写可不写。标准库实现(libstdc++、libc++)也用内联命名空间标记 ABI 兼容的不同版本。命名空间的完整用法总结见附录:命名空间的使用。

13.3 sizeof 的增强

可以直接对类的静态成员或成员的访问表达式使用 sizeof,无需对象:

struct S { static int n; double d; };
sizeof(S::n);      // ok,C++98 不允许

13.4 除法的舍入规则

C++11 明确规定整数除法向零取整(此前实现定义),且 (a / b) * b + a % b == a。

13.5 const 用法回顾(背景知识)

const 并非 C++11 新特性,此处作为理解 constexpr 的背景列出。常见用法:

const int value = 5;              // 1. 常量

char* const p1;                   // 2. 指针本身是常量(从右向左读)
const char* p2;                   //    指针指向的内容是常量

void func(const A& a);            // 3. 常引用传参:避免拷贝、防止被修改

class B {
    const int v;                  // 4. 常量成员:在构造函数初始化列表中赋值
public:
    B(int x) : v(x) {}
    void show() const;            // 5. const 成员函数:不修改成员变量
};

const B b(1);                     // 6. const 对象只能调用 const 成员函数
b.show();

const 与 constexpr 的区别: const 只保证「运行期不可修改」,其值仍可能在运行期才确定;constexpr 要求值在编译期即可计算出来,是真正的编译期常量(见第 5 节)。


14. 并发编程支持

C++11 首次把多线程纳入标准:引入内存模型与 <thread>、<mutex>、<atomic>、<condition_variable>、<future> 等头文件,跨平台并发编程成为可能。

14.1 std::thread

#include <thread>

void worker(int id, const std::string& msg) {
    std::cout << "thread " << id << ": " << msg << '\n';
}

std::thread t(worker, 1, "hello");   // 创建即开始运行
t.join();                            // 等待线程结束
// 或者 t.detach();  分离后由运行时托管

std::cout << std::thread::hardware_concurrency() << '\n';  // 硬件并发数参考值

注意:

  • std::thread 对象析构时若仍可连接(未 join 也未 detach),程序会 std::terminate——必须在析构前 join 或 detach;
  • 线程函数参数默认是拷贝的,需要传引用时用 std::ref:std::thread t(f, std::ref(obj));;
  • std::thread 可移动、不可拷贝。

14.2 互斥量与锁

#include <mutex>

std::mutex mtx;
int shared = 0;

{
    std::lock_guard<std::mutex> lock(mtx);  // RAII:构造加锁,析构解锁
    ++shared;
}

std::unique_lock<std::mutex> ulock(mtx);    // 更灵活:可中途解锁、配合条件变量
ulock.unlock();
// ...
ulock.lock();

常见互斥量:std::mutex、std::recursive_mutex(同线程可重复加锁)、std::timed_mutex(超时加锁)。

避免多锁死锁:std::lock 一次原子地锁住多个互斥量:

std::lock(m1, m2);                              // 全有或全无,不会死锁
std::lock_guard<std::mutex> lg1(m1, std::adopt_lock);
std::lock_guard<std::mutex> lg2(m2, std::adopt_lock);

14.3 条件变量

用于线程间的等待/通知,wait 必须搭配 std::unique_lock,且必须带谓词以防虚假唤醒:

std::mutex mtx;
std::condition_variable cv;
std::queue<int> tasks;

// 消费者
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return !tasks.empty(); });   // 谓词为 true 才继续
int task = tasks.front(); tasks.pop();

// 生产者
{
    std::lock_guard<std::mutex> lk(mtx);
    tasks.push(42);
}
cv.notify_one();        // 或 notify_all()

14.4 原子操作

std::atomic<T> 提供无锁的原子读写与读-改-写操作:

std::atomic<int> counter{0};
counter.fetch_add(1);                  // 原子自增
int old = counter.exchange(100);
counter.store(5);
int v = counter.load();

// CAS 操作
int expected = 5;
counter.compare_exchange_strong(expected, 6);

std::atomic_flag 是最简单的原子布尔,可实现自旋锁。原子操作默认使用 memory_order_seq_cst(最强一致性),高级用法可通过 memory_order_relaxed/acquire/release 等精细控制——除非确有性能需求并充分理解内存模型,否则用默认值。

14.5 future / promise / async / packaged_task

  • std::future:获取异步结果(get() 只能调用一次);
  • std::promise:在「生产端」设置值/异常;
  • std::async:高层接口,直接异步执行函数;
  • std::packaged_task:把可调用对象包装成任务,可延迟/在线程中执行。
// async 用法
int compute(int x) { return x * x; }
auto fut = std::async(std::launch::async, compute, 21);  // 明确异步执行
int result = fut.get();                                   // 阻塞等待结果 441

// promise / future 配对
std::promise<int> prom;
std::future<int> f = prom.get_future();
std::thread([&prom] { prom.set_value(42); }).detach();
f.get();   // 42

// packaged_task
std::packaged_task<int(int)> task(compute);
auto f2 = task.get_future();
std::thread(std::move(task), 10).detach();
f2.get();  // 100

std::async 的启动策略:

  • std::launch::async:保证在新线程中执行;
  • std::launch::deferred:延迟执行——直到调用 get()/wait() 才在调用线程同步执行;
  • 默认策略是两者的「或」,由实现决定——可能根本不异步。依赖异步行为时显式传 std::launch::async。

14.6 std::call_once

保证某个初始化逻辑在多线程下只执行一次:

std::once_flag init_flag;
void init() { /* 只会被一个线程执行一次 */ }

void thread_func() {
    std::call_once(init_flag, init);
}

14.7 volatile

C++11 明确 volatile 只用于「可能被外部修改的内存」(如硬件寄存器),不提供任何原子性/同步语义——线程同步请用 atomic 或互斥量。


15. 智能指针

C++11 用三个智能指针(<memory>)取代了有缺陷的 std::auto_ptr(C++11 弃用,C++17 移除),核心思想是 RAII:资源的生命周期绑定到对象的生命周期。

15.1 unique_ptr:独占所有权

  • 同一时刻只有一个 unique_ptr 拥有对象;
  • 不可拷贝,只能移动;
  • 零开销(无引用计数,大小等同裸指针,自定义删除器除外)。
std::unique_ptr<Widget> p(new Widget);      // C++11 写法
// auto p = std::make_unique<Widget>();     // make_unique 是 C++14

auto q = std::move(p);          // 所有权转移,p 变为 nullptr
Widget* raw = q.get();          // 获取裸指针(不转移所有权)
Widget* rel = q.release();      // 放弃所有权并返回裸指针

// 自定义删除器
std::unique_ptr<FILE, decltype(&fclose)> fp(fopen("a.txt", "r"), fclose);

// 数组版本
std::unique_ptr<int[]> arr(new int[10]);

用法建议:默认使用 unique_ptr;作为工厂函数的返回类型;需要共享所有权时再升级为 shared_ptr(unique_ptr 可隐式转换为 shared_ptr)。

15.2 shared_ptr:共享所有权(引用计数)

  • 多个 shared_ptr 共享同一对象,引用计数归零时释放;
  • 内部维护控制块(强引用计数、弱引用计数、删除器、分配器);
  • 线程安全:控制块的操作是原子的(指针指向的对象本身不是)。
std::shared_ptr<Widget> sp = std::make_shared<Widget>();  // 推荐:一次分配
std::shared_ptr<Widget> sp2 = sp;      // 计数 +1
sp2->do_something();
sp.use_count();                        // 当前强引用数
sp.reset();                            // 放弃所有权(计数 -1)

// 自定义删除器(删除器是运行时成员,不影响类型)
std::shared_ptr<FILE> fp(fopen("a.txt", "r"), fclose);

make_shared 相比 shared_ptr<T>(new T):一次内存分配(对象 + 控制块)、更强的异常安全。推荐总是用 make_shared。

注意:

  • shared_ptr 的大小是裸指针的 2 倍,拷贝有原子操作开销——接口中能用 const T&/T* 表达就不要传 shared_ptr;
  • 避免同一裸指针构造多个 shared_ptr(双重释放);
  • 类想从自身获得 shared_ptr,需要继承 std::enable_shared_from_this 并使用 shared_from_this(),不能直接 shared_ptr<T>(this)。

15.3 weak_ptr:弱引用(不计数)

weak_ptr 观察 shared_ptr 管理的对象但不增加引用计数,用于:

  1. 打破循环引用(否则计数永不为 0,内存泄漏);
  2. 缓存、观察者等「可能需要访问但不应阻止释放」的场景。
// 循环引用示例
struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node>   prev;    // 用 weak_ptr 打破环
};

std::shared_ptr<Widget> sp = std::make_shared<Widget>();
std::weak_ptr<Widget> wp = sp;

if (auto locked = wp.lock()) {     // lock():尝试提升为 shared_ptr
    locked->use();                 // 对象仍存活
} else {
    // 对象已被销毁
}
wp.expired();      // 等价于 use_count() == 0

15.4 选择建议

场景 选择
独占资源(绝大多数情况) unique_ptr
需要共享所有权 shared_ptr
打破循环、观察者、缓存 weak_ptr
管理 C API 资源(FILE、句柄) unique_ptr/shared_ptr + 自定义删除器

16. 标准库(STL)新增内容

16.1 新增容器

std::array:固定大小数组,可替代原生数组(知道大小、支持 begin/end、fill、拷贝赋值,不退化):

std::array<int, 4> a{{1, 2, 3, 4}};
a.size();  a[2];  a.fill(0);

std::forward_list:单向链表,比 list 更省内存,只支持头插(push_front/insert_after)。

无序容器(哈希表):unordered_map、unordered_set、unordered_multimap、unordered_multiset,平均 O(1) 查找(map/set 是红黑树,O(log n) 有序):

std::unordered_map<std::string, int> ages{{"alice", 30}, {"bob", 25}};
ages["carol"] = 28;
auto it = ages.find("alice");

16.2 emplace:原地构造

emplace_back/emplace/emplace_front/emplace_hint 把参数转发给元素构造函数,直接在容器内存中构造,省去临时对象的构造与移动:

struct Item { Item(int, std::string) {} };
std::vector<Item> v;
v.push_back(Item(1, "x"));   // 先构造临时对象,再移动进容器
v.emplace_back(1, "x");      // 直接在容器内构造

16.3 std::function 与 std::bind

std::function<R(Args...)> 是通用可调用对象包装器,可存储函数、函数指针、仿函数、成员函数指针、lambda、bind 表达式:

std::function<int(int, int)> op;
op = [](int a, int b) { return a + b; };   // lambda
op = std::plus<int>();                     // 仿函数
op(1, 2);

// 典型场景:回调
void register_callback(std::function<void(int)> cb);

空的 std::function 被调用会抛出 std::bad_function_call。

std::bind 把可调用对象与部分参数绑定,生成新的可调用对象(配合占位符 _1、_2...),可重排参数、减少元数:

using namespace std::placeholders;
void f(int a, int b, int c);

auto g = std::bind(f, _2, 100, _1);   // g(x, y) == f(y, 100, x)
auto h = std::bind(f, 1, 2, 3);       // 零元函数

struct Foo { void print(int); };
auto m = std::bind(&Foo::print, _1, 42);   // 绑定成员函数
Foo foo; m(foo);

实践建议:简单场景下 lambda 通常比 std::bind 更直观,C++14 之后更是如此。

16.4 std::tuple

任意类型、任意个数的组合:

std::tuple<int, std::string, double> t(1, "pi", 3.14);
std::get<1>(t);                       // "pi"
std::tuple_size<decltype(t)>::value;  // 3

int i; std::string s; double d;
std::tie(i, s, d) = t;                // 解包到已有变量

auto p = std::make_tuple(1, "x");

可用于函数返回多个值(配合 std::tie)。

16.5 <chrono> 时间库

三要素:duration(时长)、time_point(时间点)、clock(时钟)。

using namespace std::chrono;

auto start = steady_clock::now();
// ... 被测代码 ...
auto elapsed = steady_clock::now() - start;
auto ms = duration_cast<milliseconds>(elapsed).count();

seconds s(5);
minutes m = duration_cast<minutes>(s);       // 0(整数截断)

时钟:system_clock(系统时间,可转换为日历时间)、steady_clock(单调递增,测耗时用它)、high_resolution_clock。

16.6 <random> 随机数库

引擎(产生随机位)与分布(塑形)分离,替代质量低劣的 rand():

std::mt19937 gen(std::random_device{}());              // 引擎:梅森旋转
std::uniform_int_distribution<int> dice(1, 6);         // 均匀整数分布
std::normal_distribution<double> gauss(0.0, 1.0);      // 正态分布

int roll = dice(gen);
double x = gauss(gen);

16.7 <regex> 正则表达式

std::regex date_re(R"((\d{4})-(\d{2})-(\d{2}))");
std::smatch m;
std::string text = "today is 2026-08-24.";

if (std::regex_search(text, m, date_re)) {
    m[0].str();   // "2026-08-24"
    m[1].str();   // "2026"
}

// 遍历所有匹配
for (std::sregex_iterator it(text.begin(), text.end(), date_re), end;
     it != end; ++it) { /* ... */ }

std::string out = std::regex_replace(text, date_re, "$2/$3/$1");

16.8 数值与字符串转换

std::stoi("42");           // string → int(还有 stol、stoll、stoul、stoull)
std::stof("3.14");         // → float(stod、stold)
std::to_string(42);        // int → string(支持各数值类型)

sto* 系列提供 pos 出参指示解析位置,并在格式错误时抛 std::invalid_argument、溢出时抛 std::out_of_range。

16.9 非成员函数 begin / end 与 cbegin / cend

int arr[] = {1, 2, 3};
std::begin(arr);  std::end(arr);      // 对原生数组也有效
std::cbegin(v);   std::cend(v);       // 强制返回 const 迭代器

写泛型代码时优先用非成员版本,容器与数组一视同仁。

16.10 容器的其他改进

  • shrink_to_fit():请求释放多余容量;
  • cbegin/cend 成员:恒定迭代器;
  • insert 返回插入位置的迭代器;
  • 非成员 std::swap 优先于成员版本(配合 using std::swap 惯用法);
  • 无序容器提供桶接口(bucket_count、load_factor、rehash 等)与 reserve;
  • std::move_iterator 与算法配合实现「移动一段范围」。

16.11 新增算法

C++11 在 <algorithm> 中新增了一批通用算法:

算法 作用
all_of / any_of / none_of 区间内是否全部 / 存在 / 无一元素满足条件
find_if_not 查找第一个不满足条件的元素(与 find_if 相反)
copy_if / copy_n 复制满足条件的元素 / 复制 n 个元素
iota 用递增序列填充区间
is_sorted / is_sorted_until 判断是否有序 / 找到第一个无序位置
minmax / minmax_element 同时获取最小与最大值(元素位置)
is_partitioned / partition_copy / partition_point 分区判断、分区拷贝、分区点
is_permutation 是否为另一序列的排列
shuffle 随机打乱(接受随机引擎,替代废弃的 random_shuffle)
std::vector<int> v(10, 2);
if (std::all_of(v.begin(), v.end(), [](int i) { return i % 2 == 0; })) {
    std::cout << "all even\n";
}

std::vector<int> seq(10);
std::iota(seq.begin(), seq.end(), 19);          // 19 20 21 ... 28

std::vector<int> w{3, 9, 1, 4, 2, 5, 9};
auto mm = std::minmax_element(w.begin(), w.end());
// *mm.first == 1, *mm.second == 9

16.12 其他标准库新增点

  • bitset 新操作:all()(所有位都置位时返回 true)、any()、none(),补充原有的 count()/test():
    std::bitset<32> bits(1U);
    bits.all();    // false,只有第 0 位置位
    
  • 浮点数格式控制:std::hexfloat 强制以十六进制格式输出浮点数,std::defaultfloat 恢复默认表示;
  • std::mem_fn:把成员函数等成员指针包装成可调用对象,让编译器负责推断成员类型;
  • allocator::construct 可接受任意构造参数:把参数转发给元素构造函数原地构造(emplace/make_shared 背后的机制):
    alloc.construct(p, 10, 'c');    // 构造出 10 个 'c' 的字符串
    
  • 文件流支持 std::string 文件名:std::ifstream/std::ofstream 可直接接受 std::string 对象(此前只支持 C 风格字符数组)。

17. 常见陷阱与迁移建议

17.1 容器扩容:移动构造必须是 noexcept

vector/string 扩容时,若元素的移动构造不是 noexcept,标准库为保证异常安全会退回拷贝。管理资源的类一定要给移动构造/赋值加 noexcept。

17.2 花括号初始化与 initializer_list 的优先级

std::vector<int> v1(10, 20);   // 10 个元素,值为 20
std::vector<int> v2{10, 20};   // 2 个元素:10 和 20 !(匹配了 initializer_list 构造)

写容器构造时要想清楚意图;自己设计类时慎用 initializer_list 构造函数,以免抢走其他重载。

17.3 不要对返回值使用 std::move

return std::move(local); 会抑制 RVO,通常比直接 return local; 更慢。

17.4 按引用捕获的悬垂风险

存储或异步执行的 lambda 若按引用捕获局部变量,变量销毁后引用即悬垂。跨生命周期请使用值捕获(或捕获 shared_ptr)。

17.5 auto 与代理对象

auto 推导的是实际类型,遇到代理类时可能出乎意料:

std::vector<bool> vb{true, false};
auto b = vb[0];    // 不是 bool!是 std::vector<bool>::reference 代理
// bool b2 = vb[0]; 或 auto b3 = static_cast<bool>(vb[0]);

17.6 shared_ptr 常见错误

  • 同一裸指针创建多个 shared_ptr → 双重释放;
  • 在构造函数里把 this 交给 shared_ptr → 应使用 enable_shared_from_this;
  • 循环引用 → 用 weak_ptr 打破;
  • shared_ptr 按值传递开销大 → 只读场景传 const T&。

17.7 线程参数的拷贝与生命周期

std::thread 默认拷贝实参;异步/线程函数中使用引用务必保证对象存活,必要时 std::ref + 同步手段,或直接传值/移动。

17.8 迁移清单

  • 0/NULL → nullptr;
  • 显式重写加 override,禁止继承/重写用 final;
  • 拷贝语义完整的类补齐或删除移动操作(Rule of Five),移动操作加 noexcept;
  • 裸 new/delete → 智能指针(优先 unique_ptr + make_shared);
  • rand() → <random>;手写计时 → <chrono>;
  • 复杂迭代器、临时类型 → auto;遍历 → 范围 for;
  • 回调函数指针 → std::function + lambda;
  • 编译选项:-std=c++11(或更高)。

18. 参考资料


附录:命名空间的使用

命名空间并非 C++11 新特性(C++98 已有),C++11 的贡献是内联命名空间(见 §13.2)。本附录整理自原《C++11 中命名空间(namespace)的使用》一文,作为命名空间的综合参考。

A.1 为什么需要命名空间

大型程序会组合多个独立开发的库,全局名字(类、函数、模板)不可避免地相互冲突,引发命名空间污染(namespace pollution)。传统上靠加长名字前缀来规避,繁琐易错。命名空间提供了更可控的机制:分割全局命名空间,每个命名空间是一个作用域。

A.2 定义与基本规则

namespace ns {
    // 类、变量(及其初始化)、函数(及其定义)、模板、其它命名空间都可以出现
    class Widget { };
    int counter = 0;
    void helper();
    template <typename T> struct Box {};
}

要点:

  • 不能定义在函数或类的内部;可以定义在全局作用域或另一个命名空间中;花括号结束无须分号;
  • 命名空间的名字必须在定义它的作用域内唯一;
  • 命名空间内的名字可被同一命名空间内的成员直接访问;外部代码必须使用限定名(ns::helper());
  • 名字默认具有外部链接属性(const 对象例外,默认内部链接)。

命名空间可以是不连续的——同名命名空间可以多次打开,每次添加新成员(同名命名空间即便定义位置不同,仍是同一实体)。这使得接口与实现可以分离:声明放头文件、定义放源文件:

// ns.h
namespace ns { void display(); }

// ns.cpp
namespace ns { void display() { /* ... */ } }   // 重新打开,补充定义

注意:#include 应出现在打开命名空间之前(放进命名空间内部会把头文件的所有名字隐式变成该命名空间的成员)。

命名空间成员可以在外部定义(定义必须出现在所属命名空间的外层作用域中,且带完整前缀):

namespace ns {
    void display();
    class Geek { public: void display(); };
}

void ns::display() { /* ... */ }          // 命名空间外定义
void ns::Geek::display() { /* ... */ }

其它规则:

  • 模板特例化必须属于原始模板所在的命名空间(可在其中声明、在外部定义);
  • 全局命名空间:全局作用域中的名字隐式属于全局命名空间,可用 ::name 显式限定;
  • 不能在命名空间体之外以 命名空间名::成员名; 的形式添加新成员——新成员的声明必须在命名空间体内。

A.3 嵌套命名空间

namespace outer {
    int x;
    namespace inner {      // 嵌套命名空间,同时是嵌套作用域
        int x;             // 隐藏 outer::x
        void f();
    }
}
outer::inner::f();         // 外层代码访问内层名字必须加限定符

内层名字隐藏外层同名成员;嵌套的子命名空间必须定义在外层命名空间体之内(不能把子命名空间的声明与定义分开)。C++17 支持 namespace A::B::C {} 简写(见《C++17 新特性详解》§10.2)。

A.4 内联命名空间(C++11)

详见 §13.2。要点:内联命名空间中的名字可被外层命名空间直接使用(引用时可省略内联命名空间的名字,相当于被提升到父级命名空间);inline 必须写在第一次定义处;常用于接口版本管理与二进制兼容——不同内联命名空间标识接口兼容的不同版本,升级接口时避免二进制符号改变(libstdc++、libc++ 均这么做)。

A.5 未命名的命名空间

namespace {
    int internal_counter;   // 静态生命周期、内部链接
}
  • 其中的变量拥有静态生命周期(第一次使用前创建,程序结束时销毁);
  • 仅在特定文件内有效,不能跨越多个文件;不同文件的未命名命名空间互不相关(头文件里定义未命名命名空间会在每个包含者中产生不同实体,应避免);
  • 名字可以直接使用(无从限定),也不能对其使用作用域运算符;
  • 可以嵌套在其它命名空间中(此时其成员可通过外层命名空间名访问)。

取代文件级 static 声明:C 语言用 static 使全局实体仅在本文件可见;这一做法已被 C++ 标准弃用,现在应使用未命名的命名空间。

A.6 命名空间的别名

namespace Foundation { namespace V35 { namespace QueryLib { /* ... */ } } }

namespace Q = Foundation::V35::QueryLib;   // 别名与原名字等价
  • 不能在命名空间定义之前就声明别名;
  • 别名可以指向嵌套命名空间;一个命名空间可以有多个别名。

A.7 using 声明

一条 using 声明一次只引入一个成员,可清楚地知道程序中用的到底是哪个名字:

using std::cout;    // 可出现在全局、局部、命名空间作用域;类作用域中只能指向基类成员

规则:

  • 有效范围从声明处到所在作用域结束,期间隐藏外层作用域的同名实体;
  • 函数的 using 声明会引入该函数的所有重载版本(保证不违反命名空间接口);
  • 引入的名字若与本作用域中已有函数同名且形参列表相同,则报错;否则为当前作用域的重载集合添加新成员。
namespace B {
    void f(int); void f(double);
    struct g {};
    void g(char);           // 名字查找中,函数 g 隐藏 struct g
}

void demo() {
    using B::f;             // f(int)、f(double) 同时引入
    f(3.5);                 // 调用 B::f(double)

    using B::g;
    g('a');                 // 调用 B::g(char)
    struct g g1;            // 用 struct 关键字指明 struct B::g
}

A.8 using 指示

using namespace X 使该命名空间的所有名字可见,无法控制哪些名字可见:

  • 可出现在全局、局部、命名空间作用域,不能出现在类作用域;
  • 效果相当于把命名空间成员提升到「包含该命名空间和 using 指示的最近作用域」;
  • 与外层作用域的名字冲突是允许的,但使用时必须显式指明版本——二义性只在使用处才暴露(延后检测)。
namespace D { int d1; void f(char); }
using namespace D;
int d1;                     // 声明本身与 D::d1 不冲突

namespace E { int e; void f(int); }
namespace D {               // 重新打开 D(命名空间可以不连续)
    int d2;
    using namespace E;      // 传递的 using 指示
    void f(int);
}

void demo() {
    // d1++;                // error:二义——::d1 还是 D::d1?
    D::d1++;                // 显式限定消除二义
    e++;                    // ok:E::e 经传递 using 引入
    // f(1);                // error:二义——D::f(int) 还是 E::f(int)?
    f('a');                 // ok:只有 D::f(char) 匹配
}

A.9 using 声明 vs using 指示:最佳实践

using 声明 using 指示
引入粒度 单个成员 全部成员
冲突检测 声明处即可发现 延后到使用处
风险 低 高(重新引入命名空间污染)
  • 尽量用 using 声明;using 指示一条语句注入所有名字,多库环境下名字冲突会重新出现;且引入库的新版本后(新名字与现有名字冲突),原本正常的程序可能编译失败;
  • 头文件顶层作用域不要写 using 指示/声明(会把名字注入所有包含者);头文件最多在自己的函数或命名空间内使用;
  • using 指示并非一无是处:在命名空间自身的实现文件中使用是安全的。

经典的名字冲突与消解:

namespace vec {
    template <typename T> class vector { /* ... */ };
}

void demo() {
    std::vector<int> v1;        // 标准 vector
    vec::vector<int> v2;        // 自定义 vector

    { using namespace std;      // 限定在局部作用域
      vector<int> v3; }         // std::vector

    { using vec::vector;        // using 声明:只引入 vector
      vector<int> v4; }         // vec::vector
}

A.10 名字查找与 ADL

  • 命名空间内名字的查找遵循常规规则:由内向外依次查找外层作用域,直到全局命名空间;对类而言:成员函数体内 → 类(含基类)→ 外层作用域。从限定名可以反推查找次序(限定名以相反次序指出被查找的作用域);
  • 实参相关的查找(ADL):给函数传递类类型对象(含引用/指针)时,除常规查找外还会查找实参类所属的命名空间。这使得概念上属于类接口的非成员函数(如 operator<<)无须 using 声明即可使用。
posted @ 2025-03-02 13:03  main_c  阅读(34)  评论(0)    收藏  举报  来源