前言
左值引用可以参考笔者的这篇文章---从c到c++——4:引用-CSDN博客 (ps:这篇文章里的引用单只左引用笔者当时水平不高(虽然现在也不高)起错了名字)
左值引用与右值引用的定义
c++中,无论是左值引用与右值引用,用途都是在给对象起别名
左值与右值的概念
左值和右值是c++中的一个概念,严格的来说,对于系统提供的 =操作符 来说(自己提供的重载函数不算),可以放在等号左边的或者能加const的称为左值,只能放在 = 右边的称为右值,
举个例子:变量,指针的解引用表达式都为左值
常量,函数返回值,计算式都为右值
巧计:可以取地址的都为左值,不可取地址的都为右值
// 一个特殊的情况
const int& fun() {
return 1 + 2;
}
int main() {
const int& f = fun(); // 没有报错
printf("%p\n", &fun()); // 一个指针
printf("%d\n", fun()); // 3
return 0;
}
此时 fun()的返回值可以取地址,证明是左值,他不能放在=左边是因为他的返回值是const,但这个函数的返回值的的确确是一个左值(或者说返回了一个被强转为左值的右值)
ps:这是一种没什么意义的写法,大伙就当看个乐子,知道有这么回事就行,实际没什么用
左值引用与右值引用定义时的语法
左值引用需要在变量类型之后,名字之前加 &
右值引用需要在变量类型之后,名字之前加 &&
move是一个库函数,可以把左值强制转为右值
左值引用左值,右值引用右值如下
int main() {
int a = 0;
int& aa = a; // 左值引用
const int b = 0;
const int& bb = b; //const 左值引用
int&& cc = 0; // 右值引用
const int&& dd = 0; // const右值引用
const int& ee = 0; //必须加const 左值引用右值
int f = 0;
int&& ff = move(f); // 右值引用左值必须加move
const int g = 0;
const int&& gg = move(g); // 右值引用左值必须加move
return 0;
}
左值引用右值必须加const
右值引用左值需要move(左值),如果左值为const右值必须也要加const
用一个变量为右值引用,变量可以取地址,该变量是左值,----------右值引用运算的变量是一个左值
左值引用与右值引用的比较
首先,对于内置类型来说,右值引用的意义不大,我们来把代码转到反汇编看一看
可以看出其实二者的底层执行的操作没有任何区别
让我们来操作自定义类型试试
//这个是我封装库里string实现的一个MYSTL::string类,内部是stl::string+一个计数器
int tmp = 1;
namespace MYSTL {
class string {
public:
string() {
_tmp = tmp++;
std::cout << "这里调用了无参的构造函数,计数器:" << _tmp << std::endl;
}
string(const char* str) {
_tmp = tmp++;
std::cout << "这里调用了char* -> string的构造函数,计数器:" << _tmp << std::endl;
_std_string = str;
}
string(const string& s) {
_tmp = tmp++;
std::cout << "这里调用了左值引用的构造函数,计数器:" << _tmp << std::endl;
_std_string = s._std_string;
}
string(const string&& s) {
_tmp = tmp++;
std::cout << "这里调用了右值引用的构造函数,计数器:" << _tmp << std::endl;
_std_string = s._std_string;
}
string& operator=(const char* str) {
_std_string = str;
std::cout << "这里调用了char* -> string赋值重载函数,计数器:" << _tmp << std::endl;
return *this;
}
string& operator=(const string& s) {
std::cout << "这里调用了左值引用的赋值重载函数,计数器:" << _tmp << std::endl;
_std_string = s._std_string;
return *this;
}
string& operator=(const string&& s) {
std::cout << "这里调用了右值引用的赋值重载函数,计数器:" << _tmp << std::endl;
_std_string = s._std_string;
return *this;
}
protected:
private:
int _tmp;
std::string _std_string;
};
}
基于上面的类进行如下测试
int main() {
MYSTL::string s1 = "abcdef"; // 默认构造
MYSTL::string s2; // 左值赋值
s2 = s1;
MYSTL::string s3(s1); // 左值构造
MYSTL::string s4(std::move(s1));// 右值构造
MYSTL::string s5; // 右值赋值
s5 = MYSTL::string("123456");
system("pause");
}
可以看出传入参数不同,类也会执行不同的重载函数
右值引用的意义与优势
参数为右值的拷贝构造一般被称为移动构造,参数为右值的赋值重载函数一般被称为移动赋值
根据右值的定义,右值是取不出地址的值,显然我们不能直接在产生右值的作用域拿右值来进行运算,因为我们不能直接使用右值,所以,移动构造和移动重载都可以使用浅拷贝!!!
执行如下代码
int main() {
std::list<MYSTL::string> l;
l.push_back("1");
l.push_back("12");
l.push_back("123");
std::cout << std::endl;
MYSTL::string s1, s2, s3;
s1 = "1", s2 = "12", s3 = "123";
l.push_back(s1);
l.push_back(s2);
l.push_back(s3);
std::cout << std::endl;
system("pause");
return 0;
}
可以看出移动构造插入3个元素一共需要进行三次浅拷贝三次深拷贝
左值构造插入3个元素一共需要进行六次深拷贝 差距还是蛮大的
完美转发
现对于一个T类型的数据来说 , 它可以是:>1.T 2.const T 3. T& 4.const T& 5.T&& 6.const T&&
如果现在我要操作这个数据,难道我要写六个不同参数模板吗?当然可以,但是这未免也太太太太丑陋了
完美转发就是用来解决这个问题的
//语法
template<typename T>
void PerfectForward(T&& t) {
Fun(t);
}
请注意函数名中的&&不是右值引用的意思而是完美转发的意思
std::forward 完美转发在传参的过程中保留对象原生类型属性
using namespace std;
void Fun(int& x) {
cout << "void Fun(int& x)" << endl;
}
void Fun(const int& x) {
cout << "void Fun(const int& x)" << endl;
}void Fun(int&& x) {
cout << "void Fun(int&& x)" << endl;
}void Fun(const int&& x) {
cout << "void Fun(const int&& x)" << endl;
}
template<typename T>
void PerfectForward(T&& t) {
Fun(std::forward<T>(t));
}
int main()
{
int a = 0;
const int b = 0;
PerfectForward(a);
PerfectForward(b);
PerfectForward(0);
const int d = 0;
PerfectForward(move(d));
system("pause");
return 0;
}
这样就可以通过一个模板来管理全部类型函数