C++篇(15)C++11(上)

一、列表初始化

1.1 C++11中的{}

在C++98中,一般数组和结构体可以用{}初始化。但是在C++11之后试图实现一切对象皆可用{}初始化,内置类型支持,自定义类型也支持。{}初始化也叫做列表初始化。{}初始化的过程中,可以省略掉=。

1.2 C++11中的std::initializer_list

C++11库中提出了一个std::initializer_list的类,这个类的本质是底层展开一个数组,将数据拷贝过来,内部有两个指针分别指向数组的开始和结束。

二、右值引用和移动语义

C++98的语法中就有引用的语法,而C++11中新增了右值引用的语法特性。无论是左值引用还是右值引用,都是给对象取别名。

2.1 左值和右值

左值是一个表示数据的表达式(比如变量名或解引用的指针),存储在内存中,可以获取它的地址。左值可以出现在赋值符号的左边,也可以出现在赋值符号的右边。定义时const修饰符后的左值,不能给它赋值,但是可以取它的地址。

右值也是一个表示数据的表达式,要么是字面值常量,要么是表达式求值过程中创建的临时对象等。右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,右值不能取地址

#define  _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

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("1111111");
	s[0] = 'x';

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


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

	/*cout << &10 << endl;
	cout << &(x + y) << endl;
	cout << &(fmin(x, y)) << endl;
	cout << &string("1111") << endl;*/


	return 0;
}

2.2 左值引用和右值引用

Type& r1 = x 就是左值引用,左值引用就是给左值取别名。Type&& rr1 = y 就是右值引用,同理,右值引用就是给右值取别名。

左值引用不能直接引用右值,但是const左值引用可以引用右值;右值引用不能直接引用左值,但是右值引用可以引用move(左值)。

注:move是库里面的一个函数模板,本质内部是进行强制类型转换,当然它还设计一些引用折叠的知识,这个后面会细讲。

#define  _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

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("1111111");
	s[0] = 'x';
	double x = 1.1, y = 2.2;

	//左值引用给左值取别名
	int& r1 = b;
	int*& r2 = p;
	int& r3 = *p;
	string& r4 = s;
	char& r5 = s[0];

	//右值:不能取地址
	//以下几个都是常见的右值
	10;
	x + y;
	fmin(x, y);
	string("1111");

	//右值引用给右值取别名
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double&& rr3 = fmin(x, y);
	string&& rr4 = string("1111");


	//左值引用不能直接引用右值,但是const左值引用可以引用右值
	const int& r6 = 10;
	const string& r7 = string("1111");

	//右值引用不能直接引用左值,但是右值引用可以引用move(左值)
	int&& rr5 = move(b);
	string&& rr6 = move(s);


	return 0;
}

需要注意的是变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量变量表达式的属性是左值。

2.3 引用延长生命周期

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

#define  _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <string>
using namespace std;

int main()
{
	std::string s1 = "Test";
	const std::string& r2 = s1 + s1;
	//r2 += "Test";  //error

	std::string&& r3 = s1 + s1;
	r3 += "Test";

	cout << r3 << endl;

	return 0;
}

2.4 左值和右值的参数匹配

在C++98中,我们实现一个const左值引用作为参数的函数,那么实参传递左值和右值都可以匹配。C++11以后,分别重载左值引用、const左值引用、右值引用作为形参的f函数,那么实参是左值会匹配 f(左值引用),实参是const左值会匹配 f(const 左值引用),实参是右值会匹配 f(右值引用)

2.5 右值引用和移动语义的使用场景

2.5.1 左值引用主要使用场景回顾

左值引用主要使用场景是在函数中左值引用传参和左值引用传返回值时减少拷贝,同时还能修改实参和修改返回对象。左值引用已经能解决大多数场景的拷贝效率问题,但有些场景不能使用传左值引用返回(比如下面的addString和generate函数)。

class Solution
{
public:
	//传值返回需要拷贝
	string addString(string num1, string num2)
	{
		string str;
		// ...

		return str;
	}
};


class Solution
{
public:
    //这里传值返回拷贝代价太大了
	vector<vector<int>> generate(int numRows)
	{
		vector<vector<int>> vv(numRows);
		// ...

		return vv;
	}
};

那么C++11之后这里可以传右值引用作为返回值解决吗?显然不可能,因为这里的本质是返回一个局部对象,函数结束之后这个对象就销毁了。

2.5.2 移动构造和移动赋值

移动构造函数是一种构造函数。类似拷贝构造函数,移动构造函数要求第一个参数是该类型的右值引用,如果还有其它参数,额外的参数必须要有缺省值。

移动赋值是赋值运算符的重载。类似拷贝赋值函数,移动赋值函数要求第一个参数是该类型的右值引用。

对于像string/vector这样的深拷贝类型的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有意义。因为移动构造和移动赋值的第一个参数都是右值引用的类型,本质是要“窃取”引用的右值对象的资源,从而提高效率。

namespace bit
{
	class string
	{
	public:
		// ...

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

		string(string&& s) //移动构造
		{
			swap(s);
		}

		string& operator=(string&& s)
		{
			swap(s);

			return *this;
		}

	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};
}

2.5.3 右值引用和移动语义解决传值返回的问题

namespace bit
{
	string addString(string num1, string num2)
	{
		string str;
		// ...

		return str;
	}

	class string
	{
	public:
		// ...

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

		string(string&& s) //移动构造
		{
			swap(s);
		}

		string& operator=(string&& s)
		{
			swap(s);

			return *this;
		}

	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};
}

int main()
{
	//场景1
	bit::string ret = bit::addString("111111", "2222");

	//场景2
	bit::string ret;
	ret = bit::addString("111111", "2222");

	return 0;
}

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

