C++ 11【右值引用】

💓博主CSDN主页:麻辣韭菜💓

⏩专栏分类:C++修炼之路

🚚代码仓库:C++高阶🚚

🌹关注我🫵带你学习更多C++知识
  🔝🔝

1.C++ 11 简介

目录

1.C++ 11 简介

2. 统一的列表初始化  

2.1 {}初始化  

 2.2 initializer_list

3.1 auto 

3.2 decltype

 4.右值引用和移动语义

 4.2 左值引用与右值引用比较

4.3 右值引用使用场景和意义

4.4 右值引用引用左值及其一些更深入的使用场景分析  

5.完美转化

5.1模板中的&& 万能引用(引用折叠)

 5.2完美转发实际中的使用场景:


C++11是C++编程语言的一个主要更新版本,它在2011年被国际标准化组织(ISO)正式批准为ISO/IEC 14882:2011标准。这个版本引入了许多新特性和改进,旨在提高语言的表达能力、性能和易用性。以下是C++11的一些关键特性:

  1. 自动类型推断(auto关键字):允许编译器自动推断变量的类型,简化了代码编写,尤其是在模板元编程中。

  2. 基于范围的for循环:提供了一种更简洁的方式来遍历容器。

  3. lambda表达式:允许在代码中创建匿名函数,这在算法编程中非常有用。

  4. 智能指针:如std::unique_ptrstd::shared_ptr,提供了自动内存管理,减少了内存泄漏的风险。

  5. 右值引用和移动语义:允许更高效的资源管理,特别是对于临时对象。

  6. 变长模板参数:允许模板函数和类接受任意数量的模板参数。

  7. 委托构造函数:允许构造函数之间相互委托。

  8. 继承构造函数:允许派生类继承基类的构造函数。

  9. 线程库:引入了一个新的线程库,支持多线程编程。

  10. 原子操作和内存模型:提供了对多线程编程的支持,包括原子操作和内存顺序控制。

  11. 用户定义的字面量:允许用户定义新的字面量后缀。

  12. 静态断言:可以在编译时检查条件。

  13. 类型特征和类型遍历:提供了类型信息查询和类型操作的能力。

  14. 统一的初始化列表:简化了对象的初始化。

  15. 新的字符串字面量:如u8"",支持UTF-8编码。

  16. 匿名结构体和类:允许在声明中嵌套结构体和类。

  17. 属性(属性修饰符):如[[deprecated]],允许标记某些特性为已弃用。

  18. 正则表达式库:提供了正则表达式的处理能力。

C++11的这些特性使得C++语言更加强大和灵活,同时也使得编写的代码更加简洁和易于维护。

2. 统一的列表初始化  

2.1 {}初始化  

#include <iostream>


using namespace std;


struct Point
{
	int _x;
	int _y;
};
int main()
{
	int x1 = 1;
	int x2 = { 2 };
	// int x4(1);

	// 可以省略赋值符号
	int x3{ 3 };
	int array1[]{ 1, 2, 3, 4, 5 };
	int array2[5]{ 0 };
	Point p{ 1, 2 };
	// C++11中列表初始化也可以适用于new表达式中
	int* pa = new int[4]{ 0 };

	return 0;
}

​ 

C++11 扩大了用大括号括起的列表 ( 初始化列表 ) 的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号 (=) ,也可不添加

我个人感觉还是添加等号(=),比较好。

class Date
{
public:
	//explicit Date(int year, int month, int day)
	Date(int year, int month, int day)
		:_year(year)
		, _month(month)
		, _day(day)
	{
		cout << "Date(int year, int month, int day)" << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2022, 1, 1); // old style

	// C++11支持的列表初始化,这里会调用构造函数初始化
	Date d2 = { 2024, 5, 20 };
	Date d3 { 2024, 5, 21 };

	return 0;
}

 创建对象时也可以使用列表初始化方式调用构造函数初始化

​ 

 2.2 initializer_list

它提供了一种便捷的方式来接收和使用初始化列表(initializer lists)。初始化列表是一组用花括号 {} 包围的初始化值。

在 C++11 之前,当你需要初始化一个容器或对象时,你通常需要使用单独的参数或者构造函数的多个参数。std::initializer_list 的引入使得你可以一次性传递一个值的列表,这在初始化数组、容器或自定义类型时非常有用。

#include <initializer_list>
#include <iostream>

class MyClass {
public:
    MyClass(std::initializer_list<int> init) {
        for (auto value : init) {
            values.push_back(value);
        }
    }

