C++:现代 C++ 编程基石,C++11核心特性解析与实践

目录

[一.列表初始化{ }](#一.列表初始化{ })

二.initializer_list

[三. 右值引用和移动语义](#三. 右值引用和移动语义)

一).左值和右值

二).左值引用和右值引用

三).引用延长生命周期

四).左值和右值的参数匹配

五).右值引用和移动语义的使用场景

1.左值引用主要使用场景

2.移动构造和移动赋值

3.右值引用和移动语义解决传值返回

[4 右值引用和移动语义在传参中的提效](#4 右值引用和移动语义在传参中的提效)

六).左值右值类型分类

七).引用折叠

八).完美转发

四.可变参数模板

一).基本语法及原理

二).包扩展

五.新的类功能

一).默认的移动构造和移动赋值

二).defult和delete

六.lambda

一).lambda表达式语法

二).捕捉列表

三).lambda的应用

七.包装器

一).function

二).bind

八.const限定符

九.处理类型

一).auto


一.列表初始化{ }

  • C++11以后想统一初始化方式,试图实现一切对象皆可用{}初始化,{}初始化也叫做列表初始化。(注意,不是初始化列表)
  • 内置类型支持,自定义类型也支持,自定义类型本质是类型转换,中间会产生临时对象,最后优化了以后变成直接构造。
  • {}初始化的过程中,可以省略掉=
  • C++11列表初始化的本意是想实现一个大统一的初始化方式,其次他在有些场景下带来的不少便利,如容器push/inset多参数构造的对象时,{}初始化会很方便
cpp 复制代码
//日期类代码

#include<iostream>
#include<vector>
using namespace std;

