当前位置: 首页 > news >正文

【C++11】Lambda表达式+新的类功能

@TOC

Lambda表达式+可变模板参数

github地址

有梦想的电信狗

💡 前言

C++98 中,为了实现简单的 排序、过滤或回调逻辑,我们往往需要编写额外的 仿函数类
这种方式虽然功能齐全,但显得冗长、啰嗦、缺乏直观性

C++11 起,语言迎来了质的飞跃 ——
Lambda 表达式 的引入让这一切变得简洁灵活:
你无需再定义命名类,就能在算法调用处直接编写逻辑,使代码更清晰、更贴近人类思维

与此同时,C++11 对 类机制 进行了深度扩展:
新增了 移动语义=default=delete 等关键字,使得资源转移更高效对象行为更安全类设计更现代化


📘 本文主要内容:

  • Lambda 表达式 的语法结构、捕获方式与底层机制
  • ⚙️ C++11 新类特性:默认函数的生成规则与语义设计
  • 🧩 =default=delete 的典型用法与实战场景

通过本文的学习,你将理解 C++11 如何在“性能”与“可维护性”之间取得完美平衡,并能编写出既高效又优雅的现代 C++ 代码。


一、Lambda表达式

1. 场景引入

C++98中,如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法

  • 对内置类型排序:默认为升序
int array[] = {4, 1, 8, 5, 3, 7, 0, 9, 2, 6};
// 默认按照小于比较,排出来结果是升序
std::sort(array, array + sizeof(array) / sizeof(array[0]));
// 如果需要降序,需要改变元素的比较规则,使用仿函数控制
std::sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>());	// 降序控制

  • 对自定义类型排序:需要用户定义排序时的比较规则
struct Goods 
{string _name; // 名字double _price; // 价格int _evaluate; // 评价Goods(const char* str, double price, int evaluate):_name(str), _price(price), _evaluate(evaluate){}
};
  • 有如上商品类,需要对多个商品类对象进行排序,我们可以使用std::sort(v.begin(), v.end());方法,但默认写法不支持排序,因为该类没有重载比较运算符
