C++:C++11新特性---右值引用
文章目录
- 初始化方式
- 显示查看类型
- initializer_list
- decltype
- 左值引用和右值引用
- move
- 左右值引用的场景
- 万能引用和完美转发
本篇总结C++11新特性
初始化方式
C++11对参数列表的初始化有了更明确的定义,可以这样进行定义
// 列表初始化
void test1()
{
// 旧版本
int x = 0;
// 新版本
int y{ 0 };
int z = { 0 };
int arr[]{ 10,20,30 };
int* pa = new int[5]{ 1,2,3,4,5 };
}
在对类的赋值的时候,也可以这样进行赋值
struct t
{
int a;
int b;
};
void test2()
{
t* pt = new t[5]{ 1,2 };
}
显示查看类型
// 查看类型
void test3()
{
int a = 1;
cout << typeid(a).name() << endl;
auto it = map<int, int>().begin();
cout << typeid(it).name() << endl;
}
运行结果:
int
class std::_Tree_iterator<class std::_Tree_val<struct std::_Tree_simple_types<struct std::pair<int const ,int> > > >
initializer_list
这是什么呢?如何理解这个类型?先看一下在什么场景中会出现这个东西
void test4()
{
auto lit = { 1,2,3,4 };
cout << typeid(lit).name() << endl;
}
那么这个东西是干什么的呢?有什么用呢?
在C++11中,对于STL的各类容器的构造函数中,新增了这样的构造方式,有点类似于一个数组,它里面可以存储任意类型的数据,然后可以交给vector来实现构造,因此下面就要对{}进行一个对比
void test5()
{
// 利用initializer_list进行初始化
vector<int> v{ 1,2,3,4,5 };
auto lit = { 1,2,3,4 };
vector<int> vc(lit);
// 调用参数初始化列表进行初始化
int arr[]{ 1,2,3,4,5 };
}
这两种写法看似,但是实际上底层是完全不同的两种实现的方式
decltype
// decltype的一些使用使用场景
template<class T1, class T2>
void F(T1 t1, T2 t2)
{
decltype(t1 * t2) ret;
cout << typeid(ret).name() << endl;
}
void test6()
{
const int x = 1;
double y = 2.2;
decltype(x * y) ret;
decltype(&x) p;
cout << typeid(ret).name() << endl;
cout << typeid(p).name() << endl;
F(1, 'a');
}
简单来说,就是可以把你要新定义的一个类型进行人为的定义,定义成一个你想让它变成的类型,使用场景也不算多,但是可以这样进行使用
左值引用和右值引用
首先要解决一个问题,什么是左值?
之前可能会说,左值就是等号左边的值,这肯定是不对的,左值是一个表示数据的表达式,可以获取它的地址,也可以进行赋值,通常来说它出现在赋值符号的左边,右值不能出现在赋值符号的左边,定义的时候,const修饰符修饰的变量不可以对它进行赋值,但是可以对它取地址,因此,可以说左值引用就是给左值的引用,给左值取别名
常见的左值
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;
常见的右值
10;
x + y;
fmin(x, y);
而对这些起别名就是右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
对于右值引用来说,可以理解为,右值本身是不能取地址的,但是给右值取别名之后,右值就会被存储到某些地方,此时就可以取到它的地址
左值引用只能引用左值,不能引用右值,如果想引用右值需要用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;
}
move
右值只能引用右值,不能引用左值,但是右值可以引用move之后的左值
void test7()
{
// 右值引用只能右值,不能引用左值。
int&& r1 = 10;
// error C2440: “初始化”: 无法从“int”转换为“int &&”
// message : 无法将左值绑定到右值引用
int a = 10;
//int&& r2 = a;
// 右值引用可以引用move以后的左值
int&& r3 = std::move(a);
}
左右值引用的场景
那左右值引用能干啥呢?
左值引用
对比下面两种传参方式
void func(const string& str);
void func(string str);
从传参的效率上就不一样了,对于传引用来说,每次传参的代价是很低的,只需要把变量的地址给过去就可以了,但是对于传值来说就不一样了,传参的代价是相当高的,需要把原始的参数拷贝给新的值
也就是说,左值引用做参数减少了拷贝,提高效率的使用场景和价值
左值引用的缺陷
那左值引用有什么缺陷呢?
第一个是,当函数返回的对象是一个局部对象的时候,是不可以使用传引用返回的,因为这个变量出了作用域就被销毁了,因此不能使用传左值引用,只能进行传值返回,例如下面的场景
mystring::string to_string(int x)
{
mystring::string ret;
while (x)
{
int val = x % 10;
x /= 10;
ret += ('0' + val);
}
reverse(ret.begin(), ret.end());
return ret;
}
此时的string是一个局部变量,如果使用传引用返回会访问一个不存在的地址,这是不被允许的,但如果采用传值返回,又会导致增加了拷贝构造的次数,并且可能还是两次
在新的编译器中,函数得到的这个string在返回的时候,会直接赋给新的值,调用一次拷贝构造,而在旧一点的编译器中,从函数的返回值会先拷贝构造一次到一个临时常量,再从这个临时常量拷贝构造一次到外部定义的string中
而使用移动构造,可以解决这个问题,编译器会默认使用最匹配的参数进行调用,因此会优先使用移动构造
搭配move函数
move函数的作用,就是单纯的把左值转换成右值引用,由此来实现移动语义
那这样有什么用呢?该如何理解这个意思呢?
如果有下面的代码:
string s2(s1)
这样的语句代表的含义是,调用拷贝构造,这里的s1是一个左值
但是如果要是改成这样
string s2(move(s1))
这样就不一样了,把s1放move函数中,这样就可以把s1当成一个右值,而右值是可以调用移动构造的,这样就可以不用调用拷贝构造浪费空间,而是可以直接的把值置换到我们所需要的s2里面,但是这样其实是不好的,这样会导致,虽然确实把s2的值填充了,但是却把s1的值架空了
简单来说,移动构造就是把资源全部偷过来,把原来的资源都架空
再举一个例子:
int main()
{
list<string> lt;
string s1("1111");
// 这里调用的是拷贝构造
lt.push_back(s1);
// 下面调用都是移动构造
lt.push_back("2222");
lt.push_back(std::move(s1));
return 0;
}
如果只是简单的调用s1,那么s1会被当成是一个左值,而左值会调用的是拷贝构造,但是如果把它强制转换成右值,那么就会调用的是移动构造,很明显,移动构造的使用成本是要比拷贝构造低很多的
万能引用和完美转发
前面关于右值引用中和前面有比较大不同的一点就是出现了&&符号,如果把这个符号看成是右值引用的标识符,也是不对的,C++11在模板中也新增了关于&&符号,这个符号代表的是万能引用,而不是右值引用,简单来说就是,既能接收左值也能接收右值
模板的万能引用只是提供了一个可以接收左值和右值的能力,一般来说是不可以两个都接收的
实际的使用中,引用类型的作用会限制接收的类型,会变成左值,而如果想要在传递的过程中保持右值的属性,就需要用到万能引用和完美转发
下面做一个实验来验证功能
void check(int& t)
{
cout << "左值引用" << endl;
}
void check(int&& t)
{
cout << "右值引用" << endl;
}
void check(const int& t)
{
cout << "const左值引用" << endl;
}
void check(const int&& t)
{
cout << "const右值引用" << endl;
}
void test8()
{
const int a = 1;
const int& b = 1;
int c = 0;
check(a);
check(b);
check(c);
check(1);
check(move(a));
check(move(b));
}
结果也是符合预期的,把上述代码进行适当更改
void check(int& t)
{
cout << "左值引用" << endl;
}
void check(int&& t)
{
cout << "右值引用" << endl;
}
void check(const int& t)
{
cout << "const左值引用" << endl;
}
void check(const int&& t)
{
cout << "const右值引用" << endl;
}
void func1(int t)
{
check(t);
}
void test8()
{
const int a = 1;
const int& b = 1;
int c = 0;
func1(move(a));
}
此时运行结果是左值引用,这是为什么?其原因是进入func1函数后,函数参数就从右值变成左值了,此时它的右值属性就会消失,因此出现了下面的用法:
template<class T>
void func1(T&& t)
{
check(t);
}
此时运行结果是const左值引用,这说明它保持了const属性,但是依旧没有保持右值的属性,右值的属性依旧被退化成左值了
再进行改良
template<class T>
void func1(T&& t)
{
check(forward<T>(t));
}
此时运行结果就是const右值引用了,而新增的这个forward其实就是完美转发,它可以保障原来的属性,把原来这个值的属性转发出去,严格意义来说保持的是左值和右值,而如果没有万能引用运行结果依旧会丢失const属性