【C++】—— c++11新特性之 lambda

前言:

  • 上期,我们学习了有关 C++11 一些属于了解的新特性介绍。本期,我们要讲到的内容则属于 需要掌握 的知识点之一。

目录

(一)lambda 的引入

(二) lambda表达式

1、lambda表达式语法

 2、捕获列表说明

 (三)函数对象与lambda表达式

总结


(一)lambda 的引入

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

int main()
{
	int array[] = { 4,1,8,5,3,7,0,9,2,6 };

	// 默认按照小于比较,排出来结果是升序
	sort(array, array + sizeof(array) / sizeof(array[0]));
	for (auto e : array)
	{
		cout << e << " ";
	}
	return 0;
}

输出展示:

【C++】—— c++11新特性之 lambda_第1张图片

 

此时如果我们需要进行降序排序的话,此时则需要改变元素的比较规则:

int main()
{
	int array[] = { 4,1,8,5,3,7,0,9,2,6 };

	// 如果需要降序,需要改变元素的比较规则
	sort(array, array + sizeof(array) / sizeof(array[0]), greater());
	for (auto e : array)
	{
		cout << e << " ";
	}
	cout << endl;
	return 0;
}

输出展示:

【C++】—— c++11新特性之 lambda_第2张图片


 

 如果待排序元素为自定义类型,需要用户定义排序时的比较规则

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

 【解释说明】

  1. ComparePriceLess 结构体是一个函数对象类,重载了小于运算符的 () 操作符,用于比较两个 Goods 对象的价格大小。在 operator() 函数中,它将左边的 Goods 对象的价格与右边的 Goods 对象的价格进行比较,并返回结果。
  2. ComparePriceGreater 同样是一个函数对象类,重载了大于运算符的 () 操作符,用于比较两个 Goods 对象的价格大小。在 operator() 函数中,它将左边的 Goods 对象的价格与右边的 Goods 对象的价格进行比较,并返回结果。

输出展示:

【C++】—— c++11新特性之 lambda_第3张图片

 

【小结】 

  • 随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式

(二) lambda表达式

要使用 lambda 表达式对 vector v 进行排序,可以使用 std::sort() 算法,并将 lambda 表达式作为比较函数传递给该函数。

因此上诉代码逻辑就可以变为下面这样:


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

int main()
{
    vector v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };

    // 使用 lambda 表达式按价格升序排序
    sort(v.begin(), v.end(), [](const Goods& gl, const Goods& gr) 
    {
        return gl._price < gr._price;
    });

    // 输出排序后的结果
    for (auto e : v) 
    {
		cout << "商品名称: " << e._name << ", 价格: " 
             << e._price << ", 评价: " << e._evaluate << endl;
    }

    return 0;
}

输出展示:

【C++】—— c++11新特性之 lambda_第4张图片

 

上述代码就是使用C++11中的lambda表达式来解决,可以看出lambda表达式实际是一个匿名函

 

1、lambda表达式语法

lambda 匿名函数很简单,可以套用如下的语法格式:


[外部变量访问方式说明符] (参数) mutable noexcept/throw() -> 返回值类型
{
        函数体;
};


其中各部分的含义分别为:
 

a. [外部变量方位方式说明符(捕捉列表)]

  • [ ] 方括号用于向编译器表明当前是一个 lambda 表达式,其不能被省略。在方括号内部,可以注明当前 lambda 函数的函数体中可以使用哪些“外部变量”。

所谓外部变量,指的是和当前 lambda 表达式位于同一作用域内的所有局部变量。
 

b. (参数)

  • 和普通函数的定义一样,lambda 匿名函数也可以接收外部传递的多个参数。和普通函数不同的是,如果不需要传递参数,可以连同 () 小括号一起省略

c. mutable

  • 此关键字可以省略,如果使用则之前的 () 小括号将不能省略(参数个数可以为 0)。默认情况下,对于以值传递方式引入的外部变量,不允许在 lambda 表达式内部修改它们的值(可以理解为这部分变量都是 const 常量)。而如果想修改它们,就必须使用 mutable 关键字。

注意:对于以值传递方式引入的外部变量,lambda 表达式修改的是拷贝的那一份,并不会修改真
正的外部变量;
 

