C++11细节

一、认识左值右值

1. 临时对象、匿名对象、字面常量等右值不能被取地址
2. 变量表达式都是左值属性,也就意味着⼀个右值被右值引用绑定后,右值引用变量变
量表达式的属性是左值。

    int b = 1;
    int&& rrx1 = move(b);
    // 这⾥要注意的是,rr1的属性是左值,所以不能再被右值引⽤绑定,除⾮move⼀下
     int&& rrx2 = move(rr1);


3. 加不加const一般和&有关(左值引用)
4.拷贝构造函数:复制资源(深拷贝),而右值引用构造函数(移动构造)是转移资源(浅拷贝 + 资源接管)
5.左值引用不能直接引用右值,但是const左值引用可以引用右值;
右值引用不能直接引用左值,但是右值引用可以引用move(左值)
move(左值):变成右值后调用移动构造,原来左值对象的资源被掠夺了

std::move 的本质是什么?
std::move :核心作用是 “授权” 编译器使用移动语义(调用移动构造 / 移动赋值),但不会触发任何构造、析构或内存释放,自然也不会影响原对象的生命周期。
对象的生命周期由其创建方式(如栈上局部对象、堆上 new 对象、全局对象)和作用域规则决定,与是否被 std::move 转换无关:

若原对象是栈上局部对象(如 string s = "hello";),其生命周期仍到当前作用域结束(如函数返回、块结束),std::move(s) 不会让它提前销毁;
若原对象是堆上对象(如 string* p = new string("hello");),其生命周期仍由 delete p 决定,std::move(*p) 也不会让它提前释放;
若原对象是临时对象(如 string("hello")),其生命周期仍遵循 “临时对象在完整表达式结束后销毁” 的规则(除非被引用延长),std::move(string("hello")) 也不会改变这一点。

6.右值引用可用于为临时对象延长生命周期,const的左值引用也能延长临时对象生存期,但这些对象无法被修改。

#include <iostream>
#include <string>
int main()
{
	std::string s1 = "Test";
	//  std::string&& r1 = s1;     // 错误:右值引用不能绑定到左值      

	const std::string& r2 = s1 + s1;	// OK:到的左值引用延⻓⽣存期
	//  r2 += "Test";  // 错误:不能通过到const的引用修改    
      
	std::string&& r3 = s1 + s1;// OK:右值引用延⻓生存期
	r3 += "Test";	//OK:能通过非const的引用修改    
	std::cout << r3 << '\n';
	return 0;
}

7、移动构造和移动赋值:可以对临时对象等右值资源掠夺,是因为右值被右值引用后这里的s就是一个变量表达式了,是左值,正因为这样所以this指针可以和s交换内容。

// 移动构造 
string(string&& s)
{
	cout << "string(string&& s) -- 移动构造" << endl;
	swap(s);
}

void swap(string& s)
{
	::swap(_str, s._str);
	::swap(_size, s._size);
	::swap(_capacity, s._capacity);
}

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

二、引用折叠

1、使用方法:C++中不能直接定义引用的引用如 int& && r = i; ,这样写会直接报错,通过模板或typedef中的类型操作可以构成引用的引用。C++11给出了⼀个引用折叠的规则:右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用。下面举出实例:

// 由于引⽤折叠限定,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;
}

通过上面的示例代码:比如f1,只要函数模板参数有左值引用,最后的结果都是左值引用;再看f2,当函数模板参数有右值引用,T为左值就是左值引用,为右值就是右值引用。
上面的例子只是用来理解引用折叠的规则,平常都是编译器自动推导T的类型的,比如下面这个例子:

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;

	// a是左值,推导出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;
}

2、完美转发:变量表达式都是左值属性,也就意味着⼀个右值被右值引用绑定后,右值引用变量表达式的属性是左值,也就是说Function函数中t的属性是左值,那么我们把t传递给下⼀层函数Fun,那么匹配的都是左值引用版本的Fun函数。想要保持t对象的属性,就需要使用完美转发实现,就比如下面这个例子:

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); // 右值 

	return 0;
}

不管Function函数的T是int& 还是 int&&,由于表达式都是左值,传入给 Fun(t) 函数的都是左值,只会匹配void Fun(int& x) 或者 void Fun(const int& x) ,这就无法保持 t 对象原来的属性了。

但是用完美转发 Fun(forward<T> t),  如果传递给Function的实参是右值,T被推导为int,forward内部 t 被强转为右值引用返回;如果传递给Function的实参是左值,T被推导为int&,引用折叠为左值引用,forward内部t被强转为左值引用返回。

三、lambda 表达式

