1.C++11

在 2003 年 C++ 标准委员会曾经提交了一份技术勘误表,使得 C++03 这个名字已经取代了 C++98成为 了C++11 之前的最新 C++ 标准名称,不过由于 C++03(TC1) 主要是对 C++98 标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为 C++98/03 标准。

相比于 C++98/03,C++11 则带来了数量可观的变化,其中包含了约 140 个新特性,以及对 C++03 标准中约 600 个缺陷的修正,这使得 C++11 更像是从 C++98/03 中孕育出的一种新语言。

2.列表初始化

列表初始化和使用初始化列表是两个完全不一样的概念
列表初始化是C++11支持的一种定义变量的方式,初始化列表是类成员变量定义的地方)

(1){}初始化

在这里插入图片描述

C++98中,定义变量都需要使用=,但在C++11中,定义变量可以不用=,只用{}大括号就能定义变量(大括号里写变量的初始值)。这个特性在new数组时经常使用
如果初始化的数组存储的元素不是内置类型而是自定义类型,元素和元素之间的初始化要用逗号间隔,并且{}用大括号表示数组中的一个元素。实际上,C++11的列表初始化只推荐在new数组时使用,在其他地方使用会降低代码的可读性

(2)initializer_list

C++11中,一串被用大括号{}引用的数据叫做initializer_list,是C++11新加入的数据结构
在这里插入图片描述
在这里插入图片描述

STL的容器可以支持用列表初始化了,是因为它的内部的构造函数和operator=函数重载了参数是initializer_list的版本,所以当外界使用列表初始化时,内部会识别为initializer_list类型就会去调用特定的构造
在这里插入图片描述
用initializer_list可以为容器赋值,随便打开一个容器的文档,在C++11标准中都能看到形参为initializer_list的构造函数
在这里插入图片描述

用initializer_list为容器赋值的操作也是经常被使用的,这样就不用循环调用插入接口
并且initializer_list的内容不可修改,它指向的内容在常量区

所以,在初始化map对象时,可以不用使用make_pair函数或者构造pair对象,因为map实现了initializer_list的构造函数,数据被识别成 initializer_list 类型后再调用相应的构造函数进行初始化。
{“string”, “字符串”} 和 {“left”, “左”} 会被识别成一个 pair 类型,而这两个使用的 {} 括起来就被识别成 initializer_list 类型,从而去初始化对象
在这里插入图片描述

int main()
			{
				vector<int> v1 = { 1,2,3,4,5 };
				list<int> l1 = { 10, 20, 30 };
				Date d1 = { 2024, 1, 9 };
			
				return 0;
			}

其中,v1 和 l1 的初始化方式是一样的,v1 和 l1 的 {} 内的数据会被识别成 initializer_list 类型。
但是 d1 是多参数构造类型转换,是构造+拷贝构造经过优化之后直接构造({ 2024, 1, 9 }; 会被识别成一个 Date 对象,构造完成之后再去拷贝构造 d1,但是这个过程会被编译器进行优化),之中情况只产生于 {} 内的参数个数和 Date 中的构造函数的参数个数一样的时候。当他们的参数个数不匹配的时候,{} 内也会被识别成 initializer_list 类型,但会由于参数个数不匹配而报错。
当使用大括号对容器进行赋值时 v = {10, 20, 30}; ,这个时候调的是赋值重载,而不是 initializer_list 的构造.

3.右值引用和移动语义

(1)左值引用与右值引用

左值:可以被取地址,可以对它赋值,可以出现赋值符号的左边,const修饰符后的左值,不能给它赋值,但是可以取它的地址
右值:不能被取地址,可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边
左值引用:就是给左值的引用,给左值取别名。
右值引用:就是对右值的引用,给右值取别名。
在这里插入图片描述
左值引用总结:
左值引用只能引用左值,不能引用右值。
但是 const 左值引用既可引用左值,也可引用右值。
右值引用总结:
右值引用只能右值,不能引用左值。
但是右值引用可以引用 move 以后的左值。

move函数改变了左值的语义,使左值变为右值
在这里插入图片描述

右值指的是不能被取地址的值,但引用了一个右值后,右值会被存储到可写数据区中,所以可以修改引用,也能取引用的地址

(2)右值引用使用场景和意义

前面我们可以看到左值引用既可以引用左值和又可以引用右值,那为什么 C++11 还要提出右值引用呢?
我们用模拟实现实现的string举例:

namespace Raja
{
	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;
		}

		const char* c_str() const
		{
			return _str;
		}

		static string to_string(int x)
		{
			Raja::string ret;
			while (x)
			{
				int val = x % 10;
				x /= 10;
				ret += ('0' + val);
			}

			reverse(ret.begin(), ret.end());

			return ret;
		}

	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的\0
	};
}

左值引用的使用场景:

  • 引用传参和引用返回都能提高效率
void func1(Raja::string s)
{}

void func2(const Raja::string& s)
{}

int main()
{
	Raja::string s1("hello world");

	// func2 的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值
	func1(s1);
	func2(s1);

	// string operator+=(char ch) 传值返回存在深拷贝
	// string& operator+=(char ch) 传左值引用没有拷贝提高了效率
	s1 += '!';

	return 0;
}