d. noexcept/throw()

  • 可以省略,如果使用,在之前的 () 小括号将不能省略(参数个数可以为 0)。默认情况下,lambda函数的函数体中可以抛出任何类型的异常。而标注 noexcept 关键字,则表示函数体内不会抛出任何异常;使用 throw() 可以指定 lambda 函数内部可以抛出的异常类型。

e. -> 返回值类型

  • 指明 lambda 匿名函数的返回值类型。值得一提的是,如果 lambda 函数体内只有一个 return 语句,或者该函数返回 void,则编译器可以自行推断出返回值类型,此情况下可以直接省略"-> 返回值类型"。

f. 函数体

  • 和普通函数一样,lambda 匿名函数包含的内部代码都放置在函数体中。该函数体内除了可以使用指定传递进来的参数之外,还可以使用指定的外部变量以及全局范围内的所有全局变量。
     

【注意】

  1. 在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空;
  2. 因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。

接下来带大家仔细研究其相关的知识:

首先,我们先看如下代码:

int main()
{
	auto add = [](int x, int y)->int {return x + y; };

	cout << [](int x, int y)->int {return x + y; }(1, 2) << endl;

	return 0;
}

输出展示:

【C++】—— c++11新特性之 lambda_第5张图片

【解释说明】 

这行代码使用 lambda 表达式创建了一个匿名函数对象,并立即调用该函数对象,并将结果输出到标准输出流 cout 中。

[] 表示捕获列表为空,表示在 lambda 表达式中不捕获任何外部变量。

(int x, int y) -> int { return x + y; } 是 lambda 表达式的函数体部分,它接受两个 int 类型的参数 x 和 y,并返回它们的求和结果。

(1, 2) 是传递给 lambda 表达式的实际参数,即调用 lambda 函数时传递给参数 x 和 y 的具体值。

cout << [](int x, int y)->int {return x + y; }(1, 2) << endl; 的执行过程如下:

  1. 匿名 lambda 函数对象被创建。

  2. 匿名 lambda 函数对象被立即调用,传入参数 1 和 2

  3. 返回值 3 被输出到标准输出流 cout

 以上代码大家是不是看着十分的难受呀!其实,它的真正样子无非就是以下这样的:

【C++】—— c++11新特性之 lambda_第6张图片

 

 当然上述的  auto add = [](int x, int y)->int {return x + y; };  还可以写成下述这样:

【C++】—— c++11新特性之 lambda_第7张图片

 【小结】

  • 通过上述例子可以看出,lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。
     

 2、捕获列表说明

 

捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。

我们以交换两个数的值这个逻辑代码为例给大家说明: 

  • [var]:表示值传递方式捕捉变量var

例如以下代码:

int main()
{
	int x = 1,y = 5;

	auto swap = [](int x, int y)
	{
		int tmp = x;
		x = y;
		y = tmp;
	};

	swap(x, y);
	cout << x << " " << y << " " << endl;
	return 0;
}

输出展示:

【C++】—— c++11新特性之 lambda_第8张图片

 

【解释说明】

  1. 需要注意的是 lambda 表达式中的参数 x y 按值传递的副本,对它们的修改不会影响到 main() 函数中的变量x y ;
  2. 因此,在 lambda 表达式中的交换操作并不会影响 main() 函数中的值。

 此时,有的小伙伴可能会考虑加入 mutable 这个关键字来进行修饰:

int main()
{
	int x = 1,y = 5;

	//传值捕捉
	auto swap = [](int x, int y) mutable
	{
		int tmp = x;
		x = y;
		y = tmp;
	};

	swap(x, y);
	cout << x << " " << y << " " << endl;
	return 0;
}

结果展示:

【C++】—— c++11新特性之 lambda_第9张图片

 【解释说明】

在这个 lambda 表达式中,使用了 mutable 关键字来声明其为可变的,以允许在 mutable 函数体内修改按值捕获的变量。

然而,需要注意的是 mutable 表达式中的参数 x y 是按值传递的副本,对它们的修改不会影响到 main() 函数中的变量 x y

在 lambda 表达式中使用 mutable 关键字的主要目的是允许在 lambda 函数体内修改按值捕获的变量。在这个示例中,lambda 函数体内部的交换操作已经修改了参数  x y  的值,但是这些修改只影响了 lambda 函数内部的副本。