class Date
{
	public :
	Date(int year = 1, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{
		cout << "Date(int year, int month, int day)" << endl;
	} 
	Date(const Date& d)
		: _year(d._year)
		, _month(d._month)
		, _day(d._day)
	{
		cout << "Date(const Date& d)" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
cpp 复制代码
int main()
{
    // 自定义类型⽀持
    // 这里本质是⽤{ 2025, 1, 1}构造⼀个Date临时对象
    // 临时对象再去拷贝构造d1,编译器优化后合二为一变成{ 2025, 1, 1}直接构造初始化d1
    Date d1 = { 2025, 1, 1 };

	// 这⾥d2引用的是{ 2024, 7, 25 }构造的临时对象
	const Date& d2 = { 2024, 7, 25 };

    // 可以省略掉=
	Date d3{ 2024, 7, 25 };
	const Date& d4{ 2024, 7, 25 };

    // ⽐起有名对象和匿名对象传参,这⾥{}更有性价⽐
    vector<Date> v;
	v.push_back({ 2025, 1, 1 });
	return 0;
}

二.initializer_list

  • 上面的初始化已经很方便,但是对象容器初始化还是不太方便,比如一个vector对象,我想用N个值去构造初始化,那么我们得实现很多个构造函数才能支持,vector<int> v1 ={1,2,3};vector<int> v2 = {1,2,3,4,5};、
  • C++11库中提出了**⼀个std::initializer_list的类**, auto il = { 10, 20, 30 }; // thetype of il is an initializer_list ,这个类的本质是底层开一个数组,将数据拷贝过来,std::initializer_list内部有两个指针分别指向数组的开始和结束
  • 容器支持一个std::initializer_list的构造函数,也就支持任意多个值构成的 {x1,x2,x3...} 进行初始化。STL中的容器支持任意多个值构成的 {x1,x2,x3...} 进行初始化,就是通过std::initializer_list的构造函数支持的
cpp 复制代码
// STL中的容器都增加了⼀个initializer_list的构造

vector(initializer_list<value_type> il, const allocator_type& alloc =allocator_type());
list(initializer_list<value_type> il, const allocator_type& alloc =allocator_type());
map(initializer_list<value_type> il, const key_compare& comp =key_compare(), const allocator_type& alloc = allocator_type());
// ...
cpp 复制代码
template<class T>
class vector {
public:
	typedef T* iterator;
	vector(initializer_list<T> l)  //实现了initializer_list构造函数
	{
		for (auto e : l)
			push_back(e)
	}
private:
	iterator _start = nullptr;
	iterator _finish = nullptr;
	iterator _endofstorage = nullptr;
};
cpp 复制代码
int main()
{
	// {}列表中可以有任意多个值
	// 这两个写法语义上还是有差别的,第?个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", "字符串"} };
	return 0;
}

三. 右值引用和移动语义

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

一).左值和右值

  • 左值 是一个表示数据的表达式(如变量名或解引用的指针 ),一般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。
  • 右值 也是一个表示数据的表达式,要么是字面值常量、要么是表达式求值过程中创建的临时对象等 ,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边右值不能取地址
  • 值得一提的是,左值的英文简写为lvalue,右值的英文简写为rvalue。传统认为它们分别是left value、right value 的缩写。现代C++中,lvalue 被解释为loactor value的缩写,可意为存储在内存中、有明确存储地址可以取地址的对象,而rvalue 被解释为 read value,指的是那些可以提供数据值,但是不可以寻址,例如:临时变量,字面量常量,存储于寄存器中的变量等,也就是说左值和右值的核心区别就是 能否取地址

左值:

cpp 复制代码
int main()
{
	// 左值:可以取地址
	// 以下的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';

	cout << &c << endl;
	cout << (void*)&s[0] << endl;

	return 0;
}

右值:

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

	//cout << &10 << endl;  //无法取地址
	//cout << &(x+y) << endl;
	//cout << &(fmin(x, y)) << endl;
	//cout << &string("11111") << endl;

}

二).左值引用和右值引用

  • 左值引用就是给左值取别名,同样的道理,右值引用就是给右值取别名。
  • 左值引用不能直接引用右值,但是const左值引用可以引用右值。右值引用不能直接引用左值,但是右值引用可以引用move(左值)。
  • move是库里面的一个函数模板,本质内部是进行强制类型转换,当然他还涉及一些引用折叠的知识。
  • 需要注意的是变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量变量表达式的属性是左值。
  • 语法层面看,左值引用和右值引用都是取别名,不开空间。从汇编底层的角度看,底层都是用指针实现的,没什么区别。底层汇编等实现和上层语法表达的意义有时是背离的,所以不要混到一起去理解,互相佐证,这样反而是陷入迷途。

左值引用:

cpp 复制代码
int main()
{
    //左值引用给左值取别名
	int& r1 = b;
	int*& r2 = p;
	int& r3 = *p;
	string& r4 = s;
	char& r5 = s[0];
}

右值引用:

cpp 复制代码
int main()
{
    // 右值引用给右值取别名
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double&& rr3 = fmin(x, y);
	string&& rr4 = string("11111");
}
cpp 复制代码
	// 左值引用不能直接引用右值,但是const左值引用可以引用右值
	const int& rx1 = 10;
	const double& rx2 = x + y;
	const double& rx3 = fmin(x, y);
	const string& rx4 = string("11111");

	// 右值引用不能直接引用左值,但是右值引用可以引用move(左值)
	int&& rrx1 = move(b);
	int* && rrx2 = move(p);
	int&& rrx3 = move(*p);
	string&& rrx4 = move(s);
	string&& rrx5 = (string&&)s;//本质是类型转换


	int&& rr1 = 10;
	// 这里要注意的是,rr1的属性是左值,所以不能再被右值引用绑定,除非move一下
	int& r6 = rr1;
	//int&& rr6 = rr1;  //错误
	int&& rr6 = move(rr1);

三).引用延长生命周期

右值引用可用于为临时对象延长生命周期,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;
}