左值引用的短板:当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。如果是自定义类型的话,会造成深拷贝的代价。这时候右值引用的价值就体现出来了,可以使用右值引用和移动语义解决上述问题。
右值引用和移动语义:移动构造和移动赋值
内置类型的右值为
纯右值

称自定义类型的右值为
将亡值
(为什么是将亡值?自定义类型在作为函数返回值或者隐式类型转换时,会产生一个临时对象,这个临时对象具有常属性,即不能修改,不能取地址,是一个右值,临时变量的作用是:作为中间值,将自己的数据赋值给其他变量,赋值完成后临时变量销毁,即临时变量的生命周期很短)
用一个将亡值构造或赋值一个对象,如果涉及到资源管理,需要用到深拷贝吗?既然将亡值马上会被释放,并且将亡值中的数据是我们想要的,为什么不直接把将亡值的数据掠夺?将自己的资源与将亡值交换,将亡值还会释放你的资源(当你没有资源时,需要把指针置空,否则程序会崩溃)

我们在 Raja::string 中增加移动语义的构造和赋值:

// 移动构造
string(string&& s)
		:_str(nullptr)
		, _size(0)
		, _capacity(0)
 {
     cout << "移动构造" << endl;
     swap(s);
 }
 
// 移动赋值
string& operator=(string&& s)
{
	cout << "string& operator=(string&& s) -- 移动语义" << endl;
	swap(s);
	return *this;
}
int main()
{
	Raja::string ret;
	ret = Raja::string::to_string(10);

	return 0;
}

在这里插入图片描述
没有显示拷贝构造的原因:新版编译器执行了返回值优化( RVO)或着说命名返回值优化(NRVO)。这些优化技术允许编译器消除不必要的拷贝或移动操作,从而提高性能。换句话说,我们在调用值方式返回局部对象函数时,编译器不会创建临时对象,而是在调用者的上下文中直接构造这个对象,也就不用调用拷贝/移动构造函数了。(对象具有复杂的行为或当编译器无法确定优化是安全的时候,编译器可能不会执行 RVO/NRVO。)
无编译器优化结果:
在这里插入图片描述
Raja::to_string 函数中会先用返回的 ret 生成构造生成一个临时对象,但是我们可以看到,编译器把 ret 识别成了右值,即将亡值,调用了移动构造。然后在把这个临时对象做为 Raja::to_string 函数调用的返回值赋值给接收的 ret,这里调用的移动赋值。

虽然说我们的左值引用,也可以达到这样的移动构造,但是有一个问题,并不是所有的对象,资源都是可以被转移走的。移动构造之所以这么叫,就是因为移走了别人的资源。这部分资源之所以会被移走,就是因为它有右值属性。而它之所以有右值属性,要么就是这个变量是个将亡值,资源不转移就浪费了;要么就是被程序员亲自move了,程序员许可把这个对象的资源转移走。

就是这样的一个逻辑闭环,右值引用以一个既安全,又高效的方式,完成了局部变量的资源拷贝问题。而这个过程,也叫做右值引用的移动语义
右值引用的出现,C++11后,类的默认成员函数从6个变成了8个。新增两个成员函数:移动构造,移动赋值重载

STL库内部的所有容器,也都更新了移动构造和移动赋值重载

(3)右值引用引用左值及更深入的使用场景分析

按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?不一定
有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过 move 函数将左值转化为右值。

int main()
{
	Raja::string s1("hello, world!\n");
	Raja::string s2(s1);
	Raja::string s3 = move(s1);

	return 0;
}

将上述代码中的 s1 进行 move 操作,然后 move 返回一个 s1 的右值,再去构造 s3,此时会出现的问题是什么呢?
在这里插入图片描述
如上图,当我们构造完 s3 之后,由于我们将 s1 转换为了右值,所以这里调用的是移动构造,将 s3 和 s1 的资源互换,此时 s1 就变成了空串,所以我们不能随便将一个左值进行 move 操作,否则可能会产生意想不到的结果。

实质上,右值被右值引用引用以后的属性是左值
必须只能是左值,因为右值是不能直接修改,但是右值被右值引用以后,需要被修改

(4)万能引用(引用折叠)

一个模板参数T,T&&不是类型为T的右值引用,语法规定T&&为万能引用,万能引用能接收左值和右值。
但在接收右值后,t会被折叠成左值
在这里插入图片描述
在这里插入图片描述
明明我们的rri是一个右值引用,却调用了左值引用的函数重载,为什么?
右值引用后,右值引用指向的对象是右值属性,但是引用本身是左值属性

完美转发

聪明的人就会想到,调用之前move一下不就好了,比如这样:

fuc1(move(rri));

在这里插入图片描述
func1是一个万能引用(引用折叠)的函数模板,经过了折叠引用这一步,T&&这个参数类型是不确定的
在这里插入图片描述

请问如何调用func2参数的最开始的引用类型?
如果T&&是右值的话,传参后t会变成左值,那么我们可以对其进行move操作
如果T&&是左值的话,传参后t还是左值,我们无需对其进行操作
这个地方就不能粗暴的进行move了,不然会把原本就是左值的参数,给move成右值
为了解决这个情况,C++提供了一个函数模板forward,称为完美转发,其可以识别到参数的左右值类型,从而将其转化为原来的值,有了完美转发,万能引用就变得名副其实了。

template <class T>
void fuc1(T&& t)
{
    func2(forward<T>(t));
}

在这里插入图片描述

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