【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方法

  • 对内置类型排序 :默认为升序
cpp 复制代码
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>());	// 降序控制

  • 对自定义类型排序 :需要用户定义排序时的比较规则
cpp 复制代码
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());方法,但默认写法不支持排序,因为该类没有重载比较运算符
cpp 复制代码
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>无法满足需求

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

cpp 复制代码
// 仿函数
// 价格升序和降序
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;
	}
};
  • 第一次按照价格排序,第二次按照评价排序
cpp 复制代码
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());	// 评价升序

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

  • 仿函数类命名极差
  • 代码中无注释
cpp 复制代码
// 无注释且命名极差的仿函数
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表达式**,代替仿函数简化控制逻辑:
cpp 复制代码
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和返回类型可选:

cpp 复制代码
[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表达式简单使用举例

cpp 复制代码
// 最简单的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

cpp 复制代码
// 传值捕获
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表达式。
cpp 复制代码
// 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 可以被修改,但是值传递捕获,依然是外部变量的拷贝,因此没有完成交换

引用捕获

  • 使用引用捕获可以完成交换
cpp 复制代码
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;

传值和引用混合捕获

  • 无需列举变量,一键捕获所有外部变量
    • [=]() { }值捕获 当前局部域所有外部变量
    • [&]() { }引用捕获 当前局部域所有外部变量
cpp 复制代码
// 仅引用捕获, 捕获所有变量
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;

  • 值捕获与引用捕获混合

cpp 复制代码
  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;

值捕获与引用捕获的区别

cpp 复制代码
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关键字可以允许修改这些副本(注意:修改的是副本,不影响外部变量本身)。

    cpp 复制代码
    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表达式底层

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

cpp 复制代码
int main() 
{
	// 函数对象
	double rate = 0.49;
	Rate r1(rate);
	r1(10000, 2);
    
	// lambda
	auto 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算法的使用,无需预先定义函数或函数对象。

    cpp 复制代码
    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编程)中非常方便。

    cpp 复制代码
    // 模拟一个按钮点击回调
    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. 验证 移动构造 和 移动赋值 的生成条件

cpp 复制代码
#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主动要求编译器生成默认版本的成员函数

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

场景实例

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

核心价值:

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


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

有时候,你必须手写拷贝构造,是因为默认拷贝不安全(比如成员是指针),

但编译器生成的移动构造其实是安全的。

例如:

cpp 复制代码
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 且不实现,调用时才会报错(但报错在链接阶段,不直观):
cpp 复制代码
class NonCopyable
{
private:
    // 声明但不实现,外部调用会触发链接错误
    NonCopyable(const NonCopyable&);
};

  • C++11 新写法(推荐) :用 =delete 直接标记函数为 "删除",编译阶段即可报错更高效且语义清晰
cpp 复制代码
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 引入

示例

cpp 复制代码
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++"


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

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

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


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

相关推荐
在等晚安么1 小时前
力扣面试150题打卡
算法·leetcode·面试
小苏兮1 小时前
【把Linux“聊”明白】进程的概念与状态
linux·运维·服务器·学习
煤球王子2 小时前
学而时习之:C++中的枚举
c++
AI科技星2 小时前
宇宙膨胀速度的光速极限:基于张祥前统一场论的第一性原理推导与观测验证
数据结构·人工智能·经验分享·python·算法·计算机视觉
楼田莉子2 小时前
C++/Linux小项目:自主shell命令解释器
linux·服务器·开发语言·c++·后端·学习
杜子不疼.2 小时前
【Linux】网络编程入门:从一个小型回声服务器开始
linux·服务器·网络
EXtreme352 小时前
C语言指针深度剖析(2):从“数组名陷阱”到“二级指针操控”的进阶指南
c语言·开发语言·算法
luoganttcc2 小时前
介绍一下 机器人坐标转换的 RT 矩阵
算法
草莓火锅3 小时前
用c++求第n个质数
开发语言·c++·算法