当前位置:主页 > 软件编程 > C代码 >

C++11语法之右值引用的示例讲解

时间:2023-01-06 09:04:43 | 栏目:C代码 | 点击:

一、{}的扩展

在原先c++的基础上,C++11扩展了很多初始化的方法。

#include<iostream>
using namespace std;
struct A
{
	int _x;
	int _y;
};
int main()
	int a[] = { 1,2,3,4,5 };
	
	int a1[] { 1,2,3,4,5 };
	int* p = new int[5]{ 1,2,3,4,5 };
	A b = { 1,2 };//初始化
	A b2[5]{ {1,1},{2,2},{3,3},{4,4},{5,5} };
	A* pb = new A{ 1,2 };
	A* pb2 = new A[5]{ {1,1},{2,2},{3,3},{4,4},{5,5} };
	return 0;
}

结果:

全部初始化正常,vs下指针后面跟数字可以表示显示多少个。

除了上面的 new[]{}我认为是比较有意义的,很好的解决了new的对象没有构造函数又需要同时创建多个对象的场景。

除了上面的,下面的这种方式底层实现不相同。

initializer_list的讲解:

vector<int> v{1,2,3,4};

跳转initializer_list实现

实际上上面就是通过传参给initializer_list对象,这个对象相当于浅拷贝了外部的{1,2,3,4}的头指针和尾指针,这样vector的构造函数就可以通过迭代器遍历的方式一个个的push_back到自己的容器当中。上述过程initializer_list是很高效的,因为他只涉及浅拷贝指针和一个整形。

#include <iostream>
template <class T>
class initializer_list
{
public:
    typedef T         value_type;
    typedef const T&  reference; //注意说明该对象永远为const,不能被外部修改!
    typedef const T&  const_reference;
    typedef size_t    size_type;
    typedef const T*  iterator;  //永远为const类型
    typedef const T*  const_iterator;
private:
    iterator    _M_array; //用于存放用{}初始化列表中的元素
    size_type   _M_len;   //元素的个数
    
    //编译器可以调用private的构造函数!!!
    //构造函数,在调用之前,编译会先在外部准备好一个array,同时把array的地址传入模板
    //并保存在_M_array中
    constexpr initializer_list(const_iterator __a, size_type __l)
    :_M_array(__a),_M_len(__l){};  //注意构造函数被放到private中!
    constexpr initializer_list() : _M_array(0), _M_len(0){} // empty list,无参构造函数
    //size()函数,用于获取元素的个数
    constexpr size_type size() const noexcept {return _M_len;}
    //获取第一个元素
    constexpr const_iterator begin() const noexcept {return _M_array;}
    //最后一个元素的下一个位置
    constexpr const_iterator end() const noexcept
    {
        return begin() + _M_len;
    }  
};

而{}初始化,和{}调用initializer_list组合起来是可以让初始化变得方便起来的,下面的m0用了initializer_list进行初始化,但还是比较麻烦。但m1用了{}进行单个对象初始化加initializer_list的组合之后变得方便快捷起来。

#include<map>
int main()
{
	map<int, int> m0 = { pair<int,int>(1,1), pair<int,int>(2,2), pair<int,int>(3,3) };
	
	map<int, int> m1= { {1,1},{2,2},{3,3} };
	return 0;
}

小总结:

一个自定义类型调用{}初始化,本质是调用对应的构造函数;自定义类型对象可以使用{}初始化,必须要有对应的参数类型和个数;STL容器支持{}初始化,则容器必须有一个initializer_list作为参数的构造函数。

二、C++11一些小的更新

auto:
定义变量前加auto表示自动存储,表示不用管对象的销毁,但是默认定义的就是自动类型,所以这个关键字后面就不这样用了,C++11中改成了自动推导类型。

#include<cstring>
int main()
{
	int i = 10;
	auto p = &i;
	auto pf = strcmp;

	cout << typeid(p).name() << endl;
	cout << typeid(pf).name() << endl;
	return 0;
}

结果:

int *
int (__cdecl*)(char const *,char const *)

decltype

auto只能推导类型,但推导出来的类型不能用来定义对象,decltype解决了这点,推导类型后可以用来定义对象。
decltype(表达式,变量),不能放类型!

#include<cstring>
int main()
{
	int i = 10;
	auto p = &i;
	auto pf = strcmp;

	decltype(p) pi;//int*
	pi = &i;
	cout << *pi << endl;//10
	return 0;
}

nullptr

NULL在C中是0,是int类型。C++11添加nullptr表示((void*)0),避免匹配错参数。

范围for

支持迭代器就支持范围for

新容器

array,没啥用,静态的数组,不支持push_back,支持方括号,里面有assert断言防止越界,保证了安全。

