1. 从“新特性”到“新思维”:C++11的范式转变

如果你是从C++98/03时代一路走来的开发者,第一次接触C++11时,那种感觉可能不仅仅是“多了几个新功能”,而更像是一次认知上的刷新。过去,我们写C++,脑子里想的是“对象生命周期”、“手动管理资源”、“复杂的模板元编程”。而C++11之后,很多代码的写法、思考问题的角度都变了。它不仅仅是语法糖的堆砌,更是一次旨在让C++更安全、更高效、更具表达力的范式升级。很多人把学习重点放在 auto lambda 这些显眼的特性上,这没错,但今天我想和你深入聊聊C++11中那些真正重塑我们编程习惯的“第二梯队”核心特性,它们可能不那么炫酷,却实实在在地解决了我们日常开发中的诸多痛点,是写出现代、健壮C++代码的基石。

我们将聚焦于几个关键领域:如何告别裸指针的噩梦,拥抱智能指针进行资源管理;如何理解移动语义,从根本上提升程序性能;以及如何利用 lambda 表达式和函数对象,让代码既简洁又强大。这些内容,是连接C++古典时代与现代编程的桥梁,理解了它们,你才能说真正踏入了现代C++的大门。

2. 资源管理的革命:从 new/delete 到智能指针

在C++11之前,资源管理,尤其是动态内存管理,是程序员肩上最重的担子之一。每一个 new 都必须对应一个 delete ,在复杂的逻辑分支和异常处理中,确保这一点极其困难,内存泄漏几乎是大型C++项目的“标配”问题。C++11引入的智能指针,其核心思想是RAII(Resource Acquisition Is Initialization),即资源获取即初始化。对象的构造函数获取资源,析构函数释放资源。智能指针就是这种思想的典型载体,它管理的是动态分配对象的生命周期。

2.1 std::unique_ptr : 独占所有权的轻量级卫士

std::unique_ptr 如其名,表示对所指对象的独占所有权。一个对象在任何时刻只能被一个 unique_ptr 拥有。这种设计带来了两个巨大好处:一是所有权清晰,避免了多个指针指向同一对象时“谁该负责删除”的混乱;二是极高的效率,因为它几乎没有额外的开销,几乎等同于裸指针。

核心用法与原理:

#include <memory>
#include <iostream>

class Widget {
public:
    Widget() { std::cout << "Widget constructed\n"; }
    ~Widget() { std::cout << "Widget destroyed\n"; }
    void doSomething() { std::cout << "Widget working\n"; }
};

void useUniquePtr() {
    // 1. 创建独占指针
    std::unique_ptr<Widget> up1(new Widget()); // 传统方式
    auto up2 = std::make_unique<Widget>(); // C++14起推荐方式,更安全高效

    // 2. 像普通指针一样使用
    up1->doSomething();
    (*up2).doSomething();

    // 3. 所有权的转移(移动语义)
    std::unique_ptr<Widget> up3 = std::move(up1); // up1所有权转移给up3,up1变为nullptr
    // 此时 up1.get() == nullptr, up3 拥有对象

    // 4. 重置或释放
    up3.reset(); // 显式释放所管理对象,up3变为nullptr。对象在此处被销毁。
    // up2 在作用域结束时自动析构,释放其管理的对象
}

std::make_unique (C++14)是创建 unique_ptr 的首选方式。它有两个关键优势:一是异常安全。考虑 foo(std::unique_ptr<T>(new T), std::unique_ptr<U>(new U)) ,如果 new T 成功而 new U 抛出异常,那么 T 对象就会泄漏,因为 unique_ptr<T> 的构造函数还没来得及接管。而 make_unique 将对象的构造和智能指针的构造合为一步原子操作,避免了这个问题。二是代码更简洁,不需要重复写类型 T

一个常见的坑: unique_ptr 禁止拷贝,只允许移动。这意味着你不能将它放入需要拷贝操作的旧式容器算法中(除非是移动)。但这也正是其保证所有权唯一的机制。