因此,输出结果将仍然是原始值


 

  • [&var]:表示引用传递捕捉变量var

要解决上述问题,可以将 lambda 表达式的参数改为引用,以便修改原始变量的值。以下是修改后的代码示例:

【C++】—— c++11新特性之 lambda_第10张图片

 

除了上述这样的写法之外,我们还可以像下面这样写:

【C++】—— c++11新特性之 lambda_第11张图片

 


  • [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
     

当使用[=]作为lambda表达式的捕获列表时,表示以值传递的方式捕获所有父作用域中的变量(包括this指针)。这意味着lambda表达式内部可以访问这些变量的副本,但对这些变量的修改不会影响到父作用域中的原始变量。


int main() 
{
    int x = 1, y = 5;

    // 使用[=]以值传递方式捕获变量
    auto func = [=]() 
    {
        std::cout << "x: " << x << ", y: " << y << std::endl;
    };

    func();

    return 0;
}

输出展示:

【C++】—— c++11新特性之 lambda_第12张图片

 


  • [&]:表示引用传递捕捉所有父作用域中的变量(包括this)

这意味着lambda表达式内部可以通过引用访问和修改这些变量,对其进行的修改将直接影响到父作用域中的原始变量。

下面是一个示例代码,演示了使用[&]进行引用传递方式捕获变量的情况:

int main()
{
	int x = 1,y = 5;

	
	// 使用[&]以引用传递方式捕获变量
	auto func2 = [&]() 
	{
		// 访问和修改捕获的变量
		cout << "x: " << x << ", y: " << y << endl;
		x = 100;  // 修改以引用传递方式捕获的变量
		y = 200;  // 修改以引用传递方式捕获的变量
	};

	func2();

	// 输出修改后的变量值
	cout << "Modified values: x: " << x << ", y: " << y  << endl;
	return 0;
}

输出显示:

【C++】—— c++11新特性之 lambda_第13张图片

 【解释说明】

  1. 在这段代码中,使用[&]以引用传递方式捕获了变量  x y 。在lambda函数内部,可以直接访问和修改这两个变量。在调用func2()后,输出了修改前的变量值,然后输出了修改后的变量值,可以看到两个变量的值已经被成功修改。
  2. 需要注意的是,由于使用了引用传递方式捕获变量,对其进行的修改直接影响到了父作用域中的原始变量。因此,在修改后输出的结果中,x的值变为100,y的值变为200。

  • [this]:表示值传递方式捕捉当前的this指针

这意味着lambda表达式内部可以访问当前对象的成员变量和成员函数,并且对其进行读取或调用。由于是值传递方式,lambda函数内部对this指针的修改不会影响到原始对象。

下面是一个示例代码,演示了使用[this]进行值传递方式捕获当前this指针的情况:

class Test 
{
public:
	Test(int a)
		: x(a) 
	{}

	void lambdaExample() 
	{
		// 使用[this]以值传递方式捕获当前的this指针
		auto func3 = [this]() {
			// 访问当前对象的成员变量
			cout << "x: " << x << endl;

			// 调用当前对象的成员函数
			memberFunction();
		};

		func3();  // 调用lambda表达式
	}

	void memberFunction() 
	{
		cout << "This is a member function." << endl;
	}

private:
	int x;
};

int main() 
{
	Test example(1);
	example.lambdaExample();

	return 0;
}

输出展示:

【C++】—— c++11新特性之 lambda_第14张图片

 

【解释说明】

  • 在上述示例中,使用[this]以值传递方式捕获当前的this指针。在lambda中,我们可以访问当前对象的成员变量x,并调用成员函数memberFunction()。由于采用了值传递方式,对this的修改不会影响到原始对象。


【注意】
a. 父作用域指包含lambda函数的语句块


b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。

  • 比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量
  • [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量

c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。

  • 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复

d. 在块作用域以外的lambda函数捕捉列表必须为空。


e. lambda表达式之间不能相互赋值,即使看起来类型相同
 

例如有以下代码:

int main()
{
	auto f1 = [] {cout << "hello world!" << endl; };
	auto f2 = [] {cout << "hello world!" << endl; };

	f1 = f2;
	return 0;
}

输出展示:

【C++】—— c++11新特性之 lambda_第15张图片

 【解释说明】

  • 每个lambda函数实际上是一个匿名类对象,而不仅仅是一个函数指针;
  • 它们具有自定义的操作符函数,用于使其可像函数一样调用;
  • 由于复制或赋值一个lambda函数会涉及复制或赋值其内部的闭包对象,因此在C++中,lambda函数默认情况下是不可复制或赋值的。

此外允许使用一个lambda表达式拷贝构造一个新的副本

void (*PF)();

int main()
{
	auto f1 = [] {cout << "hello world!" << endl; };
	auto f2 = [] {cout << "hello world!" << endl; };

	// 允许使用一个lambda表达式拷贝构造一个新的副本
	auto f3(f2);
	f3();
	
	return 0;
}

输出展示:

【C++】—— c++11新特性之 lambda_第16张图片

 【解释说明】

  1. lambda函数可以被拷贝构造成一个新的副本。当你将一个lambda函数拷贝给另一个变量时,实际上是在创建该lambda函数的一个副本,包括其闭包对象和可调用操作符函数。因此,拷贝构造函数将f2的副本复制给了f3
  2. 需要注意的是:每个lambda函数都是一个独立的实例,它们具有自己的闭包对象和可调用操作符函数。因此,对f3的修改不会影响到f2或其他的lambda函数。

可以将lambda表达式赋值给相同类型的函数指针

void (*PF)();
int main()
{
	auto f1 = [] {cout << "hello world!" << endl; };
	auto f2 = [] {cout << "hello world!" << endl; };
	
	// 可以将lambda表达式赋值给相同类型的函数指针
	PF = f2;
	PF();

	return 0;
}

输出展示:

【C++】—— c++11新特性之 lambda_第17张图片

 【解释说明】

  1. 将lambda表达式赋值给相同类型的函数指针。当你将一个lambda函数赋值给函数指针时,实际上是在将lambda函数转换为函数指针类型
  2. 需要注意的是:lambda函数转换为函数指针后,可以通过函数指针来调用lambda函数。函数指针保存了lambda函数的地址,因此可以使用函数指针来调用对应的lambda函数。

 (三)函数对象与lambda表达式

首先,给大家看一段代码:

int main()
{
	int x = 1, y = 5;

	auto swap = [](int& x,int& y)
	{
		int tmp = x;
		x = y;
		y = tmp;
	};

	cout << sizeof(swap) << endl;
	return 0;
}

输出展示:

【C++】—— c++11新特性之 lambda_第18张图片

 

【解释说明】

  1. 在上述代码中,使用 Lambda 表达式定义了名为 swap 的匿名函数,并将其赋值给 swap 类型的变量。
  2. 输出 sizeof(swap) 会显示结果为 1。这是因为 Lambda 表达式在编译时会被转化为一个匿名类型的函数对象,而 sizeof 运算符用于获取该函数对象的大小(以字节为单位)。
  3. 由于 Lambda 表达式在编译时生成一个独立的类型,这个类型的大小由编译器决定,并不依赖于 Lambda 表达式内部的代码或捕获的变量。因此,无论 Lambda 表达式内部的代码有多长或复杂,sizeof(swap) 的结果都是 1。
  4. 注意,sizeof运算符返回的是对象的大小,而不是函数体内部的实际代码大小。

接下来,我们一起来看看 lamdba 的底层逻辑帮助大家更好的理解上述:


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

	// lamber
	auto r2 = [=](double monty, int year)->double 
	{
		return monty * rate * year;
	};
	r2(10000, 2);
	return 0;

	return 0;
}

【说明】

  1. 其实大家需要明白,在编译器看来压根就没有什么 lambda,lambda会被编译器进行处理,处理成仿函数
  2. 因此在编译器的角度只有仿函数,编译器是根据你生成的仿函数,生成了一个类,这个类是一个仿函数的类

从使用方式上来看,函数对象与lambda表达式完全一样。


函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可
以直接将该变量捕获到。

【C++】—— c++11新特性之 lambda_第19张图片

 实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的:

  • 如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。

总结

以上便是关于 lamdba 的介绍了。对于本期内容,在面试中属于考察对象,因此大家需要进行掌握。

 

你可能感兴趣的:(C++学习,c++,开发语言)