四).左值和右值的参数匹配

  • C++98中,我们实现⼀个const左值引用作为参数的函数,那么实参传递左值和右值都可以匹配
  • C++11以后,分别重载左值引用、const左值引用、右值引用作为形参的f函数,那么实参是左值会匹配f(左值引用),实参是const左值会匹配f(const 左值引用),实参是右值会匹配f(右值引用)。
  • 右值引用变量在用于表达式时属性是左值
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&& rr1 = 10;
	int a = 20;
	int& r2 = a;

	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), x本身是左值
	f(std::move(x)); // 调用 f(int&& x)

	return 0; 
}

五).右值引用和移动语义的使用场景

1.左值引用主要使用场景

左值引用 主要使用场景是在函数中左值引用传参 和左值引用传返回值时减少拷贝 ,同时还可以修改实参和修改返回对象的价值。左值引用已经解决大多数场景的拷贝效率问题,但是有些场景不能使用传左值引用返回。那么C++11以后这里可以使用右值引用做返回值解决吗?显然是不可能的,因为这里的本质是返回对象是一个局部对象,函数结束这个对象就析构销毁了,右值引用返回也无法概念对象已经析构销毁的事实。

2.移动构造和移动赋值

  • 移动构造函数 是⼀种构造函数,类似拷贝构造函数,移动构造函数要求第一个参数是该类类型的引用 ,但是不同的是要求这个参数是右值引用,如果还有其他参数,额外的参数必须有缺省值。
  • 移动赋值是一个赋值运算符的重载,他跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函数要求第⼀个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。
  • 对于像string/vector这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第一个参数都是右值引勇的类型,他的本质是要"窃取"引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从提高效率。
cpp 复制代码
void swap(string& ss)
{
	::swap(_str, ss._str);
	::swap(_size, ss._size);
	::swap(_capacity, ss._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;
}

3.右值引用和移动语义解决传值返回

  • 右值对象构造,只有拷贝构造,没有移动构造的场景
  • 右值对象构造,有拷贝构造,也有移动构造的场景

当然,还有更进一步的优化

  • 右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景
  • 右值对象赋值,既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的场景

深拷贝的自定义类型:如vector/string/map.....实现移动构造和移动赋值是有很大价值的

4 右值引用和移动语义在传参中的提效

  • 查看STL文档我们发现C++11以后容器的push和insert系列的接口否增加的右值引用版本
  • 当实参是一个左值时,容器内部继续调用拷贝构造进行拷贝,将对象拷贝到容器空间中的对象
  • 当实参是一个右值,容器内部则调用移动构造,右值对象的资源到容器空间的对象上
cpp 复制代码
int main()
{
	std::list<bit::string> lt;
	bit::string s1("111111111111111111111");
	lt.push_back(s1);
	cout << "*************************" << endl;

	lt.push_back(bit::string("22222222222222222222222222222"));
	cout << "*************************" << endl;

	lt.push_back("3333333333333333333333333333");
	cout << "*************************" << endl;

	lt.push_back(move(s1));
	cout << "*************************" << endl;
	return 0;
}

/////////
运行结果
string(char* str)
string(const string& s) --拷贝构造
* ************************

string(char* str)
string(string && s) --移动构造
~string() --析构
* ************************

string(char* str)
string(string && s) --移动构造
~string() --析构
* ************************

string(string && s) --移动构造
* ************************

~string() --析构
~string() --析构
~string() --析构
~string() --析构
~string() --析构

六).左值右值类型分类

  • C++11以后,进一步对类型进行了划分,右值 被划分纯右值 (pure value,简称prvalue)和将亡值(expiring value,简称xvalue)。
  • 纯右值是指那些字面值常量或求值结果相当于字面值或是⼀个不具名的临时对象。如: 42、true、nullptr 或者类似 str.substr(1, 2)、str1 + str2 传值返回函数调用,或者整形 a,b,a++,a+b 等。纯右值和将亡值C++11中提出的,C++11中的纯右值概念划分等价于C++98中的右值
  • 将亡值是指返回右值引用的函数的调用表达式和转换为右值引用的转换函数的调用表达,如move(x)、static_cast<X&&>(x)。
  • 泛左值 (generalized value,简称glvalue),泛左值包含将亡值左值
  • 有名字,就是glvalue;有名字,且不能被move,就是lvalue;有名字,且可以被move,就是xvalu;没有名字,且可以被移动,则是prvalue。