当实参是一个左值时,容器内部继续调用拷贝构造进行拷贝,将对象拷贝到容器空间中的对象。

当实参是一个右值时,容器内部则继续调用移动构造,右值对象的资源移动到容器空间的对象上。

下面我们把之前模拟实现的list添加上支持右值引用参数版本的push_back和insert。

namespace bit
{
	template<class T>
	struct list_node
	{
		T _data;
		list_node<T>* _prev;
		list_node<T>* _next;

		list_node(const T& x = T())
			:_data(x)
			, _prev(nullptr)
			, _next(nullptr)
		{
		}
	};

	
	template<class T>
	class list
	{
		typedef list_node<T> Node;
	public:
        // ...

		void push_back(const T& x)
		{
			insert(end(), x);
		}

		//右值版本
		void push_back(T&& x)
		{
			insert(end(), x);
		}


		void insert(iterator pos, const T& val)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(val);

			newnode->_next = cur;
			newnode->_prev = prev;
			prev->_next = newnode;
			cur->_prev = newnode;

			++_size;
		}

		//右值版本
		void insert(iterator pos, T&& val)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(val);

			newnode->_next = cur;
			newnode->_prev = prev;
			prev->_next = newnode;
			cur->_prev = newnode;

			++_size;
		}

	private:
		Node* _head;
		size_t _size = 0;
	};
}

那是不是就像上面这样简单地改一下就行了呢,测试一下发现不对:

为什么调用的还是拷贝构造而不是移动构造呢?这就又回到了之前那个问题上,右值引用的本身属性是左值,所以这里要对参数move一下。

namespace bit
{
	template<class T>
	struct list_node
	{
		T _data;
		list_node<T>* _prev;
		list_node<T>* _next;

		list_node(const T& x)
			:_data(x)
			, _prev(nullptr)
			, _next(nullptr)
		{
		}

		list_node(T&& x = T())
			:_data(move(x))
			, _prev(nullptr)
			, _next(nullptr)
		{
		}
	};

	template<class T>
	class list
	{
		typedef list_node<T> Node;
	public:
		// ...
		void swap(list<T>& lt)
		{
			std::swap(_head, lt._head);
			std::swap(_size, lt._size);
		}

		list<T>& operator=(list<T> lt)
		{
			swap(lt);
			return *this;
		}

		~list()
		{
			clear();
			delete _head;
			_head = nullptr;
		}

		void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				it = erase(it);
			}
		}

		void push_back(const T& x)
		{
			insert(end(), x);
		}

		//右值版本
		void push_back(T&& x)
		{
			insert(end(), move(x));
		}

		void insert(iterator pos, const T& val)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(val);

			newnode->_next = cur;
			newnode->_prev = prev;
			prev->_next = newnode;
			cur->_prev = newnode;

			++_size;
		}

		//右值版本
		void insert(iterator pos, T&& val)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(move(val));

			newnode->_next = cur;
			newnode->_prev = prev;
			prev->_next = newnode;
			cur->_prev = newnode;

			++_size;
		}

	private:
		Node* _head;
		size_t _size = 0;
	};
}

2.6 类型分类

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),泛左值包含将亡值和左值。

2.7 引用折叠

C++中不能直接定义引用的引用,比如int&  && r = i,这样写会直接报错。

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

#define  _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

int main()
{
	typedef int& lref;
	//typedef int&& rref;
	using rref = int&&;

	int n = 0;

	lref& r1 = n; //int&
	lref&& r2 = n; //int&
	rref& r3 = n; //int&
	rref&& r4 = 1; //int&&

	return 0;
}

像Function这样的函数模板中,T&&x 参数看起来是右值引用参数,但由于引用折叠的规则,他传递左值时就是左值引用,传递右值时就是右值引用。有些地方也把这种函数模板的参数叫做万能引用

template<class T>
void Function(T&& x)
{
    // ...
}

那这时候就有一个问题,之前模拟实现的list中,我们也实现了一份右值引用的版本push_back,那这里是不是万能引用呢?

namespace bit
{
	// ...
	template<class T>
	class list
	{
		typedef list_node<T> Node;
	public:
		// ...

		//右值版本
		void push_back(T&& x)
		{
			insert(end(), move(x));
		}

		// ...

	private:
		Node* _head;
		size_t _size = 0;
	};
}

当然不是!因为这里的T并不是通过实参传递推导的,而是由list实例化出来的。这里如果想要实现万能引用的话,需要把成员函数也写成模板函数。

namespace bit
{
	// ...
	template<class T>
	class list
	{
		typedef list_node<T> Node;
	public:
		// ...

		//右值版本
        template<class X>
		void push_back(X&& x)
		{
			insert(end(), move(x));
		}

		// ...

	private:
		Node* _head;
		size_t _size = 0;
	};
}

但是这里又会带来一个问题,如果我们传递的参数是一个左值,这里自动推导类型也是个左值引用,但是move之后就变成右值了。我们这里希望能够始终保持该参数的属性,那就需要用到下面的知识。

2.8 完美转发

Function(T&& t) 函数模板程序中,传左值实例化以后是左值引用的Function函数,传右值实例化以后是右值引用的Function函数。但是结合前面的内容,当我们想要保持对象的属性时,就需要使用完美转发来实现。

有了完美转发之后,刚才的问题也就迎刃而解了。

namespace bit
{
	// ...
	template<class T>
	class list
	{
		typedef list_node<T> Node;
	public:
		// ...

		//右值版本
        template<class X>
		void push_back(X&& x)
		{
			insert(end(), forward<X>(x));
		}

		// ...

	private:
		Node* _head;
		size_t _size = 0;
	};
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值