1、lambda 表达式本质是⼀个匿名函数对象,底层其实是仿函数,⼀般是用auto去接收 lambda 对象。lambda表达式的格式: [capture-list] (parameters)-> return type { function boby }。
[capture-list]:捕捉列表,捕捉列表能够捕捉上下用中的变量供 lambda 函数使用,捕捉列表可以传值和传引用捕捉。不可省略。因为编译器是根据[] 来判断接下来的代码是否是lambda表达式。
(parameters):参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连
同 () ⼀起省略。
-> return type:返回值类型,没有返回值时此部分可省略(void)。
{ function boby }:函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略。

using namespace std;
int main()
{
	// 一个简单的lambda表达式
	//auto add1 = [](int x, int y)->int {return x + y; };
    // 1、捕捉为空也不能省略
    // 2、返回值可以省略,可以通过返回对象自动推导
	auto add1 = [](int x, int y) {return x + y; };
	cout << add1(1, 2) << endl;

	
	// 2、参数为空可以省略
	// 4、函数体不能省略
	auto func1 = []
		{
			cout << "hello bit" << endl;
			return 0;
		};

	func1();

	int a = 0, b = 1;
	auto swap1 = [](int& x, int& y)
	{
			int tmp = x;
			x = y;
			y = tmp;
	};
	return 0;
}

2、捕捉列表

lambda 表达式中默认只能用 lambda 函数体和参数中的变量,如果想用外层作用域中的变量就需要进行捕捉。

  • 第⼀种捕捉方式是在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分割。[x, y,&z] 表示 x 和 y 值捕捉,z 引用捕捉。
using namespace std;
int main()
{
	// 只能用当前lambda局部域和捕捉的对象和全局对象
	int a = 0, b = 1, c = 2, d = 3;
	auto func1 = [a, &b]
		{
			// 传值捕捉的变量不能修改,引用捕捉的变量可以修改
			//a++;
			b++;
			int ret = a + b;
			return ret;
		};
	cout << func1() << endl;
	cout << b << endl;

	return 0;
}

 

  • 第⼆种捕捉方式是在捕捉列表中隐式捕捉,我们在捕捉列表写⼀个 = 表示隐式值捕捉,在捕捉列表写⼀个&表示隐式引用捕捉,这样我们 lambda 表达式中用了哪些变量,编译器就会自动捕捉哪些变量。
using namespace std;
int main()
{
	// 只能用当前lambda局部域和捕捉的对象和全局对象
	int a = 0, b = 1, c = 2, d = 3;

	//// 隐式值捕捉
	// 用了哪些变量就捕捉哪些变量
	auto func2 = [=]
		{
			int ret = a + b + c;
			return ret;
		};
	cout << func2() << endl;

	//// 隐式引用捕捉
	// 用了哪些变量就捕捉哪些变量
	auto func3 = [&]
		{
			a++;
			c++;
			d++;
		};
	func3();
	cout << a << " " << b << " " << c << " " << d << endl;

	
	return 0;
}

 

  •  第三种捕捉方式是在捕捉列表中混合使用隐式捕捉和显示捕捉。[=, &x]表示其他变量隐式传值捕捉, x引用捕捉;[&, x, y] 表示其他变量引用捕捉,x和y值捕捉。
using namespace std;
int main()
{
	// 只能用当前lambda局部域和捕捉的对象和全局对象
	int a = 0, b = 1, c = 2, d = 3;
	
	// 混合捕捉1
	auto func4 = [&, a, b]
		{
			//a++;
			//b++;
			c++;
			d++;
			return a + b + c + d;
		};
	func4();
	cout << a << " " << b << " " << c << " " << d << endl;


	return 0;
}


• lambda 表达式如果在函数局部域中,他可以捕捉 lambda 位置之前定义的局部变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉, lambda 表达式中可以直接使 用。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。
• 默认情况下, lambda 捕捉列表传值捕捉的过来的对象不能修改, mutable加在参数列表的后面可以取消其常量性,也就说使用该修饰符后,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参。使用该修饰符后,参数列表不可省略(即使参数为空)。
 

using namespace std;
int main()
{
	// 只能用当前lambda局部域和捕捉的对象和全局对象
	int a = 0, b = 1, c = 2, d = 3;

	// 传值捕捉本质是一种拷贝,并且被const修饰了
	// mutable相当于去掉const属性,可以修改了
	// 但是修改了不会影响外面被捕捉的值,因为是一种“拷贝”
	auto func7 = [=]()mutable
		{
			a++;
			b++;
			c++;
			d++;
			return a + b + c + d;
		};

	cout << func7() << endl;
	cout << a << " " << b << " " << c << " " << d << endl;

	return 0;
}

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值