七).引用折叠

  • C++中不能直接定义引用的引用如 int& && r = i; 这样写会直接报错,通过模板或 typedef中的类型操作可以构成引用的引用
  • 通过模板或 typedef 中的类型操作可以构成引用的引用时,这时C++11给出了一个引用折叠的规则:右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用。
cpp 复制代码
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&&
}
  • f2这样的函数模板中,T&& x参数看起来是右值引用参数,但是由于引用折叠的规则他传递左值时就是左值引用,传递右值时就是右值引用,有些地方也把这种函数模板的参数叫做万能引用。
cpp 复制代码
//由于引用折叠限定,f1实例化以后总是一个左值引用
template<class T>
void f1(T& x)
{}

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

int main()
{
	// 没有折叠->实例化为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;
}
  • 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;
}

八).完美转发

  • Function(T&& t)函数模板程序中,传左值实例化以后是左值引用的Function函数,传右值实例化以后是右值引用的Function函数。
  • 变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量表达式的属性是左值,也就是说Function函数中 t 的属性是左值,那么我们把t传递给下一层函数Fun,那么匹配的都是左值引用版本的Fun函数。这里我们想要保持t对象的属性,就需要使用完美转发实现。
  • 完美转发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(forward<T>(t));
}

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;
	// a是左值,推导出T为const int&,引用折叠,模板实例化为void Function(const int&t)
	Function(b);

	// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&&t)
	Function(std::move(b)); // const 右值

	return 0;
}

四.可变参数模板

一).基本语法及原理

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

  • template <class ...Args> void Func(Args... args) {}

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

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

  • 我们用 ... 来指出一个模板参数或函数参数的一个包,在模板参数列表中,class...或

    **typename...**指出接下来的参数表示零或多个类型列表 ;在函数参数列表中,**类型名后面跟...**指出接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板一样,每个参数实例化时遵循引用折叠规则

  • 可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数

  • 我们可以使用(**sizeof...)**运算符去计算参数包中参数的个数

cpp 复制代码
template<class...Args>
int fun(Args...args)
{
	return sizeof...(args);
}

int main()
{
    //编译的本质是使用引用折叠规则实例化出四个函数
	cout<<fun(12,12,13,13);     //4
	cout<<fun(12,12,13);        //3
	cout<<fun(12,12);           //2
	cout<<fun(12);              //1
	return 0;

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

二).包扩展

  • 对于一个参数包,我们除了能计算他的参数个数,我们能做的唯一的事情就是扩展它,当扩展一个包时,我们还要提供用于每个扩展元素的模式,扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(...)来触发扩展操作。
  • C++还支持更复杂的包扩展,直接将参数包依次展开依次作为实参给一个函数去处理。
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;
}

五.新的类功能

一).默认的移动构造和移动赋值

  • 原来C++类中,有6个默认成员函数:构造函数/析构函数/拷贝构造函数/拷贝赋值重载/取地址重载/const 取地址重载,最后重要的是前4个,后两个用处不大,默认成员函数就是我们不写编译器会生成一个默认的。C++11 新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
  • 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
  • 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意⼀个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)。
  • 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值
cpp 复制代码
class Person
{
public:
	Person(const char* name = "", int age = 0)  //构造函数
		: _name(name)
		, _age(age)
	{}
	Person(const Person& p) = default;       // 保留默认拷贝构造

	Person(Person&& p) noexcept   // 移动构造函数 非 const 右值引用
		: _name(std::move(p._name))
		, _age(p._age)
	{}

