C++移动语义

一. 浅拷贝与深拷贝

struct Node{ int size; int *p; Node() = default; Node(int x){ size = x; p = new int(size); } };

1. 浅拷贝

按位拷贝对象,如果拷贝对象中存在指针,那么两指针指向的地址相同(但是两指针的地址不一定相同)

Node shllowCopy(Node &v){ this -> size = v.size; this -> p = v.p; }

2. 深拷贝

拷贝对象的所有属性,如果拷贝对象中存在指针,那么会在堆上重新开辟一片相同大小的空间,并让指针指向该空间

Node deepCopy(Node &v){ this -> size = v.size; this -> p = new int[size]; for(int i = 0; i < size; i++){ this -> p[i] = v.p[i]; } }

二. 左值与右值

  • 左值(lvalue):表达式结束后仍然存在的持久对象
  • 右值(rvalue):表达式结束后就被销毁的临时对象

一般来说,有变量名的都是右值,没有变量名的是左值

特殊:字符字面量(如'c','a')这种是右值;但是字符串字面量,如"abcd"这种,由于它存在于静态存储区,所以字符串字面量均为左值

三. 左值引用与右值引用

1. 左值引用(&)

左值引用本质上是给左值变量起了一个别名

  • 改变任意一方,双方都改变
  • 只能绑定在左值上
  • 特殊:const int &x = 10,为常量左值引用,可以绑定右值,但是不可更改

2. 右值引用(&&)

绑定在右值上,可以延长临时对象的生命周期,并允许更改

应该注意的是,右值引用类型的变量,它本身是个左值

比如说:int &&x = 10,x的类型是右值引用,但它本身是个左值

如果你要把x再次当成右值传为另一个函数或者赋值给另一个数,那么直接传是不行的,这个时候就要用到std::move()

四. 强转右值std::move()

std::move()可以将左值强制转化为右值,类型上变成右值引用类型

作用是把原对象指向堆内存空间的指针地址转交给新对象,然后将原对象的该指针

置为nullptr

但是std::move()对基本类型,如int,double,char,指针无效,因为他们的值就存在于栈上,不指向任何外部资源

比如说:

int res = 1; int ans = std::move(res); std::cout << res << "\n";

此时你会发现输出的仍是1

如果不是的话,那么原对象会被“掏空”,比如说:

std::string s = "abc"; std::string t = std::move(s); std::cout << s << "\n";

输出的就是空

  • 注意,只有在std::move()用于创建新对象时,才触发移动构造

五. 纯右值与将亡值

1. 纯右值

没有身份,没有固定内存地址,不可被移动,用完即销毁的值/表达式

特点:

  • 没有标识符(没有名字)
  • 不能取地址
  • 一般用于初始化和计算

2.将亡值

有身份,可被移动的值/表达式

特点:

  • 通常对应一个右值引用

  • 在内存中有地址

  • 通过std::move()强转后的就是将亡值

六. 函数参数传递

问:这六次函数调用,分别各触发了几次拷贝,几次移动构造?

void f1(Node v){ return; } void f2(Node &v){ return; } void f3(Node &&v){ return; } int main(){ Node a; Node &b = a; Node c; Node d; f1(a);//1次拷贝,0次移动构造 f1(b);//1次拷贝,0次移动构造 f1(std::move(c));//0次拷贝,1次移动构造 f2(a);//0次拷贝,0次移动构造 f2(b);//0次拷贝,0次移动构造 f3(std::move(c));//0次拷贝,0次移动构造 return 0; }

七. 引用折叠

规则:只要有左值引用,结果就是左值引用;只有当两个都是右值引用时,结果才是右值引用

原始类型(T)声明的引用类型折叠后结果
T&&T&
T&&&T&
T&&&T&
T&&&&T&&

八. 万能引用

万能引用的好处在于既能绑定左值,又能绑定右值

万能引用形式上为T&&,但要成为万能引用,必须满足两个条件:

  • 必须存在类型推导,如模板,auto
  • 形式只能是T&&,如果用const修饰,就会变回普通的右值引用

易错区分:

void func(std::vector<T> &&v){ return; }

这不是万能引用!

因为它被固定为了std::vector右值引用。它只能接收std::vector的右值,不能接收std::vector的左值,更不能接收其他的类型(如int

万能引用的形式只能是T&&

  • 万能引用是贪婪的,这里模板推导的“精确匹配”优先级高于普通函数的“隐式转换”,也就是说可能会引发不必要的引用转化

九.完美转发std::forward<T>

用处是当一个函数把参数传递为另一个函数时,保留参数的原生属性

例:

void target(int&& x) { return; } template<typename T> void wrapper(T&& param) { target(param); //报错 } int main() { wrapper(10); }

在这个例子中,10确实是右值,但是在wrapper函数中,param是左值,无法进行&&的右值绑定,于是会报错

为了解决这个问题,我们引入了std::forward<T>,它能根据T推导的结果,决定把param转化为左值或者是右值,于是就可以:

template<typename T> void wrapper(T&& param) { target(std::forward<T>(param)); }