83

C++ 引用有两个属性:

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

指针则相反:

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

为什么 C++ 中没有“不可为空、可重新定位的引用或指针”?我想不出一个很好的理由为什么引用不应该是可重新安装的。

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

我想我从来没有想过“很棒,这个 ref 总是指同一个对象”。如果引用是可重新安装的,仍然可以得到如下的当前行为:

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

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

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

干杯,菲利克斯

4

17 回答 17

112

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

初始化后无法更改引用所指的内容。也就是说,一旦 C++ 引用被初始化,它就不能在以后引用不同的对象;它不能重新绑定。我过去曾被 Algol68 引用所困扰,其中r1=r2可以分配r1给所引用的对象或分配一个新的引用值r1(重新绑定r1),具体取决于r2. 我想避免在 C++ 中出现此类问题。

于 2009-04-08T02:16:42.547 回答
32

在 C++ 中,常说“引用对象”。从某种意义上说,确实如此:虽然在编译源代码时引用被作为指针处理,但引用的目的是表示在调用函数时不会复制的对象。由于引用不能直接寻址(例如,引用没有地址,& 返回对象的地址),重新分配它们在语义上没有意义。此外,C++ 已经有指针,它处理重新设置的语义。

于 2009-04-08T02:05:04.210 回答
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++ 语言中的引用所做的事情。

于 2009-04-08T02:34:14.157 回答
5

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

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

我认为参考资料上的 parashift C++ FAQ说得最好:

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

并再次在FAQ 8.5中:

与指针不同,一旦引用绑定到一个对象,它就不能“重新定位”到另一个对象。引用本身不是一个对象(它没有身份;获取引用的地址可以得到引用的地址;记住:引用是它的引用)。

于 2009-04-08T02:35:25.823 回答
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*

于 2009-04-08T02:50:54.303 回答
5

有趣的是,这里的许多答案都有些模糊甚至题外话(例如,这不是因为引用不能为零或相似,事实上,您可以轻松构建一个引用为零的示例)。

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

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

    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);
}

希望有帮助:-)

于 2015-02-10T15:20:03.757 回答
4

将 C++ 引用命名为“别名”可能会更容易混淆?正如其他人所提到的,C++ 中的引用应该是它们引用的变量,而不是对变量的指针/引用。因此,我想不出一个很好的理由它们应该是可重置的。

在处理指针时,允许 null 作为值通常是有意义的(否则,您可能需要一个引用)。如果您特别想禁止持有 null,您可以随时编写自己的智能指针类型;)

于 2009-04-08T02:16:17.867 回答
2

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

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

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

于 2009-04-08T02:04:28.067 回答
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 );
于 2009-04-08T02:55:39.893 回答
1

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

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

在 jalf 的例子中

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

如果要更改 i,请按上述方式编写。但是,如果你想改变jto mean的含义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
}
于 2013-05-30T00:30:50.487 回答
0

我想这与优化有关。

当您可以明确地知道变量的内存位时,静态优化会容易得多。指针会打破这种情况,并且可重新设置的引用也会。

于 2009-04-08T02:05:59.967 回答
0

因为有时事情不应该重新指向。(例如,对 Singleton 的引用。)

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

但主要是因为它允许使用真正是指针的东西,但它就像一个本地值对象。引用 Stroustrup 的话,C++ 努力使类实例“像 ints d 那样做”。通过 vaue 传递 int 很便宜,因为 int 适合机器寄存器。类通常比整数大,并且按值传递它们会产生很大的开销。

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

而且,特别是在运算符重载的情况下,有些地方我们应该返回一个对象,但同样,返回一个指针要便宜得多。Oncve 再次返回一个引用是我们的“out.

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

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

可能会令人困惑。

哎呀,经过 40 年的 C 语言,人们甚至不能同意指针声明应该是:

char* p;

或者

char *p;
于 2009-04-08T02:21:48.320 回答
0

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

只是为了让某人紧张,我编写了一些代码来更改结构中引用的目标。

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

于 2009-04-08T03:01:02.737 回答
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");
于 2009-04-08T11:02:09.257 回答
0

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

于 2009-04-08T22:36:35.250 回答
0

我同意接受的答案。但是对于 constness,它们的行为很像指针。

struct A{
    int y;
    int& x;
     A():y(0),x(y){}
};

int main(){
  A a;
  const A& ar=a;
  ar.x++;
}

作品。看

const 引用传递的类的引用成员行为的设计原因

于 2012-07-31T13:44:17.027 回答
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 的运行时绑定的a__vfptr或任何其他相关字段。type_id我知道的所有编译器都满足该条件(不这样做几乎没有意义)。

于 2016-10-04T19:45:25.260 回答