彻底搞懂 c++ 函数参数的 & 和 &&

&

如果你在网上看到 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 的引用赋值给了 Warehousegood 成员。然而,这里要注意,我们的 WarehouseBuilder 运行时的 good 生命周期仅仅为 WarehouseBuilder 调用时,而 Warehousegood 引用生命周期要长,因此 Warehousegood 引用会引用到一个已经不存在的 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 && whoreWhore() 生命周期不一样会发生什么。(如果你有心看我啰啰嗦嗦写到这里,并且在用 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 绑定到同一地址

当然以上都是我的推断,通过编译器的行为反推标准,非常的谭浩强,千万不要被误导了

你可能感兴趣的:(彻底搞懂 c++ 函数参数的 & 和 &&)