C++11(三)

5.1 默认的移动构造和移动赋值

1. 默认成员函数概述

  • C++98 的 6 个默认成员函数 :构造函数、析构函数、拷贝构造函数、拷贝赋值重载、取地址重载、const取地址重载。
    • 核心重点:前 4 个函数最为重要,后 2 个用处不大。
    • 默认行为:如果不手动编写,编译器会自动生成默认版本。
  • C++11 新增 2 个默认成员函数:移动构造函数、移动赋值运算符重载。

2. 默认移动构造函数的生成与规则

  • 生成条件 :如果没有自己实现移动构造函数,没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,编译器会自动生成一个默认移动构造。
  • 执行规则
    • 内置类型成员:执行逐成员按字节拷贝(浅拷贝)。
    • 自定义类型成员:检查该成员是否实现了移动构造。如果实现了,则调用其移动构造;如果没有实现,则退而调用其拷贝构造。

3. 默认移动赋值运算符重载的生成与规则

  • 生成条件 :如果没有自己实现移动赋值重载函数,没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,编译器会自动生成一个默认移动赋值。
  • 执行规则 :与默认移动构造完全类似:
    • 内置类型成员:执行逐成员按字节拷贝赋值。
    • 自定义类型成员:检查该成员是否实现了移动赋值。如果实现了,则调用其移动赋值;如果没有实现,则退而调用其拷贝赋值。

4. 移动与拷贝函数的互斥规则

  • 如果你手动提供了移动构造或者移动赋值,编译器将不会自动提供拷贝构造和拷贝赋值。
cpp 复制代码
class Person
{
public:
    Person(const char* name = "", int age = 0)
        : _name(name)
        , _age(age)
    {}

    /* 注释掉拷贝构造、拷贝赋值和析构函数,
       编译器会自动生成默认的拷贝和移动操作 */
    /*
    Person(const Person& p)
        : _name(p._name)
        , _age(p._age)
    {}
    */

    /*
    Person& operator=(const Person& p)
    {
        if (this != &p)
        {
            _name = p._name;
            _age = p._age;
        }
        return *this;
    }
    */

    /*
    ~Person()
    {}
    */

private:
    bit::string _name; // 自定义类型
    int _age;          // 内置类型
};

int main()
{
    Person s1;
    Person s2 = s1;            // 调用编译器默认生成的拷贝构造
    Person s3 = std::move(s1); // 调用编译器默认生成的移动构造
    Person s4;
    s4 = std::move(s2);        // 调用编译器默认生成的移动赋值
    return 0;
}

5.2 default 和 delete 关键字

C++11 引入了 =default=delete 关键字,让程序员能够更精细、更直观地控制类中默认成员函数的生成与使用。

1. =default:显式指定生成默认函数

  • 使用场景 :当你需要某个默认函数,但由于某些原因(例如自定义了拷贝构造),编译器不再自动生成它时,可以使用 =default 显式要求编译器生成该函数的默认版本。

  • 典型示例 :根据 C++11 规则,如果类中提供了拷贝构造函数,编译器就不会自动生成移动构造函数。此时,可以使用 =default 显式指定移动构造函数的生成:

    cpp 复制代码
    MyClass(MyClass&&) = default; // 显式要求编译器生成默认移动构造
    • 核心优势
    • 明确意图:明确表明你的意图,提高代码可读性,让其他开发者知道这是有意生成的。
    • 保持性能:显式默认化的特殊成员函数仍被认为是"平凡的(trivial)",没有额外的性能开销,且类依然可以保持为 POD 类型。

