为什么引用在C ++中不可重定位


73

C ++引用具有两个属性:

  • 他们总是指向同一个对象。
  • 它们不能为0。

指针相反:

  • 它们可以指向不同的对象。
  • 它们可以是0。

为什么在C ++中没有“不可为空的,可重定位的引用或指针”?我想不出为什么引用不可重新引用的充分理由。

编辑: 问题经常出现,因为当我要确保“关联”(在这里避免使用“引用”或“指针”一词)永远不会无效时,我通常会使用引用。

我认为我从来没有想过“太棒了,这个引用总是指向同一个对象”。如果引用是可重新定位的,则仍然可以得到如下所示的当前行为:

int i = 3;
int& const j = i;

这已经是合法的C ++,但毫无意义。

我这样重申我的问题: “'引用对象'设计的背后原理什么?为什么认为引用始终是同一对象而不是仅在声明为const时有用?”

干杯,菲利克斯


5
我知道这有点晚了,但是迈克尔·伯尔(Michael Burr)的答案似乎是最完整的。它解释了Stroustrup的哲学设计决策。其他所有人似乎都在乞求这个论点(以某种方式)。
jww

Answers:


100

Stroustrup的“ C ++的设计和演进”中给出了C ++不允许您重新绑定引用的原因:

初始化后无法更改引用所指的内容。也就是说,一旦初始化了C ++引用,以后就不能再引用其他对象了;它不能被重新绑定。过去,我一直被Algol68引用所困扰,在其中r1=r2可以通过分配给所引用r1的对象,也可以根据的类型为r1(重新绑定r1)分配新的引用值r2。我想避免在C ++中出现此类问题。


32

在C ++中,经常说“引用对象”。从某种意义上说是正确的:尽管在编译源代码时将引用作为指针处理,但是引用旨在表示调用函数时未复制的对象。由于引用不能直接寻址(例如,引用没有地址,并且返回对象的地址),因此在语义上重新分配它们是没有意义的。而且,C ++已经具有指针,用于处理重新设置的语义。


我仍然没有真正理解为什么(在我看来)有用的非null可重定位引用不在c ++中的原因。提出的问题似乎并不难解决,我认为程序的可靠性会提高。由于投票而接受此答案。
TheFogger

1
更正:“尽管引用通常被当作指针处理”
轨道竞赛

12
“引用对象”是错误的。除了临时扩展生存期的特殊情况外,引用的生存期与对象所引用的生存期不同。如果引用实际上是对象,则当引用超出范围时,该对象将被销毁(或者,只要引用的任何对象都存在,则该对象将一直存在),事实并非如此。
Ben Voigt

19

因为那样您就不会有不能为0的可重定位类型。除非您提供了3种引用/指针类型。哪一种只会使语言复杂化而得不到什么收益(然后为什么也不要添加第4种类型呢?非可重复引用可以为0?)

一个更好的问题可能是,为什么您希望引用可以重新定位?如果确实如此,那将使它们在许多情况下不那么有用。这将使编译器更难进行别名分析。

似乎Java或C#中的引用可重新访问的主要原因是因为它们完成了指针的工作。他们指向对象。它们不是对象的别名。

以下效果是什么?

int i = 42;
int& j = i;
j = 43;

在当今的C ++中,具有不可重复的引用,这很简单。j是i的别名,并且i最终取值为43。

如果参考是可重新设置的,则第三行将参考j绑定到其他值。它不再是别名i,而是别名整数43(当然是无效的)。也许是一个更简单(或至少在语法上有效)的示例:

int i = 42;
int k = 43;
int& j = i;
j = k;

具有可重新引用的参考。评估此代码后,j将指向k。使用C ++的不可重复引用,j仍指向i,并且为i分配值43。

使引用可重新定位会更改语言的语义。该引用不再是另一个变量的别名。相反,它具有自己的赋值运算符,成为一种单独的值类型。然后,引用的最常见用法之一将是不可能的。并没有任何回报。新获得的引用功能已经以指针形式存在。因此,现在我们有两种方法可以完成相同的操作,而没有方法可以完成当前C ++语言中的引用。