	Person& operator=(Person&& p)  noexcept {  //移动赋值
		if (this != &p) {
			_name = std::move(p._name);  // 调用 string 的移动赋值
			_age = p._age;
		}
		return *this;
	}

	~Person()
	{}

private:
	string _name;
	int _age;
};


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

二).defult和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) = default;       // 保留默认拷贝构造

	Person(Person&& p) noexcept   // 移动构造函数 非 const 右值引用
		: _name(move(p._name))
		, _age(p._age)
	{}

	Person& operator=(Person&& p)  noexcept {  //移动赋值
		if (this != &p) {
			_name = move(p._name);  // 调用 string 的移动赋值
			_age = p._age;
		}
		return *this;
	}

	~Person() = delete   //  会报错 "_name" 不是类 "Person" 的非静态数据成员或基类
	{}

private:
	string _name;
	int _age;
};

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

当然不止这些,有的已经写到前面的笔记中了。


六.lambda

一).lambda表达式语法

  • lambda 表达式本质是一个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。lambda 表达式语法使用层而言没有类型,所以我们一般是用auto或者模板参数定义的对象去接收 lambda 对象

  • lambda表达式的格式:capture-list (parameters)-> return type {function boby }

  • capture-list : 捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据 来判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下文中的变量供 lambda 函数使用,捕捉列表可以传值和传引用捕捉,捕捉列表为空也不能省略

  • (parameters) :参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连

    同()⼀起省略。

  • ->return type :返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。一般返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。

  • {function boby} :函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略

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

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

	auto func1 = []
	{
		cout << "hello bit" << endl;
		return 0;
	};

	func1();

	return 0;
}

二).捕捉列表

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

int main()
{
	// 只能⽤当前lambda局部域和捕捉的对象和全局对象
	int a = 0, b = 1, c = 2, d = 3;
	auto func1 = [a, &b]
		{
			// 值捕捉的变量不能修改,引⽤捕捉的变量可以修改
			//a++;
			b++;
			int ret = a + b;
			return ret;
		};
	// 隐式值捕捉
	// ⽤了哪些变量就捕捉哪些变量
	auto func2 = [=]
		{
			int ret = a + b + c;
			return ret;
		};

	// 隐式引⽤捕捉
	// ⽤了哪些变量就捕捉哪些变量
	auto func3 = [&]
		{
			a++;
			c++;
			d++;
		};

		func3();
		cout << a << " " << b << " " << c << " " << d << endl;  //1 1 3 4

		// 混合捕捉1
		auto func4 = [&, a, b]
			{
				c++;
				d++;
				return a + b + c + d;
			};

		func4();
		cout << a << " " << b << " " << c << " " << d << endl;//1 1 4 5


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

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

		// 传值捕捉本质是⼀种拷⻉,并且被const修饰了
		// mutable相当于去掉const属性,可以修改了
		// 但是修改了不会影响外⾯被捕捉的值,因为是⼀种拷⻉
		auto func7 = [=]()mutable
			{
				a++;
				b++;
				c++;
				d++;
				return a + b + c + d;
			};
		cout << func7() << endl;//17
		cout << a << " " << b << " " << c << " " << d << endl;//2 2 4 5

		return 0;
}

三).lambda的应用

  • 在学习 lambda 表达式之前,我们的使用的可调用对象只有函数指针和仿函数对象,函数指针的类型定义起来比较麻烦,仿函数要定义一个类,相对会比较麻烦。使用 lambda 去定义可调用对象,既简单又方便。
  • lambda 在很多其他地方用起来也很好用。比如线程中定义线程的执行函数逻辑,智能指针中定制删除器等。
cpp 复制代码
#include<algorithm>
#include <vector>
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<Goods> 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());


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

