ReadingLab/Discussion-for-Cpp

关于const引用的问题

PoacherBro opened this issue · 12 comments

《C++ Primer》中文版第五版第 2.4.1 章里 const的引用,有几点疑惑:

  1. 下面代码会报错
int i = 42;
const int &r1 = i;
int &r4 = r1 * 2; // 错误,即使把 r1 替换成 i,也会报错

虽然后面解释了C++会创建一个常量类型的临时量,但是这里有一个计算r1 * 2,所以这里意思是如果表达式里有常量,会把字面量(这里是2)也设置为常量吗?

  1. 如上所描述,那么常量在赋值的时候也会和普通变量一样做一次拷贝吗?譬如
const int i = 32;
const int j = i; // 是否也有拷贝?拷贝的是地址还是32这个值对象?
  1. 看很多博客说常量引用不占用空间,那么引用是如何存储这个名字的?
pezy commented
  1. 引用就是"别名",r1 * 2 没有名字(后面你会知道,这是个右值),所以没法起别名,报错。
  2. 拷贝的是值。你可以试试,通过 const_cast改变 i 的值,j 的值不会变化。
  3. const 引用记的是地址,所以仅占用了指针大小的空间。

多谢大大。

  1. 既然右值没有办法起别名,那为什么const int &r4 = r1 * 2又可以呢?
  2. 引用记的是地址,那是不是可以理解引用是一种特殊的指针,只是C++内部已经帮我们做好了“地址引用”和“解引用”的操作?
pezy commented
  1. const 引用很强大,它既可以指向左值,也可以指向右值。
  2. 可以这样理解。

菜鸡补充一点
原问题 1 中,字面值本身都是常量。

多谢两位解惑。

跳到书的13.6.1章节,看到右值引用描述,觉得可以用右值和左值来理解。
要求转换的表达式、字面量和返回右值的表达式 => 这是右值,其他都是左值。结合 @pezy 说的“ const引用既可以指向左值,又可以指向右值”,就好理解了。

还得继续学习拷贝、赋值、销毁和移动对象这些内容 :)。

@pezy 再次请教一下,关于函数返回引用的问题。

int& func()
{
    int i = 0;
    return i;
}

int main()
{
    int ri = func();
    std::cout << ri << std::endl;
}

我在VS2012-64位版本编译和运行没有问题,但是在G++-8.1.1编译,会报警告
image

image

这是为什么?函数fun()里面的i是局部变量,按理来说在函数调用完成后应该会被销毁,再引用的话是会报错的吧?迷惑了。

pezy commented

函数fun()里面的i是局部变量,按理来说在函数调用完成后应该会被销毁,再引用的话是会报错的吧?

说的没错啊,你运行一下试试看?肯定 segmentation fault 了吧?

另,VS2012 也太老了,好歹也用个 2015 啊。

编译和运行都没有问题。在配置里设置的编译警告是/W3,后来改成/W4,能看到警告,但是运行还是没有问题,正常。
在g++会报错。

在SO问了这个问题,根据大神的说法,了解了这个属于C++的Undefined behavior,不同的编译器可能处理不同。

另外可以看看之后有一个人问的SO - Passing non-lvalue as const reference argument. Is the temp created in local scope or caller scope?,关于const引用是否会延长临时量的声明周期。

总结来说,临时量的声明周期不会因为第二次引用而延长,后面对临时量的引用都可能导致dangling reference。

  1. 返回指向局部变量的引用,这本身就是未定义的. 编译器并不会做什么保证.
  2. 返回指向局部变量的常量引用,这个是c++的一个特性,能够延长位于栈上的局部变量的生存周期(延长至和当前常量引用的作用域)

可参考: https://herbsutter.com/2008/01/01/gotw-88-a-candidate-for-the-most-important-const/

Normally, a temporary object lasts only until the end of the full expression in which it appears. However, C++ deliberately specifies that binding a temporary object to a reference to const on the stack lengthens the lifetime of the temporary to the lifetime of the reference itself, and thus avoids what would otherwise be a common dangling-reference error. In the example above, the temporary returned by f() lives until the closing curly brace. (Note this only applies to stack-based references. It doesn’t work for references that are members of objects.)

(最佳)实践

一般不单独返回引用(大概率是用错了),而是和输入引用搭配用来级联调用,比如常见的输入输出流:

ostream & print(ostream &os, std::string message) {
    return os << message << std::endl;
}
// 调用
std::cout << "hello" << print(std::cout, "paul");

而对于const Type &作为返回值应用比较多一些,用起来来比Type &作为输入函数要简单点(少了一步变量声明和初始化动作)

@ender233 多谢分享。确实,对于引用来说多数是作用于函数形参和返回值。

对于引用延长临时生命周期,确实是会,不过有三种异常情况。
参考cpp官网cpp - Reference initialization里面关于Lifetime of a temporary的说明,对于函数返回的局部变量的引用是其中一种。接收的引用是不能延长它的生命周期的。

总结一下就是

In general, the lifetime of a temporary cannot be further extended by "passing it on": a second reference, initialized from the reference to which the temporary was bound, does not affect its lifetime.

你链接的那篇文章,里面的代码并不是返回一个引用,而是一个字面量。

你链接的那篇文章,里面的代码并不是返回一个引用,而是一个字面量

无论是字面量还是局部对象,都在栈上,都可以用const Type &绑定。