foward_list,没啥用,单链表,只能在节点的后面插入。

unordered_map,很有用,后面讲

unordered_set,很有用,后面讲

三、右值引用

左值
作指示一个数据表达式(变量名或解引用的指针)。
左值可以在赋值符号左右边,右值不能出现在赋值符号的左边。

const修饰符后的左值,不能给他赋值,但是可以取他的地址。左值引用就是给左值的引用,给左值取别名。

左值都是可以获取地址,基本都可以可以赋值
但const修饰的左值,只能获取地址,不能赋值。

右值?
右值也是一个数据的表达式,如字面常量,表达式返回值,传值返回的函数的返回值(不能是左值引用返回)。右值不能取地址,不能出现在赋值符号的左边。

关键看能不能取地址

给右值取别名就用右值引用,右值引用是左值了,放在赋值符号的左边了。

右值不能取地址,但是给右值引用后,引用的变量是可以取地址的,并且可以修改!
右值引用存放的地方在栈的附近。

int main()
{
	int&& rra = 10;
	//不想被修改 const int&& rra
	cout << &rra << endl;
	rra = 100;
	return 0;
}

左值引用总结:

右值引用总结:
右值引用只能引用右值,不能引用左值。
右值引用可以引用move以后的左值。

左值引用可以接着引用左值引用,右值引用不可以。
原因:右值引用放到左边表示他已经是一个左值了,右值引用不能引用左值!

int main()
{
	int a = 10;
	int& ra = a;
	int& rb = ra;
	int&& rra = 10;
	int&& rrb = rra;//err:无法从“int”转换为“int && "
	return 0;
}

匹配问题:

void func(const int& a)
{
	cout << "void func(const int& a)" << endl;
}
void func(int&& a)
	cout << "void func(int&& a)" << endl;
int main()
	int a = 10;
	func(10);
	func(a);
	return 0;

右值在有右值引用会去匹配右值引用版本!

右值真正的用法

本质上引用都是为了减少拷贝,提高效率。而左值引用解决了大部分的场景,但是左值引用在传值返回的时候比较吃力,由右值引用来间接解决。

左值引用在引用传参可以减少拷贝构造,但是返回值的时候避免不了要调用拷贝构造。

传参用左值拷贝和右值拷贝都一样,但是返回值如果用右值引用效率会高,并且通常左值引用面临着对象出了作用域销毁的问题。所以这就是右值引用的一个比较厉害的用法。

返回对象若出了作用域不存在,则用左值引用返回和右值引用返回都是错误的。

std::move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝,所以可以提高利用效率,改善性能。所以当作函数返回值的时候如果对象不存在左值引用和右值引用都会报错!

场景:返回的对象在局部域中栈上存在,返回该对象必须用传值返回,并且有返回对象接受,这个时候编译器优化,将两次拷贝构造优化成一次拷贝构造。

测试用的string类

#include<assert.h>
namespace ljh
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;
		}
		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			//cout << "string(char* str)" << endl;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}
		// s1.swap(s2)
		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}
		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;
			string tmp(s._str);
			swap(tmp);
		}
		// 赋值重载
		string& operator=(const string& s)
		{
			cout << "string& operator=(string s) -- 深拷贝" << endl;
			string tmp(s);
			swap(tmp);
			return *this;
		}
		// 移动构造
		/*string(string&& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(string&& s) -- 移动语义" << endl;
			swap(s);
		}*/
		// 移动赋值
		string& operator=(string&& s)
		{
			cout << "string& operator=(string&& s) -- 移动语义" << endl;
			swap(s);
			return *this;
		}
		~string()
		{
			delete[] _str;
			_str = nullptr;
		}
		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}
		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				strcpy(tmp, _str);
				delete[] _str;
				_str = tmp;
				_capacity = n;
			}
		}
		void push_back(char ch)
		{
			if (_size >= _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}
			++_size;
			_str[_size] = '\0';
		}
		//string operator+=(char ch)
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}
		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的\0
	};
}

临时变量如果是4/8字节,通常在寄存器当中,但是如果是较大的内存,会在调用方的函数栈帧中开辟一块空间用于接受,这就是临时对象。

临时对象存在的必要性
当我们不需要接受返回值,而是对返回的对象进行直接使用,这个时候被调用的函数中的对象出了函数栈帧就销毁了,所以在栈帧销毁前会将对象拷贝到调用方栈帧的一块空间当中,我们可以用函数名对这个临时对象直接进行操作的(通常不能修改这个内存空间,临时变量具有常性)。

分析下面几组图片代码的效率

