1. 左值和右值
- 左值(L-value):能用“取地址&”运算符获得对象的内存地址,表达式结束后依然存在的持久化对象。左值可以出现在等号左边也能够出现在等号右边。
- 右值(R-value):不能用“取地址&”运算符获得对象的内存地址,表达式结束后就不再存在的临时对象。只能出现在等号右边。
- 可以做出以下三点理解:
1)当一个对象被用作右值的时候,用的是对象的值(内容);而被用作左值的时候,用的是对象的身份(在内存中的位置)。总之:左值看地址,右值看内容。
2)所有的具名变量或者对象都是左值,而右值不具名,如常见的右值有非引用返回的临时变量、运算表达式产生的临时变量、原始字面量和lambda表达式等。
很难得到左值和右值的真正定义,但是有一个可以区分左值和右值的便捷方法:看能不能对表达式取地址,如果能,则为左值,否则为右值。
3)右值要么是字面常量,要么是在表达式求值过程中创建的对象。
特例:因为可以用&取得字符串字面值常量的地址,虽然它不能被赋值,但它是一个左值。
int main()
{
char *p = "1234";
printf("%d\n", p);
printf("%d\n", &"1234");
}
- 为什么右值不能用&取地址呢?
1)对于临时对象,它可以存储于寄存器中,所以没办法用“取地址&”运算符;
2)对于(非字符串)常量,它可能被编码到机器指令的“立即数”中,所以没办法用“取地址&”运算符。
2. 左值引用和右值引用
使用引用的目的就在于减少不必要的拷贝。
- 左值引用:对左值的引用,就是给左值取别名。其基本语法如下:
Type &引用名 = 左值表达式;
-变量名实质上是一段连续存储空间的别名,是一个标号(门牌号),通过变量的名字可以使用存储空间。
- 对一段连续的内存空间只能取一个别名吗?
在C++中新增加了引用的概念,引用可以看作一个已定义变量的别名,于是我们就可以通过引用为一个内存空间取多个别名。
int main()
{
int a = 0;
int &b = a;
b = 11;
return 0;
}
-普通引用在声明时必须用其它的变量进行初始化,引用作为函数参数声明时不进行初始化。
struct Teacher
{
char name[64];
int age;
};
void printfT(Teacher *pT) { cout << pT->age << endl; }
/*
* pT是t1的别名, 相当于修改了t1
*/
void printfT2(Teacher &pT) { pT.age = 33; }
/*
* pT和t1的是两个不同的变量,t1 copy一份数据给pT, 只会修改pT变量 ,不会修改t1变量
*/
void printfT3(Teacher pT) { pT.age = 45; }
int main()
{
Teacher t1;
t1.age = 35;
printfT(&t1);
printfT2(t1);
printf("t1.age:%d\n", t1.age) // 33
printfT3(t1);
printf("t1.age:%d\n", t1.age); //35
return 0;
}
- 对于引用语法,C++编译器背后做了什么工作呢?
首先我们知道引用单独定义时,必须初始化,说明它很像一个常量。又因为引用是一个内存空间的别名所以它可以取地址。
故我们可以得到引用的本质:
1)引用在C++中的内部实现是一个常指针。Type& name <=> Type* const name
2)C++编译器在编译过程中使用常指针作为引用的内部实现,因此引用所占用的空间大小与指针相同。
3) 从使用的角度,引用会让人误会其只是一个别名,没有自己的存储空间。这是C++为了实用性而做出的细节隐藏。
- 函数返回值是引用(引用当左值)
当函数返回值为引用时,若返回栈变量,不能成为其它引用的初始值,不能作为左值使用。若返回静态变量或全局变量,
可以成为其他引用的初始值,即可作为右值使用,也可作为左值使用。
对于引用的理解可以直接看成指针,因为栈变量在函数结束后,内存空间就被释放了,所以这个指针指向的内容就不对了。
- 对指针的引用
struct Teacher
{
char name[64];
int age;
};
// 指针的引用
int getTe(Teacher* &myp)
{
myp = (Teacher *)malloc(sizeof(Teacher));
myp->age = 34;
return 0;
}
int main()
{
Teacher *p = NULL;
getTe(p);
printf("age:%d\n", p->age);
return 0;
}
- 常引用(const T &)
int main()
{
int a = 10;
int &b = a; //普通引用
const int &c = a; //常量引用:只能通过c读取a的内存空间
// 常量引用初始化分为两种
// 1. 变量 初始化 常量引用
int x = 20;
const int& y = x;
printf("y:%d\n", y);
// 2. 常量 初始化 常量引用
// int &m = 10; // 引用是内存空间的别名 字面量10没有内存空间 没有方法做引用
const int &m = 10;
return 0;
}
const引用结论
1)Const & int e 相当于 const int * const e
2)普通引用相当于 int *const e
3)当使用常量(字面量)这类右值对const引用进行初始化时,C++编译器会为常量值分配空间,并将引用名作为这段空间的别名。
初始化后,将生成一个只读变量。只有常引用才可以用右值表达式初始化,这一点很重要,因为如果不加const,那么这个
临时的对象是无法进行传递给左值引用的,比如
MyString s = MyString("hello") // 这个临时对象本身就存在于内存空间,所以无需为这个右值分配空间
因为MyString("hello")是一个临时对象,即右值,所以MyString实现的拷贝构造函数参数不加const就会报错。
- 右值引用:对右值的引用,就是给右值取别名。其基本语法如下:
Type &&引用名 = 右值表达式; // 如果是左值表达式,绑定就会出错。这里虽然是个右值引用,但左侧的具名变量本身是个左值
- 开始介绍右值引用之前,先得了解到底啥是临时对象?
在C++中创建对象是一个费时、废空间的一个操作,有些固然必不可少,但还有一些对象却在我们不知道的情况下创建了。
1)以值的方式给函数传参
给函数传参有两种方式----按值传递和按引用传递。按值传递时,首先将需要传给函数的参数,调用拷贝构造函数创建
一个副本,所有在函数里的操作都是针对这个副本的,也正是因为这个原因,在函数体里对该副本进行任何操作,都不会影响原参数。
class Test
{
public:
int a, b;
public:
Test(Test& t) : a(t.a), b(t.b) { printf("Copy function!\n"); }
Test(int m = 0,int n = 0) : a(m), b(n) { printf("Construct function!\n"); }
virtual ~Test() {}
public:
int GetSum(Test ts)
{
int tmp = ts.a + ts.b;
ts.a = 1000; //此时修改的是tm的一个副本
return tmp;
}
};
int main()
{
Test tm(10,20);
printf("Sum = %d \n",tm.GetSum(tm));
printf("tm.a = %d \n",tm.a);
return 0;
}
当函数执行结束后,这个临时的对象就会被销毁了。可以将 int GetSum(Test ts)改成 int GetSum(Test &ts) 来避免产生这个拷贝了。
2)类型转换生成的临时对象
int main()
{
Test tm(10,20), sum;
sum = 1000; // 调用 Test(int m = 0,int n = 0) 构造函数,还会调用一次赋值运算符
printf("Sum = %d \n",tm.GetSum(sum));
}
3)函数返回一个对象
当函数需要返回一个对象,他会在栈中创建一个临时对象或也叫匿名对象(如果是类对象,则会调用拷贝构造函数),存储函数的返回值。
这个临时对象在表达式 sum = Double(tm) 结束后就自动销毁了,这个临时对象就是右值。
按理说下面这个例子中Double函数返回时会触发拷贝构造函数,但实际运行后却没有,猜想是被编译器优化了,可以在编译时设置编译
选项-fno-elide-constructors用来关闭返回值优化效果。
class Test
{
public:
int a;
public:
Test(Test& t) : a(t.a) { printf("Copy Construct!\n"); }
Test(int m = 0) : a(m) { printf("Construct!\n"); }
virtual ~Test() {};
public:
Test& operator=(const Test& t)
{
a = t.a;
printf("Assignment Operator!\n");
return *this;
}
};
Test Double(Test& ts)
{
Test tmp;
tmp.a = ts.a * 2;
return tmp;
}
int main()
{
Test tm(10), sum;
sum = Double(tm);
printf("sum.a = %d\n",sum.a);
return 0;
}
- 引入右值引用的目的:右值引用是C++11中新增加的一个很重要的特性,它主要用来解决以下问题。
1)函数返回临时对象造成不必要的拷贝操作:通过使用右值引用,右值不会在表达式结束之后就销毁了,而是会被“续命”,
的生命周期将会通过右值引用得以延续,和变量的声明周期一样长。
int g_constructCount = 0;
int g_copyConstructCount = 0;
int g_destructCount = 0;
class Test
{
public:
Test() { cout << "construct: " << ++g_constructCount << endl; }
Test(const Test& a) { cout << "copy construct: " << ++g_copyConstructCount << endl; }
~Test() { cout << "destruct: " << ++g_destructCount << endl; }
};
Test GetTestObj() { return Test(); }
int main()
{
Test a = GetTestObj();
return 0;
}
// 上面代码关掉返回值优化后输出:
construct: 1 // return Test()
copy construct: 1 // 临时对象构造
destruct: 1 // return Test()对象销毁
copy construct: 2 // a对象构造
destruct: 2 // 临时对象销毁
destruct: 3 // a对象销毁
//-------------------------------------------------------------------------------------------------
// 但是如果使用右值引用来接收返回值呢?
int main()
{
Test &&a = GetTestObj();
return 0;
}
// 输出如下
construct: 1 // return Test()
copy construct: 1 // 临时对象构造
destruct: 1 // return Test()对象销毁
destruct: 2 // a这个对象其实就是那个临时对象了,main结束后才销毁
通过右值引用,比之前少了一次拷贝构造和一次析构,原因在于右值引用绑定了右值,让临时右值的生命周期延长了。
我们可以利用这个特点做一些性能优化,即避免临时对象的拷贝构造和析构。
2)通过右值引用传递临时参数:使用字面值(如1、3.15f、true),或者表达式等临时变量作为函数实参传递时,按左值引用传递参数会被编译器阻止。
而进行值传递时,将产生一个和参数同等大小的副本。C++11提供了右值引用传递参数,不申请局部变量,也不会产生参数副本。
static float global = 1.111f;
void offset(float &&f) { global += f; } // 通过右值引用传递参数
void offset(float& f) { global -= f; } // 重载了offset函数,而且是左值传递
float getFloat() { return 4.444f; }
int main()
{
float u = 10.000f;
cout << "global:" << global << endl;
offset(3.333f); // 这里会调用右值引用参数的函数
cout << "global:" << global << endl;
offset(getFloat() + 2.222);
cout << "global:" << global << endl;
offset(u); // 执行的是按左值引用的offset函数,右值引用无法初始化为左值.
cout << "global:" << global << endl;
return 0;
}
对于非模板函数,函数参数有确定的类型,右值引用只能与右值绑定,只接收右值实参,可以将它看作是临时变量的别名,不会将临时
变量再复制1次,和按值传递相比提高了效率。这一点同3)进行区别。
3)模板函数中如何按照参数的实际类型进行转发:当右值引用和模板结合的时候,就复杂了。T&&
并不一定表示右值引用,它可能是个左值
引用又可能是个右值引用。如果函数模板表示的是右值引用的话,肯定是不能传递左值的,但是事实却是可以。这里的&&
是一个未定义的引用类型,
称为universal references
,它必须被初始化,它是左值引用还是右值引用却决于它的初始化,如果它被一个左值初始化,它就是一个左值引用;
如果被一个右值初始化,它就是一个右值引用。
注意:只有当发生自动类型推断时(如函数模板的类型自动推导,或auto关键字),&&
才是一个universal references
。
// Test是一个特定的类型,不需要类型推导,所以&&表示右值引用
template<typename T>
class Test
{
Test(Test&& rhs);
};
// 右值引用
void f1(Test&& param);
// 在调用这个f之前,这个vector<T>中的推断类型已经确定了,所以调用f函数的时候没有类型推断了,所以是右值引用
template<typename T>
void f2(std::vector<T>&& param);
// universal references仅仅发生在 T&& 下面,任何一点附加条件都会使之失效, 所以是右值引用
template<typename T>
void f3(const T&& param);
// 这里T的类型需要推导,所以 && 是一个 universal references
template<typename T>
void f(T&& param);
int main()
{
int x = 1;
int && a = 2;
string str = "hello";
f(1); // 参数是右值 T 推导成了int, 所以是int&& param, 右值引用
f(x); // 参数是左值 T 推导成了int&, 所以是int&&& param, 折叠成 int&, 左值引用
f(a); // 虽然 a 是右值引用,但它还是一个左值,T推导成了int&
f(str); // 参数是左值, T 推导成了string&
f(string("hello")); // 参数是右值, T 推导成了string
f(std::move(str)); // 参数是右值, T 推导成了string
}
所以最终还是要看T
被推导成什么类型,如果T
被推导成了string
,那么T&&
就是string&&
,是个右值引用,如果T
被推导为string&
,
就会发生类似string& &&
的情况,对于这种情况,c++11
增加了引用折叠的规则,本质如下:
所有的引用折叠最终都代表一个引用,要么是左值引用,要么是右值引用。规则就是:
如果任一引用为左值引用,则结果为左值引用。否则(即两个都是右值引用),结果为右值引用。
引用折叠存在四种情形,根据上面的规则我们可以知道:
1)左值-左值 T& & <=>
int &``
2)左值-右值 T& && <=>
int &``
3)右值-左值 T&& & <=> int &
4)右值-右值 T&& && <=> int &&
因为1,2,3中都存在一个左值引用。
原文链接: https://www.cnblogs.com/yanghh/p/12976780.html
欢迎关注
微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍
原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/197977
非原创文章文中已经注明原地址,如有侵权,联系删除
关注公众号【高性能架构探索】,第一时间获取最新文章
转载文章受原作者版权保护。转载请注明原作者出处!