2. =delete:显式删除函数(禁止生成/调用)

  • 使用场景:如果想要限制某些默认函数的生成,禁止对象进行某种操作。

  • C++98 的旧做法 :将函数设置为 private 并且只声明不定义。

    • 缺点:代码不够直观;错误检测被延迟到链接阶段(而非编译阶段);类的成员函数和友元函数依然可以调用它,只是链接时会报错。
  • C++11 的新做法 :在函数声明后加上 =delete,指示编译器不生成对应函数的默认版本。被 =delete 修饰的函数称为删除函数(deleted function)

    cpp 复制代码
    MyClass(const MyClass&) = delete; // 显式删除拷贝构造

    核心优势:
    编译期报错:任何尝试调用删除函数的代码都会导致编译错误,错误信息比 C++98 的链接错误更清晰。
    绝对禁止:使用 =delete 删除的函数无法通过任何方法调用,即使是成员函数或友元函数中的代码也无法调用。
    适用范围广:不仅适用于成员函数,还可以用于非成员函数、运算符重载以及模板函数(例如禁止特定类型的隐式转换或模板实例化)。

5.4 final 与 override

1. override:显式标记重写基类虚函数

  • 核心作用:用于派生类中,显式声明并强制校验该函数是否真正重写了基类的虚函数。
  • 解决的问题 :防止因函数名拼写错误、参数列表不匹配、返回值类型错误或漏写 virtual 等疏忽,导致"看似重写,实则未重写(变成隐藏或新增函数)"的隐性错误。
  • 核心优势 :将原本运行时的多态失效问题,提前到编译期暴露并报错,极大降低了调试成本,提高了代码安全性。

2. final:禁止继承或禁止重写

  • 作用于类 :标记在类名后,表示该类是"最终类",禁止被其他类继承(终止继承链)。
  • 作用于虚函数 :标记在虚函数声明后,表示该函数是"最终实现",禁止在任何派生类中被重写
  • 核心优势
    • 保障安全与明确意图:保护核心类或关键逻辑不被意外扩展或篡改。
    • 编译器优化 :明确告知编译器该函数无拓展、无重写,编译器可执行去虚拟化优化(消除虚函数查表间接寻址开销,直接静态绑定调用),提升代码执行效率。

6. STL 中的一些变化

1. 新增容器

  • 核心重点 :新增的容器中最具实际价值的是 unordered_mapunordered_set(基于哈希表实现,查找效率极高)。这两个容器在前面已经进行了非常详细的讲解。
  • 其他新增容器 :如 std::arraystd::forward_liststd::tuple 等,大家只需了解其基本概念即可。

2. 容器的新接口

  • 核心重点 :最重要的新接口与右值引用移动语义 相关。主要包括:
    • push_back / insert 的右值引用版本(支持移动构造,减少拷贝)。
    • emplace 系列接口(如 emplace_back,支持传入参数包直接在容器内存中构造对象,性能最优)。
    • 支持 std::initializer_list 版本的构造函数(支持使用花括号 {} 进行统一初始化)。
    • 以上核心接口在前面均已详细讲解。
  • 其他小改动 :还有一些无关痛痒的接口,如 cbegin() / cend()(返回 const 迭代器)、crbegin() / crend()(返回 const 反向迭代器)等,在实际开发中遇到时查阅文档即可。

3. 遍历方式革新

  • 范围 for 循环(Range-based for loop):极大地简化了容器的遍历操作,无需再手动编写复杂的迭代器或下标,该特性在容器部分也已详细讲解。

7. Lambda 表达式

7.1 Lambda 表达式语法

1. 本质与类型
  • 本质 :Lambda 表达式本质上是一个匿名函数对象(闭包)。与普通函数不同的是,它可以直接定义在函数内部。
  • 类型接收 :从语法使用层面而言,Lambda 表达式没有显式的类型名。因此,我们一般使用 auto 关键字或者模板参数来定义对象,以接收 Lambda 对象。
2. 语法格式
cpp 复制代码
[capture-list] (parameters) -> return_type { function_body }

