C++11新特性

前言

C++11 是 C++ 历史上最重要、最颠覆的一次升级,彻底重构了现代 C++ 的编程范式。它不仅补全了 C++98 的大量缺陷,还引入了移动语义、完美转发、lambda 表达式、智能指针、并发支持等革命性特性,让代码更简洁、更高效、更安全。

一、C++11 是什么?

C++11 是 2011 年发布的 C++ 标准,之前叫 C++0x。它是自 C++98 以来最大的一次更新,间隔长达 8 年。

从此 C++ 进入现代 C++ 时代。

二、列表初始化:统一一切初始化

•C++11 想做一件事:让所有对象都能用 {} 初始化 ,{}初始化也叫做列表初始化

•内置类型⽀持,⾃定义类型也⽀持,⾃定义类型本质是类型转换,中间会产⽣临时对象,最后优化 了以后变成直接构造

•{}初始化的过程中,可以省略掉=。

• C++11列表初始化的本意是想实现⼀个⼤统⼀的初始化⽅式,其次他在有些场景下带来的不少便 利,如容器push/inset多参数构造的对象时,{}初始化会很⽅便。

C++98中⼀般数组和结构体可以⽤{}进⾏初始化:

cpp 复制代码
struct Point
{
    int _x;
    int _y;
};

int main()
{
    int array1[] = { 1, 2, 3, 4, 5 };
    int array2[5] = { 0 };
    Point p1 = { 1, 2 };
    return 0;
}

C++11的列表初始化:

cpp 复制代码
int main()
{
    int array2[]{ 1, 2, 3, 4, 5 };
    Point p2{ 1, 2 };
    std::vector<int> v1={1,2,3,4,5};
    return 0;
}

列表初始化在初始化时,如果出现类型截断,是会报警告或者错误的。

C++11中的std::initializer_list:容器批量初始化

•上⾯的初始化已经很⽅便,但是对象容器初始化还是不太⽅便,⽐如⼀个vector对象,我想⽤N个 值去构造初始化,那么我们得实现很多个构造函数才能⽀持, vector v1 = {1,2,3};vector v2 = {1,2,3,4,5};

• C++11库中提出了⼀个std::initializer_list的类,这个类的本质是底层开⼀个数组,将数据拷⻉ 过来,std::initializer_list内部有两个指针分别指向数组的开始和结束。STL 容器支持 {} 初始化,就是靠它。

cpp 复制代码
// {}列表中可以有任意多个值 
// 这两个写法语义上还是有差别的,第⼀个v1是直接构造,
// 第⼆个v2是构造临时对象+临时对象拷⻉v2+优化为直接构造
 
vector<int> v1({ 1,2,3,4,5 });
vector<int> v2 = { 1,2,3,4,5 };
const vector<int>& v3 = { 1,2,3,4,5 };
// 这⾥是pair对象的{}初始化和map的initializer_list构造结合到⼀起⽤了
 
map<string, string> dict = { {"sort", "排序"}, {"string", "字符串"}};

// initializer_list版本的赋值⽀持 
v1 = { 10,20,30,40,50 };

三、右值引⽤和移动语义

C++98的C++语法中就有引⽤的语法,⽽C++11中新增了的右值引⽤语法特性,C++11之后我们之前学 习的引⽤就叫做左值引⽤。⽆论左值引⽤还是右值引⽤,都是给对象取别名。

3.1 左值和右值

左值 是⼀个表⽰数据的表达式(如变量名或解引⽤的指针),⼀般是有持久状态,存储在内存中,我 们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边 。定义时const 修饰符后的左值,不能给他赋值,但是可以取它的地址

cpp 复制代码
// 左值:可以取地址
// 以下的p、b、c、*p、s、s[0]就是常见的左值
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10;
string s("111111");
s[0] = 'x';

右值 也是⼀个表⽰数据的表达式,要么是字⾯值常量、要么是表达式求值过程中创建的临时对象 ,生命周期只有一行,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址,右值可以实现移动语义、完美转发

cpp 复制代码
// 右值:不能取地址
double x = 1.1, y = 2.2;
// 以下几个10、x + y、fmin(x, y)、string("11111")都是常见的右值
10;
x + y;
fmin(x, y);
string("11111");

左值和右值的核⼼区别就是能否取地址。