注意: std::auto_ptr 在C++11中已被废弃,正是因为它尝试用拷贝语法实现移动语义,导致语义混乱和潜在错误。 unique_ptr 是它的正确替代品。

2.2 std::shared_ptr std::weak_ptr : 共享所有权与打破循环引用

当需要多个实体共享同一个对象的所有权时, std::shared_ptr 登场了。它通过引用计数来管理对象的生命周期。每多一个 shared_ptr 指向该对象,引用计数加1;每有一个 shared_ptr 被销毁或重置,引用计数减1。当计数变为0时,管理的内存被自动释放。

共享指针的基本使用:

void useSharedPtr() {
    auto sp1 = std::make_shared<Widget>(); // 引用计数 = 1
    {
        auto sp2 = sp1; // 拷贝构造,引用计数 = 2
        sp2->doSomething();
    } // sp2 析构,引用计数 = 1
    // sp1 仍然有效
    sp1->doSomething();
} // sp1 析构,引用计数 = 0,Widget对象被销毁

std::make_shared 同样是推荐创建方式,它通常比直接 new 更高效,因为它可以将对象本身和控制块(存储引用计数等元数据)分配在连续的内存块中。

循环引用问题与 weak_ptr shared_ptr 最大的陷阱是循环引用。如果两个对象各自持有一个指向对方的 shared_ptr ,那么它们的引用计数永远无法降到0,导致内存泄漏。

struct Node {
    std::shared_ptr<Node> next;
    ~Node() { std::cout << "Node destroyed\n"; }
};

void circularReference() {
    auto node1 = std::make_shared<Node>();
    auto node2 = std::make_shared<Node>();
    node1->next = node2;
    node2->next = node1; // 循环引用形成!
} // 作用域结束,node1和node2的引用计数仍为1,对象永不销毁,内存泄漏。

为了解决这个问题,C++11引入了 std::weak_ptr weak_ptr 是一种“弱引用”,它指向一个由 shared_ptr 管理的对象,但不会增加该对象的引用计数。这意味着它不拥有对象的所有权,因此不会阻止对象的销毁。你需要通过 lock() 成员函数来尝试获取一个可用的 shared_ptr

使用 weak_ptr 打破循环引用:

struct SafeNode {
    std::weak_ptr<SafeNode> next; // 使用 weak_ptr 而非 shared_ptr
    ~SafeNode() { std::cout << "SafeNode destroyed\n"; }
};

void safeReference() {
    auto node1 = std::make_shared<SafeNode>();
    auto node2 = std::make_shared<SafeNode>();
    node1->next = node2;
    node2->next = node1; // 循环引用被打破,因为 weak_ptr 不增加计数

    // 使用 weak_ptr
    if (auto sp = node1->next.lock()) { // 尝试提升为 shared_ptr
        sp->doSomething(); // 对象还存在,安全使用
    } else {
        std::cout << "The object no longer exists.\n";
    }
} // 作用域结束,node1和node2引用计数归零,对象被正确销毁。

经验之谈: 在设计对象关系时,如果关系不是严格的“拥有”(ownership),而是“观察”(observation)或“临时使用”,就应该优先考虑使用 weak_ptr 。例如,在观察者模式中,主题(Subject)持有观察者(Observer)的 weak_ptr 列表,这样即使观察者被销毁,也不会影响主题,主题在通知前只需检查 weak_ptr 是否有效即可。

3. 性能飞跃的关键:理解移动语义与右值引用

这是C++11最深刻、也最难理解的变化之一,但它带来的性能提升是颠覆性的。要理解移动语义,必须先理解左值(lvalue)和右值(rvalue)。

简单区分:

  • 左值 :有持久身份、可以取地址的表达式。例如变量、函数返回的引用、解引用指针等。 int a = 5; 中的 a 是左值。
  • 右值 :临时对象、字面量(除了字符串字面量)、返回非引用类型的函数调用等。例如 10 x + y 的结果, std::string(“hello”) 。它们即将消亡,无法取地址(临时对象有地址但很快失效)。

