如果你在网上看到 c++ 的几种传参方式,肯定就分成两种,“值传递”和“引用传递”。值传递很简单,复制一份就是了;“引用传递”就说的马马虎虎了。“传递的是实参的本身”,说起来很轻松,实际上很有问题。最简单的一个问题就是:“实参”本身不是一个东西怎么办?例如:
void f_ck(int & i) {
i++;
}
...
f_ck(1); // 编译不通过,VS答曰:非常量的引用值必须是左值。
...
当然我们后来也明白,&
是左值引用,所谓左值就是实际在运行时内存中存在的值,而不是 1
这种写死在运行代码中的值。
有时候即使我们赋予了左值,依然会出问题,比如下面这段代码:
#include "stdafx.h"
#include
#include
using namespace std;
class Good {
public:
string goodname;
Good() {
goodname = "not_good";
cout << "Good constructed" << endl;
}
~Good() {
cout << "Good destoyed" << endl;
}
};
class Warehouse {
public:
Good & good;
Warehouse(Good & good) : good(good)
{
}
static Warehouse* WarehouseBuilder() {
Good good;
return new Warehouse(good);
}
};
int main()
{
Warehouse * warehouse = Warehouse::WarehouseBuilder();
cout << warehouse->good.goodname << endl;
getchar();
return 0;
}
这段代码很简单,调用了一个 Builder
函数,返回了一个类。其中 WarehouseBuilder
将栈中的 good
的引用赋值给了 Warehouse
的 good
成员。然而,这里要注意,我们的 WarehouseBuilder
运行时的 good
生命周期仅仅为 WarehouseBuilder
调用时,而 Warehouse
的 good
引用生命周期要长,因此 Warehouse
的 good
引用会引用到一个已经不存在的 Good
,这时我们运行的 cout << warehouse->good.goodname << endl;
就会报错。
以下是输出:
Good constructed
Good destoyed
报错信息为:
0x0F8F69E6 (msvcp140d.dll)处(位于 ConsoleApplication2.exe 中)引发的异常: 0xC0000005: 读取位置 0xCCCCCCCC 时发生访问冲突。
可以看到我们这里有一个 0xCCCCCCCC
的填充内存,这是调试模式下 good
中的 string
析构时留下的印记,如果我们改成 Release
呢?
这时并没有任何报错。因为 VS
没有为我们进行析构时的填充。
那么怎么解决这个问题呢?很显然,你需要 new
一个。
&&,就是右值引用了。根据某些博客的定义,右值是可以出现在等式右边的值,但实际上呢?
一个合理的猜测是:&& 和 & 作为右值时,有类似的行为,只是我们对这个值的权限不一样。右值 && 显然不能放在等号左边。下面用 VS 验证一下。
结果就打脸了。
class Whore {
public:
string nickname;
};
class Brothel {
public:
Whore && whore_;
Brothel(Whore && whore): whore_(whore) { //编译不通过,无法将右值引用绑定到左值,原因之后解释
}
};
如果我们换个写法:
class Whore {
public:
string nickname;
};
class Brothel {
public:
Whore && whore_;
Brothel(Whore && whore) { // 编译不通过,“Brothel::whore_”: 必须初始化引用
whore_ = whore;
}
};
再换种写法:
class Whore {
public:
string nickname;
};
class Brothel {
public:
Whore & whore_;
Brothel(Whore && whore): whore_(whore) {
}
};
这里,将成员 whore_
变成了 左值引用。
然后我们再看,能否“正常地”初始化 Brothel
...
Whore && whore = Whore();
Brothel(whore); // 编译不通过
...
为啥这样不行呢?这不是幻觉,按理说,Whore
也是右值引用,它凭什么不能传递给 Brothel
的构造函数作为参数呢?
根据我的理解,原理是这样的:
当进行
Whore && whore = Whore();
时,这一操作的实质是 Whore()
本来是个匿名的实体,它 本来不被需要,就像 int a, b; b = a + 1;
中的 a + 1
一样,是转瞬即逝的值,算完就扔掉了(复制给 b
了,结果就不再被需要了),但是我们用 Whore && whore = Whore();
其实就是告诉编译器,我们给它起了个名字,叫做 whore
。它被固定下来了。
我们可以这样操作:
whore.nickname = "Crystal";
完美!就像 Whore whore = Whore()
一样。
Whore constructed
Crystal
所以 Whore && whore = Whore();
执行以后,whore
就变成了实打实的 Whore()
(literally,这一行的Whore()
)的一个别名,它被捕获了,whore
的语义发生了变化,它就是 Whore()
。
照理来讲,如果我们认为 Whore()
是一个右值,一个表达式里算完就可以扔掉不要的值,那么 我们执行
Whore && whore = Whore();
似乎是在延长 Whore()
的生命周期。
到这里,我们就可以填上刚才那个 “原因之后解释” 的坑了。
因为传递参数进来以后,whore
已经变成了实体,它有了名字,自然不能再绑定给另一个右值了。
这很奇怪。这不符合 c++ 一贯的风格。那么我们看看如果 Whore && whore
和 Whore()
生命周期不一样会发生什么。(如果你有心看我啰啰嗦嗦写到这里,并且在用 VS 验证,记得把 Release 调成 Debug )
Whore && F_ckWhore() {
return Whore(); // 返回值捕获了匿名值 Whore(),但是我们接下来可以看到 Whore() 去世了,引用无效了。
}
int main()
{
Whore && whore2 = F_ckWhore();
cout << whore2.nickname << endl; // 0x5D6669E6 (msvcp140d.dll)处(位于 ConsoleApplication2.exe 中)引发的异常: 0xC0000005: 读取位置 0xFFFFFFFF 时发生访问冲突
getchar();
return 0;
}
可以看到,这里确实 Whore()
作为在 F_ckWhore()
的栈上生存的玩意,确实在函数返回后被析构了。
我们可以这样认为,用 type && r
去引用一个匿名的右值(右值当然是匿名的),确实延长了其生命周期,但是这有个限度,就是不能超过栈的生命周期。它的内部原理可能是强制中间变量存储在栈上,而不允许其被优化掉。
总结一下,c++ 中的左值就像指针,它可以捕获实实在在的实体,但是我们要注意被捕获值的生命周期。不要随便把生命周期和栈同步的实体传给了它;
右值其实也是指针,但是它功能是专门捕获匿名的实体(可以理解为编译产生的中间变量)。同时我们要注意,右值在定义时捕获了实体以后,右值的名字就变成了被捕获的实体。更加明确一些,就是(这是我猜的)
//你的程序
int && b = a + 1;
------
//编译过程
t1 = a + 1;
b 和 t1 绑定到同一地址
当然以上都是我的推断,通过编译器的行为反推标准,非常的谭浩强,千万不要被误导了