std::vector<Goods> v = { 	{ "苹果", 2.1, 5 }, { "香蕉", 3, 4 },{ "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };
// std::sort(v.begin(), v.end());	// 默认不支持排序,因为该类没有重载比较运算符

以上场景sort函数的使用要求该自定义类型实现比较运算符的重载:即operator<operator>函数,但存在如下问题:

  • operator<operator>函数在类中都只能实现一次,如果要对商品的多种不同属性排序,如第一次对价格排序,第二次对评价排序,第三次按照名字排序,该情况下operator<operator>无法满足需求

此时可以使用仿函数来控制排序逻辑:以下为用于比较的仿函数

// 仿函数
// 价格升序和降序
struct ComparePriceGreater {bool operator()(const Goods& gl, const Goods& gr) {return gl._price > gr._price;}
};
struct ComparePriceLess {bool operator()(const Goods& gl, const Goods& gr) {return gl._price < gr._price;}
};
// 评价升序和降序
struct CompareEvaluateGreater {bool operator()(const Goods& gl, const Goods& gr) {return gl._evaluate > gr._evaluate;}
};
struct CompareEvaluateLess {bool operator()(const Goods& gl, const Goods& gr) {return gl._evaluate < gr._evaluate;}
};
  • 第一次按照价格排序,第二次按照评价排序
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 },{ "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };// operator< 或 operator> 不能解决问题
// 针对以上问题,C++98中 只有仿函数能解决问题
std::sort(v.begin(), v.end(), ComparePriceGreater());	// 价格降序
std::sort(v.begin(), v.end(), ComparePriceLess());		// 价格升序
std::sort(v.begin(), v.end(), CompareEvaluateGreater());	// 评价降序
std::sort(v.begin(), v.end(), CompareEvaluateLess());	// 评价升序

在这里插入图片描述


但不可避免,别人会使用仿函数写出可读性极差的代码,给我们带来阅读困扰,如:

  • 仿函数类命名极差
  • 代码中无注释
// 无注释且命名极差的仿函数
struct Compare1 {bool operator()(const Goods& gl, const Goods& gr) {return gl._price > gr._price;}
};
struct Compare2 {bool operator()(const Goods& gl, const Goods& gr) {return gl._price < gr._price;}
};
struct Compare3 {bool operator()(const Goods& gl, const Goods& gr) {return gl._evaluate > gr._evaluate;}
};
struct Compare4{bool operator()(const Goods& gl, const Goods& gr) {return gl._evaluate < gr._evaluate;}
};
// 排序逻辑
std::sort(v.begin(), v.end(), Compare1());	// 这种写法可读性极差
std::sort(v.begin(), v.end(), Compare2());
std::sort(v.begin(), v.end(), Compare3());
std::sort(v.begin(), v.end(), Compare4());

总结

  • 万一别人写的仿函数命名为 Compare1Compare2等不规范的命名,如果再没有代码注释,可读性极差
  • 比较对象的比较条目多了之后,对多个条目排序需要写多个仿函数,一方面使代码略显臃肿,一方面增加阅读困难

综上

  • 随着C++语法的发展,对于控制比较逻辑来说,仿函数的写法显得太复杂笨重了!
  • 每次仅仅为了实现一个比较算法,都要重新去写一个类.
  • 如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名

这些都给编程者带来了极大的不便。

因此,在C++11语法中出现了**Lambda表达式**


  • 使用**Lambda表达式**,代替仿函数简化控制逻辑:
vector<Goods> v = {{ "苹果", 2.1, 5 }, { "香蕉", 3, 4 },{ "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };// 直接使用 Lambda 表达式解决比较问题
std::sort(v.begin(), v.end(), [](const Goods& gl, const Goods& gr)->bool {return gl._price > gr._price;});	// 价格降序std::sort(v.begin(), v.end(), [](const Goods& gl, const Goods& gr)->bool {return gl._price < gr._price;});		// 价格升序std::sort(v.begin(), v.end(), [](const Goods& gl, const Goods& gr)->bool {return gl._evaluate > gr._evaluate;});	// 评价降序std::sort(v.begin(), v.end(), [](const Goods& gl, const Goods& gr)->bool {return gl._evaluate < gr._evaluate;});	// 评价升序

Lambda表达式本质是一个==匿名函数对象==,接下来介绍Lambda表达式的语法


2. Lambda的语法

Lambda表达式的基本语法格式如下,包含四个部分,其中捕获列表函数体是必需的,参数列表、mutable和返回类型可选:

[capture-list] (parameter-list) mutable -> return-type { Function Body }
  • 捕获列表 (Capture List)
    • 总是出现在Lambda函数的开始位置,编译器根据==[]==判断代码是否为Lambda函数
    • 捕捉列表能够捕捉上下文中的变量供Lambda函数使用,一般捕获范围是父作用域
    • 捕获列表 (Capture List)定义了 Lambda 表达式如何访问其所在作用域中的外部变量

  • 参数列表 (Parameter List)
    • 类似于普通函数的参数列表,定义 lambda 接收的参数。如无需传参,可连同()一起省略

  • mutable默认情况下,按值捕获lambda函数是一个const成员函数,无法对参数进行修改mutable可以取消其常量性

    • 使用该修饰符时,参数列表不可省略(即使参数为空)

      默认情况下,lambdaoperator()const 成员函数,因此不能修改其值捕获的副本。

      • 不是“值捕获的变量不可修改”,而是“值捕获的副本不能在默认情况下修改”。
      • 如果加上 mutable,值捕获的副本就能修改;但无论如何,外部变量本身不会受副本影响。

  • 返回类型 (return-type):用追踪返回类型形式声明Lambda函数返回值的类型,通常可以省略。返回值可以省略的情况:

    • 返回值类型明确时:可省略,由编译器对返回类型进行推导。
    • 没有返回值时:可省略

  • 函数体 (Function Body)lambda要执行的代码逻辑。在该函数体内,可使用其参数和所有捕获到的变量

  • 注意:在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空

    • 因此C++11中最简单的lambda函数为:[]{}; 但该lambda函数不能做任何事情

Lambda表达式简单使用举例

// 最简单的lambda表达式,什么都不做,没有任何意义
[]{}; // 一个打印Hello World的无参lambda,通过函数调用操作符 () 立即执行
[](){ std::cout << "Hello, World!" << std::endl; }(); // 省略参数列表和返回值类型,返回值类型由编译器推导为int
int a = 3, b = 4;
[=]{ return a + 3; };	// 值捕获// 定义一个 lambda 对象,省略了返回值类型,无返回值类型
auto fun1 = [&](int c){ b = a + c; };	// 引用捕获, 形参为 c
fun1(10)	// 通过函数调用操作符() 传参后调用执行
cout << a <<" " << b << endl;//各部分都很完善的lambda函数
auto fun2 = [=, &b](int c)->int { return b += a + c; };	// b 变量引用捕获,其余变量值捕获
cout << fun2(10) << endl;// 将一个lambda赋值给auto变量接受,然后调用
auto greet = [](){ std::cout << "Hello, World!" << std::endl; };
greet();	// 调用输出  Hello, World!
  • 通过上述例子可以看出,Lambda表达式实际上可以理解为无名函数,该函数无法直接调用。如果想要直接调用,可借助auto将其赋值给一个函数对象,通过该对象调用

3. 捕获列表详解

捕获列表Lambda 表达式最独特的特性,它让Lambda能够访问外部变量

Lambda 捕获的范围

  • Lambda 只能捕获Lambda定义处可见的自动变量(automatic variables),也就是当前Lambda所处的局部作用域里的局部变量
  • 它不能直接捕获:
    • 全局变量 / 全局静态变量(因为它们在全局可访问,不需要捕获)。
    • 形参以外的其他函数内部的局部变量(作用域不在可见范围)。
  • Lambda 捕获方式灵活多样,下表总结了主要的捕获方式及其含义:
捕获方式含义
[]不捕获任何外部变量。
[&]引用方式捕获所有外部变量(小心悬挂引用)。
[=]值方式捕获所有外部变量(捕获的是副本,修改副本不影响原值)。默认情况下,lambdaoperator()const 成员函数,因此不能修改其值捕获的副本
[var]仅以值方式捕获变量 var
[&var]仅以引用方式捕获变量 var
[=, &var]默认以值方式捕获所有变量,但变量 var以引用方式捕获。
[&, var]默认以引用方式捕获所有变量,但变量 var以值方式捕获。
[this]捕获所在类的 this指针,从而可以访问类成员。

传值捕获与mutable

// 传值捕获
int a = 1, b = 3;
double rate = 2.5555;auto add1 = [rate](int x, int y) { return (x + y) * rate; };
cout << add1(a, b) << endl;

以上代码为传值捕获,将(x + y)的结果乘rate后返回


  • mutable的使用:以下是一个用于交换Lambda表达式。
// mutable的用法
int c = 8, d = 9;
cout << c << " " << d << endl;
auto swap1 = [c, d]() mutable ->void {int tmp = c;c = d;d = tmp;};
cout << c << " " << d << endl;

此处为值捕获lambda函数捕获列表中的形参c、d是外部c,d的副本传值捕获默认无法修改形参,因此默认无法完成交换

  • 这里形参列表后使用了mutable修饰:默认情况下,按值捕获lambda函数const函数,无法对参数进行修改mutable可以取消其常量性
  • mutable让捕获到的 cd 可以被修改,但是值传递捕获,依然是外部变量的拷贝,因此没有完成交换

在这里插入图片描述


引用捕获

  • 使用引用捕获可以完成交换
int c = 8, d = 9;
cout << c << " " << d << endl;
// 引用捕获
auto swap2 = [&c, &d]()->void {auto tmp = c;c = d;d = tmp;};
swap2();	// 调用Lambda表达式
cout << c << " " << d << endl;

在这里插入图片描述


传值和引用混合捕获

  • 无需列举变量,一键捕获所有外部变量
    • [=]() { }值捕获当前局部域所有外部变量
    • [&]() { }引用捕获当前局部域所有外部变量
// 仅引用捕获, 捕获所有变量
int a = 1, b = 2, c = 3, d = 4;cout << a << " " << b << " " << c << " " << d << endl;
// auto func1 = [=]() {	// 一键捕获所有外部变量  值捕获
auto func1 = [&]() {	// 一键捕获所有外部变量  引用捕获++a;++b;++c;++d;};
func1();
cout << a << " " << b << " " << c << " " << d << endl;

在这里插入图片描述


  • 值捕获与引用捕获混合

  •   int a = 1, b = 2, c = 3, d = 4;const int e = 6;cout << a << " " << b << " " << c << " " << d << e << endl;// 引用捕获 父作用域所有变量,值捕获 c 变量cout << &e << endl;auto func1 = [&, c]() {++a;++b;//++c;	// 值捕获的 c 不可修改++d;cout << &e << endl;cout << typeid(e).name() << endl;};func1();cout << a << " " << b << " " << c << " " << d << e << endl;
    

    在这里插入图片描述


值捕获与引用捕获的区别

int x = 10;
auto value_lambda = [x] { std::cout << x << std::endl; }; // 值捕获,捕获的是x的副本(此时为10)
auto ref_lambda = [&x] { std::cout << x << std::endl; };  // 引用捕获,捕获的是x的引用x = 20; // 修改外部x的值value_lambda(); // 输出:10,因为值捕获的是副本,不受外部x修改的影响
ref_lambda();  // 输出:20,因为引用捕获的是引用,会反映外部x的变化
  • mutable关键字:默认情况下,以值方式捕获的变量在lambda函数体内是只读的const)。使用 mutable关键字可以允许修改这些副本(注意:修改的是副本,不影响外部变量本身)。

    int x = 10;
    auto lambda = [x]() mutable { x += 1; // 没有mutable则编译错误;有mutable则允许修改值捕获的副本std::cout << "Inside: " << x << std::endl;
    };lambda(); // 输出:Inside: 11
    std::cout << "Outside: " << x << std::endl; // 输出:Outside: 10 (外部x未改变)
    

4. Lambda表达式与仿函数

Lambda表达式之间不能相互赋值

Lambda表达式对象之间不能相互赋值,即使看起来类型相同。

在这里插入图片描述
在这里插入图片描述


Lambda表达式对象之间不能相互赋值的本质原因是底层类型不同

  • <lambda_1><lambda_2>类型不同。Lambda的底层是编译器生成了重载了()的类,本质是生成了仿函数对象,因此类型不同,不能赋值

仿函数和Lambda表达式底层

代码样例
class Rate {
public:Rate(double rate) : _rate(rate){}double operator()(double money, int year){return money * _rate * year;}
private:double _rate;
};

int main() 
{// 函数对象double rate = 0.49;Rate r1(rate);r1(10000, 2);// lambdaauto r2 = [=](double monty, int year)->double { return monty * rate * year; };r2(10000, 2);
}

main函数调用

  • Lambda仿函数都执行了相同的逻辑:
    • 执行的操作都是money * _rate * year

底层汇编

我们来查看以上仿函数调用Lambda表达式调用底层汇编

  • 仿函数

    • 在这里插入图片描述
  • Lambda表达式

    • 在这里插入图片描述

  • 可以看到,仿函数和Lambda底层都是调用了operator()

  • 因此:

    • Lambda编译后就是一个仿函数
    • 捕捉列表的本质就是在进行函数传参

5. 注意事项总结

  1. 父作用域指包含 lambda 函数的语句块
  2. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。如:
    • [=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量
    • [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
  3. 捕捉列表不允许变量重复传递,否则会导致编译错误。如:
    • [=, a]:编译报错,=已经以值传递方式捕捉了所有变量,再值捕获a重复
  4. 在块作用域以外的Lambda函数捕捉列表必须为空。
  5. 块作用域中lambda 函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
  6. lambda表达式之间不能相互赋值

C++Lambda 表达式**值捕获默认不能修改的设计哲学**:

C++ 设计者希望 lambda 默认是“无副作用”的可重入函数对象。
因此值捕获的变量默认是 const,要修改必须显式 mutable,以示“我知道我在改状态”。


6. Lambda表达式的常见应用场景

  1. STL算法中的谓词:Lambda极大地简化了STL算法的使用,无需预先定义函数或函数对象。

    std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};// 使用lambda查找第一个偶数
    auto it = std::find_if(numbers.begin(), numbers.end(), [](int n) { return n % 2 == 0; });// 使用lambda对所有元素进行平方操作
    std::transform(numbers.begin(), numbers.end(), numbers.begin(), [](int n) { return n * n; });// 使用lambda计数大于5的元素
    int count = std::count_if(numbers.begin(), numbers.end(), [](int n) { return n > 5; });// 使用lambda降序排序
    std::sort(numbers.begin(), numbers.end(), [](int a, int b) { return a > b; });
    
  2. 作为回调函数:在异步操作、事件处理(如GUI编程)中非常方便。

    // 模拟一个按钮点击回调
    button.onClick([]() { std::cout << "Button clicked!" << std::endl; }
    );// 在线程中执行任务
    std::thread t([]{std::cout << "Running in a thread." << std::endl;
    });
    t.join();
    

二、新的类功能

1. 新增的默认成员函数

C++98 的类中,包含6个默认成员函数。默认成员函数就是我们不显式写,编译器会默认生成的成员函数

在这里插入图片描述

到了 C++11 标准,默认成员函数新增 移动构造函数移动赋值运算符重载 ,进一步丰富了类的资源转移能力。


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

生成规则

如果你没有自己手动实现移动构造函数,且没有手动实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动构造。

默认移动构造函数完成什么工作

默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。


总结如下

我们不写,编译器自动生成默认移动构造函数,需同时满足两个条件

  1. 类中 没有手动实现移动构造函数
  2. 类中 没有手动实现析构函数、拷贝构造函数、拷贝赋值重载函数的任意一个(这三者只要有一个手动实现,就会抑制默认移动构造的生成)

默认移动构造函数的行为

  • 内置类型成员(如:intdouble指针等):直接按字节拷贝,本质是浅拷贝
  • 自定义类型成员检查该成员是否实现了移动构造
    • 若实现,则调用其移动构造
    • 若未实现,则调用其拷贝构造

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

生成规则

如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。

默认赋值运算符重载完成什么工作

默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)


总结如下

我们不写,编译器自动生成默认移动赋值移动赋值函数,需同时满足两个条件

  1. 类中 没有手动实现移动赋值重载函数
  2. 类中 没有手动实现析构函数、拷贝构造函数、拷贝赋值重载函数的任意一个(任一手动实现,都会抑制默认移动赋值的生成)

默认移动赋值运算符重载的行为

  • 内置类型成员(如:intdouble指针等):直接按字节拷贝,本质是浅拷贝
  • 自定义类型成员检查该成员是否实现了移动赋值
    • 若实现,则调用其移动赋值
    • 若未实现,则调用其拷贝赋值

注:整体逻辑与默认移动构造高度相似,只是操作对象从“构造新对象”变为“赋值给已有对象"


3. 移动语义与拷贝语义的互斥性

PS:“如果你手动提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。”

若手动提供了 移动构造函数移动赋值重载函数说明当前类是深拷贝的类,默认的拷贝构造函数和拷贝赋值重载函数无法实现深拷贝,因此 不会再自动生成默认的拷贝构造函数和拷贝赋值重载函数
(反之,若手动实现了拷贝语义相关函数,默认移动语义函数也不会自动生成,需开发者按需抉择 。)


4. 验证 移动构造 和 移动赋值 的生成条件

#include <iostream>
#include <utility>  // 用于std::move
using namespace std;namespace mySpace
{class string{private:const char* _str;public:string(const char* str = ""): _str(str){cout << "mySpace::string 构造函数" << endl;}string(const string& other): _str(other._str)  // 浅拷贝指针{cout << "mySpace::string 拷贝构造" << endl;}string(string&& other) noexcept: _str(move(other._str)){other._str = nullptr;  // 避免悬挂指针cout << "mySpace::string 移动构造" << endl;}string& operator=(const string& other){if (this != &other){_str = other._str;cout << "mySpace::string 拷贝赋值" << endl;}return *this;}string& operator=(string&& other) noexcept{if (this != &other){_str = move(other._str);other._str = nullptr;  // 避免悬挂指针cout << "mySpace::string 移动赋值" << endl;}return *this;}~string(){cout << "mySpace::string 析构函数" << endl;}};
}class Person
{
private:mySpace::string _name;  // 字符串类型的姓名int _age;               // 整型的年龄public:Person(const char* name = "", int age = 0): _name(name)  // 调用mySpace::string的构造函数, _age(age)    // 直接初始化int成员{cout << "Person 构造函数" << endl;}//注意:根据规则,手动实现拷贝构造会抑制默认移动构造的生成Person(const Person& p): _name(p._name)  // 调用mySpace::string的拷贝构造, _age(p._age)    // 拷贝int成员{cout << "Person 拷贝构造" << endl;}//注意:根据规则,手动实现拷贝赋值会抑制默认移动赋值的生成Person& operator=(const Person& p){if (this != &p)  // 防止自赋值{_name = p._name;  // 调用mySpace::string的拷贝赋值_age = p._age;    // 赋值int成员cout << "Person 拷贝赋值" << endl;}return *this;}// 注意:根据规则,手动实现析构函数会抑制默认移动构造和移动赋值的生成~Person(){cout << "Person 析构函数" << endl;}/*  注意:*       1. 由于我们手动实现了“拷贝构造、拷贝赋值和析构函数”*       2. 根据C++规则,编译器不会自动生成默认的“移动构造”和“移动赋值”*       3. 如果需要移动语义,必须手动实现*   所以:如果你想看到默认生成的“移动构造”和“移动赋值”,可以将person类中手动实现的“拷贝构造、拷贝赋值和析构函数”注释掉*/
};int main()
{/*--------------------测试:默认构造函数--------------------*/cout << "Person s1;" << endl;Person s1;  // 调用Person的构造函数,使用默认参数/*--------------------测试:拷贝构造--------------------*/cout << "\nPerson s2 = s1;" << endl;Person s2 = s1;  // 调用Person的拷贝构造函数/*--------------------测试:移动构造--------------------*/cout << "\nPerson s3 = move(s1);" << endl;Person s3 = move(s1);/* 说明:*      1. 由于Person类手动实现了拷贝构造和析构函数*      2. 编译器不会生成默认移动构造,这里实际会调用拷贝构造*//*--------------------测试:拷贝赋值--------------------*/cout << "\ns2 = s1;" << endl;s2 = s1;  // 调用Person的拷贝赋值运算符/*--------------------测试:移动赋值--------------------*/cout << "\ns3 = move(s1);" << endl;s3 = move(s1);/* 说明:*      1. 由于Person类手动实现了拷贝赋值和析构函数*      2. 编译器不会生成默认移动赋值,这里实际会调用拷贝赋值*/cout << "\n程序结束,对象开始析构" << endl;return 0;
}

在这里插入图片描述


5. 一个类何时需要手动实现移动构造?

  • 类内的成员类型 vs 是否需要手动移动语义
成员变量类型特性默认移动语义是否可用是否建议手动实现
仅内置类型按值拷贝即可✅ 是❌ 不需要
仅 STL 容器 / 智能指针自带移动构造/赋值✅ 是❌ 不需要
含裸指针(堆内存)无法安全移动❌ 否必须手动实现
含文件句柄/Socket等系统资源独占资源❌ 否必须手动实现
含自定义类型,且该类型未定义移动语义会退化为拷贝⚠️ 不推荐建议手动实现
含自定义类型,且该类型已定义移动语义可完美转移✅ 是❌ 不需要

2. 类的成员变量缺省值

  • 成员变量声明时设置缺省值,作用是供构造函数的初始化列表使用。

  • 若构造函数未在初始化列表里显式对该成员初始化,初始化列表就会用这个缺省值来初始化成员。


3. 强制生成默认函数的关键字default

用 =default 显式生成默认函数

=default主动要求编译器生成默认版本的成员函数

  • 解决因手动实现其他函数导致默认函数被抑制的问题

场景实例

  • 若手动实现了拷贝构造函数,编译器会默认抑制移动构造函数的生成。
  • 此时若仍需要编译器生成默认移动构造,可显式声明:
class MyClass
{
public://1.手动实现拷贝构造,抑制了默认移动构造MyClass(const MyClass& other) { /* 自定义逻辑 */ }//2.用 =default 显式要求编译器生成默认移动构造MyClass(MyClass&& other) = default;
};

核心价值:
让开发者在 “需要自定义部分函数(如:拷贝构造)” 的同时,保留编译器自动生成的其他默认函数(如:移动构造),避免手动实现所有函数的冗余。


典型场景:拷贝语义需要自定义,但移动语义仍然安全

有时候,你必须手写拷贝构造,是因为默认拷贝不安全(比如成员是指针),
但编译器生成的移动构造其实是安全的。

例如:

class Buffer 
{std::unique_ptr<char[]> _data;size_t _size;public:// 自定义拷贝构造(unique_ptr不能拷贝)Buffer(const Buffer& other): _data(new char[other._size]), _size(other._size){memcpy(_data.get(), other._data.get(), _size);}// 👇 让编译器默认生成移动构造Buffer(Buffer&&) = default;Buffer& operator=(Buffer&&) = default;
};

✅为什么这样写是合理的?

  • 确实需要手写拷贝逻辑(深拷贝 unique_ptr 所指内容,但 unique_ptr 本身不支持拷贝);
  • unique_ptr移动语义 是完全安全的;
  • 所以 = default 让编译器自动生成高效的移动版本。

📦 结论:

“我手写拷贝,是为了深拷;
我 default 移动,是为了性能。”


4. 禁止生成默认函数的关键字delete

用 =delete 显式删除默认函数

=delete主动禁止编译器生成默认版本的成员函数

  • 替代 C++98 中 “将函数声明为 private 且不实现” 的蹩脚写法

如果要禁止一个类的拷贝构造C++98C++11 有不同风格的做法:

  • C++98 旧写法(不推荐):若要禁止拷贝构造,需将其声明为 private 且不实现,调用时才会报错(但报错在链接阶段,不直观):
class NonCopyable
{
private:// 声明但不实现,外部调用会触发链接错误NonCopyable(const NonCopyable&);
};

  • C++11 新写法(推荐):用 =delete 直接标记函数为 “删除”,编译阶段即可报错更高效且语义清晰
class NonCopyable
{
public:// 用 =delete 显式禁止编译器生成拷贝构造NonCopyable(const NonCopyable&) = delete;
};int main()
{NonCopyable a;// 编译报错:调用了被删除的函数(拷贝构造)NonCopyable b = a;
}

典型应用场景:

  • 禁止对象拷贝std::unique_ptr 需独占资源)
  • 禁止某些无意义的默认函数:禁止基本类型的隐式转换构造)

5. =default 和 =delete 对比

对比项=default=delete
功能显式要求编译器 生成默认版本 的函数显式要求编译器 禁用(删除) 该函数
主要目的保留编译器自动生成的行为,防止被其他定义抑制禁止某些函数被使用(例如拷贝、赋值、隐式转换等)
典型使用场景当类已定义了其他特殊成员函数(例如析构或拷贝构造)导致移动构造或默认构造被抑制时,希望仍然自动生成默认版本防止误用(例如禁止拷贝、禁止隐式类型转换、禁止特定构造方式)
使用语法ClassName(const ClassName&) = default;ClassName(const ClassName&) = delete;
作用时间编译期,指示编译器自动生成函数体编译期,指示编译器删除函数定义(不能被调用)
是否可以应用于所有成员函数✅ 可以应用于构造函数、析构函数、拷贝/移动构造、赋值运算符等默认生成的函数✅ 可以用于任何函数(包括普通函数与模板),不仅限于默认成员函数
是否影响函数重载解析❌ 不影响重载选择(函数依然存在)✅ 被删除的函数仍参与重载解析,但最终无法调用(会导致编译错误)
常见用途举例强制生成移动构造函数:MyClass(MyClass&&) = default;禁止拷贝构造函数:MyClass(const MyClass&) = delete;
与手动定义的区别告诉编译器“你帮我生成”告诉编译器“别生成也别用”
语义意图✅ “我希望保留默认行为”❌ “我希望彻底禁止此行为”
出现版本C++11 引入C++11 引入

示例

class Example 
{
public:Example() = default;              // 显式要求生成默认构造函数Example(const Example&) = delete; // 禁止拷贝Example(Example&&) = default;     // 强制生成移动构造Example& operator=(Example&&) = delete; // 禁止移动赋值
};

=default:想让编译器帮你生成。
=delete:不想让编译器帮你生成,也不想别人用。


🚀 结语

C++11 的到来,让 C++ 从传统的“面向对象”语言,正式迈入了“现代化与高抽象化”的时代。

  • Lambda 表达式 —— 让算法逻辑更简洁自然
  • 移动语义 —— 让资源转移更高效安全
  • =default / =delete —— 让类行为更可控、更清晰

掌握这些特性,不仅仅是语法层面的提升,更意味着你已经踏入了现代 C++ 思维方式的门槛。
当你能够熟练地在项目中使用 Lambda 与移动语义 时,也就真正理解了什么是——

💬 “高效与优雅并存的现代 C++”


以上就是本文的所有内容了,如果觉得文章对你有帮助,欢迎 点赞⭐收藏 支持!如有疑问或建议,请在评论区留言交流,我们一起进步

分享到此结束啦
一键三连,好运连连!

你的每一次互动,都是对作者最大的鼓励!


征程尚未结束,让我们在广阔的世界里继续前行! 🚀

http://www.dtcms.com/a/601439.html

相关文章:

  • C语言编译工具 | 探讨常用C语言编译工具的选择与使用
  • SCT2A26——5.5V-100V Vin,4A峰值电流限制,高效率非同步降压DCDC转换器,兼容替代LM5012
  • 手机网站搜索框代码网上做网站怎么防止被骗
  • 滑动窗口(同向双指针)
  • C语言嵌入式编程实战指南(四):进阶技术和未来展望
  • Mac上的C语言编译软件推荐与使用指南 | 如何选择适合你需求的C语言编译器
  • 做建站较好的网站wordpress edit.php
  • 【大语言模型】-- Function Calling函数调用
  • STM32项目分享:花房环境监测系统
  • 第1章 认识Qt
  • JDK 25 重大兼容性 Bug
  • MyBatis多表联查返回List仅一条数据?主键冲突BUG排查与解决
  • c 做网站方便吗手机企业wap网站
  • el-table有固定列时样式bug
  • Vue项目中 安装及使用Sass(scss)
  • 珠海本地网站设计公司什么网站可以发布信息
  • UEFI+GPT平台一键安装Windows方法
  • GPT‑5 全面解析与开发者接入指南
  • 站优云seo优化页面模板这样选
  • dism++功能实操备份与还原
  • 动态型网站建设哪里便宜app开发需要用到哪些工具
  • 网站建设的什么是网站建设的第一阶段佛山市房产信息网
  • React 18
  • CVPR 2025|电子科大提出渐进聚焦Transformer:显著降低超分辨率计算开销
  • CTFHub Web进阶-Linux:动态装载
  • Nginx域名与SSL证书配置完整流程
  • 美食网站要怎么做自己做相册的网站
  • 全国 网站备案 数量电子设计工程官网
  • 一、UDP以太网帧格式
  • 网络协议设计原则简介和资料推荐