右值引用( T&& :就是绑定到右值的引用。它的出现,让我们能够识别出那些“即将消亡”的临时对象。

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

传统拷贝是“深拷贝”:为新对象分配一块新内存,然后把旧对象的数据逐个复制过来。对于持有大量资源的对象(如 std::vector , std::string ),这很昂贵。

移动语义的思想是:既然那个旧对象(右值)马上就要被销毁了,我们何不“偷”走它的资源(比如内部指针),直接交给新对象呢?这样新对象就免去了分配和复制的大量开销,而旧对象被“掏空”后处于有效但未定义的状态(通常可安全析构)。

一个简单的自定义类示例:

class Buffer {
private:
    char* data_;
    size_t size_;
public:
    // 传统拷贝构造函数(深拷贝)
    Buffer(const Buffer& other) : size_(other.size_) {
        data_ = new char[size_];
        std::copy(other.data_, other.data_ + size_, data_);
        std::cout << "Copied buffer, size = " << size_ << "\n";
    }

    // 移动构造函数(C++11新增)
    Buffer(Buffer&& other) noexcept // noexcept 很重要,标准库容器会利用它优化
        : data_(other.data_), size_(other.size_) { // “窃取”资源
        other.data_ = nullptr; // 将源对象置于有效但空的状态
        other.size_ = 0;
        std::cout << "Moved buffer, size = " << size_ << "\n";
    }

    // 移动赋值运算符
    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data_; // 释放当前资源
            data_ = other.data_; // 窃取资源
            size_ = other.size_;
            other.data_ = nullptr;
            other.size_ = 0;
            std::cout << "Move-assigned buffer\n";
        }
        return *this;
    }

    ~Buffer() { delete[] data_; }
    // ... 其他成员函数
};

void testMoveSemantics() {
    Buffer buf1(100); // 假设有构造函数分配100字节
    Buffer buf2 = std::move(buf1); // 调用移动构造函数,高效!
    // 此时 buf1 的 data_ 为 nullptr,不能再使用其内容
    Buffer buf3(200);
    buf3 = std::move(buf2); // 调用移动赋值运算符
}

std::move 本身并不移动任何东西,它只是一个简单的类型转换工具,将其参数无条件转换为右值引用。它告诉编译器:“这个对象我愿意被移动,请优先调用移动语义的函数”。真正的移动操作发生在移动构造函数或移动赋值运算符中。

为什么移动构造函数要加 noexcept 标准库容器(如 std::vector )在重新分配内存( realloc )时,为了提供强异常安全保证,会判断元素的移动构造函数是否 noexcept 。如果是,它就用移动(更快);如果不是,它就用拷贝(更安全)。为你的移动操作标记 noexcept 能带来显著的性能提升。

3.2 移动语义如何提升实际代码性能

最直接的受益者是标准库容器和字符串。当你向容器中插入一个临时对象,或者从函数返回一个局部容器时,移动语义会悄无声息地发挥作用,避免不必要的深拷贝。

std::vector<std::string> createAndProcess() {
    std::vector<std::string> vec;
    vec.reserve(10);
    for (int i = 0; i < 10; ++i) {
        std::string tempStr = "String_" + std::to_string(i);
        // 在C++98中,这里会发生拷贝,将tempStr的内容复制到vec中。
        // 在C++11中,因为tempStr是左值,理论上也会拷贝。但我们可以优化:
        vec.push_back(std::move(tempStr)); // 明确移动,避免拷贝。
        // 或者更常见的是,直接插入右值:
        vec.push_back("String_" + std::to_string(i)); // 这个临时字符串是右值,会自动触发移动构造。
    }
    // 返回局部变量vec。在C++98中,这里需要拷贝(或者依赖NRVO编译器优化)。
    // 在C++11中,编译器会优先尝试移动构造返回值。
    return vec; // 很可能发生移动,高效。
}

