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

时间:2009-04-08 01:58:56

标签: c++ pointers reference language-design

C ++引用有两个属性:

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

指针恰恰相反:

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

为什么C ++中没有“不可空,可重复的引用或指针”?我想不出为什么参考不应该是可重复的好理由。

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

我认为我从没想过“这个引用始终指的是同一个对象”。如果引用是可重用的,那么仍然可以获得当前的行为:

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

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

我重申了我的问题:“'参考背后的理由是对象'设计?为什么认为引用总是是同一个对象,而不仅仅是当声明为const时?“

干杯,菲利克斯

17 个答案:

答案 0 :(得分:86)

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

  

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

答案 1 :(得分:32)

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

答案 2 :(得分:18)

因为那样你就没有可重复的类型,它不能为0.除非你包括3种类型的引用/指针。哪个只会使语言变得非常复杂(然后为什么不添加第四种类型?不可重复的参考,可以是0?)

更好的问题可能是,为什么您希望引用可重新组合?如果是的话,这会使它们在很多情况下变得不那么有用。这会使编译器更难进行别名分析。

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

以下是什么影响?

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

在今天的C ++中,使用不可重新引用的引用,它很简单。 j是i的别名,我最终得到值43。

如果引用已重新定位,则第三行将引用j绑定到不同的值。它不再是别名,而是整数文字43(当然无效)。或者更简单(或至少在语法上有效)的例子:

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

使用可重复引用。在评估此代码后,j将指向k。 使用C ++的不可重复引用,j仍然指向i,并且我被赋值为43。

使引用可重新更改语言的语义。引用不能再是另一个变量的别名。相反,它成为一个单独的值类型,具有自己的赋值运算符。然后,最常见的参考用法之一是不可能的。没有任何东西可以获得交换。新获得的引用功能已经以指针的形式存在。所以现在我们有两种方法可以做同样的事情,而且无法用当前的C ++语言做什么引用。

答案 3 :(得分: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*的函数更多的信息。

答案 4 :(得分:5)

引用不是指针,它可以在后台实现为指针,但其核心概念不等同于指针。引用应该与它所引用的对象*is*类似。因此您无法更改它,也不能为NULL。

指针只是一个保存内存地址的变量。 指针本身有一个自己的内存地址,并且在该内存地址中它保存另一个内存地址,据说它指向它。 引用不相同,它没有自己的地址,因此无法更改为“保留”其他地址。

我认为parashift C++ FAQ on references说得最好:

  

重要提示:即使a   参考通常使用   底层程序集中的地址   语言,请不要想到   引用作为一个有趣的指针   到一个对象。参考是   宾语。它不是指针   对象,也不是对象的副本。它   是对象。

再次在FAQ 8.5中:

  

与指针不同,一旦引用就是   绑定到一个对象,它不可能   “重新安置”到另一个对象。该   引用本身不是一个对象(它   没有身份;以地址为   引用为您提供地址   指称;记住:参考   是它的指示物。)

答案 5 :(得分:4)

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

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

答案 6 :(得分:3)

有趣的是,这里的许多答案有点模糊或者甚至在这一点上(例如它不是因为引用不能为零或类似,实际上,你可以很容易地构造一个引用为零的例子)。 / p>

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

  • 指针使您可以执行以下两项操作:更改指针后面的值(通过->*运算符),并更改指针本身(直接指定{ {1}})。例如:

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

    p = 0;

换句话说,如果允许您重新设置引用,那将是不明确的。通过引用传递时更有意义:

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

希望有所帮助: - )

答案 7 :(得分:2)

C ++引用可能有时被强制为 0 与一些编译器(这样做只是一个坏主意*,它违反了标准*)。

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

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

答案 8 :(得分: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 );

答案 9 :(得分:1)

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

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

在jalf的例子中

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

如果您想更改i,请按上述方式编写。但是,如果您想将j的含义更改为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
}

答案 10 :(得分:0)

因为有时事情不应该重新指向。 (例如,对单身人士的提及。)

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

但主要是因为它允许使用具有真正指针的东西,但其作用类似于本地值对象。 C ++努力引用Stroustrup,使类实例“像插件一样”。通过vaue传递int是很便宜的,因为int适合于机器寄存器。类通常比int大,并且按值传递它们会产生很大的开销。

能够传递一个“看起来像”值对象的指针(通常是int或者两个整数的大小)允许我们编写更清晰的代码,而无需解除引用的“实现细节”。并且,与运算符重载一起,它允许我们使用与int使用的语法类似的语法来编写类。特别是,它允许我们编写具有语法的模板类,该语法可以同样应用于原语,如整数和类(如复数类)。

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

指针很难。不是你,也许,而不是任何实现指针的人只是一个内存地址的值。但回想起我的CS 101课程,他们绊倒了一些学生。

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

可能令人困惑。

哎呀,经过40年的C,人们仍然不能同意指针声明应该是:

char* p;

char *p;

答案 11 :(得分:0)

我认为它与优化有关。

当您可以明确地知道变量意味着什么位的内存时,静态优化会更容易 。指针打破这种情况,也可以重新引用参考。

答案 12 :(得分:0)

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

为了让别人紧张,我写了一些代码来改变结构中引用的目标。

不,我不建议重复我的伎俩。如果移植到一个完全不同的架构,它将会破裂。

答案 13 :(得分: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");

答案 14 :(得分:0)

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

答案 15 :(得分:0)

我同意接受的答案。 但对于constness来说,它们的行为与指针非常相似。

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

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

的工作原理。 见

Design reasons for the behavior of reference members of classes passed by const reference

答案 16 :(得分: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;
}

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