    void print() const {
        for (auto value : values) {
            std::cout << value << " ";
        }
        std::cout << std::endl;
    }

private:
    std::vector<int> values;
};

int main() {
    MyClass obj1 = {1, 2, 3, 4, 5}; // 使用列表初始化 MyClass
    obj1.print(); // 输出: 1 2 3 4 5

    MyClass obj2{6, 7, 8, 9, 10}; // 直接初始化 MyClass
    obj2.print(); // 输出: 6 7 8 9 10

    return 0;
}

 std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=

的参数,这样就可以用大括号赋值

class Date
{
public:
	//explicit Date(int year, int month, int day)
	Date(int year, int month, int day)
		:_year(year)
		, _month(month)
		, _day(day)
	{
		cout << "Date(int year, int month, int day)" << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	vector<int> v1 = { 1,2,3,4,5 };
	vector<int> v2 = { 10,20,30 };
	vector<int> v3 = { 10,20,30,1,1,2,2,2,2,2,2,1,1,1,1,1,1,1,1,2,1,1,2 };

	list<int> lt1 = { 1,2,3,4,5 };
	list<int> lt2 = { 10,20,30 };

	auto i1 = { 10,20,30,1,1,2,2,2,2,2,2,1,1,1,1,1,1,1,1,2,1,1,2 };
	auto i2 = { 10,20,30 };
	cout << typeid(i1).name() << endl;
	cout << typeid(i2).name() << endl;

	initializer_list<int>::iterator it1 = i1.begin();
	initializer_list<int>::iterator it2 = i2.begin();
	cout << it1 << endl;
	cout << it2 << endl;
	 //*it1 = 1;

	initializer_list<int> i3 = { 10,20,30 };
	initializer_list<int>::iterator it3 = i3.begin();
	cout << it3 << endl;

	Date d1(2023,5,20);
	Date d2(2023,5,21);
	// initializer_list<Date>
	vector<Date> vd1 = {d1, d2};
	vector<Date> vd2 = { Date(2023,5,20), Date(2023,5,21) };
	vector<Date> vd3 = { {2023,5,20}, {2023,5,20} };

	map<string, string> dict = { {"sort", "排序"},{"string", "字符串"},{"Date", "日期"} };
	pair<string, string> kv1 = { "Date", "日期" };
	pair<string, string> kv2 { "Date", "日期" };

	return 0;
}

 

3.1 auto 

C++98 auto 是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局
部的变量默认就是自动存储类型,所以 auto 就没什么价值了。 C++11 中废弃 auto 原来的用法,将
其用于实现自动类型腿断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初
始化值的类型。
int main()
{
    int i = 10;
    auto p = &i;
    auto pf = strcpy;
    cout << typeid(p).name() << endl;
    cout << typeid(pf).name() << endl;
    map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
    //map<string, string>::iterator it = dict.begin();
    auto it = dict.begin();
    return 0;
}

3.2 decltype

 关键字decltype将变量的类型声明为表达式指定的类型。

int main()
{
	const int x = 1;
	double y = 2.2;

	cout << typeid(x * y).name() << endl;

	decltype(x * y) ret; // ret的类型是double
	decltype(&x) p;      // p的类型是const int*
	cout << typeid(ret).name() << endl;
	cout << typeid(ret).name() << endl;
	cout << typeid(p).name() << endl;

	// vector存储的类型跟x*y表达式返回值类型一致
	// decltype推导表达式类型,用这个类型实例化模板参数或者定义对象
	vector<decltype(x* y)> v;

	return 0;
}

 

 4.右值引用和移动语义

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

 什么是左值?什么是左值引用?

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

int main()
{
    // 以下的p、b、c、*p都是左值
    int* p = new int(0);
    int b = 1;
    const int c = 2;
    // 以下几个是对上面左值的左值引用
    int*& rp = p;
    int& rb = b;
    const int& rc = c;
    int& pvalue = *p;
    return 0;
}
什么是右值?什么是右值引用?
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值 ( 这个不能是左值引
用返回 ) 等等, 右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能
取地址 。右值引用就是对右值的引用,给右值取别名。

int main()
{
    double x = 1.1, y = 2.2;
    // 以下几个都是常见的右值
    10;
    x + y;
    fmin(x, y);
    // 以下几个都是对右值的右值引用
    int&& rr1 = 10;
    double&& rr2 = x + y;
    double&& rr3 = fmin(x, y);
    // 这里编译会报错:error C2106: “=”: 左操作数必须为左值
    10 = 1;
    x + y = 1;
    fmin(x, y) = 1;
    return 0;
}
需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可
以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地
址,也可以修改rr1
int main()
{
     double x = 1.1, y = 2.2;
     int&& rr1 = 10;
     const double&& rr2 = x + y;
     rr1 = 20;
     rr2 = 5.5;  // 报错
     return 0;
}

