C++11新增特性及右值引用

1. 统一的列表初始化

1.1 {}初始化

在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自 定义的类型,使用初始化列表时,可添加等号(=),也可不添加。

struct Point
{
 int _x;
 int _y;
};
int main()
{
 int x1 = 1;
 int x2{ 2 };
 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;
}

1.2 std::initializer_list

此类型用于访问C++初始化列表中的值,该列表是类型的元素列表.这种类型的对象是由编译器根据初始化列表声明自动构造,该定义列表声明是用大括号括起来的逗号分隔的元素列表:const T

std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator= 的参数,这样就可以用大括号赋值。

int main()
{
 vector<int> v = { 1,2,3,4 };
 list<int> lt = { 1,2 };
 // 这里{"sort", "排序"}会先初始化构造一个pair对象
 map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
// 使用大括号对容器赋值
v = {10, 20, 30};
 return 0;
}

2. 声明

c++11提供了多种简化声明的方式,尤其是在使用模板时。

2.1 auto

在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。

int main() 
{ 
int i = 10; 
auto p = &i; 
auto dict = { {"sort", "排序"}, {"insert", "插入"} }; 
//map::iterator it = dict.begin(); 
auto it = dict.begin(); 
return 0; 
}

2.2 decltype

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

// decltype的一些使用使用场景
template<class T1, class T2>
void F(T1 t1, T2 t2)
{
decltype(t1 * t2) ret;
cout << typeid(ret).name() << endl;
}
int main()
{
const int x = 1;
double y = 2.2;
decltype(x * y) ret; // ret的类型是double
decltype(&x) p;      // p的类型是int*
cout << typeid(ret).name() << endl;
cout << typeid(p).name() << endl;
F(1, 'a');
return 0;
}

2.3 nullptr

由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能表示指针常量,又能表示 整形常量。

所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。

3. 右值引用和移动语义

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,无论左值引用还是右值引用,都是给对象取别名。

3.1 左值

左值是一个表示数据的表达式我们可以获取它的地址并且一般情况下(无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;
return 0;
}

3.2 右值

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

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,所以在把一个右值传参的时候该参数就会退化为左值。如果不想rr1被修改,可以用const int&& rr1 去引用。

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

左值引用总结:

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

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

右值引用总结:

1. 右值引用只能右值,不能引用左值。 2. 但是右值引用可以move以后的左值,即move可以接收一

个左值然后返回一个右值。

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

3.4 左值引用的使用场景和意义

1.左值引用解决了传参时存在的拷贝问题

string add_string(string& s1, string& s2)
{
	string s = s1 + s2;
	return s;
}

int main()
{
	string str;
	string hello = "Hello";
	string world = "world";
	str = add_string(hello, world);
	return 0;
}

以上代码中,add_string函数需要接收两个string类型的参数,此时我们使用传引用传参,就可以避免两个string的拷贝消耗。

2.左值引用解决了一部分返回值的拷贝问题

int main()
{
string s1="Hello World";
 // string& operator+=(char ch) 传左值引用没有拷贝提高了效率
 s1 += '!';
 return 0;
}

但是我们再看到以下情况:

string say_hello()
{
	string s = "Hello World";
	return s;
}
int main()
{
	string str;
	str = say_hello();
	return 0;
}

以上代码中,say_hello依然返回hello world这个字符串,但是s是一个局部变量,因为出了函数就会被销毁,如果str想要接收到s,那么就会先拷贝构造一个临时变量,然后临时变量再拷贝构造出str。

但我们已经通过s创建好了一个字符串,我们为了得到一个字符串hello world,中间经过了这么两次拷贝构造(编译器可能会优化为只有一次拷贝构造)。就因为这是一个局部变量,s不能出作用域。我们有没有办法直接把局部变量创建好的hello world移交给作用域外部的str,免去临时变量的拷贝构造?此时就需要用到右值引用。

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

C++把即将离开作用域的非引用类型的返回值当成右值,这种类型的右值也称为将亡值 。s即将被销毁,此时s就是一个右值了,右值的意思就是:这个变量的资源可以被迁移走。

移动语义

