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

目录
  • 一、{}的扩展
    • initializer_list的讲解:
    • 跳转initializer_list实现
  • 二、C++11一些小的更新
    • decltype
    • nullptr
    • 范围for
    • 新容器
  • 三、右值引用
    • 右值真正的用法
  • 完美转发
  • 默认成员函数
  • 总结

一、{}的扩展

在原先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;
}

左值引用总结:

  • 左值引用只能引用左值,不能引用右值。
  • 但是const修饰的左值引用既可以引用左值,又可以引用右值。在没有右值引用的时候就必须采用这种方式了。
  • 左值最重要的特征是都可以取地址,即使自定义类型也有默认的取地址重载。

右值引用总结:
右值引用只能引用右值,不能引用左值。
右值引用可以引用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取地址移动构造,移动赋值。

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

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

总结

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

到此这篇关于C++11语法之右值引用的文章就介绍到这了,更多相关C++11右值引用内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 深入了解c++11 移动语义与右值引用

    1.移动语义 C++11新标准中一个最主要的特性就是提供了移动而非拷贝对象的能力.如此做的好处就是,在某些情况下,对象拷贝后就立即被销毁了,此时如果移动而非拷贝对象会大幅提升性能.参考如下程序: //moveobj.cpp #include <iostream> #include <vector> using namespace std; class Obj { public: Obj(){cout <<"create obj" << e

  • C++11中std::move、std::forward、左右值引用、移动构造函数的测试问题

    关于C++11新特性之std::move.std::forward.左右值引用网上资料已经很多了,我主要针对测试性能做一个测试,梳理一下这些逻辑,首先,左值比较熟悉,右值就是临时变量,意味着使用一次就不会再被使用了.针对这两种值引入了左值引用和右值引用,以及引用折叠的概念. 1.右值引用的举例测试 #include <iostream> using namespace std; ​ //创建一个测试类 class A { public: A() : m_a(55) { } ​ int m_a;

  • C++11的右值引用的具体使用

    C++11 引入了 std::move 语义.右值引用.移动构造和完美转发这些特性.由于这部分篇幅比较长,分为3篇来进行阐述. 在了解这些特性之前,我们先来引入一些问题. 一.问题导入 函数返回值是传值的时候发生几次对象构造.几次拷贝? 函数的形参是值传递的时候发生几次对象构造? 让我们先来看一段代码, // main.cpp #include <iostream> using namespace std; class A{ public: A(){ cout<<"cla

  • 一文搞懂C++11万能引用和右值引用

    目录 前言 正文 万能引用 结语 参考: 前言 我们通过一个问题来进入今天的话题:1.形如 “type&&” 的结构,就是右值引用吗?2.以下哪些属于右值引用? ① void fun(Widget && param);② Widget && var1= Widget();③ auto && var2 = var1;④ template<typename T> void f(std::vector<T>&&

  • C++11 模板参数的“右值引用”是转发引用吗

    在C++11中,&&不再只有逻辑与的含义,还可能是右值引用: void f(int&& i); 但也不尽然,&&还可能是转发引用: template<typename T> void g(T&& obj); "转发引用"(forwarding reference)旧称"通用引用"(universal reference),它的"通用"之处在于你可以拿一个左值绑定给转发引用

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

    目录 一.{}的扩展 initializer_list的讲解: 跳转initializer_list实现 二.C++11一些小的更新 decltype nullptr 范围for 新容器 三.右值引用 右值真正的用法 完美转发 默认成员函数 总结 一.{}的扩展 在原先c++的基础上,C++11扩展了很多初始化的方法. #include<iostream> using namespace std; struct A { int _x; int _y; }; int main() int a[]

  • 浅析C++11中的右值引用、转移语义和完美转发

    1. 左值与右值: C++对于左值和右值没有标准定义,但是有一个被广泛认同的说法:可以取地址的,有名字的,非临时的就是左值;不能取地址的,没有名字的,临时的就是右值. 可见立即数,函数返回的值等都是右值;而非匿名对象(包括变量),函数返回的引用,const对象等都是左值. 从本质上理解,创建和销毁由编译器幕后控制的,程序员只能确保在本行代码有效的,就是右值(包括立即数);而用户创建的,通过作用域规则可知其生存期的,就是左值(包括函数返回的局部变量的引用以及const对象),例如: int& fo

  • 详解C++11中的右值引用与移动语义

    C++11的一个最主要的特性就是可以移动而非拷贝对象的能力.很多情况都会发生对象的拷贝,有时对象拷贝后就立即销毁,在这些情况下,移动而非拷贝对象会大幅度提升性能. 右值与右值引用 为了支持移动操作,新标准引入了一种新的引用类型--右值引用,就是必须绑定到右值的引用.我们通过&&而不是&来获得右值引用.右值引用一个重要的特性就是只能绑定到将要销毁的对象. 左值和右值是表达式的属性,一些表达式生成或要求左值,而另一些则生成或要求右值.一般而言,一个左值表达式表示的是一个对象的身份,而右

  • C++11右值引用和移动语义的实例解析

    目录 基本概念 左值 vs 右值 左值引用 vs 右值引用 右值引用使用场景和意义 左值引用的使用场景 左值引用的短板 右值引用和移动语义 右值引用引用左值 右值引用的其他使用场景 完美转发 万能引用 完美转发保持值的属性 完美转发的使用场景 总结 基本概念 左值 vs 右值 什么是左值? 左值是一个表示数据的表达式,如变量名或解引用的指针. 左值可以被取地址,也可以被修改(const修饰的左值除外). 左值可以出现在赋值符号的左边,也可以出现在赋值符号的右边. int main() { //以

  • C++标准之(ravalue reference) 右值引用介绍

    1.右值引用引入的背景 临时对象的产生和拷贝所带来的效率折损,一直是C++所为人诟病的问题.但是C++标准允许编译器对于临时对象的产生具有完全的自由度,从而发展出了CopyElision.RVO(包括NRVO)等编译器优化技术,它们可以防止某些情况下临时对象产生和拷贝.下面简单地介绍一下CopyElision.RVO,对此不感兴趣的可以直接跳过: (1)CopyElision CopyElision技术是为了防止某些不必要的临时对象产生和拷贝,例如: 复制代码 代码如下: structA{ A(

  • C++右值引用与移动构造函数基础与应用详解

    目录 1.右值引用 1.1左值右值的纯右值将亡值右值 1.2右值引用和左值引用 2.移动构造函数 2.1完美的移动转发 1.右值引用 右值引用是 C++11 引入的与 Lambda 表达式齐名的重要特性之一.它的引入解决了 C++ 中大量的历史遗留问题, 消除了诸如 std::vector.std::string 之类的额外开销, 也才使得函数对象容器 std::function 成为了可能. 1.1左值右值的纯右值将亡值右值 要弄明白右值引用到底是怎么一回事,必须要对左值和右值做一个明确的理解

  • C++11右值引用和转发型引用教程详解

    右值引用 为了解决移动语义及完美转发问题,C++11标准引入了右值引用(rvalue reference)这一重要的新概念.右值引用采用T&&这一语法形式,比传统的引用T&(如今被称作左值引用 lvalue reference)多一个&. 如果把经由T&&这一语法形式所产生的引用类型都叫做右值引用,那么这种广义的右值引用又可分为以下三种类型: 无名右值引用 具名右值引用 转发型引用 无名右值引用和具名右值引用的引入主要是为了解决移动语义问题. 转发型引用的引

  • C++11右值引用和std::move语句实例解析(推荐)

    右值引用(及其支持的Move语意和完美转发)是C++0x将要加入的最重大语言特性之一.从实践角度讲,它能够完美解决C++中长久以来为人所诟病的临时对象效率问题.从语言本身讲,它健全了C++中的引用类型在左值右值方面的缺陷.从库设计者的角度讲,它给库设计者又带来了一把利器.从库使用者的角度讲,不动一兵一卒便可以获得"免费的"效率提升- 下面用实例来深入探讨右值引用. 1.什么是左值,什么是右值,简单说左值可以赋值,右值不可以赋值.以下面代码为例,"A a = getA();&q

随机推荐