C++ 11新特性:列表初始化,右值引用与移动语义
1. 列表初始化
在C++98中一般数组和结构体可以用{}进行初始化
#include<iostream>
struct peng
{
int _x;
int _y;
};
int main()
{
int arr1[] = { 1,2,3,4,5,6,7 };
int arr2[5] = { 0 };
peng p = { 2,6 };
return 0;
}
C++11想一切对象皆可用{}初始化,{}初始化也叫做列表初始化。
- 内置类型支持,自定义类型也支持,自定义类型本质是类型转换,中间会产生临时对象,再经过编译器优化后变成了直接构造。
- {}初始化(列表初始化)的过程中可以省略掉=
- C++列表初始化的本意是实现一个统一的初始化方式,另外再其他场景下带来了一些便利。
#include<iostream>
#include<vector>
using namespace std;
class peng
{
public:
peng(const peng& p)
:_x(p._x)
,_y(p._y)
{
cout << "对象" << endl;
}
peng(int x=1,int y=1)
{
cout<<"直接构造" << endl;
}
private:
int _x;
int _y;
};
int main()
{
int x1 = { 21 };
//本质是构造一个peng临时对象
//临时对象再去拷贝构造d1,编译器优化后合二为一变成{2,6}直接构造初始化
peng p1 = { 2,6 };
//p2引用{3,9}构造的临时对象。
const peng& p2 = { 3,9 };
//C++98支持单参数时类型转换,也可以不用{}
peng p3 = { 2 };
peng p4 = 2;
//{}构造可以省略掉=
peng p5{ 1,2 };
int x2{ 2 };
const peng& p6{ 1,1 };
return 0;
}
但是只有用{}初始化时才能省略=
peng p 2055;
//即为错误示范
容器push/insert多参数构造对象时用{}会很方便
vector<peng> v;
v.push_back(p1);
v.push_back(peng(1, 2));
//相比较有名对象和匿名对象传参,{}更有性价比
v.push_back({ 2,2 });
2. std::initializer_list
对象容器的初始化还是不太方便的,例如一个vector对象,我想用N各值去构造初始化,那么我们得实现很多个构造函数才能支持
vector<int> v1 = { 1,2,3 };
vector<int> v2 = { 1,2,3,4,5,6 };
STL容器都增加了一个initializer_list的构造函数如下所示:
// STL中的容器都增加了⼀个initializer_list的构造
vector(initializer_list<value_type> il, const allocator_type& alloc =
allocator_type());
list(initializer_list<value_type> il, const allocator_type& alloc =
allocator_type());
map(initializer_list<value_type> il, const key_compare& comp =
key_compare(), const allocator_type& alloc = allocator_type());
// ...
vector的实现为
template<class T>
class vector {
public:
typedef T* iterator;
vector(initializer_list<T> l)
{
for (auto e : l)
push_back(e)
}
private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _endofstorage = nullptr;
};
容器的赋值也支持initializer_list的版本
vector& operator= (initializer_list<value_type> il);
map& operator= (initializer_list<value_type> il);
initializer_list存储在栈上,可以直接构造vector
#include<iostream>
#include<vector>
#include<string>
#include<map>
using namespace std;
int main()
{
std::initializer_list<int> inli;
inli = { 1,2,3,4 };
cout << sizeof(inli) << endl;
//这三个指针的值接近说明数组存在栈上
int i = 0;
cout << inli.begin() << endl;
cout << inli.end() << endl;
cout << &i << endl;
//{}列表中可以有任意多个值
//这两种写法语义上有差别,第一个v1是直接构造,
//第二个v2是构造临时对象+临时对象拷贝,编译器将其优化为直接构造
vector<int>v1({ 1,2,3,4 });
vector<int> v2 = { 1,2,3,4 };
vector<int> v3(inli);
for (auto e : v3)
{
cout << e << endl;
}
return 0;
}
3. 右值引用
C++11中新增了右值引用的语法,无论左值引用还是右值引用都是给对象起别名。
3.1 左值和右值
左值是一个表示数据的表达式(如变量名或解引用的指针),一般是存储在内存中我们可以获取它的地址,左值可以出现在赋值符号左边,也可以出现在赋值符号右边。const修饰的左值不能给他赋值,但是可以取它的地址。
右值也是一个表达数据的表达式,可以是字面值常量,也可以是一个临时对象,右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,右值不能取地址。
左值与右值的核心区别是:能否取地址
#include<iostream>
#include<vector>
#include<string>
#include<map>
using namespace std;
int main()
{
//左值:可以取地址
//如下
int* p = new int();
int a = 0;
const int b = a;
*p = 2;
string s("666666");
s[0] = 'c';
cout << &p << endl;
cout << &a << endl;
cout << &b << endl;
cout << &s << endl;
//printf("%p\n", &s[0]);
cout << (void*)&s[0] << endl;
//右值:不能取地址
float x = 6.6, y = 3.3;
//如下
111;
x + y;
fmin(x, y);//求两个浮点值的最大值
string("666666");//匿名对象
return 0;
}
3.2 左值引用和右值引用
用一段伪代码表示
type& r1=x;
type&& r2=y;
其中type是数据类型,第一个语句就是左值引用,左值引用就是给左值取别名,第二个就是右值引用,即给右值取别名。
- 左值引用不能直接引用右值,但是const左值引用可以引用右值
- 右值引用不能直接引用左值,但是右值引用可以引用move(左值),(move是库里的一个函数模板,本质上是进行强制类型转换,将左值引用强转为右值引用)。
- 变量表达式都是左值属性,一个右值被右值引用绑定后,右值引用变量的属性是左值(如上面的r2是一个左值)
在语法层面上,左值引用和右值引用都是取别名,不开空间。从汇编底层来看下面代码中r1与rr1底层都是用指针实现的,没有什么区别。底层汇编等实现和上层语法表达的意义有时是背离的,所以不要连到一起去理解,不然反而会迷茫。
#include<iostream>
#include<vector>
using namespace std;
int main()
{
//左值:可以取地址
//如下
int* p = new int();
int a = 0;
const int b = a;
*p = 2;
string s("666666");
s[0] = 'c';
cout << &p << endl;
cout << &a << endl;
cout << &b << endl;
cout << &s << endl;
//printf("%p\n", &s[0]);
cout << (void*)&s[0] << endl;
//右值:不能取地址
float x = 6.6, y = 3.3;
//如下
111;
x + y;
fmin(x, y);//求两个浮点值的最大值
string("666666");//匿名对象
//左值引用给左值取别名
int& r1 = a;
int*& r2 = p;
int& r3 = *p;
string& r4 = s;
char& r5 = s[0];
//右值引用给右值取别名
int&& rr1 = 111;
float&& rr2 = x + y;
float&& rr3 = fmin(x, y);
string&& rr4 = string("666666");
//const左值引用可以引用右值
const int& rl1 = 111;
const float& rl2 = x + y;
const float& rl3 = fmin(x, y);
const string& rl4 = string("666666");
//通过move引用左值
int&& rrl1 = move(a);
int*&& rrl2 = move(p);
int&& rrl3 = move(*p);
string&& rrl4 = move(s);
int&& rrl5 = (int&&)a;
return 0;
}
3.3 引用延长生命周期
右值引用可用于为临时对象延长生命周期,const的左值引用也能延长临时对象生命周期,但是这些对象无法被修改。
#include<iostream>
#include<string>
using namespace std;
int main()
{
string s1 = "ccc";
//string& r = s1 + s1; //不能绑定到左值
const string& r1 = s1 + s1;//不能修改
string&& r2 = s1 + s1;//可以修改
r2 +="aaa";
cout << r2 << endl;
return 0;
}
3.4 左值和右值的参数匹配
- 在C++98中,我们实现一个const左值引用作为参数的函数,那么实参传递左值和右值都可以匹配
- C++11后分别重载左值引用、const左值引用与右值引用作为形参的一个函数,实参是左值会匹配函数名(左值引用);实参是const左值会匹配函数名(const 左值引用);实际参是右值会匹配函数名(右值引用);
4. 引用的意义
我们之前使用左值引用的目的是:函数中传参和传返回值时减少拷贝,同时还可以修改实参和修改返回对象的价值。
左值引用已经解决了大多数的拷贝效率问题,但是对于有些传值返回需要拷贝的场景,如果传临时变量的引用返回,就会出错,临时变量的生命周期只在函数内,出了函数,变量就销毁了。
而右值引用显然也无法解决这一问题
5. 移动语义
5.1 移动构造与移动赋值
移动构造函数是一种构造函数,类似与拷贝构造函数,移动构造函数要求第一个参数是该类型的引用,但是不同的是要求这个参数是右值引用,如果还有其它参数,额外的参数必须要有缺省值
移动赋值是一个赋值运算符的重载,他跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函数要求第一个参数是该类类型的引用,并且要求其是右值引用。
例如string类这样的深拷贝类,或包含深拷贝的成员变量的类,移动构造和移动赋值才有意义
//拷贝构造
string(const string& s1)
:_str(new char [s1.capacity()+1])
, _size(s1.size())
, _capacity(s1.capacity())
{
strcpy(_str, s1.c_str());
}
string(string&& s)//移动构造
:_str(nullptr)
{
swap(s);
}
直接窃取引用右值对象的资源,而不是去拷贝资源从而提高效率。
看似与拷贝的现代写法很像,实际上现代写法的本质也开辟了额外空间,没有性能提升;
string(const string& s1)
{
string tmp(s1._str);
cout << "拷贝构造" << endl;
swap(tmp);
}
5.2 类型划分
C++11以后,进一步对类型进行了划分,右值分为纯右值和将亡值。泛左值包含将亡值和左值
- 纯右值指那些子面值常量或求值结果相当于字面值或是一个不具名的临时对象。如:111、true、nullptr或者整形a、b、a++、a+b等(C++11中纯右值的概念等同于C++98中的右值)。
- 将亡值是指返回右值引用的函数的调用表达式和转换为右值引用的转换函数的调用表达如,move(x)、static_cast<X&&>(x)
我们在list的模拟实现中看一下
在C++11中多了右值引用的版本,我们自己的模拟实现里对push_back进行一个重载
void push_back(const T& val)
{
Node* qrev = head->qrev;
Node* newNode = new Node(val);
_size++;
qrev->next = newNode;
newNode->next = head;
newNode->qrev = qrev;
head->qrev = newNode;
}
void push_back(T&& val)
{
Node* qrev = head->qrev;
Node* newNode = new Node(move(val));
_size++;
qrev->next = newNode;
newNode->next = head;
newNode->qrev = qrev;
head->qrev = newNode;
}
注意右值的引用val是一个左值需要move一下。我们还要对节点的构造来增添一个移动构造
Node(T&& v)
:next(nullptr)
, qrev(nullptr)
, val(v)//
{
cout << "移动构造" << endl;
}
结果为
6. 引用折叠与完美转发
引用折叠
C++中不能直接定义引用如
int&&& r=i;
这样写会直接报错
而通过模板或typedef中的类型操作可以构成引用的引用,C++11给出了引用折叠的规则:
右值引用的右值引用 折叠成右值引用,所有其他组合均折叠成左值引用
f2这样的函数模板中,T&& x参数看起来是右值引用参数,由于引用折叠的规则,它传递左值时就是左值引用,传递右值时就是右值引用,这种函数模板的参数也叫做万能引用。
// 由于引⽤折叠限定,f1实例化以后总是⼀个左值引⽤
template<class T>
void f1(T& x)
{}
// 由于引⽤折叠限定,f2实例化后可以是左值引⽤,也可以是右值引⽤
template<class T>
void f2(T&& x)
{}
int main()
{
typedef int& lref;
typedef int&& rref;
int n = 0;
lref& r1 = n; // r1 的类型是 int&
lref&& r2 = n; // r2 的类型是 int&
rref& r3 = n; // r3 的类型是 int&
rref&& r4 = 1; // r4 的类型是 int&&
// 没有折叠->实例化为void f1(int& x)
f1<int>(n);
f1<int>(0); // 报错
// 折叠->实例化为void f1(int& x)
f1<int&>(n);
f1<int&>(0); // 报错
// 折叠->实例化为void f1(int& x)
f1<int&&>(n);
f1<int&&>(0); // 报错
// 折叠->实例化为void f1(const int& x)
f1<const int&>(n);
f1<const int&>(0);
// 折叠->实例化为void f1(const int& x)
f1<const int&&>(n);
f1<const int&&>(0);
// 没有折叠->实例化为void f2(int&& x)
f2<int>(n); // 报错
f2<int>(0);
// 折叠->实例化为void f2(int& x)
f2<int&>(n);
f2<int&>(0); // 报错
// 折叠->实例化为void f2(int&& x)
f2<int&&>(n); // 报错
f2<int&&>(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 Function(T&& t)
{
Fun(t);
}
int main()
{
// 10是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(10); // 右值
int a;
// a是左值,推导出T为int&,引⽤折叠,模板实例化为void Function(int& t)
Function(a); // 左值
// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(std::move(a)); // 右值
const int b = 8;
// a是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int& t)
Function(b); // const 左值
// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&& t)
Function(std::move(b)); // const 右值
return 0;
}
在上面函数模版程序中,传左值实例化以后是左值引用的函数,传右值实例化以后是右值引用的函数。
但是变量表达式都是左值属性,即一个右值被右值引用绑定后,右值引用变量表达式的属性是左值,也就是说我们Fun函数匹配的都是左值引用版本的
用move显然也是不能解决问题的,因为使用move只会让Fun函数只匹配右值引用
我们想要保持t对象的属性就需要使用完美转发实现
完美转发本质上是一个函数模板,功能为:实参传左值就推导成左值引用,传右值就推导成右值引用
他主要还是通过引用折叠的方式实现,如下代码所示
template <class _Ty>
_Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept
{ // forward an lvalue as either an lvalue or an rvalue
return static_cast<_Ty&&>(_Arg);
}
Function传给forward函数模板参数_Ty是int&时,_Ty&&,引用折叠变为int&,强转为int&返回
当传的参数_Ty是int&&时,_Ty&&引用折叠还是int&&强转为int&&返回
static_cast<type>是C++规范的类型转换方式,type代表要转换的数据类型。
std::remove_reference
是 C++ 标准库中的一个类型特征(type trait),它定义在头文件<type_traits>
中。这个类型特征用于移除一个类型中的引用部分,无论是左值引用(&
)还是右值引用(&&
),都将其转换为对应的非引用类型。具体来说,如果你有一个类型
T
,那么std::remove_reference<T>::type
会给出T
去掉引用后的类型。如果T
本身不是引用类型,那么结果就是T
本身。
这篇就到这里啦ヾ( ̄▽ ̄)Bye~Bye~
(๑′ᴗ‵๑)I Lᵒᵛᵉᵧₒᵤ❤