#include<iostream>
#include<string>
#include<vector>
using namespace std;
class mystring
{
public:
	//构造
	mystring()
		:_s(nullptr)
		, _sz(0)
	{}
	//析构
	~mystring()
	{
		delete[] _s;
		_s = nullptr;
		_sz = 0;
	}
	//拷贝构造
	mystring(const mystring& s)
		:_s(nullptr)
		,_sz(s._sz)
	{
		cout << "mystring(const mystring& s)  拷贝构造" << endl;
		_s = new char[_sz + 1];
		strcpy(_s, s._s);
	}
	//移动构造
	//由于s是一个将亡值,具有右值属性,移动构造函数直接把它的资源转移
	mystring(mystring&& s)
		:_s(nullptr)
		, _sz(s._sz)
	{
		cout << "mystring(mystring&& s)  移动构造" << endl;
		swap(_s, s._s);
		swap(_sz, s._sz);
	}
	//拷贝赋值
	mystring operator=(const mystring& s)
	{
		cout << "mystring operator=(const mystring& s)  拷贝赋值" << endl;
		mystring tmp(s);
		swap(_s, tmp._s);
		swap(_sz, tmp._sz);
		return *this;
	}
	//移动赋值
	mystring operator=(mystring&& s)
	{
		cout << "mystring operator=(mystring&& s)  移动赋值" << endl;
		swap(_s, s._s);
		swap(_sz, s._sz);
		return *this;
	}
private:
	char* _s;
	int _sz;
};

这个移动构造函数的参数是一个mystring&&类型,也就是一个右值引用。函数主体部分,通过一个swap函数把参数s_s指针成员与自己的_s成员进行交换,数s_sz指针成员与自己的_sz成员进行交换。由于指针指向字符串数组,此时相当于把s的字符串数组交换给自己,这样就完成了对右值引用的数据转移。移动赋值同理。

mystring say_hello()
{
	mystring s = "Hello World";
	return s;
}
int main()
{
	mystring str;
	str = say_hello();
	return 0;
}

依旧对上述代码进行研究,若无移动赋值,会进行两次拷贝构造(编译器可能会优化为只有一次拷贝构造) ,但有了移动赋值,就只有一次移动赋值。

移动构造,移动赋值之所以这么叫,就是因为移走了别人的资源。这部分资源之所以会被移走,就是因为它有右值属性。而它之所以有右值属性,就是因为这个变量是个将亡值,资源不转移就浪费了。就是这样的一个逻辑闭环,右值引用以一个既安全,又高效的方式,完成了局部变量的资源拷贝问题。而这个过程,也叫做右值引用的移动语义。

原来C++类中,有6个默认成员函数: 1. 构造函数 2. 析构函数 3. 拷贝构造函数 4. 拷贝赋值重载 5. 取地址重载 6. const 取地址重载 。最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。 因为右值引用的出现,C++11 新增了两个:移动构造函数和移动赋值运算符重载,类的默认成员函数从6个变成了8个。

    //移动构造
	//由于s是一个将亡值,具有右值属性,移动构造函数直接把它的资源转移
	mystring(mystring&& s)
		:_s(nullptr)
		, _sz(s._sz)
	{
		cout << "mystring(mystring&& s)  移动构造" << endl;
		swap(_s, s._s);
		swap(_sz, s._sz);
	}
    //移动赋值
	mystring& operator=(mystring&& s)
	{
		cout << "mystring operator=(mystring&& s)  移动赋值" << endl;
		swap(_s, s._s);
		swap(_sz, s._sz);
		return *this;
	}

它们的特点是:参数为右值引用,函数体内部通过交换别人的指针到自己手上,实现高效的资源转移。

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

当然,STL库内部的所有容器,也都更新了移动构造移动赋值重载。例如C++11的vector构造函数和operator=:

万能引用

模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力。如果传过来的是T&,则 && 推演为 T&,如果传过来的是T&&,则 && 推演为 T&&。

template<class T>
void PerfectForward(T&& t)
{
	cout << "void PerfectForward(T&& t)" << endl;
}
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;
}

我们可以看到程序运行的结果,该函数既能接收左值,又能接收右值。

但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,例如下面代码。

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<class T>
void PerfectForward(T&& t)
{
	//cout << "void PerfectForward(T&& t)" << endl;
	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;
}

其程序运行的结果全为使用了Fun左值引用函数,我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发。

完美转发

std::forward 完美转发在传参的过程中保留对象原生类型属性。使用如下:

template<class T>
void PerfectForward(T&& t)
{
	Fun(forward<T>(t));
}

  • 7
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我要满血复活

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值