补充:lambda的原理

  • lambda 的原理和范围for很像,编译后从汇编指令层的角度看,压根就没有 lambda 和范围for这样的东西。范围for底层是迭代器,而lambda底层是仿函数对象,也就说我们写了⼀个lambda 以后,编译器会生成⼀个对应的仿函数的类。
  • 仿函数的类名是编译按一定规则生成的,保证不同的 lambda 生成的类名不同,lambda参数 /返回类型 / 函数体 就是仿函数operator()的 参数 / 返回类型 / 函数体, lambda 的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参,当然隐式捕捉,编译器要看使用哪些就传那些对象。

七.包装器

一).function

  • template <class T> class function;
  • template <class Ret, class... Args> class function<Ret(Args...)>;
  • 他被定义**<functional>** 头文件中。std::function 是一个类模板,也是一个包装器。 std::function 的实例对象可以包装存储其他的可以调用对象,包括函数指针、仿函数、 lambda 、 bind 表达式等,存储的可调用对象被称为 std::function 的目标。若 std::function 不含目标,则称它为空。调用空 std::function 的目标导致抛出 std::bad_function_call 异常。
  • 函数指针、仿函数、 lambda 等可调用对象的类型各不相同, std::function 的优势就是统一类型,对他们都可以进行包装,这样在很多地方就方便声明可调用对象的类型。
cpp 复制代码
#include<functional>

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

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

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)> f1 = f;
	function<int(int, int)> f2 = Functor();
	function<int(int, int)> f3 = [](int a, int b) {return a + b; };  //lambda
	cout << f1(1, 2) << endl;
	cout << f2(1, 2) << endl;
	cout << f3(1, 2) << endl;

	// 包装静态成员函数
	// 成员函数要指定类域并且前面加&才能获取地址
	function<int(int, int)> f4 = &Plus::plusi;
	cout << f4(1, 2) << endl;


	// 包装普通成员函数
	// 普通成员函数还有一个隐含的this指针参数,所以绑定时传对象或者对象的指针过去都可以
	function<double(Plus*, double, double)> f5 = &Plus::plusd;
	Plus pl;
	cout << f5(&pl, 1.111, 1.1) << endl;

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

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

	return 0;
}

二).bind

  • template <class Fn, class... Args> /* unspecified */ bind (Fn&& fn, Args&&... args);
  • template <class Ret, class Fn, class... Args> /* unspecified */ bind (Fn&& fn, Args&&... args);
  • bind 是⼀个函数模板,它也是一个可调用对象的包装器,可以把他看做一个函数适配器对接收的fn可调用对象进行处理后返回一个可调用对象。bind 可以用来调整参数个数和参数顺序。bind 也在<functional>这个头文件中。
  • 调用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 复制代码
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;  //50

	auto sub2 = bind(Sub, _2, _1);
	cout << sub2(10, 5) << endl;  //-50

	// 调整参数个数 (常用)
	auto sub3 = bind(Sub, 100, _1);
	cout << sub3(5) << endl;   //950

	auto sub4 = bind(Sub, _1, 100);
	cout << sub4(5) << endl;   //-950

	//// 分别绑死第123个参数
	auto sub5 = bind(SubX, 100, _1, _2);
	cout << sub5(5, 1) << endl;  //940

	auto sub6 = bind(SubX, _1, 100, _2);
	cout << sub6(5, 1) << endl;  //-960

	auto sub7 = bind(SubX, _1, _2, 100);
	cout << sub7(5, 1) << endl;  //-960

	//// 成员函数对象进行绑死,就不需要每次都传递了
	function<double(Plus&&, double, double)> f6 = &Plus::plusd;
	Plus pd;
	cout << f6(move(pd), 1.1, 1.1) << endl;   //22
	cout << f6(Plus(), 1.1, 1.1) << endl;     //22

	function<double(double, double)> f7 = bind(&Plus::plusd, Plus(), _1, _2);
	cout << f7(1.1, 1.1) << endl;    //22
	return 0;
}

八.const限定符

