【C++】C++11(二)

今天我们主要围绕右值进行炸开展开讲解!

目录

右值引用以及移动语义:

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

无论左值引用还是右值引用,都是给对象取别名。

左值与左值引用:

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

右值与右值引用:

右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值不能取地址。右值引用就是对右值的引用,给右值取别名。


注意 :右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,是不是感觉很神奇,这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。

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

左值引用总结:

  1. 左值引用只能引用左值,不能引用右值。
  2. 但是const左值引用既可引用左值,也可引用右值。
cpp 复制代码
int main()
{
	// 左值引用只能引用左值,不能引用右值。
	int a = 10;
	int& ra = a;
	//int& ra2 = 10;  // 编译失败,因为10是右值
	// const左值引用既可引用左值,也可引用右值。
	const int& ra3 = 10;
	const int& ra4 = a;
	return 0;
}

右值引用总结:

  1. 右值引用只能右值,不能引用左值。
  2. 但是右值引用可以move以后的左值。
cpp 复制代码
int main()
{
	// 右值引用只能右值,不能引用左值。
	int&& r1 = 10;
	// error C2440: "初始化": 无法从"int"转换为"int &&"
	// message : 无法将左值绑定到右值引用
	int a = 10;
	int&& r2 = a;

	// 右值引用可以引用move以后的左值
	int&& r3 = std::move(a);

	return 0;
}

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

那么左值引用既可以引用左值,加上const也可以引用右值,为什么要搞出来一个右值引用呢?

先说结论:是为了进行区分左值还是右值!编译器时钟都会自动匹配最适合的。

那么这么区分的意义是什么呢?


那我们需要先普及一个小知识:

右值通常分为

  • 纯右值
  • 将亡值

纯右值通常就是那些字面常量,

而将亡值通常是针对自定义类型的,为什么叫将亡值呢?

自定义类型右值对象一般是匿名对象、返回值、临时对象等,这些对象立马就会被释放

注意:内置类型也有将亡值哦


下图是个简要描述右值引用大概得用途,但是并没有详细说明怎么做到的,随后会通过代码一步一步分析:

我们现在有个string类(不完整)

cpp 复制代码
namespace cyc
{
	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 s1(_str);
			s1 += ch;
			return s1;
		}
		
		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的\0
	};
}

因为我们可以使用右值引用来区分左值与右值,便可以对右值进行一些小心思!


比如拷贝构造与赋值运算符重载

我们先来看移动拷贝

cpp 复制代码
string(string&& s)
	:_str(nullptr)
{
	cout << "string(string&& s) -- 移动拷贝" << endl;
	swap(s);
}

没错,就这短短的一段代码就完成了资源的转移!

cpp 复制代码
int main()
{
	cyc::string s1("hello world");

	cyc::string ret1 = s1; // 左值
	cyc::string ret2 = s1 + '!';// 右值

	return 0;
}

当没有写移动构造时:

当写了时:

这里就构成了移动构造。

我们简单来看一下他是怎样优化的,优化前后是怎样的

现在来看一看移动赋值:

cpp 复制代码
string& operator=(string&& s)
{
	cout << "string& operator=(string&& s) -- 移动语义" << endl;
	swap(s);
	return *this;
}

当没有实现移动构造与移动赋值时:

为什么会出现3次深拷贝呢?

因为我们的赋值本质上是复用拷贝构造(现代写法)。

再来看实现了移动构造与移动赋值

同样,我们一样分析一下编译器是如何进行优化的

于是此时我们可以进行一下总结:

左值引用与右值引用都是给对象取别名。

左值引用是直接减少拷贝,但有一些问题没有解决,比如传局部变量返回,对象拷贝问题。

而右值引用是间接减少拷贝,因为我们右值引用的存在,我们可以进行区分并对右值进行一些别的操作,比如移动构造,移动赋值(本质是资源的转移)。

STL中的验证(VS下)

我们的STL中也都存在了移动拷贝,移动赋值...

c++官网可查哦

可以清楚的看到将s1(原本为左值)move后进行拷贝构造,直接将资源进行了转移!

同样,移动赋值也是如此。

但是不仅仅只有如上,对于STL插入接口也有右值引用版本!

一个关于链表插入的形象示意图,可以看到我们push时直接将资源转移,而不是进行拷贝构造