int main() {
    auto myVec = createAndProcess(); // 高效的移动构造或RVO
    // ...
}

实操心得: 在编写自己的资源管理类(如管理文件句柄、网络连接、GPU内存)时,务必实现移动构造函数和移动赋值运算符。这不仅能让你自己的类更高效,也能让它在标准库容器中表现更好。同时,对于函数参数,如果函数需要接管参数的所有权(即“沉没”参数),应该按值传递(利用移动)或者使用右值引用参数 (T&&) ,而不是常量左值引用 (const T&)

4. 匿名函数的艺术:Lambda表达式与函数对象

在C++11之前,如果我们想传递一段可调用的逻辑,要么使用函数指针(功能受限),要么定义一个完整的函数对象(Functor,即重载了 operator() 的类),这通常很繁琐。 lambda 表达式的出现,让我们能够就地定义匿名函数,极大地提升了代码的简洁性和表现力,尤其是在算法库( <algorithm> )的使用中。

4.1 Lambda表达式的基本语法与捕获

一个lambda表达式的基本形式是: [capture-list] (parameters) -> return-type { body }

其中, -> return-type parameters 在某些情况下可以省略。

核心在于捕获列表 [capture-list] :它决定了lambda体内部如何访问外部作用域的变量。

  • [] :不捕获任何外部变量。
  • [=] :以值的方式捕获所有外部变量(在lambda创建时拷贝)。
  • [&] :以引用的方式捕获所有外部变量。
  • [var] :以值的方式捕获特定变量 var
  • [&var] :以引用的方式捕获特定变量 var
  • [=, &var] :默认以值捕获,但变量 var 以引用捕获。
  • [&, var] :默认以引用捕获,但变量 var 以值捕获。
  • [this] :捕获当前类对象的 this 指针,从而可以访问成员变量和函数。

示例:

#include <iostream>
#include <vector>
#include <algorithm>

void lambdaDemo() {
    int base = 100;
    std::vector<int> nums = {1, 2, 3, 4, 5};

    // 1. 简单lambda,不捕获
    auto print = [] (int x) { std::cout << x << " "; };
    std::for_each(nums.begin(), nums.end(), print);
    std::cout << "\n";

    // 2. 以值捕获外部变量
    auto addBaseByValue = [base] (int x) { return x + base; };
    std::transform(nums.begin(), nums.end(), nums.begin(), addBaseByValue);
    // nums 现在是 {101, 102, 103, 104, 105}
    // 注意:base的拷贝在lambda创建时发生,后续修改base不影响lambda内的值。

    // 3. 以引用捕获外部变量
    int sum = 0;
    std::for_each(nums.begin(), nums.end(), [&sum] (int x) { sum += x; });
    std::cout << "Sum = " << sum << "\n"; // 输出 515
    // 直接操作外部sum,无需返回值。

    // 4. 通用lambda (C++14),使用auto参数
    auto genericAdd = [] (auto a, auto b) { return a + b; };
    std::cout << genericAdd(1, 2) << ", " << genericAdd(1.5, 2.3) << "\n";

    // 5.  mutable lambda:允许修改以值方式捕获的变量
    int counter = 0;
    auto incrementer = [counter] () mutable { // 没有mutable,不能修改counter的拷贝
        ++counter; // 修改的是lambda内部捕获的副本
        std::cout << "Internal counter: " << counter << "\n";
    };
    incrementer(); // 输出 1
    incrementer(); // 输出 2
    std::cout << "External counter: " << counter << "\n"; // 输出 0,外部变量未变
}

4.2 Lambda的本质与 std::function

Lambda表达式在编译器看来,是一个匿名类的对象,这个类重载了 operator() 。所以,每个lambda表达式都有自己独特的、编译器生成的类型。这意味着你不能直接用两个不同的lambda表达式给同一个具体的lambda类型变量赋值,但你可以用 auto 来接收。

当我们需要存储或传递一个可调用对象,并且其类型可能多变(普通函数、lambda、函数对象)时,就需要 std::function 。它是一个通用的、类型擦除的可调用对象包装器。