3.2 右值引用 &&

  • Type& r1 = x; Type&& rr1 = y; 第⼀个语句就是左值引⽤,左值引⽤就是给左值取别 名,第⼆个就是右值引⽤,同样的道理,右值引⽤就是给右值取别名。
  • 左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值,因为右值具有常属性。
  • 右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值)。

关于move:

move不会改变对象本身属性,下一行还是左值。move底层就是把左值强转成右值。

下面情况:

cpp 复制代码
std::string&& s = std::string("hello");

虽然=右边的是临时对象,因此是右值,但是s本身不是右值而是左值,因此右值引用的变量只是引用的是右值而已,但其本身还是左值。

3.4引⽤延⻓⽣命周期

右值引⽤可⽤于为临时对象延⻓⽣命周期,const的左值引⽤也能延⻓临时对象⽣存期,但这些对象⽆法被修改。

cpp 复制代码
int main()
{
    std::string s1 = "Test";
    // std::string&& r1 = s1; // 错误:不能绑定到左值
    const std::string& r2 = s1 + s1; // OK:到 const 的左值引⽤延⻓⽣存期
    // r2 += "Test"; // 错误:不能通过到 const 的引⽤修改
    std::string&& r3 = s1 + s1; // OK:右值引⽤延⻓⽣存期
    r3 += "Test"; // OK:能通过到⾮ const 的引⽤修改
    std::cout << r3 << '\n';
    return 0;
}

左值引用和右值引用的最终目的是减少拷贝,提高效率。

左值引用还可以修改参数或返回值,方便使用。

3.5左值和右值的参数匹配

在之前C++的函数重载中我们知道,编译器会选择最合适的函数进行调用。

1.带const的对象一定选择带const参数的函数。

2.普通左值可以走带const的也可以走不带const的参数函数,如果两种都有,优先走不带const参数的函数。

3.右值对象可以传带const的或者右值引用的参数,但还是优先右值引用

cpp 复制代码
void f(int& x)
{
    std::cout << "左值引⽤重载 f(" << x << ")\n";
}
 
void f(const int& x)
{
    std::cout << "到 const 的左值引⽤重载 f(" << x << ")\n";
}
 
void f(int&& x)
{
    std::cout << "右值引⽤重载 f(" << x << ")\n";
}
 
int main()
{
    int i = 1;
    const int ci = 2;
    f(i); // 调⽤ f(int&)
    f(ci); // 调⽤ f(const int&)
    f(3); // 调⽤ f(int&&),如果没有 f(int&&) 重载则会调⽤ f(const int&)
    f(std::move(i)); // 调⽤ f(int&&)
    int&& x = 1;
    f(x); // 调⽤ f(int& x)
    f(std::move(x)); // 调⽤ f(int&& x)
    return 0;
}

3.6 移动构造 / 移动赋值

1.移动构造函数是⼀种构造函数,类似拷⻉构造函数,移动构造函数要求第⼀个参数是该类类型的引 ⽤,但是不同的是要求这个参数是右值引⽤,如果还有其他参数,额外的参数必须有缺省值。

  1. 移动赋值是⼀个赋值运算符的重载,他跟拷⻉赋值构成函数重载,类似拷⻉赋值函数,移动赋值函 数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤。

3.对于像string/vector/map这样的深拷⻉的类或者包含深拷⻉的成员变量的类,移动构造和移动赋值才有 意义,因为移动构造和移动赋值的第⼀个参数都是右值引⽤的类型,他的本质是要**"掠夺"**引⽤的 右值对象的资源,⽽不是像拷⻉构造和拷⻉赋值那样去拷⻉资源,从而提⾼效率。

cpp 复制代码
// 移动构造
string(string&& s)
{
   cout << "string(string&& s) -- 移动构造" << endl;
   swap(s);
}

// 移动赋值
string& operator=(string&& s)
{
    cout << "string& operator=(string&& s) -- 移动赋值" << endl;
    swap(s);
    return *this;
}

3.7类型分类(了解)

1.C++11以后,进⼀步对类型进⾏了划分,右值被划分纯右值(purevalue,简称prvalue)和将亡值 (expiring value,简称xvalue)。

  1. 纯右值是指那些字⾯值常量或求值结果相当于字⾯值或是⼀个不具名的临时对象。如:true 、 nullptr 或者类似 形 a 42 、str.substr(1, 2)、str1 + str2 传值返回函数调⽤,或者整型b,a++ ,a+b 等。纯右值和将亡值C++11中提出的,C++11中的纯右值概念划分等价于 C++98中的右值。