各部分详细说明

  • [capture-list](捕获列表)

    • 位置 :总是出现在 Lambda 函数的开始位置,编译器正是根据 [] 来判断接下来的代码是否为 Lambda 表达式。
    • 作用:用于捕捉上下文(外部作用域)中的变量,供 Lambda 函数体内部使用。
    • 方式:支持传值捕捉和传引用捕捉(具体细节在 7.2 中细讲)。
    • 注意 :即使捕获列表为空,[] 也绝对不能省略。
  • (parameters)(参数列表)

    • 作用:与普通函数的参数列表功能类似。
    • 省略规则 :如果不需要传递参数,可以连同 () 一起省略。
  • -> return_type(返回值类型)

    • 作用:使用追踪返回类型的形式声明函数的返回值类型。
    • 省略规则:如果没有返回值,或者返回值类型明确可由编译器自动推导,此部分通常可以省略。
  • { function_body }(函数体)

    • 作用:包含 Lambda 的具体实现逻辑,与普通函数完全类似。
    • 内部访问:在函数体内,除了可以使用传入的参数外,还可以使用所有通过捕获列表捕捉到的外部变量。
    • 注意 :即使函数体为空,{} 也绝对不能省略。

7.2 捕获列表详解

  • 捕获的必要性

    Lambda 表达式默认只能使用其函数体内部定义的变量和参数列表中的参数。如果需要在 Lambda 内部使用外层作用域中的变量,就必须通过捕获列表进行捕获。

  • 捕获方式分类

    • 显式捕获 :在捕获列表中明确指定变量及其捕获方式,多个变量之间用逗号分隔。例如 [x, y, &z] 表示 xy 按值捕获,z 按引用捕获。
    • 隐式捕获 :在捕获列表中仅写 =&[=] 表示隐式值捕获,[&] 表示隐式引用捕获。编译器会自动捕获 Lambda 函数体内实际使用到的外部变量。
    • 混合捕获 :结合隐式捕获与显式捕获使用。
      • [=, &x]:默认按值捕获,唯独 x 按引用捕获。
      • [&, x, y]:默认按引用捕获,唯独 xy 按值捕获。
      • 语法规则 :使用混合捕获时,第一个元素必须是 &=。当默认捕获为 & 时,后续显式捕获的变量必须是值捕获;当默认捕获为 = 时,后续显式捕获的变量必须是引用捕获。
  • 捕获的作用域与限制

    • Lambda 表达式只能捕获定义在它之前的局部变量。
    • 静态局部变量和全局变量不需要也不能被捕获,Lambda 内部可以直接访问它们。
    • 如果 Lambda 表达式定义在全局作用域中,其捕获列表必须为空。
  • mutable 关键字与常量性

    • 默认情况下,Lambda 的函数调用运算符是 const 的,这意味着按值捕获的变量在 Lambda 内部是只读的,无法被修改。
    • 在参数列表后加上 mutable 关键字可以解除这一限制,允许修改按值捕获的变量副本(注意:修改的仅是副本,不会影响外部原始变量)。
    • 注意 :一旦使用了 mutable 关键字,即使 Lambda 没有参数,参数列表 ()绝对不能省略

7.3 Lambda 的应用

1. 替代传统可调用对象

在学习 Lambda 表达式之前,C++ 中常用的可调用对象主要是函数指针仿函数(函数对象)

  • 函数指针:类型定义较为繁琐,且无法携带状态。
  • 仿函数 :需要额外定义一个类并重载 operator(),代码量较大,相对麻烦。
  • Lambda 的优势:使用 Lambda 定义可调用对象,既简单又方便,可以直接在需要的地方内联编写逻辑,极大地提高了代码的可读性和开发效率。
2. 广泛的应用场景

除了配合 STL 算法(如 sortfind_if 等)使用外,Lambda 在其他场景中也极为好用:

  • 多线程编程 :在创建线程(如 std::thread)时,直接定义线程的执行逻辑。
  • 智能指针 :在创建智能指针(如 std::shared_ptr)时,使用 Lambda 定制专属的删除器(Deleter)。
3. 实战案例:配合 STL 进行自定义排序

在需要实现多种自定义比较逻辑时,Lambda 的优势尤为明显。以下是一个商品排序的对比示例:

传统仿函数做法

cpp 复制代码
struct Goods {
    string _name;      // 名字
    double _price;     // 价格
    int _evaluate;     // 评价
    Goods(const char* str, double price, int evaluate)
        : _name(str), _price(price), _evaluate(evaluate) {}
};