不可避免的,下面的这个过程必然要调用两次拷贝构造,编译器对于连续拷贝构造优化成不生成临时对象,由func::ss直接拷贝给main的str,我们如果只有前面所学的左值引用,func中的string ss在出了func后销毁,这个时候引用的空间被销毁会出现问题,这个时候显得特别无力。
在连续的构造+拷贝构造会被编译器进行优化,这个优化要看平台,但大部分平台都会做这个处理。

结果:

即使下面这种情况,在main接受没有引用的情况下,依旧会调用一次拷贝构造,跟上面并没有起到一个优化的作用。

结果:

解决方案:添加移动构造,添加一个右值引用版本的构造函数,构造函数内部讲s对象(将亡值)的内容直接跟要构造的对象交换,效率很高!!

string(string&& s):_str(nullptr), _size(0), _capacity(0){cout << "string(string&& s) -- 移动语义" << endl;swap(s);}

有了移动构造,对于上面的案例就变成了一次拷贝构造加一次移动构造。**编译器优化后将ss判断为将亡值,直接移动构造str对象,不产生临时对象了,就只是一次移动构造,效率升高!!**同理移动赋值!

结果:

下面情况是引用+移动构造,但是编译器优化就会判断ss出了作用域还存在,反而会拿ss拷贝构造str,这个时候起不到优化的作用!

结果:

以上采用将对象开辟在堆上或静态区都能够采用引用返回解决问题,但是有一个坏处?
引入多线程的概念,每个线程执行的函数当中若有大量的堆上的数据或者静态区的数据,相当于临界资源变多,要注意访问临界资源要加锁。而每个栈区私有栈,会相对好些

右值:
1、内置类型表达式的右值,纯右值。
2、自定义类型表达式的右值,将亡值。

将亡值:

string(string&& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(string&& s) -- 移动语义" << endl;
			swap(s);
		}
int main()
{
	ljh::string& str = func2();
	vector<ljh::string> v;
	v.push_back("1234656");//传进去的就是右值,是用"1234656"构造一个string对象传入,就是典型的将亡值
}

移动构造:
将亡值在出了生命周期就要销毁了,构造的时候可以将资源转移过要构造的对象,让将亡的对象指向NULL,相当于替它掌管资源。移动构造不能延续对象的生命周期,而是转移资源。且移动构造编译器不优化本质是一次拷贝构造+一次移动构造(从将亡值(此时返回值还是一个左值)给到临时变量),再有临时变量给到返回值接受对象(移动构造);
编译器优化做第一次优化,会将将亡值当作右值,此时要进行两次移动构造,编译器第二次优化,直接进行一次移动构造,去掉生成临时对象的环节。
只有需要深拷贝的场景,移动构造才有意义,跟拷贝构造一样,浅拷贝意义不大。

move的真正意义:
表示别人可以将这个资源进行转移走。

int main()
{
//为了防止这种情况,也要添加移动赋值。
	ljh::string str1;
	str1 = "123456";
}

c++11的算法swap的效率跟容器提供的swap效率一样了。

vector提供的插入的右值引用版本,就是优化了传右值情况,如果C++98则需要拷贝放入,而有右值就可以直接移动构造。两个接口的效率差不多。
大多数容器的插入接口都做了右值引用版本!!

完美转发

模板函数或者模板类用的&&即万能引用。
模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力。而forward才能将这种右值特性保持下去。

但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,

此时右值在万能引用成为左值,可能会造成本身右值可以移动构造,却变成左值只能拷贝构造了。
Fun(std::forward<T>(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; }
void Func(int x) {
 // ......
}
template<typename T>
void PerfectForward(T&& t) {
 Fun(t);
}
int main()
{
 PerfectForward(10);           // 右值
 int a;
 PerfectForward(a);            // 左值
 PerfectForward(std::move(a)); // 右值
 const int b = 8;
 PerfectForward(b);      // const 左值
 PerfectForward(std::move(b)); // const 右值
 return 0; }

默认成员函数

C++11 新增了两个:移动构造函数和移动赋值运算符重载。

现在有8个:构造函数,析构函数,拷贝构造,拷贝赋值,取地址,const取地址移动构造,移动赋值。

移动构造函数的默认生成的要求比较严格:
如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载都没有实现。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型
成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如
实现了移动构造就调用移动构造没有实现就调用拷贝构造
如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
同理移动赋值。

即对于深拷贝的类,最好所有的构造,析构,拷贝,赋值重载,移动拷贝,移动赋值都写上。

总结

左值引用通常在传参和传返回值的过程中减少拷贝,这是利用左值引用的语法特性。一般做不到的部分,通常选择传参的时候传引用也可以解决,不通过返回值接受。
右值引用,一般是在深拷贝的类,实现移动构造和移动赋值,能够解决左值引用无法做到的传返回值的效率问题。

您可能感兴趣的文章:

相关文章