顶层const和底层const

  • 指针本身是一个对象,它有可以指向另一个对象,因此指针涉及到本身是不是const和指向对象是不是const的问题,C++用为了好区分,把本身被const修饰叫做顶层const,把指向的对象被const修饰叫做底层const。
  • 大多数对象被const修饰都叫顶层const,指针被const修饰时,*左边的const叫底层const,*右边的const叫做顶层const。*左边的const修饰指针指向的对象,*右边的const修饰指针本身。
  • const修饰引用时,这个const是底层const
cpp 复制代码
int main()
{
	int i = 0;
	int* const p1 = &i;  // 顶层const
	const int ci = 42;   // 顶层const
	const int* p2 = &ci; // 底层const
	const int& r = ci;   // 底层const
	return 0;
}

九.处理类型

一).auto

  • auto是一个类型说明符,他让编译器替我们分析表达式的类型,auto x = y + z; 编译器自动根据y+z相加的结果来推导x的类型,在⼀些类型比较长的场景,如迭代器遍历时非常有用。
  • 编译器推导auto类型时,有时候也会和初始值的类型不一样,编译器会适当的改变结果类型,使其更符合初始化规则。首先使用引用类型初始化auto类型时,真正参与初始化的是引用对象的值,所以编译器推导auto为引用对象的类型,而不是引用。其次一个带有const属性的值初始化auto对象推导时忽略掉顶层const,保留底层const。
  • 如果想使用auto推导出顶层const,需要明确的指出: const auto x = ci;
  • auto不能自动推导出引用类型,所以我们如果想将auto推导为引用类型,则需要明确指出。并且带有const属性的值初始化auto对象推导时忽略掉顶层const,保留底层const的规则仍然适用
cpp 复制代码
int main()
{
	int i = 0;
	const int ci = 42;	    // 顶层const
	int* const p1 = &i;     // 顶层const
	const int* p2 = &ci;    // 底层const
	const int& ri1 = ci;    // 底层const
	const int& ri2 = i;		// 底层const
	int& ri3 = i;
	auto r1 = ci;   // r1类型为int,忽略掉顶层const
	r1++;
	auto r2 = p1;   // r2类型为int*,忽略掉顶层const
	r2++;
	auto r3 = p2;   // r3类型为const int*,保留底层const
	//(*r3)++;		// 报错
	auto r4 = ri1;  // r4类型为int,因为引⽤对象初始化auto类型时,推导为引⽤对象的类型,其次会忽略掉顶层const
	r4++;
	auto r5 = ri2;  // r5类型为int,因为引⽤对象初始化auto类型时,推导为引⽤对象的类型
	r5++;
	auto r6 = ri3; // r6类型为int,因为引⽤对象初始化auto类型时,推导为引⽤对象的类型
	r6++;
	const auto r7 = ci; // r7类型为const int
	//r7++;			// 报错
	auto& r8 = ri1; // r8类型为const int&,因为const 因为为底层const 被保留
	//r8++;			// 报错
	auto& r9 = ri2; // r9类型为const int&,因为const 因为为底层const 被保留
	//r9++;			// 报错
	auto& r10 = ri3; // r8类型为int&
	//r10++;		// 报错
	return 0;
}
相关推荐
Scott9999HH4 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
2401_841495645 小时前
【操作系统】进程同步与互斥实验报告
c++·算法·操作系统·进程·并发·同步·互斥
fqbqrr5 小时前
2607C++,soui与安卓
c++·soui
码智社5 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海5 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖6 小时前
JDK 26 新特性详解
java·开发语言
马优晨6 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
a1117768 小时前
2FA 验证码生成器(github登录验证 app)
笔记·学习
我的xiaodoujiao8 小时前
API 接口自动化测试详细图文教程学习系列32--Allure测试报告2
python·学习·测试工具·pytest
fqbqrr8 小时前
2607C++,使用微软detours勾挂工具
c++