#include <functional>
#include <iostream>

void useStdFunction() {
    // 存储一个无参无返回值的lambda
    std::function<void()> task = []() { std::cout << "Task done.\n"; };
    task();

    // 存储一个接收int返回bool的lambda
    std::function<bool(int)> isEven = [](int x) { return x % 2 == 0; };
    std::cout << isEven(4) << "\n"; // 输出 1 (true)

    // 存储一个通过捕获列表持有状态的lambda
    int threshold = 50;
    std::function<bool(int)> isAboveThreshold = [threshold](int x) { return x > threshold; };
    // 即使threshold离开了作用域,isAboveThreshold内部仍保存着其拷贝的值。

    // std::function 也可以绑定普通函数、成员函数等。
}

性能提示: std::function 由于类型擦除和动态分配,会带来一定的开销(通常很小)。在性能极度敏感的循环内部,直接使用具体类型的lambda对象(用 auto )或函数指针可能更快。但在需要灵活性(如回调函数列表、事件系统)的场合, std::function 是不可或缺的工具。

一个结合算法库的实战例子:

// 传统方式:需要先定义一个函数或函数对象
bool oldWayCompare(int a, int b) { return a > b; }
std::sort(vec.begin(), vec.end(), oldWayCompare);

// C++11 lambda方式:就地定义,意图更清晰
std::sort(vec.begin(), vec.end(), [](int a, int b) { return a > b; });

// 更复杂的场景:根据对象的某个成员排序
struct Person { std::string name; int age; };
std::vector<Person> people = { /* ... */ };
std::sort(people.begin(), people.end(),
          [](const Person& a, const Person& b) { return a.age < b.age; });

Lambda让代码的意图和实现紧贴在一起,极大地提高了可读性和编写效率。

5. 类型推导的进化: auto decltype

auto 关键字在C++11中被赋予了新的含义:自动类型推导。它让编译器根据初始化表达式来推导变量的类型。这并非动态类型,推导发生在编译期,类型一旦确定就不再改变。

5.1 auto 的使用场景与限制

主要优势:

  1. 简化冗长类型名 :特别是迭代器和模板类型。
    std::vector<std::map<std::string, std::list<int>>> complexContainer;
    // 不用 auto
    std::vector<std::map<std::string, std::list<int>>>::iterator it = complexContainer.begin();
    // 用 auto
    auto it = complexContainer.begin(); // 清晰简洁
    
  2. 避免类型截断 :确保使用正确的类型。
    std::vector<int> vec;
    // 错误:size() 返回 size_t (无符号),与 int 可能不同,且比较有符号/无符号警告
    for (int i = 0; i < vec.size(); ++i) { /* ... */ }
    // 正确:使用 auto
    for (auto i = 0; i < vec.size(); ++i) { /* ... */ } // i 被推导为 int,仍有警告
    // 最佳:直接使用 size_t 或 auto
    for (size_t i = 0; i < vec.size(); ++i) { /* ... */ }
    for (auto i = vec.size(); i-- > 0; ) { /* ... */ } // 另一种倒序循环
    
  3. 配合Lambda :Lambda类型是唯一的,只能用 auto std::function 存储。
    auto lambda = [](int x) { return x * x; };
    

注意事项与限制:

  • auto 必须要有初始化表达式。
  • auto 会忽略顶层 const 和引用。如果需要,需手动加上。
    const int ci = 10;
    auto a = ci; // a 是 int (const被忽略)
    const auto b = ci; // b 是 const int
    int& ri = i;
    auto c = ri; // c 是 int (引用被忽略)
    auto& d = ri; // d 是 int&
    
  • 在函数参数、返回值(C++14起支持 auto 返回类型)和类成员变量(C++17起支持)中, auto 的使用有特定规则和场景。

5.2 decltype :获取表达式的声明类型

decltype 用于查询表达式的类型,它返回的是该表达式的“声明类型”,包括顶层 const 和引用。

