C++98中提出了引用的概念,引用即别名,引用变量与其引用实体共用同一块内存空间,而引用的底层是通过指针来实现的,因此使用引用,可以提高程序的可读性。
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
int main()
{
int a = 10;
int b = 20;
Swap(a, b);
return 0;
}
为了提高程序运行效率,C++11中引入了右值引用,右值引用也是别名,但其只能对右值引用。
int Add(int a, int b)
{
return a + b;
}
int main()
{
const int&& ra = 10;
//引用函数返回值,返回值是一个临时变量,为右值
int&& rRet = Add(10, 20);
return 0;
}
为了与C++98中的引用进行区分,C++11将该种方式称之为右值引用。
左值与右值
什么是左值?什么是右值?
左边的就是左值?右边的就是右值?注意这个是C语法就留下的坑,就像左移和右移一样,左移是向高位移动,右移是向低位移动。这里左右不是方向。左边的值不一定是左值,右边的值也不一定是右值。
- 可以修改的一般是左值,左值通常是变量。
- 右值通常是常量,表达式或者函数返回值(临时对象)。
int main()
{
int x = 1, y = 2;
// 左值引用的定义
int a = 0;
int& b = a;
// 左值引用不能引用右值, const左值引用可以
//int& e = 10;
//int& f = x + y;
const int& e = 10;
const int& f = x + y;
// 右值引用的定义
int&& c = 10;
int&& d = x + y;
// 右值引用不能引用左值,但是可以引用move后的左值
//int&& m = a;
int&& m = move(a);
return 0;
}
template<class T>
void f(const T& a)
{
cout << "void f(const T& a)" << endl;
}
template<class T>
void f(const T&& a)
{
cout << "void f(const T&& a)" << endl;
}
int main()
{
int x = 10;
f(x); // 这里会匹配左值引用参数的f
f(10);// 这里会匹配右值引用参数的f
return 0;
}
C++11又将右值区分为:纯右值和将亡值
纯右值:基本类型的常量或者临时对象
将亡值:自定义类型的临时对象
应用:右值引用的移动构造和移动赋值,可以减少拷贝
class String
{
public:
String(const char* str = "")
{
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
//拷贝构造
String(const String& s)
{
cout<<"String(const String& s)-拷贝构造-效率低"<<endl;
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
}
//移动构造
String(String&& s)
:_str(nullptr)
{
// 传过来的是一个将亡值,反正你都要亡了,我的目的是跟你有一样大的空间,一样的值
// 不如把你的空间换给我
cout << "String(String&& s)-移动构造-效率高" << endl;
swap(_str, s._str);
}
//赋值重载
String& operator=(const String& s)
{
cout << "String& operator=(const String& s)-拷贝赋值-效率低" << endl;
if (this != &s)
{
char* newstr = new char[strlen(s._str) + 1];
strcpy(newstr, s._str);
delete[] _str;
_str = newstr;
}
return *this;
}
//移动赋值
String& operator=(String&& s)
{
cout << "String& operator=(String&& s)-移动赋值-效率高" << endl;
swap(_str, s._str);
return *this;
}
~String()
{
delete[] _str;
}
// s1 + s2
String operator+(const String& s2)
{
String ret(*this);
//ret.append(s2);
return ret; // 返回的是右值
}
// s1 += s2
String& operator+=(const String& s2)
{
//this->append(s2);
return *this; // 返回是左值
}
private:
char* _str;
};
String f(const char* str)
{
String tmp(str);
return tmp; // 这里返回实际是拷贝tmp的临时对象
}
int main()
{
String s1("左值");
String s2(s1); // 参数是左值
String s3(f("右值-将亡值"));// 参数是右值-将亡值(传递给你用,用完我就析构了)
//String s4(move(s1));
String s5("左值");
s5 = s1;
s5 = f("右值-将亡值");
return 0;
}
应用:当传值返回时,返回是右值,结合前面学的移动构造和移动赋值,可以减少拷贝
class Solution1 {
public:
vector<string> letterCombinations(string digits) {
vector<string> v;
//...
return v;
}
};
class Solution2 {
public:
vector<vector<int>> generate(int numRows) {
vector<vector<int>> vv;
//...
return vv;
}
};
int main()
{
String s1("s1");
String s2("s2");
String s3 = s1 += s2; // 拷贝构造
String s4 = s1 + s2; // 移动构造
// 现实中不可避免存在传值返回的场景,传值返回的拷贝返回对象的临时对象。
// 如果vector只实现参数为const左值引用深拷贝,那么下面的代价就很大
// vector(const vector<T>& v)->深拷贝
// 但是如果vector实现了参数右值引用的移动拷贝,那么这里效率就会很高
// vector(vector<T>&& v) ->移动拷贝
// 结论:右值引用本身没太多意义,右值引用的实现了移动构造和移动赋值
// 那么面对接收函数传值返回对象(右值)等等场景,可以提高效率
vector<string> v = Solution1().letterCombinations("abcd");
vector<vector<int>> vv = Solution2().generate(5);
return 0;
}
总结
右值引用做参数和作返回值减少拷贝的本质是利用了移动构造和移动赋值
左值引用和右值引用本质的作用都是减少拷贝,右值引用本质可以认为是弥补左值引用不足的地方, 他们两相辅相成。
左值引用:解决的是传参过程中和返回值过程中的拷贝
做参数:void push(T x) -> void push(const T& x) 解决的是传参过程中减少拷贝
做返回值:T f2() -> T& f2() 解决的返回值过程中的拷贝
ps:但是要注意这里有限制,如果返回对象出了作用域不在了就不能用传引用, 这个左值引用无法解决,等待C++11右值引解决
右值引用:解决的是传参后,push/insert函数内部将对象移动到容器空间上的问题.+ 传值返回接收返回值的拷贝
做参数: void push(T&& x) 解决的push内部不再使用拷贝构造x到容器空间上,而是移动构造过去
做返回值:T f2(); 解决的外面调用接收f2()返回对象的拷贝,T ret = f2(),这里就是右值引用的移动构造,减少了拷贝。
完美转发
完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。
void Func(int x)
{
// ......
}
template<typename T>
void PerfectForward(T t)
{
Fun(t);
}
PerfectForward为转发的模板函数,Func为实际目标函数,但是上述转发还不算完美,完美转发是目标函数总希望将参数按照传递给转发函数的实际类型转给目标函数,而不产生额外的开销,就好像转发者不存在一样。
所谓完美:函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相应实参是右值,它就应该被转发为右值。这样做是为了保留在其他函数指针对转发而来的参数的左右值属性进行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)。
C++11通过forward函数来实现完美转发, 比如:
void Fun(int &x){ cout << "lvalue ref" << endl; }
void Fun(const int &x){ cout << "const lvalue ref" << endl; }
void Fun(int &&x){ cout << "rvalue ref" << endl; }
void Fun(const int&& x){ cout << "const rvalue ref" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
// 右值引用会在第二次调用后的参数传递过程中右值属性丢失,下一层调用会全部识别为左值
// 完美转发解决
Fun(std::forward<T>(t));
}
int main()
{
PerfectForward(10); // rvalue ref
int a;
PerfectForward(a); // lvalue ref
PerfectForward(std::move(a)); // rvalue ref
const int b = 8;
PerfectForward(b); // const lvalue ref
PerfectForward(std::move(b)); // const rvalue ref
return 0;
}