3.将亡值是指返回右值引⽤的函数的调⽤表达式和转换为右值引⽤的转换函数的调⽤表达,如 move(x) 、static_cast(x)。

4.泛左值(generalizedvalue,简称glvalue),泛左值包含将亡值和左值。

3.8 引⽤折叠

1.C++中不能直接定义引⽤的引⽤如 int& && r = i; ,这样写会直接报错,通过模板或typedef 中的类型操作可以构成引⽤的引⽤。

  1. 通过模板或typedef中的类型操作可以构成引⽤的引⽤时,这时C++11给出了⼀个引⽤折叠的规 则:右值引⽤的右值引⽤折叠成右值引⽤,所有其他组合均折叠成左值引⽤

下⾯的程序中很好的展⽰了模板和typedef时构成引⽤的引⽤时的引⽤折叠规则,⼤家需要⼀个⼀ 个仔细理解⼀下。

cpp 复制代码
// 由于引用折叠限定,f1实例化以后总是一个左值引用
template<class T>
void f1(T& x)
{}

// 由于引用折叠限定,f2实例化后可以是左值引用,也可以是右值引用
template<class T>
void f2(T&& x)
{}

int main()
{
	typedef int& lref;
	typedef int&& rref;
	int n = 0;

	// 引用折叠
	lref& r1 = n; // r1 的类型是 int&
	lref&& r2 = n; // r2 的类型是 int&
	rref& r3 = n; // r3 的类型是 int&
	rref&& r4 = 1; // r4 的类型是 int&&

	// 没有折叠->实例化为void f1(int& x)
	f1<int>(n);
	//f1<int>(0); // 报错

	// 折叠->实例化为void f1(int& x)
	f1<int&>(n);
	//f1<int&>(0); // 报错

	// 折叠->实例化为void f1(int& x)
	f1<int&&>(n);
	//f1<int&&>(0); // 报错

	// 折叠->实例化为void f1(const int& x)
	f1<const int&>(n);
	f1<const int&>(0);

	// 折叠->实例化为void f1(const int& x)
	f1<const int&&>(n);
	f1<const int&&>(0);

	// 没有折叠->实例化为void f2(int&& x)
	//f2<int>(n); // 报错
	f2<int>(0);

	// 折叠->实例化为void f2(int& x)
	f2<int&>(n);
	//f2<int&>(0); // 报错

	// 折叠->实例化为void f2(int&& x)
	//f2<int&&>(n); // 报错
	f2<int&&>(0);

	return 0;
}

下面T&& t参数看起来是右值引用参数,但是由于引用折叠的规则,它传递左值时就是左值引用,传递右值时就是右值引用,所以有些地方也把这种函数模板的参数叫做万能引用。

Function(T&&t)函数模板程序中,假设实参是int右值,模板参数T的推导int,实参是int左值,模 板参数T的推导int&,再结合引⽤折叠规则,就实现了实参是左值,实例化出左值引⽤版本形参的 Function,实参是右值,实例化出右值引⽤版本形参的Function。

cpp 复制代码
 //万能引用 
template<class T>
void Function(T&& t)
{
	int a = 0;
	T x = a;
	//x++;

	cout << &a << endl;
	cout << &x << endl << endl;
} 
int main()
{
    // 10是右值,推导出T为int,模板实例化为void Function(int&& t),没有折叠
    Function(10);

    int a;
	// a是左值,推导出T为int&,引用折叠,模板实例化为void Function(int& t)
	Function(a); // 左值

	std::move(a)//是右值,推导出T为int,模板实例化为void Function(int&& t),没有折叠
	Function(std::move(a));

	const int b = 8;
	// b是左值,推导出T为const int&,引用折叠,模板实例化为void Function(const int& t)
	// 所以Function内部会编译报错,x不能++
	Function(b);    // const 左值

	std::move(b)//右值,推导出T为const int,模板实例化为void Function(const int&& t),没有折叠
	// 所以Function内部会编译报错,x不能++
	Function(std::move(b)); // const 右值

	return 0;
}

3.9完美转发

完美转发forward本质是⼀个函数模板,他主要还是通过引⽤折叠的⽅式实现,下⾯⽰例中传递给 Function的实参是右值,T被推导为int,没有折叠,forward内部t被强转为右值引⽤返回;传递给 Function的实参是左值,T被推导为int&,引⽤折叠为左值引⽤,forward内部t被强转为左值引⽤ 返回。