常见用途:

  1. 声明与某个表达式类型相同的变量
    int i = 0;
    const int& cr = i;
    decltype(cr) y = i; // y 的类型是 const int&
    
  2. 在泛型编程中,依赖模板参数推导返回类型 (常与 auto 和尾置返回类型结合,在C++11中用于推导函数模板返回类型)。
    template<typename T, typename U>
    auto add(T t, U u) -> decltype(t + u) { // 尾置返回类型,decltype推导t+u的类型
        return t + u;
    }
    
  3. decltype(auto) (C++14) :它用 decltype 的规则来推导 auto ,主要用于完美转发函数返回值。
    template<typename Func, typename... Args>
    decltype(auto) call(Func f, Args&&... args) { // 完美保持返回值的类型(包括引用)
        return f(std::forward<Args>(args)...);
    }
    

auto decltype 的对比:

  • auto 根据初始化器推导类型,会忽略顶层 const 和引用。
  • decltype 根据给定的表达式推导类型,保留顶层 const 和引用。
  • decltype((variable)) (注意双括号)会产生引用类型,而 decltype(variable) 不会(除非 variable 本身就是引用)。这是一个需要小心的细节。

6. 统一初始化与列表初始化

C++11引入了花括号 {} 初始化语法,旨在提供一种统一、安全的初始化方式。

6.1 花括号初始化的优势

  1. 统一语法 :可以用于初始化几乎所有类型。
    // 内置类型
    int x{5};
    int y = {5}; // 等号可选
    // 数组
    int arr[]{1, 2, 3};
    // 结构体/类(如果所有成员都是public)
    struct Point { int x; int y; };
    Point p{10, 20};
    // 标准库容器
    std::vector<int> vec{1, 2, 3, 4, 5};
    std::map<std::string, int> dict{{"apple", 1}, {"banana", 2}};
    
  2. 防止窄化转换 :花括号初始化禁止可能导致数据丢失的隐式转换。
    int a = 3.14; // 警告,但允许,a=3
    int b{3.14};   // 错误!从 double 到 int 需要窄化转换
    char c{999};   // 错误!999超出char范围(假设char是8位)
    
  3. 避免“最令人烦恼的解析”
    class Timer { /* ... */ };
    // 这行代码的本意是定义一个Timer对象,但编译器会解析为一个返回Timer的函数声明!
    Timer t1(); // 函数声明,名为t1,无参,返回Timer
    Timer t2{}; // 正确!使用花括号初始化,定义了一个Timer对象t2。
    

6.2 std::initializer_list 与构造函数重载

当使用花括号初始化一个类对象时,编译器会优先匹配接收 std::initializer_list 作为参数的构造函数。这有时会导致令人意外的行为。

#include <iostream>
#include <vector>

class Widget {
public:
    Widget(int a, int b) { std::cout << "Called Widget(int, int)\n"; }
    Widget(std::initializer_list<int> list) {
        std::cout << "Called Widget(std::initializer_list<int>), size = " << list.size() << "\n";
    }
};

int main() {
    Widget w1(10, 20); // 调用 Widget(int, int)
    Widget w2{10, 20}; // 调用 Widget(std::initializer_list<int>)!可能不是你想要的结果。
    Widget w3({10, 20}); // 明确传递initializer_list
    Widget w4{10}; // 调用 Widget(std::initializer_list<int>),size=1
    // 如果想用花括号调用第一个构造函数,需要额外注意
    Widget w5({10, 20}); // 或者像w3一样
}

经验之谈: 在设计自己的类时,如果提供了 std::initializer_list 构造函数,需要意识到它会被花括号初始化“强烈偏好”。这可能会影响其他构造函数的调用。在标准库中, std::vector 就是一个典型例子: std::vector<int> v(5, 10); 创建5个元素,每个都是10。而 std::vector<int> v{5, 10}; 创建2个元素:5和10。这是一个容易混淆的地方,使用时需格外小心。

