【C++:C++11】C++11新特性深度解析:从类新功能、Lambda表达式到包装器实战

🔥小叶-duck个人主页

❄️个人专栏《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》
《算法题讲解指南》--优选算法
《算法题讲解指南》--递归、搜索与回溯算法
《算法题讲解指南》--动态规划算法

未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游


目录

前言

一.新的类功能:更精细的默认函数与初始化控制

1、新增默认成员函数:默认移动构造和移动赋值

2、成员函数声明时要给缺省值

[3、 defult 和 delete:控制默认函数生成](#3、 defult 和 delete:控制默认函数生成)

[3.1 概念](#3.1 概念)

[3.2 示例演示](#3.2 示例演示)

[4、final 和 override](#4、final 和 override)

[二. STL中的一些变化](#二. STL中的一些变化)

1、新的容器

2、新的接口

3、范围for

[三. lambda 表达式:简洁的匿名函数对象](#三. lambda 表达式:简洁的匿名函数对象)

[1、 lambda 核心语法](#1、 lambda 核心语法)

[1.1 示例演示:](#1.1 示例演示:)

2、捕捉列表:灵活复用外层变量

[2.1 实际示例:](#2.1 实际示例:)

[3、lambda 的应用场景:替代仿函数与函数指针](#3、lambda 的应用场景:替代仿函数与函数指针)

[4、lambda 的原理:底层是仿函数](#4、lambda 的原理:底层是仿函数)

[4.1 lambda 底层原理](#4.1 lambda 底层原理)

[4.2 实际示例:](#4.2 实际示例:)

[四. 包装器:统一可调用对象的类型](#四. 包装器:统一可调用对象的类型)

[1、 std::function:可调用对象的 "容器"](#1、 std::function:可调用对象的 “容器”)

[1.1 核心用法:](#1.1 核心用法:)

[1.1.1 函数指针、仿函数、lambda 的 function 类型统一](#1.1.1 函数指针、仿函数、lambda 的 function 类型统一)

[1.1.2 成员函数的 function 类型统一](#1.1.2 成员函数的 function 类型统一)

[1.2 重写 逆波兰表达式求值](#1.2 重写 逆波兰表达式求值)

题目链接:

题目描述:

C++算法代码:

代码分析:

[2、std::bind:可调用对象的 "适配器"](#2、std::bind:可调用对象的 “适配器”)

[2.1 核心用法:](#2.1 核心用法:)

[2.2 实战场景:银行复利产品](#2.2 实战场景:银行复利产品)

文章小结

结束语


前言

C++11 不仅优化了泛型编程和性能(如右值引用移动语义引用折叠可变参数模板等 ),还对类机制、可调用对象、资源管理进行了重大改变。从类的默认函数控制lambda 匿名函数 ,到 function/bind 包装器 ,再到智能指针,这些特性彻底改变了 C++ 的编程范式,让代码更简洁、安全、灵活。

一.新的类功能:更精细的默认函数与初始化控制

C++11 扩展了类的核心能力,允许开发者更灵活地控制默认成员函数、初始化方式,解决了传统 C++ 中类设计的诸多痛点。

1、新增默认成员函数:默认移动构造和移动赋值

原来C++类中,有6个默认成员函数:构造函数 / 析构函数 / 拷贝构造函数 / 拷贝赋值重载 /取地址重载 / const取地址重载 ,最后重要的是前4个,后两个用处不大(介绍类和对象时也没怎么提),默认成员函数 就是我们不写编译器会生成一个默认 的。

C++11新增了两个默认成员函数:移动构造函数移动赋值运算符重载 ,是专门用于**"窃取" 右值对象的资源** ,提升效率

核心规则

  • 如果没有自己实现移动构造函数 / 移动赋值重载函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个(需要注意是三者都没有实现)。那么编译器会自动生成一个默认移动构造 / 默认移动赋值。默认生成的移动构造函数 / 移动赋值重载函数。
  • 默认移动构造 / 移动赋值:对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造 / 移动赋值,如果实现了就调用移动构造 / 移动赋值,没有实现就调用拷贝构造 / 拷贝赋值
  • 手动提供移动构造 / 赋值 ,编译器不再自动生成拷贝构造 / 赋值

实际示例

cpp 复制代码
#include<iostream>
#include<string.h>
using namespace std;

//===========================移动构造与移动赋值===========================
namespace xiaoye
{
	struct string
	{
		void swap(string& s)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}

		string(const char* str = "")
			:_size(strlen(str))
			,_capacity(_size)
		{
			cout << "string(char* str)-构造" << endl;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		// 拷贝构造
		string(const string& s)
		{
			cout << "string(const string& s) -- 拷贝构造" << endl;

			reserve(s._capacity); //拷贝构造需要额外再开辟空间进行拷贝数据
			for(int i = 0; i < s._size; i++)
			{
				push_back(i);
			}
		}

		//拷贝赋值
		string& operator=(const string& s)
		{
			cout << "string& operator=(const string& s) -- 拷贝赋值" << endl;
			if (this != &s)
			{
				_str[0] = '\n';
				_size = 0;
				reserve(s._capacity); //拷贝赋值需要额外再开辟空间进行拷贝赋值数据
				for (int i = 0; i < s._size; i++)
				{
					push_back(i);
				}
			}
			return *this;
		}

		void reserve(size_t new_capacity)
		{
			if (new_capacity > _capacity)
			{
				char* tmp = new char[new_capacity + 1];
				if (_str)//_str不为空才能进行delete,否则会报错
				{
					strcpy(tmp, _str);
					delete[]_str;

				}
				_str = tmp;
				_capacity = new_capacity;
			}
		}

		// 移动构造:窃取右值资源
		string(string&& s) 
		{
			cout << "string(string&& s) -- 移动构造" << endl;
			swap(s); // 交换当前对象与右值对象的资源
		}

		// 移动赋值:窃取右值资源
		string& operator=(string&& s)
		{
			cout << "string& operator=(string&& s) -- 移动赋值" << endl;
			swap(s); // 交换当前对象与右值对象的资源
			return *this;
		}

		void push_back(char ch)
		{
			if (_size >= _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}

			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}

		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0;
	};
}

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:
	xiaoye::string _name;
	int _age;
};

int main()
{
	Person s1;
	Person s2 = s1;
	Person s3 = std::move(s1);
	Person s4;
	s4 = std::move(s2);
	return 0;
}

我们会发现Person只实现了构造,所以编译器会自动生成Person的默认移动构造和移动赋值,Person的 _name 成员变量也就调用了string的移动构造和移动赋值。

而如果我们实现了析构,就会导致编译器无法自动生成Person的默认移动构造和移动赋值 ,而我们没有实现移动构造和移动赋值,则编译器就会调用Person的拷贝构造和拷贝赋值,但是这两个我们也没有实现,所以编译器就会自动生成Person的默认拷贝构造和拷贝赋值。

那么初始化列表初始化_name时就会调用string的拷贝构造和拷贝赋值了:

2、成员函数声明时要给缺省值

成员变量声明时给的缺省值(类内成员初始化)会在构造函数的初始化阶段使用。具体来说:

如果某个成员变量没有在初始化列表中显式初始化,声明的地方有缺省值则用缺省值进行初始化编译器会自动在初始化列表中使用这个缺省值来初始化它;如果该成员在初始化列表中被显式初始化了,那么显式初始化的值会覆盖声明时的缺省值,这个我们在类和对象部分介绍过了。如果印象不深的可以回顾一下:

吃透C++类和对象(下):初始化列表深度解析

下面是对缺省值的一个小总结,不想看具体内容可以看看下面的总结复习一下:

3、 defult 和 delete:控制默认函数生成

3.1 概念

C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造 ,就不会生成移动构造 了,那么我们可以使用default 关键字显示指定移动构造生成

如果能想要限制某些默认函数的生成 ,在C++98 中,是该函数设置成private(私有) ,并且 只声明不实现,这样只要其他人想要调用就会报错。

在C++11中更简单,只需在该函数声明加上【 = delete】 即可,该语法指示编译器不生成对应函数的默认版本 ,称【= delete】修饰的函数为删除函数

  • default:强制编译器生成默认函数(如手动实现拷贝构造后,仍想保留默认移动构造);
  • delete:禁止编译器生成默认函数(如禁止拷贝构造,避免对象拷贝)。

3.2 示例演示

4、final 和 override

final 和 override 这两个关键字其实在前面的继承和多态两个章节中已经进行了详细的讲解,感兴趣的可以回顾一下前面的文章:

C++多态入门(上):从概念本质、语法规则到虚函数重写,结合实战代码的全方位指南
C++ 继承入门(上):从基础概念定义到默认成员函数,吃透类复用的核心逻辑

我也将 final 和 override 两个关键字的简单使用场景展示成表格方便大家复习:

关键字 作用对象 主要目的 简单使用场景举例
final 禁止该类被继承。 class Base final { }; class Derived : public Base { };// 错误:无法继承 final 类
final 虚函数 禁止该虚函数在派生类中被重写。 virtual void func() final { }; void func() override { } // 错误:无法重写 final 函数
override 虚函数 显式声明意图重写基类的虚函数,让编译器检查签名是否正确。 virtual void baseFunc(int); void baseFunc(int) override; // 正确 void baseFunc(float) override; // 错误:函数签名不匹配 或者void basefunc(int) override;// 错误:函数签名不匹配

核心思想总结:

  • final:用于"禁止"进一步扩展(继承或重写)。
  • override:用于"明确"并"验证"重写行为,防止因笔误导致的错误,提高代码安全性。

二. STL中的一些变化

1、新的容器

  • 下图中圈起来的就是STL中新的容器,但是实际最有用的是unordered_mapunordered_set。这两个容器我们前面已经进行了非常详细的讲解,并且利用哈希桶进行了模拟实现。感兴趣的可以回顾一下前面的文章。其他的大家了解一下即可。

【C++:unordered_set和unordered_map】 深度解析:使用、差异、性能与场景选择

2、新的接口

  • STL中容器的新接口也不少,最重要的就是右值引用和移动语义 相关的push/insert/emplace 系列接口移动构造与移动赋值 ,还有initializer_list 版本的构造等,这些前面都有讲过,还有一些无关痛痒的如 cbegin/cend 等需要时查查文档即可。

3、范围for

  • 容器的范围 for 遍历,这个在第一个容器string部分中也进行了详细的讲解,感兴趣的可以回顾一下。

C++ string 类使用超全攻略(上):创建、遍历及容量操作深度解析

三. lambda 表达式:简洁的匿名函数对象

lambda 表达式 本质是 "匿名仿函数",可在函数内部定义,无需单独声明类,极大简化可调用对象的定义。

1、 lambda 核心语法

lambda 表达式语言使用层 而言没有类型 ,所以我们一般是用auto 或者 模版参数定义的对象去接受 lambda 对象。

格式

cpp 复制代码
[capture-list] (parameters) -> return-type { function-body }
  • capture-list \]:**捕捉列表(不可省)** ,捕捉外层作用域变量供函数体使用;捕捉列表总是出现在 lambda 函数的开始位置,编译器根据 \[ \] 来判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下文的变量供函数使用,捕捉列表可以**传值** 或**传引用捕捉** ,具体的后面还会再讲。**捕捉列表为空也不能省略**。

  • -> return-type:返回值类型(可省) ,用追踪返回类型形式声明函数的返回值类型,没有返回值的时候这部分可以直接省略。一般返回值类型明确的情况下,也可以省略,编译器可自动推导(但还是建议写一下的);另外这种形式普通函数也可以用,但是用的比较少。
  • { function-body }:函数体(不可省) ,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使用其参数外,还可以使用所以捕获到的变量,函数体就算为空也不可以省略

1.1 示例演示

cpp 复制代码
//===========================lambda表达式语法===========================
int main()
{
	//简单的lamba表达式
	//无捕捉、无参数、返回值自动推导:
	auto func1 = [] {cout << "Hello world" << endl; };
	func1(); // 输出:Hello world
	// 有参数、有返回值:
	auto add = [](int x, int y)->int {return x + y; };
	cout << add(1, 2) << endl; // 输出:3

	// 关于那些东西可以省略,哪些不可以
	// 1. 就算捕捉为空也是不可以省略的
	// 2. 参数为空可以直接省,()都不用了
	// 3. 返回值可以省略,可以通过返回对象自动推导
	// 4. 函数体不可以省略

	//对于上面这种函数体非常短的可以都写在一行,而对于函数体比较长的lambda表达式就可以像函数那样进行书写
	int a = 0, b = 1;
	auto swap1 = [](int& x, int& y)
	{
		int temp = x;
		x = y;
		y = temp;
	}; //但写成函数的样子时,一定要注意虽然写法像函数,但lambda表达式本质还是一个匿名函数对象,结尾一定要加分号
	swap1(a, b);
	cout << a << ":" << b << endl;
	return 0;
}

2、捕捉列表:灵活复用外层变量

捕捉列表控制外层变量的访问方式,支持值捕捉、引用捕捉、混合捕捉,核心规则如下:

  • lambda 表达式中默认 只能用 lambda 函数体参数中的变量 ,如果想用外层作用域中的变量 就需要进行捕捉
  • 第一种捕捉方式:是在捕捉列表中显示传值捕捉传引用捕捉 ,捕捉的多个变量用逗号分割。[x,y,&z] 表示x 和 y 是值捕捉z 是引用捕捉
  • 第二种捕捉方式:是在捕捉列表中隐式捕捉 ,我们在捕捉列表写一个**=表示隐式值捕捉**,在捕捉列表写一个 &表示隐式引用捕捉,这样我们 lambda 表达式中用了那些变量,编译器就会自动捕捉那些变量
  • 第三种捕捉方式:是在捕捉列表中混合使用 隐式捕捉和显示捕捉。[ =,&X ] 表示其他变量隐式值捕捉x 引用捕捉[ &,x,y ] 表示其他变量引用捕捉x 和 y 值捕捉 。当使用混合捕捉时,第一个元素必须是& 或 =,并且 & 混合捕捉时,后面的捕捉变量必须是值捕捉,同理 = 混合捕捉时,后面的捕捉变量必须是引用捕捉。
  • lambda 表达式如果在函数局部域中,他可以捕捉 lambda 位置之前定义的变量不能捕捉静态局部变量和全局变量(也就是说 [ ] 内部不能填静态局部变量和全局变量否则会报错)静态局部变量全局变量不需要捕捉 , lambda 表达式中可以直接使用 。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空
  • 默认情况下, lambda 捕捉列表被 const 修饰 的,也就是说传值捕捉的过来的对象不能修改mutable 加在参数列表的后面可以取消其常量性 ,也就说使用该修饰符后,传值捕捉的对象就可以修改 了,但是修改还是形参对象不会影响实参。使用该修饰符后,参数列表不可省略(即使参数为空)。

用一个表格进行总结:

捕捉方式 含义 示例
[ var ] 值捕捉 var(拷贝,默认 const,不可修改) [ a ] { return a * 2; }
[ &var ] 引用捕捉 var(可修改外层变量) [ &a ] { a++; }
[ = ] 隐式值捕捉所有使用的外层变量 [ = ] { return a + b; }
[ & ] 隐式引用捕捉所有使用的外层变量 [ & ] { a++; b++; }
[ =, &var ] 隐式值捕捉 + 显式引用捕捉 var [ =, &a ] { a++; return b; }
[ &, var ] 隐式引用捕捉 + 显式值捕捉 var [ &, a ] { b++; return a; }
[ this ] 捕捉当前类的 this 指针(类成员函数中使用) [ this ] { _a1++; }

关键注意

  • 值捕捉的变量默认被const修饰,需修改时加 mutable**(仅修改拷贝,不影响外层变量)**;
  • 全局变量、静态变量无需捕捉,可直接使用;
  • 捕捉列表不能为空**(无变量捕捉时写[])**。
  • 如果是在类里面就算是值捕捉,成员变量也是可以修改的。

2.1 实际示例

cpp 复制代码
//===========================捕捉列表===========================
int x;
// 这里捕捉列表必须为空,因为全局变量不用捕捉就可以用, 没有可以被捕捉的变量(静态成员变量也是同样的道理)
auto func1 = []()
	{
		x++;
	};

int main()
{
	int a = 0, b = 1, c = 2, d = 3;
	// 只能使用当前 lambda 局部域捕捉到的对象和全局对象
	// 捕获列表的意义:本质是更方便的使用当前局部域的对象
	auto func1 = [a, &b] //由于参数列表为空,所以可以省略不写
		{
			// 值捕捉的变量不可以修改,引用捕捉的可以修改
			//a++;
			b++;
			int ret = a + b;
			x++;//全局变量
			return ret;
		};
	cout << func1() << endl;

	cout << "***********************************************" << endl;

	// 加了这个mutable之后值捕捉也可以修改a了,这里()就不可以省了就算没参数
	// 传值捕捉本质是一种拷贝,并且被 const 修饰了
	// mutable 相当于去掉 const 属性,可以修改了
	// 但是修改了不会影响外面被捕捉的值,因为是⼀种拷贝
	auto func2 = [c, &d]() mutable
		{
			c++;// 去掉a的const属性,但是修改的是拷贝的参数a,外面被捕捉的a并没有进行修改
			d++;
			int ret = c + d;
			x++;//全局变量
			cout << "lambda表达式内部c:" << c << endl;

			return ret;
		};
	//这里一定要注意的是: lambda 表达式是一个匿名函数对象,一定要注意是一个对象
	//也就是说代码到这里的时候是并没有执行上述函数体内部操作的,因为上面只是我们定义这个对象
	//只有当我们类似仿函数那样使用该对象:func2()时,才会执行函数体内部操作

	cout << "执行lambda表达式之前c:" << c << endl;
	cout << "执行lambda表达式之前d:" << d << endl;
	cout << func2() << endl;
	cout << "执行lambda表达式之后c:" << c << endl;
	cout << "执行lambda表达式之后d:" << d << endl;

	cout << "***********************************************" << endl;

	//隐式值捕捉
	a = 0, b = 1, c = 2, d = 3;
	//  lambda 位置之前定义了哪些变量就捕捉哪些变量
	auto func3 = [=]
		{
			int ret = a + b + c + d;
			return ret;
		};
	cout << func3() << endl;

	cout << "***********************************************" << endl;

	// 隐式引用捕捉
	a = 0, b = 1, c = 2, d = 3;
	//  lambda 位置之前定义了哪些变量就捕捉哪些变量
	auto func4 = [&]
		{
			a++;
			b++;
			c++;
			d++;
		};
	cout << a << " " << b << " " << c << " " << d << endl;
	func4();
	cout << a << " " << b << " " << c << " " << d << endl;

	cout << "***********************************************" << endl;

	// 混合捕捉的=或&一定是最前面的,而且如果前面是=后面必须是引用捕捉,前面是&后面必须是值捕捉
	// 混合捕捉1
	a = 0, b = 1, c = 2, d = 3;
	auto func5 = [&, a, b]
		{
			//a++;
			//b++;
			c++;
			d++;
			int ret = a + b + c + d;
			return ret;
		};
	cout << a << " " << b << " " << c << " " << d << endl;
	cout << func5() << endl;
	cout << a << " " << b << " " << c << " " << d << endl;

	cout << "***********************************************" << endl;

	// 混合捕捉2
	a = 0, b = 1, c = 2, d = 3;
	auto func6 = [=, &a, &b]
		{
			a++;
			b++;
			//c++;
			//d++;
			return a + b + c + d;
		};
	cout << a << " " << b << " " << c << " " << d << endl;
	cout << func6() << endl;
	cout << a << " " << b << " " << c << " " << d << endl;

	cout << "***********************************************" << endl;

	// 局部的静态和全局变量都不能捕捉,也不需要捕捉
	static int m = 0;
	auto func7 = []
		{
			int ret = x + m;
			return ret;
		};
	return 0;
}

在类里面的使用

cpp 复制代码
//===========================类中的捕捉列表===========================
class A
{
public:
	void func()
	{
		int x = 0, y = 1;

		auto f1 = [=]
			{
				// 为什么这里是隐式的值捕捉,_a1还可以修改呢?
				//原因在于_a1其实没有被捕捉,被捕捉的其实是this指针,那为什么会是this指针呢?
				//因为lambda能捕捉的只能是局部对象和函数参数,而我们知道类中的成员函数的参数列表中都含有隐式的this指针
				//所以这个this指针就被lambda捕获了
				//所以不能被修改的其实是this指针本身(其实this指针本身本来也不能进行修改)
				//而this指针访问的成员变量还是可以进行修改的
				_a1++;
				//实际效果:(*this)._a1++;
				return x + y + _a1 + _a2;
			};

		cout << f1() << endl;

		auto f2 = [&]
			{
				x++;
				_a1++;
				return x + y + _a1 + _a2;
			};

		cout << f2() << endl;

		// 捕捉this指针的本质就是为了可以访问成员变量
		auto f3 = [x, this]
			{
				_a1++;
				return x + _a1 + _a2;
			};

		cout << f3() << endl;
	}
private:
	int _a1 = 0;
	int _a2 = 1;
};

int main()
{
	A a;
	a.func();
	return 0;
}

3、lambda 的应用场景:替代仿函数与函数指针

传统排序需要定义仿函数,lambda 则简洁高效:

  • 在学习 lambda 表达式之前,我们使用的可调用对象只有函数指针和仿函数对象,函数指针的类型定义起来比较麻烦,仿函数则是要定义一个类,相对会比较麻烦。使用 lambda 去定义可调用对象,即简单又方便。
  • lambda 在很多其他地方用起来也是很好用的。比如线程中定义线程的执行函数逻辑,智能指针中定制删除器等,lambda 的应用还是很广范的,以后我们会不断接触到。
cpp 复制代码
//===========================lambda 的应用场景:替代仿函数与函数指针===========================
#include<vector>
#include<algorithm>
struct Goods
{
	std::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;
	}
};

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;
	}
};

int main()
{
	vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3.0, 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(), CompareEvaluateLess());
	//sort(v.begin(), v.end(), CompareEvaluateGreater());

	//auto priceLess = [](const Goods& gl, const Goods& gr)
	//{
	//	return gl._price < gr._price;
	//};

	//sort(v.begin(), v.end(), priceLess);
	//这样写lambda表达式其实看上去和实现仿函数没什么太大区别,所以下面还有更加简洁的使用lambda表达式

	sort(v.begin(), v.end(), [](Goods& g1, Goods& g2) {return g1._price < g2._price; });
	sort(v.begin(), v.end(), [](Goods& g1, Goods& g2) {return g1._price > g2._price; });
	sort(v.begin(), v.end(), [](Goods& g1, Goods& g2) {return g1._evaluate < g2._evaluate; });
	sort(v.begin(), v.end(), [](Goods& g1, Goods& g2) {return g1._evaluate > g2._evaluate; });
	
	return 0;
}

4、lambda 的原理:底层是仿函数

4.1 lambda 底层原理

编译器会将 lambda 表达式编译为一个匿名仿函数类:

  • 捕捉列表的变量成为该类的成员变量;
  • operator()的参数、返回值、函数体与 lambda 一致;
  • 不同 lambda 对应不同的仿函数类(类名由编译器自动生成,每个 lambda 的类型都是完全不一样的)。

lambda 的原理和范围for很像,编译后从汇编指令层的角度看,压根就没有lambda和范围for这样的东西。范围for底层是迭代器 ,而lambda 底层是仿函数对象 ,也就说我们写了一个 lambda 以后,编译器会生成一个对应的仿函数的类。

仿函数的类名是编译按一定规则生成的,保证不同的 lambda 生成的类名不同 ,lambda 参数 / 返回类型 / 函数体就是仿函数 operator() 的参数/返回类型/函数体,lambda 的捕捉列表 本质是生成的仿函数类的成员变量 ------ 也就是说捕捉列表的变量 都是lambda 类构造函数的实参。 当然隐式捕捉不同,编译器也不是傻瓜,实际上,编译器只会看lambda 函数体内部 使用了哪些变量才会捕捉哪些对象,而并不是一股脑的全部进行捕捉。隐式捕捉其实更像是给予一种能使用外部的局部变量和函数参数的权限。

4.2 实际示例:

cpp 复制代码
//===========================lambda 的原理:底层是仿函数===========================
int x = 0;
// lambda参数/返回类型 / 函数体就是仿函数operator()的参数 / 返回类型 / 函数体
// lambda 的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参
int main()
{
	int a = 0, b = 1, c = 2, d = 3;
	class lambada5
	{
	public:
		lambada5(int a_, int& b_)
			:a(a_)
			, b(b_)
		{
		}

		int operator()(int x)
		{
			//a++;
			++b;
			return a + b + x;
		}
	private:
		const int a;//对应值捕捉
		int& b;// 对应引用捕捉
	};

	// 可以看看汇编层
	auto func = [a, &b](int x)
		{
			//a++;
			++b;
			return a + b + x;
		};
	cout << func(1) << endl;

	// 等价于
	/*lambada5 func(a, b);*/
	return 0;
}

汇编层部分代码展示

  • 由此可以很明确的看出 lambda 底层的调用逻辑,其实就是仿函数

四. 包装器:统一可调用对象的类型

C++ 中的可调用对象 包括函数指针仿函数lambda成员函数,但它们类型各不相同,难以统一管理。std::function 和 std::bind 包装器解决了这一问题。

1、 std::function:可调用对象的 "容器"

std::function类模板,可包装任意符合 "返回值 (参数类型)" 签名的可调用对象,统一类型接口。

参考文档function - C++ Reference

  • std::function 是一个类模板 ,也是一个包装器 std::function 的实例对象可以包装存储其他的可以调用对象,包括函数指针仿函数lambdabind表达式 等,存储的可调用对象被称为 std::function目标。若std::function不含目标,则称它为空。调用空 std::function 的目标导致抛出std::bad_function_call异常。
  • 函数指针、仿函数、 lambda 等可调用对象的类型各不相同 , std::function 的优势就是统一类型,对他们都可以进行包装 ,这样在很多地方就方便声明可调用对象的类型,下面的第二个代码样例展示了 std::function 作为 unordered_map 的参数,实现字符串和可调用对象的映射表功能。

1.1 核心用法:

1.1.1 函数指针仿函数lambda 的 function 类型统一
cpp 复制代码
//===========================C++11:包装器===========================
//-----------------------------function-----------------------------
#include<functional>

//函数指针
int f(int a, int b)
{
	return a + b;
}

//仿函数
struct Functor
{
public:
	int operator()(int a, int b)
	{
		return a + b;
	}
};

//lambda表达式
auto func1 = [](int a, int b)
	{
		return a + b;
	};

int main()
{
	// 类型擦除 
	function<int(int, int)>f1 = f; //函数指针
	function<int(int, int)>f2 = Functor(); //仿函数对象
	function<int(int, int)>f3 = func1; //lambda对象
	cout << f1(1, 1) << endl;
	cout << f2(1, 1) << endl;
	cout << f3(1, 1) << endl;

	cout << "***********************************************" << endl;

	vector<function<int(int, int)>> v; //将不同可调用对象类型进行统一,即可使用容器进行存放
	v.push_back(f);
	v.push_back(Functor());
	v.push_back(func1);
	for (auto& f : v)
	{
		cout << f(1, 1) << endl;
	}
}
1.1.2 成员函数的 function 类型统一
cpp 复制代码
class Plus
{
public:
	Plus(int n = 10)
		:_n(n)
	{
	}

	//静态成员函数
	static int plusi(int a, int b)
	{
		return a + b;
	}

	//成员函数
	double plusd(double a, double b)
	{
		return (a + b) * _n;
	}

private:
	int _n;
};

int main()
{
	// 静态成员函数,下面两种写法都可以,用第二种可以统一规范
	// function<int(int, int)> f4 = Plus::plusi;
	function<int(int, int)> f4 = &Plus::plusi;
	cout << f4(1, 1) << endl;

	// 成员函数,必须带&,这就是C++11的语法规定记住就行,
	//所以为了避免成员函数忘了加&,我们就可以统一静态成员函数和成员函数都加上&
	//function<double(double, double)>  f5 = &Plus::plusd; //error
	function<double(Plus*, double, double)>  f5 = &Plus::plusd;
	//这里我们会发现上面的代码会报错,而加上Plus*就没问题了,这是为什么呢?
	//首先我们要知道function圆括号外面的类型表示可调用对象的返回类型
	//function圆括号里面的类型是参数类型,所以我们需要保证可调用对象的参数类型必须和function保持一致
	//那么对于成员函数来说我们在前面类和对象中讲解过:普通成员函数都隐含有一个this指针参数
	//这个this指针是不能显式调用的,但却是存在的,所以function也需要考虑在内
	//所以绑定时传对象或者对象的指针过去都可以
	Plus ps;
	cout << f5(&ps, 1.1, 1.1) << endl;

	function<double(Plus, double, double)>  f6 = &Plus::plusd;
	// Plus ps;
	cout << f6(ps, 1.1, 1.1) << endl;

	//一般推荐写下面这两种写法,通过传匿名对象就可以不用先创建一个类对象再进行传入
	function<double(Plus, double, double)>  f7 = &Plus::plusd;
	cout << f7(Plus(), 1.1, 1.1) << endl;

	function<double(Plus&&, double, double)>  f8 = &Plus::plusd;
	cout << f8(Plus(), 1.1, 1.1) << endl;

	auto pf1 = &Plus::plusd;
	Plus* ptr = &ps;
	cout << (ps.*pf1)(1.1, 1.1) << endl;
	cout << (ptr->*pf1)(1.1, 1.1) << endl;

	return 0;
}

1.2 重写 逆波兰表达式求值

题目链接:

150. 逆波兰表达式求值 - 力扣(LeetCode)

题目描述:

这道算法题在前面讲解容器stack的时候就已经就讲解过了,当时由于我们还没有接触C++11这些新语法,所以当时实现的方法非常的简单粗暴,而现在我们学习了C++11这些新语法之后就可以对这道题的代码进行优化。

我们可以先把之前的传统代码再次展示出来:

cpp 复制代码
class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<int> st;
        for(auto& str : tokens)
        {
            // 判断四种运算符
            if(str == "+" || str == "-" || str == "*" || str == "/")
            {
                // 运算符
                int right = st.top();
                st.pop();
                int left = st.top();
                st.pop();
                switch(str[0]) // 大坑:switch...case语句只能是int类型
                {
                    case '+':
                        st.push(left + right);
                        break;
                    case '-':
                        st.push(left - right);
                        break;
                    case '*':
                        st.push(left * right);
                        break;
                    case '/':
                        st.push(left / right);
                        break;
                }
            }
            else
            {
                // 运算数
                st.push(stoi(str)); // 字符串转整型,to_string
            }
        }
        return st.top();
    }
};

我们可以现学现用,使用 unordered_map 映射 string 和 function 的方式实现一下。

这种方式的最大优势之一是方便扩展,假设还有其他运算,我们增加 unordered_map 中的映射即可,算法实现如下所示:

C++算法代码
cpp 复制代码
class Solution {
public:
    int evalRPN(vector<string>& tokens) 
    {
        stack<int> s;
        unordered_map<string, function<int(int, int)>> hash = {
            {"+", [](int a, int b){return a + b;}}, 
            {"-", [](int a, int b){return a - b;}}, 
            {"*", [](int a, int b){return a * b;}}, 
            {"/", [](int a, int b){return a / b;}}, 
        };
        for(int i = 0; i < tokens.size(); i++)
        {
            if(hash.count(tokens[i]))
            {
                int right = s.top();
                s.pop();
                int left = s.top();
                s.pop();
                s.push(hash[tokens[i]](left, right));
            }
            else
            {
                s.push(stoi(tokens[i]));
            }
        }
        return s.top();
    }
};
代码分析:

2、std::bind:可调用对象的 "适配器"

std::bind函数模板 ,可调整可调用对象参数个数、顺序,绑定固定参数返回一个新的可调用对象

参考文档: bind - C++ Reference

  • bind 是一个函数模板,它也是一个可调用对象的包装器,可以把他看做一个函数适配器 ,对接收的 fn 可调用对象 进行处理后返回一个可调用对象。 bind 可以用来调整参数个数和参数顺序。 bind 也在这个头文件中。

调用bind的一般形式:

cpp 复制代码
auto newCallable =bind(callable,arg_list);
  • 其中 newCallable 本身是一个可调用对象 ,arg_list 是一个逗号分隔的参数列表 ,对应给定的callable 的参数。当我们调用 newCallable 时,newCallable 会调用 callable,并传给它arg_list 中的参数。
  • arg_list 中的参数可能包含形如**_n 的名字** ,其中 n 是一个整数,这些参数是占位符 ,表示newCallable 的参数,它们占据了传递给 newCallable 的参数的位置 。数值n 表示生成的可调用对象中参数的位置 :_1 为 newCallable 的第一个参数,_2 为第二个参数,以此类推。这里需要注意 _n 针对的是 newCallable 的第n个参数,并非是 callable 的第n个参数,而bind 的参数列表和 callable 的参数列表是一一对应 的,下面有图例展示,这里一定不要混淆。_1 / _2 / _3...这些占位符放到placeholders 的一个命名空间中。

2.1 核心用法:

cpp 复制代码
//-----------------------------bind-----------------------------
//_n占位符
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;

int Sub(int a, int b)
{
	return (a - b) * 10;
}

int SubX(int a, int b, int c)
{
	return (a - b - c) * 10;
}

int main()
{
	// bind 本质返回一个仿函数对象
	// 调整参数顺序(不常用)
	// _1 代表第一个实参
	// _2 代表第二个实参
	// ............

	//调整参数顺序(实际应用意义不大):
	auto f1 = bind(Sub, _1, _2);
	auto f2 = bind(Sub, _2, _1);

	// _1 代表第一个实参
	// _2 代表第二个实参
	//一定要注意这里的第几个实参针对的是bind返回的可调用对象f1、f2而言
	//也就是说 bind 的参数列表中的参数顺序和 Sub 中的参数列表一一对应
	//对于f2来说:_2 还是对应 Sub 中的参数a, _1 还是对应 Sub 中的参数b
	cout << f1(10, 5) << endl;
	cout << f2(10, 5) << endl;

	cout << "***********************************************" << endl;

	// 调整参数个数(实际应用价值很大)
	// 分别绑死第1、2、3个参数:

	auto f3 = bind(SubX, 10, _1, _2);
	cout << f3(15, 5) << endl;
	// _1 代表第一个实参
	// _2 代表第二个实参
	// 底层operator(),调整SubX,第一个参数10,15,5

	cout << "***********************************************" << endl;

	auto f4 = bind(SubX, _1, 10, _2);
	cout << f4(15, 5) << endl;
	// 底层operator(),调整SubX,第一个参数15,10,5

	cout << "***********************************************" << endl;

	auto f5 = bind(SubX, _1, _2, 10);
	cout << f5(15, 5) << endl;
	// 底层operator(),调用SubX,第一个参数15,5,10

	cout << "***********************************************" << endl;

	// 利用 bind 改进
	//对于上面学习的function统一成员函数的类型时,每次第一个参数都需要加上对象的指针
	//这样其实会比较麻烦,能不能不传呢?其实是可以的,通过bind将成员函数对象提前进行绑死,我们就不需要再传入了
	function<double(Plus, double, double)> f6 = &Plus::plusd;
	cout << f6(Plus(), 1.1, 1.1) << endl;
	cout << f6(Plus(), 2.2, 1.1) << endl;
	cout << f6(Plus(), 3.3, 1.1) << endl;

	cout << "***********************************************" << endl;

	// 绑定成员函数(需传入this指针或对象)
	function<double(double, double)> f7 = bind(&Plus::plusd, Plus(), _1, _2);
	/*Plus p;
	function<double(double, double)> f7 = bind(&Plus::plusd, &p, _1, _2);*/

	cout << f7(1.1, 1.1) << endl;
	cout << f7(2.2, 1.1) << endl;
	cout << f7(3.3, 1.1) << endl;
	return 0;
}

2.2 实战场景:银行复利产品

cpp 复制代码
//计算银行复利的lambda
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;

int main()
{
	auto func1 = [](double rate, double money, int year)
		{
			double ret = money;
			for (int i = 0; i < year; i++)
			{
				ret += ret * rate;
			}

			return ret - money;
		};

	//年利率1.5%
	//3年利息
	function<double(double)> func_r1_5_y3 = bind(func1, 0.015, _1, 3);
	//5年利息
	function<double(double)> func_r1_5_y5 = bind(func1, 0.015, _1, 5);
	//10年利息
	function<double(double)> func_r1_5_y10 = bind(func1, 0.015, _1, 10);

	//本金10w
	cout << func_r1_5_y3(100000) << endl;
	cout << func_r1_5_y5(100000) << endl;
	cout << func_r1_5_y10(100000) << endl;

	cout << "***********************************************" << endl;

	//年利率10%
	//3年利息
	function<double(double)> func_r10_y3 = bind(func1, 0.1, _1, 3);
	//5年利息
	function<double(double)> func_r10_y5 = bind(func1, 0.1, _1, 5);
	//10年利息
	function<double(double)> func_r10_y10 = bind(func1, 0.1, _1, 10);

	//本金10w
	cout << func_r10_y3(100000) << endl;
	cout << func_r10_y5(100000) << endl;
	cout << func_r10_y10(100000) << endl;
	return 0;
}

文章小结

核心总结与避坑指南:

1. 类新功能 避坑

  • 手动实现拷贝构造 / 赋值后,默认移动构造 / 赋值不再生成,需手动实现或用 default 强制生成;
  • delete 可禁止拷贝(如单例模式),default 可强制生成默认函数(如移动构造)。

2. lambda 避坑

  • 值捕捉 的变量默认 const ,修改需加 mutable(仅影响拷贝);
  • 引用捕捉需确保外层变量生命周期长于 lambda,避免悬垂引用。

3. 包装器避坑

  • function 包装成员函数 时,需传入 this 指针或对象隐含第一个参数);
  • bind 的占位符 _n 对应新可调用对象第 n 个参数顺序不可混淆

结束语

到此,C++11的类新功能、Lambda表达式以及包装器 function和bind 就全部讲解完了。

C++11 的类新功能、lambda、包装器和智能指针,共同构建了现代 C++ 的核心编程模型。类新功能让类设计更灵活,lambda 简化了可调用对象定义,包装器统一了接口类型,智能指针则保障了资源安全。这些特性并非孤立存在,实际开发中常结合使用(如 lambda 作为function 的参数等)。

而C++11我们还剩下一个智能指针的知识点,但讲解智能指针之前我们会先讲解C++的异常,当我们把异常和智能指针讲解完了C++的主线知识我们也就彻底结束了。不知不觉C++就讲解了这么多知识点,提前也是先感谢大家对我的支持,虽然C++的主线快结束了,但是后面我还是会对C++的一些额外知识补充作为加餐文章为大家进行讲解,感谢大佬们的支持!

C++参考文档:
https://legacy.cplusplus.com/reference/
https://zh.cppreference.com/w/cpp
https://en.cppreference.com/w/

相关推荐
qq_12084093712 小时前
Three.js 大场景分块加载实战:从全量渲染到可视集调度
开发语言·javascript·数码相机
csbysj20202 小时前
Pandas 常用函数
开发语言
一个行走的民2 小时前
C++ Lambda 表达式语法详解
c++
小小码农Come on2 小时前
C++访问QML控件-----QML访问C++对象属性和方法
java·开发语言·c++
代码中介商2 小时前
C语言函数完全指南:从基础到实践
c语言·开发语言
Yungoal2 小时前
项目层级结构
c++
思茂信息3 小时前
CST交叉cable的串扰(crosstalk)仿真
服务器·开发语言·人工智能·php·cst
lolo大魔王3 小时前
Go语言的反射机制
开发语言·后端·算法·golang
那个失眠的夜3 小时前
AspectJ
java·开发语言·数据库·spring