如果没有完美转发的情况:

cpp 复制代码
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }

template < class T>
void Function(T&& t)
{
	Fun(t);//传右值会属性退化成左值,达不到我们要的效果
}

有完美转发的情况:

cpp 复制代码
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }

template < class T>
void Function(T&& t)
{
	Fun(forward<T>(t));//可以保持属性
}

四、 可变参数模板

4.1 基本语法及原理

C++11⽀持可变参数模板,也就是说⽀持可变数量参数的函数模板和类模板,可变数⽬的参数被称 为参数包,存在两种参数包:模板参数包,表⽰零或多个模板参数;函数参数包:表⽰零或多个函 数参数。

• template void Func(Args... args) {}

• template void Func(Args&... args) {}

• template void Func(Args&&... args) {}

在模板参数列表中,class...或 typename...指出接下来的参数表⽰零或多个类型列表;

在函数参数列表中,类型名后⾯跟...指出 接下来表⽰零或多个形参对象列表;

函数参数包可以⽤左值引⽤或右值引⽤表⽰,跟前⾯普通模板 ⼀样,每个参数实例化时遵循引⽤折叠规则。

可以使⽤sizeof...运算符去计算参数包中参数的个数。

cpp 复制代码
template<typename... Args>
void print(Args... args) 
{
    // 使用 sizeof...(args) 获取参数个数
    std::cout << sizeof...(args) << std::endl;
}

下面用例子讲解一下:

cpp 复制代码
template <class ...Args>
void Print(Args&&... args)
{
	cout << sizeof...(args) << endl;
}
int main()
{
	double x = 2.2;
	Print();                       // 包⾥有0个参数
	Print(1);	                   // 包⾥有1个参数
	Print(1, string("xxxxx"));     // 包⾥有2个参数
	Print(1.1, string("xxxxx"), x);// 包⾥有3个参数    

	return 0;
}
// 原理1:编译本质这⾥会结合引⽤折叠规则实例化出以下四个函数
void Print();
void Print(int&& arg1);
void Print(int&& arg1, string&& arg2);
void Print(double&& arg1, string&& arg2, double& arg3);
    

// 原理2:更本质去看没有可变参数模板,我们实现出下面这样的多个
// 函数模板才能⽀持这⾥的功能,有了可变参数模板,我们进⼀
// 步被解放,他是类型泛化基础上叠加数量变化,让我们泛型编程更灵活。
void Print();

template <class T1>
void Print(T1&& arg1);

template <class T1, class T2>
void Print(T1 && arg1, T2 && arg2);

template <class T1, class T2, class T3>
void Print(T1&& arg1, T2&& arg2, T3&& arg3);

总结:

模板:一个函数模板实例化出多个不同类型参数的函数。

可变参数模板:一个可变参数模板函数实例化出多个不同参数个数的模板函数。可看作是模板的模板。

4.2 包扩展

像上面代码例子,如果我们不仅要得到模板参数个数,还想解析出参数包里的内容,要怎么做呢?

由此C++11新增了包扩展

cpp 复制代码
 //包扩展(解析出参数包的内容)
void ShowList()
{
	// 编译器时递归的终止条件,参数包是0个时,直接匹配这个函数
	cout << endl;
}

template <class T, class ...Args>
void ShowList(T&& x, Args&&... args)
{
	// 运行时
	/*if (sizeof...(args) == 0)
		return;*/

	cout << x << " ";
	// args是N个参数的参数包
	// 调用ShowList,参数包的第一个传给x,剩下N-1传给第二个参数包
	ShowList(args...);
}

template <class ...Args>
void Print(Args&&... args)
{
	ShowList(args...);
}
int main()
{
	double x = 2.2;
	Print(); // 包里有0个参数
	Print(1); // 包里有1个参数
	Print(1, string("xxxxx")); // 包里有2个参数
	Print(1.1, string("xxxxx"), x); // 包里有3个参数

	return 0;
}

对于⼀个参数包,我们除了能计算他的参数个数,我们能做的唯⼀的事情就是扩展它,当扩展⼀个 包时,我们还要提供⽤于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元 素应⽤模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(...)来触发扩展操作。底层 的实现细节如下图所⽰。

4.3 empalce系列接⼝

C++11以后STL容器新增了empalce系列的接⼝,empalce系列的接⼝均为模板可变参数,功能上 兼容push和insert系列,但是empalce还⽀持新玩法,假设容器为container,empalce还⽀持 直接插⼊构造T对象的参数,这样有些场景会更⾼效⼀些,可以直接在容器空间上构造T对象。

