突破编程_C++_基础教程(函数(二))

6 Lambda 表达式

注:该部分内容涉及到 C++11 新特性。
Lambda表达式是 C++11 提出来的一个新特性,类似于一个匿名函数,拥有捕获所在作用域中变量的能力,能够将函数做为对象一样使用,通常用来实现回调函数、代理等功能。

6.1 Lambda 表达式的语法格式

Lambda 表达式的基本语法如下:
捕获列表(参数列表) mutable(可选) 异常属性(可选) -> 返回类型 { 函数体 }
捕捉列表(capture clause):其对应符号为 []。捕捉列表是 Lambda 表达式的起始标识,编译器根据 []来判断接下来的代码是否为 Lambda 表达式,捕捉列表能够捕捉上下文中的变量供Lambda 表达式使用。
参数列表(parameters):与普通函数的参数列表一致,如果没有参数传递,则可以连同括号一起省略。
mutable:Lambda 表达式默认是一个 const 函数, mutable 可以取消其常量性。注意当使用该修饰符时,参数列表不可省略(参数为空时也要加上括号)。
异常属性(noexcept):与普通函数的异常属性用法一致。该关键字告诉编译器,函数中不会发生异常,这有利于编译器对程序做更多的优化。如果 noexecpt 函数在运行时向外抛出了异常(没有被内部捕获),程序会直接终止。
-> 返回类型:用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可以省略。也可以使用 decltype 进行类型推导。
函数体:在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量以及捕获到的 this 指针。
如下是一个简单的 Lambda 表达式 例子:

#include 
using namespace std;

int main()
{
	[] {
		printf("test\n");
	}();

	auto lambdaFunc = [] {
		printf("test\n");
	};
	lambdaFunc();

	return 0;
}

上面代码的输出为:

test
test

从上面代码的执行可以看出,Lambda 表达式可以在后面用括号运行,也可以赋给一个变量。注意这里变量 lambdaFunc 的类型为 auto ,这是由于编译器会根据 Lambda 表达式创建一个类(上面代码中的 lambdaFunc 既为该类的一个对象),类名为 lambda_ 加上一串随机数,在编译前该类名未知。

6.2 Lambda 表达式的本质

Lambda 表达式本质是一个函数对象(也被称作是仿函数),以如下 Lambda 表达式为例:

auto lambdaFunc = [] {
	printf("test\n");
};
lambdaFunc();

这段代码相当于如下:

class lambda_****			//这里的星号表示随机数
{
public:
	void operator()()
	{
		printf("test\n");
	}
};
lambda_**** lambdaFunc;
lambdaFunc();

6.3 Lambda 表达式的捕获方式

Lambda 表达式的捕获列表用于捕获当前函数作用域的变量。捕获方式有三种,分别是值捕获、引用捕获以及混合捕获。

6.3.1 值捕获

值捕获分为两种情况,当 Lambda 表达式被 mutable 修饰时,则在 Lambda 表达式内部保存一份外部变量的副本,对这个副本进行操作不会影响原来的变量,当 Lambda 表达式没有被 mutable 修饰时,则以 const 引用的方式将外部变量传递到 Lambda 表达式中,在表达式中可以访问变量,但是不能修改变量。使用 = 可以将函数作用域的所有变量以值捕获方式传入到表达式中(包括成员函数以及 this 指针)。如下为代码示例:

#include 
using namespace std;

int main()
{
	int a = 1;
	int b = 2;
	auto lambdaFunc1 = [a] {
		printf("a = %d\n", a);
	};
	lambdaFunc1();

	auto lambdaFunc2 = [=] {
		printf("a = %d\n", a);
		printf("b = %d\n", b);
	};
	lambdaFunc2();

	return 0;
}

上面代码的输出为:

a = 1
a = 1
b = 2

6.3.2 引用捕获

引用捕获是指 Lambda 表达式内部直接使用外部变量的引用,对这个引用进行操作会影响原来的变量。使用 & 可以将函数作用域的所有变量(包括成员函数以及 this 指针)以引用捕获方式传入到表达式中。如下为代码示例:

#include 
using namespace std;

int main()
{
	int a = 1;
	int b = 2;
	auto lambdaFunc1 = [&a] {
		a = 2;
		printf("a = %d\n", a);
	};
	lambdaFunc1();

	auto lambdaFunc2 = [&] {
		b = 3;
		printf("a = %d\n", a);
		printf("b = %d\n", b);
	};
	lambdaFunc2();

	return 0;
}