完美转发:

好抽象的名字哈哈

观察如下代码与结果,为什么和我们当前的理解不一致?

明明存在的右值哪里去了,为什么右值属性丢失了呢?

回忆一下我们之前说过引用右值后,可以修改引用,这是不是太违背常理了呢?

其实不然,这样是有很大的深意de!

如果还是引用后还是右值,那么说明此时这个对象是不可修改的,那我们如何能转移这个对象的资源呢?

那么我们此时就有一个困扰了,有些情景下会要传多次右值,比如我们list的push_back(复用insert进行实现),此时就需要完美转发保存他的右值属性,否则虽然可以调用右值引用的push_back,但却无法调用右值引用的insert,最终造成还是左值引用的问题。

此时就需要完美转发来帮助我们!

在模板下使用forward<T>(目标对象)即可

cpp 复制代码
void PerfectForward(T&& t)
{
	Fun(forward<T>(t));
}

关于const引用延长生命周期:

这个是C++11之前必须要有的东西,但有了右值引用后就不在这么必要了。

具体为什么请看讲解~

我们知道匿名对象的生命周期只有一行。

但是const引用后很明显的看到延长了Ref的生命周期。

那么这么做的目的到底是什么呢?

答案在于传参!

我们一般会穿匿名对象给类成员函数,而我们也会进行调用一些其中的成员,如果生命周期只有一行,那还怎么玩呢?

所以const& 延长生命周期是很有必要的。

那么我们直接使用const& 传值返回岂不是一个很妙的想法?哈哈

答案是不可以的,因为出了作用于局部变量就销毁了,即使我们可以访问,但也很有可能访问到的不是我们期望的值。

那我们只使用const& 接收呢?

答案是可以的,原因在于我们引用的是临时对象,而延长了生命周期。

新的类功能:

默认成员函数:

默认成员函数

原来C++类中,有6个默认成员函数:

  1. 构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 拷贝赋值重载
  5. 取地址重载
  6. const 取地址重载

最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。

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

针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:

如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。

如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

cpp 复制代码
class Person
{
public:
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{}
private:
	cyc::string _name;
	int _age;
};
int main()
{
	Person s1;
	Person s2 = s1;
	Person s3 = std::move(s1);
	Person s4;
	s4 = std::move(s2);
	return 0;
}

default:

强制生成默认函数的关键字default:

C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。

cpp 复制代码
class Person
{
public:
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{}
	Person(const Person& p)
		:_name(p._name)
		, _age(p._age)
	{}
	Person(Person&& p) = default;
private:
	cyc::string _name;
	int _age;
};
int main()
{
	Person s1;
	Person s2 = s1;
	Person s3 = std::move(s1);
	return 0;
}

delete:

禁止生成默认函数的关键字delete:

如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。

cpp 复制代码
class Person
{
public:
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{}
	Person(const Person& p) = delete;
private:
	cyc::string _name;
	int _age;
};
int main()
{
	Person s1;
	Person s2 = s1;
	Person s3 = std::move(s1);
	return 0;
}

可变模板参数:

持续新~~~~

相关推荐
爱吃喵的鲤鱼几秒前
linux进程的状态之环境变量
linux·运维·服务器·开发语言·c++
DARLING Zero two♡27 分钟前
关于我、重生到500年前凭借C语言改变世界科技vlog.16——万字详解指针概念及技巧
c语言·开发语言·科技
7年老菜鸡28 分钟前
策略模式(C++)三分钟读懂
c++·qt·策略模式
Gu Gu Study29 分钟前
【用Java学习数据结构系列】泛型上界与通配符上界
java·开发语言
Ni-Guvara37 分钟前
函数对象笔记
c++·算法
似霰41 分钟前
安卓智能指针sp、wp、RefBase浅析
android·c++·binder
芊寻(嵌入式)1 小时前
C转C++学习笔记--基础知识摘录总结
开发语言·c++·笔记·学习
獨枭1 小时前
C++ 项目中使用 .dll 和 .def 文件的操作指南
c++
霁月风1 小时前
设计模式——观察者模式
c++·观察者模式·设计模式
橘色的喵1 小时前
C++编程:避免因编译优化引发的多线程死锁问题
c++·多线程·memory·死锁·内存屏障·内存栅栏·memory barrier