 4.2 左值引用与右值引用比较

左值引用总结:
1. 左值引用只能引用左值,不能引用右值。
2. 但是const左值引用既可引用左值,也可引用右值

int main()
{
    // 左值引用只能引用左值,不能引用右值。
    int a = 10;
    int& ra1 = a;   // ra1为a的别名
    //int& ra2 = 10;   // 编译失败,因为10是右值
    // const左值引用既可引用左值,也可引用右值。
    const int& ra3 = 10;
    const int& ra4 = a;
    return 0;
}

右值引用总结:
1. 右值引用只能右值,不能引用左值。
2. 但是右值引用可以move以后的左值。

int main()
{
     // 右值引用只能右值,不能引用左值。
     int&& r1 = 10;
 
     // error C2440: “初始化”: 无法从“int”转换为“int &&”
     // message : 无法将左值绑定到右值引用
     int a = 10;
     int&& r2 = a;
     // 右值引用可以引用move以后的左值
     int&& r3 = std::move(a);
     return 0;
}

4.3 右值引用使用场景和意义

既然左值引用可以引用左值和引用右值,那C++11搞出右值引用岂不是多此一举?不要忘了左值引用还有短板没有解决。

我们通过代码来演示右值引用的意义

通过上面的代码示例我们可以看到 传值a+b会调用参数是右值引用的func函数。当然有人觉得这不能说明右值引用的意义。那如果传值是个自定义类型的参数比如string这样的容器又或者unordered_map 这样的?

比如下面这种场景

#pragma once
#include <iostream>
#include <list>
#include <vector>
#include <cassert>
#include <map>

using namespace std;
namespace gx
{
	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()
		{
			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);
			}
			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}
		//string operator+=(char ch)
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}
		string operator+(char ch)
		{
			string tmp(*this);
			tmp += ch;
			return tmp;
		}
		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的\0
	};
}
#include "String.h"
int main()
{
	gx::string s1("hello world");
	gx::string s2(s1);
	gx::string s3 = (s2 + '!');


	return 0;
}

s2+'!' 是个右值 右值又称为将亡值。将亡值也就意味着执行完这条语句。s2+'!'所在的空间会被析构函数回收。那我们这时再拷贝一个和它一样的大的空间岂不是浪费了?能不能把它的空间直接给s3? 

当然可以 我们在头文件String.h增加这段代码

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

 移动构造代价就比深拷贝的代价低了很多。效率就变高了!

之前的学习我们知道左值引用使用场景:

一是引用传参二是引用返回

但是引用返回如果是函数内的局部对象,是不能用引用返回的。

这就意味局部对象返回时是要拷贝的。那如果返回的局部对象是一颗红黑树?那拷贝就极大降低了效率。 

举个例子 string 的to_string这个函数。

gx::string to_string(int value)
	{
		bool flag = true;
		if (value < 0)
		{
			flag = false;
			value = 0 - value;
		}

		gx::string str;
		while (value > 0)
		{
			int x = value % 10;
			value /= 10;

			str += ('0' + x);
		}

		if (flag == false)
		{
			str += '-';
		}

		std::reverse(str.begin(), str.end());
		return str;
	}

 

那如果是C++11之后的编译器呢?

 

编译器直接优化成移动构造 

右值引用和移动语义解决上述问题:
gx ::string 中增加移动构造, 移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不
用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己
移动构造中没有新开空间,拷贝数据,所以效率提高了。  
不仅仅有移动构造,还有移动赋值:
gx::string 类中增加移动赋值函数,再去调用gx ::to_string(1234) ,不过这次是将
gx::to_string(1234) 返回的右值对象赋值给 ret1 对象,这时调用的是移动构造。
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}

 

这里运行后,我们看到调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象
接收,编译器就没办法优化了。gx::to_string函数中会先用str生成构造生成一个临时对象,但是
我们可以看到,编译器很聪明的在这里把str识别成了右值,调用了移动构造。然后在把这个临时
对象做为gx::to_string函数调用的返回值赋值给ret1,这里调用的移动赋值

 

 C++11后STL中的容器都是增加了移动构造和移动赋值:

这里就列举移动构造感兴趣的可以去cplusplus官网看看。

4.4 右值引用引用左值及其一些更深入的使用场景分析  

按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能
真的需要用右值去引用左值实现移动语义。 当需要用右值引用引用一个左值时,可以通过 move
函数将左值转化为右值 C++11 中, std::move() 函数 位于 头文件中,该函数名字具有迷惑性,
并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义
int main()
{
     gx::string s1("hello world");
     // 这里s1是左值,调用的是拷贝构造
     gx::string s2(s1);
     // 这里我们把s1 move处理以后, 会被当成右值,调用移动构造
     // 但是这里要注意,一般是不要这样用的,因为我们会发现s1的
     // 资源被转移给了s3,s1被置空了。
     gx::string s3(std::move(s1));
     return 0;
}

STL容器插入接口函数也增加了右值引用版本:

以链表为例

 从上面运行结果来看,减少了深拷贝。那就意味着效率就提高了。当然move以后也有风险。s1就悬空了没有了。

一般来说我们写法不会用move而是下面这种 

 

那如果是C++98 那就是拷贝构造。而11之后就是移动构造。

总结:

左值引用减少拷贝,提高效率
右值引用也是减少拷贝,提高效率
但是他们的角度不同,左值引用是直接减少拷贝
右值引用是间接减少拷贝,识别出是左值还是右值,如果是右值,则不再深拷贝,直接移动拷贝!提高效率。

5.完美转化

5.1模板中的&& 万能引用(引用折叠)

 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
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<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;
}

先看上面一段代码的 按照常理 PerfectForward(std::move(a));  PerfectForward(std::move(b));

是会调用void Fun(int&& x) { cout << "右值引用" << endl; }和void Fun(const int&& x) { cout << "const 右值引用" << endl; }函数

 

运行结果却都是左值引用?这是为什么?

因为右值本身就具有常性不可修改 PerfectForward这个函数参数是万能引用 当识别是右值时,编译器发现是右值时,会开辟一段空间,而这个开辟的空间会和传参的值进行资源转换。而在转换的过程中会出现属性丢失。也就是说原本的右值属性变成了左值属性。而这时在fun这函数进行传参t时,而这个t就变成左值。那调用时就会调用参数是左值的fun

那如何解决这个问题?

那就需要用到完美转发 forward<>()

 std::forward<T>(t)在传参的过程中保持了t的原生类型属性。

 5.2完美转发实际中的使用场景:

就比如list 我们使用push_back 时,我们后来又实现了insert这个插入函数

如果push_back函数套用insert这个函数就会出现上面的情况。传参时出现属性丢失。

原本是右值的移动构造就会变成拷贝构造。 效率就降低了。

 

template<class T>
struct ListNode
{
	ListNode* _next = nullptr;
	ListNode* _prev = nullptr;
	T _data;
};
template<class T>
class List
{
	typedef ListNode<T> Node;
public:
	List()
	{
		_head = new Node;
		_head->_next = _head;
		_head->_prev = _head;
	}
	void PushBack(T&& x)
	{
		//Insert(_head, x);
		Insert(_head, std::forward<T>(x));
	}
	void PushFront(T&& x)
	{
		//Insert(_head->_next, x);
		Insert(_head->_next, std::forward<T>(x));
	}
	void Insert(Node* pos, T&& x)
	{
		Node* prev = pos->_prev;
		Node* newnode = new Node;
		newnode->_data = std::forward<T>(x); // 关键位置
		// prev newnode pos
		prev->_next = newnode;
		newnode->_prev = prev;
		newnode->_next = pos;
		pos->_prev = newnode;
	}
	void Insert(Node* pos, const T& x)
	{
		Node* prev = pos->_prev;
		Node* newnode = new Node;
		newnode->_data = x; // 关键位置
		// prev newnode pos
		prev->_next = newnode;
		newnode->_prev = prev;
		newnode->_next = pos;
		pos->_prev = newnode;
	}
private:
	Node* _head;
};
int main()
{
	List<gx::string> lt;
	lt.PushBack("1111");
	lt.PushFront("2222");
	return 0;
}

如果我们把任意一个关键位置的forward的去掉,都会是拷贝构造。 

 右值引用非常的绕,希望大家下去可以多看几遍。有什么不懂的可以私信我。关注我带你学习更多C++知识。下节预告可变参数模板、lambda。也是非常重要!!!

  • 29
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值