上面代码的输出为:

a = 2
a = 2
b = 3

6.3.3 混合捕获

引用捕获是指 Lambda 表达式的捕获列表捕获多个变量,既有值捕获的变量也有引用捕获的变量。混合使用时,要求捕获列表中第一个元素必须是隐式捕获( & 或 = ),然后后面可以跟上显式指定的变量名和符号。如下为代码示例:

#include 
using namespace std;

int main()
{
	int a = 1;
	int b = 2;
	auto lambdaFunc = [=,&b] {
		b = 3;
		printf("a = %d\n", a);
		printf("b = %d\n", b);
	};
	lambdaFunc();

	return 0;
}

上面代码的输出为:

a = 1
b = 3

6.3.4 捕获 this 指针

Lambda 表达式可以捕获 this 指针,使 Lambda 表达式可以在自定义的函数体内使用当前类的成员函数,这是因为捕获 this 后隐式的在成员变量前加了 this 。捕获 this 指针后,Lambda 表达式的函数体可以调用当前类的全部公有及私有成员变量与成员函数。如下为样例代码:

#include 
using namespace std;

class Student
{
public:
	Student() {}
	~Student() {}

public:
	string getName()
	{
		return m_name;
	}

	void printName()
	{
		auto lambdaFunc = [this] {
			this->m_name = "zhangsan";							//修改私有成员变量
			printf("name = %s\n", this->getName().c_str());		//调用公有成员函数
		};
		lambdaFunc();
	}

private:
	string m_name;
};

int main()
{
	Student st;
	st.printName();

	return 0;
}

上面代码的输出为:

name = zhangsan

重点注意
不要二次传递 this 指针,有可能会使程序崩溃,比如:

class Student
{
public:
	Student() {}
	~Student() {}

public:
	template<class Fn>
	void printName(Fn func)
	{
		auto lambdaFunc = [&,this] {
			func;
		};
		lambdaFunc();
	}

};

class Teacher
{
public:
	Teacher() {}
	~Teacher() {}

public:
	void rollCall()
	{
		Student st;
		st.printName([this] {			//这里的 this 指针很容易引起程序崩溃
		});
	}
};

6.3 Lambda 表达式的参数列表

Lambda 表达式的参数列表与普通函数参数列表一致;如果不需要参数,则可以省略此项(括号也可以省略)。如下为样例代码:

#include 
using namespace std;

int main()
{
	int a = 1;
	int b = 2;
	auto lambdaFunc = [=, &b](int c) {
		b = 3;
		printf("a+b+c = %d\n", a+b+c);
	};
	lambdaFunc(6);

	return 0;
}

上面代码的输出为:

a+b+c = 10

6.4 Lambda 表达式的限定符 mutable

Lambda 表达式的参数列表与普通函数参数列表一致;如果不需要参数,则可以省略此项(括号也可以省略)。如下为样例代码:

#include 
using namespace std;

int main()
{
	int a = 1;
	auto lambdaFunc1 = [&a] {
		a = 2;					//OK
	};

	auto lambdaFunc2 = [a] {
		a = 2;					//错误
	};
	
	auto lambdaFunc3 = [a]() mutable {
		a = 2;					//OK:注意此时参数列表的括号不能省略
	};

	return 0;
}

6.5 Lambda 表达式的异常说明符 noexcept

Lambda 表达式的异常说明符 noexcept 与普通函数的异常属性用法一致。该关键字告诉编译器,函数中不会发生异常,这有利于编译器对程序做更多的优化。如果 noexecpt 函数在运行时向外抛出了异常(没有被内部捕获),程序会直接终止。如下为样例代码:

#include 
using namespace std;

int main()
{
	int a = 1;
	auto lambdaFunc1 = [&a] {
		if (2 == a)
		{
			throw(0);			//OK
		}
	};

	auto lambdaFunc2 = [a]() noexcept {
		if (2 == a)
		{
			throw(0);			//告警:warning C4297: 'main::::operator ()': function assumed not to throw an exception but does
		}
	};

	auto lambdaFunc3 = [a]() mutable {
		if (2 == a)
		{
			throw(0);			//OK
		}
	};

	return 0;
}

6.6 Lambda 表达式使用注意点