7. 其他不容忽视的实用特性

除了上述重量级特性,C++11还包含许多提升开发效率和代码质量的小特性。

7.1 范围 for 循环

提供了一种遍历容器或序列的简洁语法。

std::vector<int> vec = {1, 2, 3, 4, 5};
// 传统迭代器方式
for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) {
    std::cout << *it << " ";
}
// 范围for循环 (C++11)
for (int value : vec) { // 按值访问,会拷贝
    std::cout << value << " ";
}
for (const int& value : vec) { // 按常量引用访问,无拷贝,推荐
    std::cout << value << " ";
}
for (auto& value : vec) { // 使用auto,按引用访问,可修改元素
    value *= 2;
}

它的原理是编译器将其转换为基于迭代器的普通循环。要求遍历的对象有 begin() end() 成员函数,或者可以通过ADL找到对应的 begin end 自由函数。

7.2 nullptr 字面量

用于替代宏 NULL (通常是0)。 nullptr 具有明确的指针类型 std::nullptr_t ,可以隐式转换为任何指针类型,但不会转换为整数类型。这解决了函数重载时 NULL 可能被当作整数0导致的二义性问题。

void foo(int);
void foo(char*);
foo(NULL); // 调用哪个?可能调用foo(int),不符合直觉。
foo(nullptr); // 明确调用foo(char*)

7.3 强类型枚举 enum class

传统C++枚举存在枚举常量污染外层作用域、隐式转换为整数等问题。

// 传统枚举
enum Color { Red, Green, Blue };
enum TrafficLight { Red, Yellow, Green }; // 错误!Red和Green重定义
int i = Red; // 隐式转换,OK

// 强类型枚举 (C++11)
enum class Color { Red, Green, Blue };
enum class TrafficLight { Red, Yellow, Green }; // OK,作用域独立
Color c = Color::Red;
// int i = Color::Red; // 错误!不能隐式转换
int j = static_cast<int>(Color::Red); // 需要显式转换

enum class 的枚举常量位于枚举类的作用域内,不会污染外部,且不能隐式转换为整数,更加安全。

7.4 静态断言 static_assert

编译期断言。如果断言条件为假,编译器会报错并显示指定的错误信息。常用于检查模板参数、平台特性等。

static_assert(sizeof(int) == 4, "int must be 4 bytes on this platform.");
template<typename T>
class Widget {
    static_assert(std::is_arithmetic<T>::value, "T must be arithmetic type.");
    // ...
};

7.5 =default =delete

控制默认函数的生成。

  • =default :显式要求编译器生成默认版本的函数(如默认构造函数、析构函数、拷贝控制成员)。即使你提供了其他构造函数,也可以用这个来保留默认版本。
  • =delete :禁止编译器生成某个函数,或禁止某个函数被调用。常用于禁止拷贝(实现不可拷贝的类),或删除某些不希望的函数重载。
class NonCopyable {
public:
    NonCopyable() = default;
    ~NonCopyable() = default;
    // 禁止拷贝
    NonCopyable(const NonCopyable&) = delete;
    NonCopyable& operator=(const NonCopyable&) = delete;
    // 允许移动
    NonCopyable(NonCopyable&&) = default;
    NonCopyable& operator=(NonCopyable&&) = default;
};

void foo(int);
void foo(double) = delete; // 禁止使用double参数调用foo
// foo(3.14); // 错误!该重载已被删除

C++11的这些特性,从资源管理、性能优化到语法便利性,全方位地革新了C++的编程体验。掌握它们,意味着你从“古典C++”程序员,进化成了“现代C++”的实践者。这不仅仅是语法的更新,更是一种编程思维方式的转变:更多地依赖语言机制来保证安全与效率,而非完全依赖程序员的自觉。在实际项目中,尤其是新启动的项目中,积极、合理地运用这些特性,能显著提升代码的质量和可维护性。当然,迁移旧代码时需要谨慎评估,但为新代码选择现代C++,无疑是更明智的选择。

更多推荐