1
+1。关键部分是“新获得的针对引用的功能已经以指针的形式存在”。
j_random_hacker

7
已经3种类型。可能为空且不可重设的类型为int* const。为了保持一致,应该为4,因为可能为零的位置和可重新定位的位置非常正交。
MSalters

2
是的,它们是正交的,但是语言不必提供所有组合。问题是,增加的表达能力是否会超过增加的复杂性?C ++本身已经足够复杂,因此与“尚不存在”相比,他们需要更好的理由来添加功能。
jalf

2
为“ +1”表示“因此,现在我们有两种方法可以做同一件事,而没有任何方法可以做当前C ++语言中的引用。
Earth Engine

没有理由为什么j = k必须重置j。您可以坚持引用现在具有的当前语义,并添加另一个操作/语法来重新定位,例如&j =&k。
pavon,

5

引用不是指针,它可以在后台实现为指针,但其核心概念并不等同于指针。引用应该像它所引用*is*的对象一样看待。因此,您不能更改它,并且它不能为NULL。

指针只是一个保存内存地址的变量。 指针本身具有自己的内存地址,并且在该内存地址内保存着另一个指向的内存地址引用不一样,它没有自己的地址,因此不能更改为“保留”另一个地址。

我认为参考文献中parashift C ++常见问题说得最好:

重要说明:即使经常使用底层汇编语言中的地址来实现引用,也请不要将引用视为指向对象的有趣指针。引用是对象。它不是指向对象的指针,也不是对象的副本。它是对象。

并在FAQ 8.5中再次:

与指针不同,一旦将引用绑定到对象,就不能将其“重新定位”到另一个对象。引用本身不是对象(它没有身份;使用引用的地址会为您提供引用对象的地址;请记住:引用是其引用对象)。


5

可重新定位的引用在功能上与指针相同。

关于可空性:您不能保证在编译时此类“可重复引用”为非NULL,因此任何此类测试都必须在运行时进行。您可以编写一个智能的指针样式的类模板来实现此目的,该模板在初始化或分配为NULL时会引发异常:

struct null_pointer_exception { ... };

template<typename T>
struct non_null_pointer {
    // No default ctor as it could only sensibly produce a NULL pointer
    non_null_pointer(T* p) : _p(p) { die_if_null(); }
    non_null_pointer(non_null_pointer const& nnp) : _p(nnp._p) {}
    non_null_pointer& operator=(T* p) { _p = p; die_if_null(); }
    non_null_pointer& operator=(non_null_pointer const& nnp) { _p = nnp._p; }

    T& operator*() { return *_p; }
    T const& operator*() const { return *_p; }
    T* operator->() { return _p; }

    // Allow implicit conversion to T* for convenience
    operator T*() const { return _p; }

    // You also need to implement operators for +, -, +=, -=, ++, --

private:
    T* _p;
    void die_if_null() const {
        if (!_p) { throw null_pointer_exception(); }
    }
};

有时这可能有用-与non_null_pointer<int>参数接受相比,带有参数的函数当然可以向调用者传递更多的信息int*


5
错误。您可以轻松保证可重新引用不为空。重新设置时,也将采用左值,就像初始化一样。例如int foo [0] = {0}; int ref = foo [0]; ref =&= foo [1] / *重新安装ref,未分配给foo [0] * /
MSalters

1
@MSalters:不,与初始化常规引用相比,要求左值不能保证可重用引用为非null -请参见8.3.2.4。例如:“ int&r = * static_cast <int *>(0);”。“ x被禁止”和“ x产生不确定的行为”之间存在着巨大的差异。
j_random_hacker

1
@MSalters:作为附带问题,您可以在不牺牲任何信息内容的情况下以较少的敌对态度来表达您的评论。但是您选择不-为什么?
j_random_hacker

1
实际上,8.3.2.4陈述了我所说的内容:“在定义良好的(C ++)程序中不能存在空引用”。C ++标准基本上涵盖了两个主题:什么是定义明确的C ++程序以及它们的行为方式。发明了“鼻恶魔”以明确该标准的局限性。
MSalters