(1)不允许以相同方式重复捕捉。例如[=, a],前者已经表示以值捕获所有外部变量,后者就重复了。但是[=, &a]是被允许的,因为它们的捕获方式不同。
(2)Lambda 表达式可以捕获父作用域中的局部变量,但是不能捕获其他作用域或者非局部变量。
(3)Lambda 表达式之间不能相互赋值,因为它们本质是函数对象,其对应类的名字是随机的。
(4)Lambda 表达式默认是内联函数,可以被编译器自动内联展开,从而减少函数调用的次数。但是,并不是所有的 Lambda 表达式都会被内联,这取决于编译器的实现和优化策略。

7 通用多态函数封装器(std::function)

注:该部分内容涉及到 C++11 新特性。
C++11 引入的 function 函数对象是一种通用、多态的函数封装。function 可以对任何可以调用对象进行封装,这些目标实体包括普通函数、Lambda 表达式、类的成员函数、函数指针以及函数对象。
function 的使用方式与函数指针很类似,不过其相对于函数指针这种可调用实体,是类型安全的。(函数指针本质上还是指针,类型不安全)
如下为各类可调用对象:

#include 

using namespace std;

int add(int a,int b)							//普通函数
{
	return a + b;
}

class Student
{
public:
	Student() {}
	~Student() {}

public:
	void operator ()()							//仿函数(函数对象)
	{
		printf("hello\n");
	}
	string getName()							//类的成员函数
	{
		return m_name;
	}

private:
	string m_name;
};

int main()
{
	typedef int(*ADDFUNC)(int, int);
	ADDFUNC f1 = add;							//函数指针

	auto lambdaFunc = [](int a, int b)->int {	//Lambda 表达式
		return a + b;
	};

	return 0;
}

7.1 function 存储普通函数

如下为样例代码:

#include 
#include 
using namespace std;

int add(int a,int b)
{
	return a + b;
}

int main()
{	
	function<int(int, int)> func1 = add;
	function<int(int, int)> func2 = bind(add,placeholders::_1, placeholders::_2);	//使用 bind

	return 0;
}

注意使用 bind 的方式,placeholders::_1 与 placeholders::_2 是占位符,代表这个位置将在函数调用时,被传入的参数所替代。

7.2 function 存储 Lambda 表达式

如下为样例代码:

#include 
#include 
using namespace std;

int main()
{	
	auto lambdaFunc = [](int a, int b)->int {
		return a + b;
	};

	function<int(int, int)> func1 = lambdaFunc;
	function<int(int, int)> func2 = bind(lambdaFunc,placeholders::_1, placeholders::_2);	//使用 bind

	return 0;
}

注意 Lambda 表达式之间不能相互赋值,因为它们本质是函数对象,其对应类的名字是随机的。但是当使用 function 将其存储后,即可相互赋值。

7.3 function 存储类的成员函数

如下为样例代码:

#include 
#include 
using namespace std;

class Student
{
public:
	Student() {}
	~Student() {}

public:
	string getName()
	{
		return m_name;
	}

private:
	string m_name;
};
int main()
{	
	Student st;
	function<string()> func2 = bind(&Student::getName,&st);

	return 0;
}

注意对象的成员函数属于类,所以其存储位置在对象外的空间中,由所有的类对象共享。因此, Student 类中的 getName() 成员函数,不是属于 st 对象的,而是属于 Student 类。所以使用 &类名::成员函数名 的形式将该成员函数赋给 function 对象。

7.4 function 存储函数指针

如下为样例代码:

#include 
#include 
using namespace std;

int add(int a,int b)
{
	return a + b;
}

int main()
{	
	typedef int(*ADDFUNC)(int, int);
	ADDFUNC f1 = add;

	function<int(int, int)> func1 = f1;
	function<int(int, int)> func2 = bind(f1, placeholders::_1, placeholders::_2);	//使用 bind

	return 0;
}

7.5 function 存储函数对象

如下为样例代码:

#include 
#include 
using namespace std;

class Student
{
public:
	Student() {}
	~Student() {}

public:
	void operator ()()
	{
		printf("hello\n");
	}
};

int main()
{	
	Student st;
	function<void()> func1 = st;
	function<void()> func2 = bind(st);			//使用 bind

	return 0;
}

你可能感兴趣的:(突破编程_C++_基础教程,c++)