cpp 复制代码
template <class... Args> void emplace_back (Args&&... args);
template <class... Args> iterator emplace (const_iterator position,
Args&&... args);
cpp 复制代码
int main()
{
    list<pair<string,int>> lt;
    lt.push_back({"苹果",1});
    lt.emplace_back("苹果,1);
}

上面例子我们使用push_back需要先创造一个临时对象才能插入。但是emplace由于支持可变参数,我们可以直接按照顺序插入元素,跳过了临时对象创建这一过程。

传递参数包过程中,如果是 Args&&... args 的参数包,要⽤完美转发参数包,⽅式如下 std::forward(args)... ,否则编译时包扩展后右值引⽤变量表达式就变成了左 值。

emplace_back总体⽽⾔是更⾼效,推荐以后使⽤emplace系列替代insert和push系列。

总结:emplace系列兼容push系列和insert的功能。

部分场景下emplace可以直接构造,push和insert是构造+移动构造或构造+拷贝构造,所以emplace综合而言更好用更强大。

五、新的类功能

  1. 原来C++类中,有6个默认成员函数:构造函数/析构函数/拷⻉构造函数/拷⻉赋值重载/取地址重 载/const取地址重载,最后重要的是前4个,后两个⽤处不⼤,默认成员函数就是我们不写编译器 会⽣成⼀个默认的。C++11新增了两个默认成员函数,移动构造函数和移动赋值运算符重载

  2. 如果你没有⾃⼰实现移动构造函数,且没有实现析构函数、拷⻉构造、拷⻉赋值重载中的任意⼀ 个。那么编译器会⾃动⽣成⼀个默认移动构造。默认⽣成的移动构造函数,对于内置类型成员会执 ⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调⽤ 移动构造,没有实现就调⽤拷⻉构造。

3.如果你没有⾃⼰实现移动赋值重载函数,没有实现析构函数、拷⻉构造、拷⻉赋值重载中的任意 ⼀个,那么编译器会⾃动⽣成⼀个默认移动赋值。默认⽣成的移动构造函数,对于内置类型成员会 执⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调 ⽤移动赋值,没有实现就调⽤拷⻉赋值。(默认移动赋值跟上⾯移动构造完全类似)

4.如果你提供了移动构造或者移动赋值,编译器不会⾃动提供拷⻉构造和拷⻉赋值。

5.1 成员变量声明时给缺省值

成员变量声明时给缺省值是给初始化列表⽤的,如果没有显⽰在初始化列表初始化,就会在初始化列 表⽤这个却绳⼦初始化,这个我们在类和对象部分讲过了,忘了就去复习吧。

5.2defult和delete

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

• 如果能想要限制某些默认函数的⽣成,在C++98中,是该函数设置成private,并且只声明补丁已, 这样只要其他⼈想要调⽤就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语 法指⽰编译器不⽣成对应函数的默认版本,称=delete修饰的函数为删除函数。

cpp 复制代码
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(Person&& p) = default;//原本不会生成,现在强制生成
    //Person(const Person& p) = delete;
private:
    bit::string _name;
    int _age;
};

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

5.3final与override

final用于继承当中修饰类或派生类的虚函数,意思为该类无法被继承/该虚函数不能被重写。

override用于修饰派生类的虚函数,强制检查是否重写基类的虚函数。

这个我们在继承和多态章节已经进⾏了详细讲过了,忘了就去复习吧。

六、lambda

1.lambda 表达式本质是⼀个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。

2.lambda 表达式语法使⽤层⽽⾔没有类型,所以我们⼀般是⽤auto或者模板参数定义的对象去接收lambda对象。

3.lambda表达式格式:

cpp 复制代码
[capture-list](parameters) -> return_type {function body }
[捕捉列表]     (参数列表)   -> 返回类型     {函数体}

// 1、捕捉为空也不能省略
// 2、参数为空可以省略
// 3、返回值可以省略,可以通过返回对象自动推导
// 4、函数体不能省略

使用示例:

cpp 复制代码
auto add = [](int x, int y)->int {return x + y; };
cout << add(1, 2) << endl;

(int x,int y)其实就是函数的参数;->int则是函数的返回值类型,也可以不写让系统去推导;

{}里面则是函数体。

6.1捕捉列表