// 需要为每一种比较规则单独定义一个结构体
struct ComparePriceLess {
    bool operator()(const Goods& gl, const Goods& gr) {
        return gl._price < gr._price;
    }
};

struct ComparePriceGreater {
    bool operator()(const Goods& gl, const Goods& gr) {
        return gl._price > gr._price;
    }
};
int main()
{
 vector<Goods> v = { { "苹果", 2.1, 5 }, { "⾹蕉", 3, 4 }, { "橙⼦", 2.2, 3 
}, { "菠萝", 1.5, 4 } };
 // 类似这样的场景,我们实现仿函数对象或者函数指针⽀持商品中 
 // 不同项的⽐较,相对还是⽐较⿇烦的,那么这⾥lambda就很好⽤了 
 sort(v.begin(), v.end(), ComparePriceLess());
 sort(v.begin(), v.end(), ComparePriceGreater());
 sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
 return g1._price < g2._price;
 });
 sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
 return g1._price > g2._price; 
 });
 sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
 return g1._evaluate < g2._evaluate;
 });
 sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
 return g1._evaluate > g2._evaluate;
 });
 return 0;
}

7.4 Lambda 的底层原理

1. 本质:编译器生成的仿函数对象

Lambda 表达式和范围 for 循环一样,本质上都是语法糖 。从汇编指令层面来看,底层压根就没有 Lambda 和范围 for 这样的概念。范围 for 底层被展开为迭代器操作,而 Lambda 底层则是一个仿函数(Functor)对象

2. 编译器的转换过程

当我们编写一个 Lambda 表达式时,编译器会在后台自动生成一个对应的匿名仿函数类。具体转换规则如下:

  • 匿名类名 :编译器会按照一定的内部规则生成一个唯一的类名(类似于 __Lambda123 的形式),以保证不同的 Lambda 表达式生成的类名互不冲突。
  • operator() 重载 :Lambda 的参数列表、返回值类型和函数体,会被直接转换为该匿名仿函数类中 operator() 的参数、返回类型和函数体。这使得 Lambda 对象可以像普通函数一样被调用。
  • 捕获列表与成员变量 :捕获列表中的变量,本质上会被生成为该仿函数类的成员变量
    • 按值捕获的变量,会成为类中的普通成员变量。
    • 按引用捕获的变量,会成为类中的引用成员变量。
  • 构造函数:编译器会自动为该匿名类生成一个构造函数。捕获列表中的变量就是构造函数的实参,用于在创建 Lambda 对象时初始化其内部的成员变量。
  • 隐式捕获的处理 :对于隐式捕获(如 [=][&]),编译器会在编译期分析 Lambda 函数体,自动识别出实际使用了哪些外部变量,并将它们作为实参传递给构造函数。
相关推荐
星恒随风2 小时前
C++ 类和对象入门(四):日期类 Date 的运算符重载实现详解
开发语言·c++·笔记·学习
wuminyu3 小时前
Java锁机制之park与futex系统级协同机制解析
java·linux·c语言·jvm·c++
Qt程序员13 小时前
Linux RCU 原理与应用
linux·c++·内核·linux内核·rcu
qeen8713 小时前
【C++】类与对象之类的默认成员函数(二)
android·c语言·开发语言·c++·笔记·学习
王老师青少年编程14 小时前
信奥赛C++提高组csp-s之搜索进阶(记忆化搜索案例实践3)
c++·记忆化搜索·方格取数·csp·信奥赛·csp-s·提高组
Titan202415 小时前
Linux动静态库
linux·服务器·c++
j_xxx404_16 小时前
MySQL表操作硬核解析:从 CREATE TABLE 到磁盘文件、ALTER TABLE 与 DDL 风险
运维·服务器·数据库·c++·mysql·adb·ai
wuminyu16 小时前
Java锁机制之park和unpark源码剖析
java·linux·c语言·jvm·c++
玖玥拾17 小时前
C/C++ 基础笔记(十一)类的进阶
c语言·c++·设计模式·