3
另外,您可以安全地缩写“ WRONG”。一直下降到0个字符。
j_random_hacker

4

将C ++引用命名为“别名”可能会减少混淆?就像其他人提到的那样,C ++中的引用应该作为变量它们是指,不是作为一个指针/参考的变量。因此,我想不出应该重置它们的充分理由。

当处理指针时,将null用作值通常是有意义的(否则,您可能需要引用)。如果您特别想禁止保留null,则可以始终编写自己的智能指针类型;)


+1为智能指针的想法;我也会说同样的话。如果现有标准已经可以完成,则不考虑该语言的新功能。
Mark Ransom

4

有趣的是,这里的许多答案有点模糊,甚至不重要(例如,并非因为引用不能为零或相似,实际上,您可以轻松地构造一个引用为零的示例)。

无法重新设置参考的真正原因很简单。

  • 指针使您可以做两件事:更改指针后面的值(通过->*运算符),以及更改指针本身(直接分配=)。例:

    int a;
    int * p = &a;
    1. 更改值需要取消引用: *p = 42;
    2. 更改指针: p = 0;
  • 引用仅允许您更改值。为什么?由于没有其他语法可以表示重设。例:

    int a = 10;
    int b = 20;
    int & r = a;
    r = b; // re-set r to b, or set a to 20?

换句话说,如果允许您重新设置参考,那将是模棱两可的。通过引用传递时更有意义:

void foo(int & r)
{
    int b = 20;
    r = b; // re-set r to a? or set a to 20?
}
void main()
{
    int a = 10;
    foo(a);
}

希望有帮助:-)


2

某些编译器有时会强制C ++引用0 (这样做*是一个坏主意,并且违反了标准*)。

int &x = *((int*)0); // Illegal but some compilers accept it

编辑:据了解比我更好的各种标准的人说,以上代码产生“未定义的行为”。在GCC和Visual Studio的至少某些版本中,我已经看到了这样做的预期结果:等效于将指针设置为NULL(并在访问时导致NULL指针异常)。


“这真是个坏主意”:因为这样一个小动物的效用是有限的。
dmckee ---前主持人小猫

不过,它可能是偶然发生的,因此最好知道这种可能性。我以前曾在这个主题上做过练习。
Mark Ransom

该代码是非法的:在有效的C ++代码中,绝对不能发生这种情况。因此,否,正如C ++标准所保证的那样,引用永远不能为0。
康拉德·鲁道夫

C ++引用不能为“ 0”。这就像说“非静态函数与静态函数相同,因为它们不需要调用对象”。
Johannes Schaub-litb

2
在这样的讨论中,“未定义的行为”借口很好,但在尝试查找错误时却无济于事。尽管未定义,但该行为通常是可以预测的。而且阴险,因为坠机往往离起因还很远。
Mark Ransom

1

您不能这样做:

int theInt = 0;
int& refToTheInt = theInt;

int otherInt = 42;
refToTheInt = otherInt;

...出于同样的原因,为什么secondInt和firstInt在这里没有相同的值:

int firstInt = 1;
int secondInt = 2;
secondInt = firstInt;
firstInt = 3;

assert( firstInt != secondInt );

1

这实际上不是答案,而是解决此限制的方法。

基本上,当您尝试“重新绑定”引用时,实际上是在以下上下文中尝试使用相同的名称来引用新值。在C ++中,这可以通过引入块作用域来实现。

以杰夫为例

int i = 42;
int k = 43;
int& j = i;
//change i, or change j?
j = k;

如果要更改i,请按照上面的说明进行编写。但是,如果您想更改jmean的含义k,则可以执行以下操作:

int i = 42;
int k = 43;
int& j = i;
//change i, or change j?
//change j!
{
    int& j = k;
    //do what ever with j's new meaning
}

0

我想这与优化有关。

当您可以清楚地知道一个变量意味着什么内存时,静态优化就容易得多。指针会破坏这种情况,并且可重设的引用也会被破坏。


0

因为有时候事情不应该是指向对象的。(例如,对单例的引用。)