capture-list\]为lambda的捕捉列表。 •lambda 表达式中默认只能⽤ lambda 函数体和参数中的变量,如果想⽤外层作⽤域中的变量就 需要进⾏捕捉。 • 第⼀种捕捉⽅式是在捕捉列表中显⽰的传值捕捉和传引⽤捕捉,捕捉的多个变量⽤逗号分割。\[x,y,\&z\]表⽰x和y值捕捉,z引⽤捕捉。 • 第⼆种捕捉⽅式是在捕捉列表中隐式捕捉,我们在捕捉列表写⼀个=表⽰隐式值捕捉,在捕捉列表 写⼀个\&表⽰隐式引⽤捕捉,这样我们 lambda 表达式中⽤了那些变量,编译器就会⾃动捕捉那些 变量。 • 第三种捕捉⽅式是在捕捉列表中混合使⽤隐式捕捉和显⽰捕捉。\[=,\&x\]表⽰其他变量隐式值捕捉, x引⽤捕捉;\[\&,x,y\]表⽰其他变量引⽤捕捉,x和y值捕捉。**当使⽤混合捕捉时,第⼀个元素必须是 \&或=,并且\&混合捕捉时,后⾯的捕捉变量必须是值捕捉,同理=混合捕捉时,后⾯的捕捉变量必 须是引⽤捕捉。** ```cpp int main() { int a = 0, b = 1, c = 2, d = 3; auto func1 = [a, &b] { // 只能⽤当前lambda局部域和捕捉的对象和全局对象 // 值捕捉的变量不能修改,引⽤捕捉的变量可以修改 //a++; b++; int ret = a + b; return ret; }; cout << func1() << endl; // 隐式值捕捉 // ⽤了哪些变量就捕捉哪些变量 auto func2 = [=] { int ret = a + b + c; return ret; }; cout << func2() << endl; } ``` ### 6.2 lambda的应⽤ • 在学习 lambda 表达式以前,我们的使⽤的可调⽤对象只有函数指针和仿函数对象,函数指针的 类型定义起来⽐较⿇烦,仿函数要定义⼀个类,相对会⽐较⿇烦。使⽤lambda去定义可调用对象,既简单⼜⽅便。 • lambda 在很多其他地⽅⽤起来也很好⽤。⽐如线程中定义线程的执⾏函数逻辑,智能指针中定 制删除器等, lambda 的应⽤还是很⼴泛的,以后我们会不断接触到。 ```cpp #include #include struct Goods { string _name; // 名字 double _price; // 价格 int _evaluate; // 评价 // ... Goods(const char* str, double price, int evaluate) :_name(str) , _price(price) , _evaluate(evaluate) { } }; 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; } }; int main() { vector v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3}, { "菠萝", 1.5, 4 } }; // 类似这样的场景,我们实现仿函数对象或者函数指针支持商品中 // 不同项的比较,相对还是比较麻烦的,那么这里lambda就很好用了 //仿函数: // 价格升序 //sort(v.begin(), v.end(), Compare1()); // 价格降序 //sort(v.begin(), v.end(), Compare2()); //lambda: sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) { return g1._price < g2._price; }); sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) { return g1._price > g2._price; }); sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) { return g1._evaluate < g2._evaluate; }); sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) { return g1._evaluate > g2._evaluate; }); return 0; } ``` ### 6.3lambda的原理 lambda 的原理和范围for很像,编译后从汇编指令层的⻆度看,压根就没有 lambda 和范围for 这样的东西。范围for底层是迭代器,⽽lambda底层是仿函数对象,也就说我们写了⼀个lambda 以后,编译器会⽣成⼀个对应的仿函数的**类**。 ## 七、包装器 ### 7.1 function std::function 是⼀个类模板,也是⼀个包装器。 std::function 的实例对象可以包装存 储其他的可以调⽤对象,包括函数指针、仿函数、 象被称为 lambda 、bind 表达式等,存储的可调对象被称为 std::function 的⽬标。若 std::function 不含⽬标,则称它为空。调⽤空 std::function 的⽬标导致抛出std::bad_function_call异常。 ```cpp #include template class function; template class function; ``` 以上是 function 的原型,他被定义头⽂件中。 包装器就类似于之前的函数指针,只不过包装器不仅能实现函数指针的作用,只要能够调用,就可以使用包装器。 ```cpp #include #include function func; func = [](int a, int b) { return a + b; }; cout << func(2, 3); // 输出 5 ``` function内部的\即为对象的返回值,(int,int)为参数列表。 function也能组成数组,要求就是返回值和参数都相同的可调用对象,因此和函数指针极为相似。 ```cpp #include #include #include auto func1 = [](int x, int y)->int {return x + y; }; auto func2 = [](int x, int y)->int {return x - y; }; auto func3 = [](int x, int y)->int {return x * y; }; auto func4 = [](int x, int y)->int {return x / y; }; function func[4] = { func1,func2,func3,func4 }; vector v = { 5,4,3,2,1 }; sort(v.begin(), v.end(), [](int x, int y)->int {return x < y; }); for (auto& it : v) { cout << it << " "; } cout << func[0](1, 2) << endl; ``` ### 7.2bind bind 是⼀个函数模板,它也是⼀个可调⽤对象的包装器,可以把他看做⼀个函数适配器,对接收 的fn可调⽤对象进⾏处理后返回⼀个可调⽤对象。 bind 可以⽤来调整参数个数和参数顺序。bind 也在这个头⽂件中。 ```cpp simple(1) template /* unspecified */ bind (Fn&& fn, Args&&... args); with return type (2) template /* unspecified */ bind (Fn&& fn, Args&&... args); ``` 调⽤bind的⼀般形式: 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为第⼆个参数,以此类推。_1/_2/_3....这些占 位符放到placeholders的⼀个命名空间中。 使用示例: ```cpp #include 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() { auto sub1 = bind(Sub, _1, _2); cout << sub1(10, 5) << endl; auto sub2 = bind(Sub, _2, _1); cout << sub2(10, 5) << endl; // 调整参数个数 (常用) auto sub3 = bind(Sub, 100, _1); cout << sub3(5) << endl; auto sub4 = bind(Sub, _1, 100); cout << sub4(5) << endl; // 分别绑死第123个参数 auto sub5 = bind(SubX, 100, _1, _2); cout << sub5(5, 1) << endl; auto sub6 = bind(SubX, _1, 100, _2); cout << sub6(5, 1) << endl; auto sub7 = bind(SubX, _1, _2, 100); cout << sub7(5, 1) << endl; } ``` bind本身是一个函数模板,也是一个可调用对象,因此可以用function来组成一个数组 必须要注意的是,bind内部必须有可调用对象的所有参数,那么就会发现bind内部还多了个\&c 因为类函数的第一个参数默认是传递**this**指针,因此bind所对应的也必须传递c的地址 ## **八、STL中⼀些变化** 1.C++11新增了一些STL中的容器,但是实际**最有⽤的是unordered_map和unordered_set**。这 两个我们前⾯已经进⾏了⾮常详细的讲解,其他的⼤家了解⼀下即可。 2.STL中容器的新接⼝也不少,**最重要的就是右值引⽤和移动语义相关的push/insert/emplace系列** 接⼝和移动构造和移动赋值,还有initializer_list版本的构造等,这些前⾯都讲过了,还有⼀些⽆关 痛痒的如cbegin/cend等需要时查查⽂档即可。 3.容器的范围for遍历,这个在容器部分也讲过了,C++11中已经去除了auto声明自动类型变量的功能,只可以用来进行变量类型推到。

相关推荐
xiaoye-duck4 小时前
【C++:C++11】核心特性实战:详解C++11列表初始化、右值引用与移动语义
开发语言·c++·c++11
睡一觉就好了。4 小时前
二叉搜索树
c++
希望永不加班4 小时前
SpringBoot 事件机制:ApplicationEvent 与监听器
java·开发语言·spring boot·后端·spring
whitelbwwww4 小时前
C++进阶--类和模板
c++
今天又在学代码写BUG口牙4 小时前
MFC 定时器轮询实现按住按钮进度条增加(鼠标悬停/长按检测)
c++·mfc·定时器·鼠标·轮询·长按事件
14年ABAP码农5 小时前
ABAP - call API with x-www-form-urlencoded
开发语言
SuniaWang5 小时前
Java 17实战:Record与密封类的黄金搭档
java·开发语言·python
2401_827499995 小时前
python项目实战10-网络机器人03
开发语言·python·php
AIminminHu5 小时前
OpenGL渲染与几何内核那点事-项目实践理论补充(三-1-(3):番外篇-当你的CAD打开“怪兽级”STL时:从内存爆炸到零拷贝的极致优化)
开发语言·c++·线程·多线程