因为在函数中知道您的参数不能为null是很棒的。

但主要是因为它允许使用具有确实是指针的东西,但其作用类似于局部值对象。C ++尽力引用Stroustrup,使类实例“作为int d做”。通过值传递int很便宜,因为int可以放入机器寄存器中。类通常大于整数,并且按值传递它们会产生大量开销。

能够传递“看起来像”一个值对象的指针(通常是一个int的大小,或者可能是两个int的大小),使我们可以编写更简洁的代码,而无需取消引用的“实现细节”。并且,连同运算符重载,它使我们可以使用类似于int的语法来编写类。尤其是,它允许我们使用可以同样应用于原语(例如int)和类(例如复数类)的语法编写模板类。

而且,尤其是对于运算符重载,在某些地方我们应该返回一个对象,但是同样,返回指针要便宜得多。再一次,返回引用是我们的“努力。

而且指针很难。也许不适合您,也不适合任何意识到指针只是内存地址值的人。但是回想起我在CS 101上课时,他们让很多学生绊倒了。

char* p = s; *p = *s; *p++ = *s++; i = ++*p;

可能会造成混乱。

哎呀,经过40年的C开发,人们仍然甚至无法同意指针声明是否应为:

char* p;

要么

char *p;

正确的方法是:char * p。意见分歧在于风格,这是主观的。
GManNickG

1
@GMan:你那里有错字。您说正确的方法是“ char p”,其中显然是“ char p”,因为类型是指向char的指针。只是一个有用的,有效的指针。:)
John Dibling 09年

@John:不幸的是,C和C ++语法规则将声明分为两部分。为了说明:“ char * p,c;” 声明1个称为p的指针到字符,以及一个称为c的字符(非指针到字符)。我知道,。:/
j_random_hacker 2009年

char *p说那*p是一个char。我来自Pascal的背景,var p: ^Char语法会在哪里,但是当我看到char*C / C ++中的Pascal样式的指针语法时,我仍然觉得很奇怪……尤其是当一次定义多个变量时,例如char* x, y, z
Mark K Cowan

0

我一直想知道为什么他们没有为此做一个引用赋值运算符(例如:=)。

为了让别人感到不安,我编写了一些代码来更改结构中引用的目标。

不,我不建议重复我的把戏。如果移植到足够不同的体系结构,它将中断。


0

认真一点:恕我直言,使它们与指针略有不同;)您知道您可以编写:

MyClass & c = *new MyClass();

如果以后还可以写:

c = *new MyClass("other")

将任何引用与指针一起有意义吗?

MyClass * a =  new MyClass();
MyClass & b = *new MyClass();
a =  new MyClass("other");
b = *new MyClass("another");

0

C ++中的引用不可为空的事实是它们只是别名的副作用。



0

如果您想要一个成员变量作为引用,并且希望能够重新绑定它,则有一个解决方法。虽然我发现它有用且可靠,但请注意它在内存布局上使用了一些(非常弱)的假设。由您决定是否在您的编码标准之内。

#include <iostream>

struct Field_a_t
{
    int& a_;
    Field_a_t(int& a)
        : a_(a) {}
    Field_a_t& operator=(int& a)
    {
        // a_.~int(); // do this if you have a non-trivial destructor
        new(this)Field_a_t(a);
    }
};

struct MyType : Field_a_t
{
    char c_;
    MyType(int& a, char c)
        : Field_a_t(a)
        , c_(c) {}
};

int main()
{
    int i = 1;
    int j = 2;
    MyType x(i, 'x');
    std::cout << x.a_;
    x.a_ = 3;
    std::cout << i;
    ((Field_a_t&)x) = j;
    std::cout << x.a_;
    x.a_ = 4;
    std::cout << j;
}

这不是很有效,因为您需要为每个可重新分配的引用字段使用单独的类型并使它们成为基类。同样,这里有一个很弱的假设,即具有单一引用类型的类将没有可能会破坏MyType的运行时绑定的__vfptr或任何其他type_id相关字段。我知道的所有编译器都满足该条件(不这样做就没有意义了)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.