题 C ++中指针变量和引用变量之间有什么区别?


我知道引用是语法糖,因此代码更容易读写。

但有什么区别?


以下答案和链接摘要:

  1. 指针可以重新分配任意次数,而绑定后无法重新分配引用。
  2. 指针无处可指(NULL),而引用总是指一个对象。
  3. 您无法使用指针获取引用的地址。
  4. 没有“参考算术”(但你可以获取引用所指向的对象的地址,并在其上执行指针算术,如 &obj + 5)。

澄清一个误解:

C ++标准非常小心,以避免规定编译器的方式   实现引用,但每个C ++编译器都实现   引用作为指针。也就是说,声明如下:

int &ri = i;

如果它没有完全优化分配相同数量的存储空间   作为指针,并放置地址   的 i 进入那个存储区。

因此,指针和引用都使用相同数量的内存。

作为基本规则,

  • 使用函数参数和返回类型中的引用来提供有用的自记录接口。
  • 使用指针实现算法和数据结构。

有趣的读物:


2625
2017-09-11 20:03


起源


我认为第2点应该是“允许指针为NULL但引用不是。只有格式错误的代码才能创建NULL引用,并且其行为未定义。” - Mark Ransom
指针只是另一种类型的对象,与C ++中的任何对象一样,它们可以是变量。另一方面,参考文献从不是对象, 只要 变量。 - Kerrek SB
这编译没有警告: int &x = *(int*)0; 在gcc上。引用确实可以指向NULL。 - Calmarius
reference是一个变量别名 - Khaled.K
我喜欢第一句话是完全谬误的。引用有自己的语义。 - Lightness Races in Orbit


答案:


  1. 可以重新分配指针:

    int x = 5;
    int y = 6;
    int *p;
    p =  &x;
    p = &y;
    *p = 10;
    assert(x == 5);
    assert(y == 10);
    

    引用不能,必须在初始化时分配:

    int x = 5;
    int y = 6;
    int &r = x;
    
  2. 指针在堆栈上有自己的内存地址和大小(x86上为4个字节),而引用共享相同的内存地址(使用原始变量),但也会占用堆栈上的一些空间。由于引用与原始变量本身具有相同的地址,因此可以将引用视为同一变量的另一个名称。注意:指针指向的内容可以在堆栈或堆上。同上一个参考。我在这个陈述中的主张并不是指针必须指向堆栈。指针只是一个保存内存地址的变量。此变量位于堆栈上。由于引用在堆栈上有自己的空间,并且因为地址与它引用的变量相同。更多关于 堆栈与堆。这意味着编译器不会告诉您存在引用的真实地址。

    int x = 0;
    int &r = x;
    int *p = &x;
    int *p2 = &r;
    assert(p == p2);
    
  3. 您可以指向指向提供额外间接级别的指针的指针。而引用仅提供一个间接层。

    int x = 0;
    int y = 0;
    int *p = &x;
    int *q = &y;
    int **pp = &p;
    pp = &q;//*pp = q
    **pp = 4;
    assert(y == 4);
    assert(x == 0);
    
  4. 指针可以分配 nullptr 直接,而参考不能。如果你足够努力,并且知道如何,你可以制作一个参考地址 nullptr。同样,如果你足够努力,你可以引用一个指针,然后该引用可以包含 nullptr

    int *p = nullptr;
    int &r = nullptr; <--- compiling error
    int &r = *p;  <--- likely no compiling error, especially if the nullptr is hidden behind a function call, yet it refers to a non-existent int at address 0
    
  5. 指针可以迭代一个数组,你可以使用 ++转到指针指向的下一个项目,和 + 4 去第五个元素。无论指针指向的对象是什么大小。

  6. 指针需要取消引用 * 访问它指向的内存位置,而引用可以直接使用。指向类/结构的指针使用 -> 访问它的成员,而引用使用a .

  7. 指针是保存内存地址的变量。无论引用如何实现,引用都具有与其引用的项相同的内存地址。

  8. 引用不能填充到数组中,而指针可以是(由用户@litb提及)

  9. Const引用可以绑定到临时值。指针不能(不是没有一些间接):

    const int &x = int(12); //legal C++
    int *y = &int(12); //illegal to dereference a temporary.
    

    这使得 const& 更安全地用于参数列表等等。


1376
2018-02-27 21:26



...但是取消引用NULL是未定义的。例如,您无法测试引用是否为NULL(例如,&ref == NULL)。 - Pat Notz
2号是 不 真正。引用不仅仅是“同一个变量的另一个名称”。引用可以以与指针非常相似的方式传递给函数,存储在类等中。它们独立于它们指向的变量而存在。 - Derek Park
布莱恩,这个堆栈并不重要。引用和指针不必占用堆栈空间。它们都可以在堆上分配。 - Derek Park
Brian,变量(在这种情况下是指针或引用)需要空间的事实 不 意味着它需要堆栈上的空间。指针和参考可能不仅仅是 点 到了堆,他们实际上可能是 分配 在堆上。 - Derek Park
另一个重要的差异:引用不能填充到数组中 - Johannes Schaub - litb


什么是C ++参考(对于C程序员

一个 参考 可以被认为是一个 常量指针 (不要与指向常量值的指针混淆!)与自动间接,即编译器将应用 * 经营者为您服务。

必须使用非null值初始化所有引用,否则编译将失败。获取引用的地址既不可能 - 地址运算符将返回引用值的地址 - 也不可能在引用上进行算术运算。

C程序员可能不喜欢C ++引用,因为当间接发生时,或者如果参数通过值或指针传递而不查看函数签名,它将不再是显而易见的。

C ++程序员可能不喜欢使用指针,因为它们被认为是不安全的 - 虽然引用并不比常量指针更安全,除了在最微不足道的情况下 - 缺乏自动间接的便利性并带有不同的语义内涵。

请考虑以下声明 C ++ FAQ

即使引用通常使用地址中的地址来实现   基础汇编语言,请做  把参考想象成一个   有趣的看指向一个对象。一个参考  物体。它是   不是指向对象的指针,也不是对象的副本。它  该   目的。

但如果参考  是对象,怎么会有悬空参考?在非托管语言中,引用不可能比指针更“安全” - 通常只是不能跨范围边界可靠地对值进行别名!

为什么我认为C ++引用很有用

来自C背景,C ++引用可能看起来像一个有点愚蠢的概念,但是应该尽可能使用它们而不是指针:自动间接  方便,参考在处理时变得特别有用 RAII  - 但不是因为任何感知到的安全优势,而是因为它们使写作惯用代码不那么尴尬。

RAII是C ++的核心概念之一,但它与复制语义非常简单地交互。通过引用传递对象避免了这些问题,因为不涉及复制。如果语言中没有引用,则必须使用指针,这些指针使用起来比较麻烦,因此违反了语言设计原则,即最佳实践解决方案应该比替代方案更容易。


301
2017-09-11 21:43



@kriss:不,你也可以通过引用返回一个自动变量来获得一个悬空引用。 - Ben Voigt
@kriss:在一般情况下,编译器几乎不可能检测到。考虑一个成员函数,它返回对类成员变量的引用:这是安全的,编译器不应该禁止。然后,具有该类的自动实例的调用者调用该成员函数,并返回该引用。 Presto:悬挂参考。是的,这会给你带来麻烦,@ kriss:这就是我的观点。许多人声称引用优于指针的优点是引用始终有效,但事实并非如此。 - Ben Voigt
@kriss:不,对自动存储持续时间的对象的引用与临时对象非常不同。无论如何,我只是在你的语句中提供一个反例,你只能通过解除引用无效指针来获得无效引用。 Christoph是正确的 - 引用并不比指针更安全,只使用引用的程序仍然可以打破类型安全。 - Ben Voigt
引用不是一种指针。它们是现有对象的新名称。 - catphive
@catphive:如果按语言语义进行操作,则为true;如果实际查看实现,则为true; C ++是一种比C更“神奇”的语言,如果你从引用中删除魔法,你最终会得到一个指针 - Christoph


如果你想变得非常迂腐,你可以用引号做一件事,你不能用指针做:延长临时对象的生命周期。在C ++中,如果将const引用绑定到临时对象,则该对象的生命周期将成为引用的生命周期。

std::string s1 = "123";
std::string s2 = "456";

std::string s3_copy = s1 + s2;
const std::string& s3_reference = s1 + s2;

在此示例中,s3_copy复制作为串联结果的临时对象。而s3_reference本质上成为临时对象。它实际上是对临时对象的引用,该临时对象现在具有与引用相同的生命周期。

如果你试试这个没有 const 它应该无法编译。您不能将非const引用绑定到临时对象,也不能为此处获取其地址。


152
2017-09-11 21:06



但是这个用例是什么? - Ahmad Mushtaq
好吧,s3_copy将创建一个临时的,然后复制构造到s3_copy,而s3_reference直接使用临时。然后要真正迂腐,你需要查看返回值优化,从而允许编译器在第一种情况下忽略复制结构。 - Matt Price
@digitalSurgeon:魔力很强大。对象的生命周期由以下事实扩展 const & 绑定,并且只有当引用超出范围时才会出现析构函数 实际 调用引用类型(与引用类型相比,可以是基类)。由于它是参考,因此两者之间不会发生切片。 - David Rodríguez - dribeas
C ++ 11的更新:最后一句应该是“你不能将非常量左值引用绑定到临时”,因为你 能够绑定非const 右值 引用临时,它具有相同的生命周期延伸行为。 - Oktalist
@AhmadMushtaq:关键用途是 派生类。如果没有涉及继承,你也可以使用值语义,由于RVO /移动构造,它将是便宜的或免费的。但如果你有 Animal x = fast ? getHare() : getTortoise() 然后 x 将面临经典的切片问题 Animal& x = ... 会正常工作。 - Arthur Tacca


与流行的观点相反,可能有一个NULL引用。

int * p = NULL;
int & r = *p;
r = 1;  // crash! (if you're lucky)

当然,使用参考文件要困难得多 - 但是如果你管理它,你会撕掉你的头发试图找到它。参考文献是  在C ++中本质上是安全的!

从技术上讲,这是一个 无效的参考,不是空引用。 C ++不支持空引用作为您可能在其他语言中找到的概念。还有其他类型的无效引用。 任何 无效的引用引发了幽灵 未定义的行为就像使用无效指针一样。

在分配给引用之前,实际错误是在NULL指针的解引用中。但是我不知道任何编译器会在这种情况下产生任何错误 - 错误会传播到代码中的某个点。这就是让这个问题如此阴险的原因。大多数情况下,如果你取消引用一个NULL指针,你就会在那个位置崩溃,并且不需要太多的调试就可以搞清楚。

我上面的例子简短而且做作。这是一个更真实的例子。

class MyClass
{
    ...
    virtual void DoSomething(int,int,int,int,int);
};

void Foo(const MyClass & bar)
{
    ...
    bar.DoSomething(i1,i2,i3,i4,i5);  // crash occurs here due to memory access violation - obvious why?
}

MyClass * GetInstance()
{
    if (somecondition)
        return NULL;
    ...
}

MyClass * p = GetInstance();
Foo(*p);

我想重申,获取空引用的唯一方法是通过格式错误的代码,一旦你拥有它,你就会得到未定义的行为。它 决不 检查空引用是否有意义;比如你可以试试 if(&bar==NULL)... 但编译器可能会优化语句不存在!有效的引用永远不能为NULL,因此从编译器的视图来看,比较总是错误的,并且可以自由地消除 if 作为死代码的子句 - 这是未定义行为的本质。

避免麻烦的正确方法是避免取消引用NULL指针来创建引用。这是实现这一目标的自动化方法。

template<typename T>
T& deref(T* p)
{
    if (p == NULL)
        throw std::invalid_argument(std::string("NULL reference"));
    return *p;
}

MyClass * p = GetInstance();
Foo(deref(p));

对于那些具有更好写作技巧的人来看这个问题,请参阅 空引用 来自Jim Hyslop和Herb Sutter。

有关解除引用空指针的危险的另一个示例,请参阅 尝试将代码移植到另一个平台时暴露未定义的行为 作者:Raymond Chen。


104
2017-09-11 20:07



有问题的代码包含未定义的行为。从技术上讲,除了设置它之外你不能用空指针做任何事情,并比较它。一旦你的程序调用未定义的行为,它就可以做任何事情,包括在你向大老板发表演示之前看似正常工作。 - KeithB
mark有一个有效的参数。指针可能为NULL并且因此你必须检查的参数也不是真的:如果你说一个函数需要非NULL,那么调用者必须这样做。所以如果调用者没有调用未定义的行为。就像马克做坏参考一样 - Johannes Schaub - litb
描述是错误的。此代码可能会也可能不会创建NULL引用。它的行为是不确定的。它可能会创建一个完全有效的参考。它可能根本无法创建任何引用。 - David Schwartz
@David Schwartz,如果我在谈论事情必须按照标准运作的方式,那你就是对的。但那是 不 我正在谈论的内容 - 我正在讨论使用非常流行的编译器实际观察到的行为,并根据我对典型编译器和CPU架构的了解来推断 大概 发生。如果你认为引用优于指针因为它们更安全并且不认为引用可能是坏的,那么有一天你会被一个简单的问题困住,就像我一样。 - Mark Ransom
取消引用空指针是错误的。任何执行此操作的程序,甚至初始化引用都是错误的。如果要从指针初始化引用,则应始终检查指针是否有效。即使这样成功,底层对象也可能随时被删除,留下引用引用不存在的对象,对吧?你说的是好东西。我认为这里真正的问题是当你看到一个指针应该至少被断言时,不需要检查引用是否为“nullness”。 - t0rakka


除了语法糖,参考是一个 const 指针( 指向一个 const)。您必须在声明引用变量时确定它所引用的内容,并且以后不能更改它。

更新:现在我再考虑一下,有一个重要的区别。

const指针的目标可以通过获取其地址并使用const转换来替换。

参考目标不能以任何方式替换UB。

这应该允许编译器对引用进行更多优化。


97
2017-09-11 22:10



我认为这是迄今为止最好的答案。其他人谈论引用和指针,就像他们是不同的野兽,然后列出他们的行为不同。它不会让事情变得更容易。我一直都把参考文献理解为一个 T* const 使用不同的语法糖(这恰好消除了你的代码中的大量*和&)。 - Carlo Wood
想象试图做 cout << 42 << "hello" << endl 没有参考 - pm100
简短而且最好的答案 - Kad
“const指针的目标可以通过获取其地址并使用const转换来替换。”这样做是未定义的行为。看到 stackoverflow.com/questions/25209838/... 详情。 - dgnuff
试图更改引用的引用或const指针(或任何const标量)的值是非等的。您可以做什么:删除通过隐式转换添加的const限定条件: int i; int const *pci = &i; /* implicit conv to const int* */ int *pi = const_cast<int*>(pci); 没关系 - curiousguy


你忘记了最重要的部分:

使用指针的成员访问 -> 
使用引用的成员访问 .

foo.bar 是 明确地 优于 foo->bar 以同样的方式  是 明确地 优于 Emacs的 :-)


96
2017-09-19 12:23



@Orion Edwards>使用指针的成员访问 - >使用引用的成员访问。这不是100%真实。您可以引用指针。在这种情况下,您将使用 - > struct Node {Node * next;来访问取消引用的指针的成员; };节点*第一; // p是对指针void foo的引用(Node *&p){p-> next = first; } Node * bar = new Node; FOO(巴); - OP:你熟悉rvalues和左值的概念吗?
智能指针同时具备。 (智能指针类上的方法)和 - >(基础类型上的方法)。 - JBRWilkinson
@ user6105 猎户座爱德华兹 声明实际上是100%真实。 “访问[de]引用指针的成员” 指针没有任何成员。指针引用的对象具有成员,对这些成员的访问正是如此 -> 提供对指针的引用,就像指针本身一样。 - Max Truxa
这是为什么 . 和 -> 与vi和emacs有关:) - artm
@artM - 这是一个笑话,对非母语英语人士来说可能没有意义。我很抱歉。要解释一下,vi是否比emacs更好是完全主观的。有些人认为vi远远优于其他人,而其他人则认为恰恰相反。同样,我认为使用 . 比使用更好 ->,但就像vi vs emacs一样,它完全是主观的,你无法证明什么 - Orion Edwards


实际上,引用并不像指针。

编译器保持对变量的“引用”,将名称与内存地址相关联;这是在编译时将任何变量名转换为内存地址的工作。

创建引用时,只告诉编译器为指针变量指定另一个名称;这就是为什么引用不能“指向null”,因为变量不能,也不能。

指针是变量;它们包含其他变量的地址,或者可以为null。重要的是指针有一个值,而引用只有一个它正在引用的变量。

现在对实际代码的一些解释:

int a = 0;
int& b = a;

在这里,您不会创建指向的另一个变量 a;你只是在保存值的内存内容中添加另一个名称 a。这个记忆现在有两个名字, a 和 b,它可以使用任一名称来解决。

void increment(int& n)
{
    n = n + 1;
}

int a;
increment(a);

调用函数时,编译器通常会为要复制的参数生成内存空间。函数签名定义了应该创建的空格,并给出了应该用于这些空间的名称。将参数声明为引用只是告诉编译器使用输入变量内存空间而不是在方法调用期间分配新的内存空间。说你的函数将直接操作调用范围中声明的变量似乎很奇怪,但请记住,在执行编译代码时,没有更多的范围;只有普通的平坦内存,你的功能代码可以操纵任何变量。

现在可能存在编译器在编译时可能无法知道引用的情况,例如使用extern变量时。因此,引用可能会也可能不会被实现为底层代码中的指针。但是在我给你的例子中,它很可能不会用指针实现。


57
2017-09-01 03:44



引用是对l值的引用,不一定是对变量的引用。因此,它更接近指针而不是真正的别名(编译时构造)。可以引用的表达式的示例是* p或甚至* p ++ - Arkadiy
是的,我只是指出一个事实,即引用可能并不总是按照新指针的方式在堆栈上推送一个新变量。 - Vincent Robert
@VincentRobert:它的行为与指针相同......如果函数内联,则引用和指针都将被优化掉。如果有函数调用,则需要将对象的地址传递给函数。 - Ben Voigt
int * p = NULL; int&r = * p;引用指向NULL; if(r){} - > boOm;) - sree
这个对编译阶段的关注似乎很好,直到你记得引用可以在运行时传递,此时静态别名出现在窗口之外。 (然后,参考文献是 平时 实现为指针,但标准不需要此方法。) - underscore_d


引用与指针非常相似,但它们经过精心设计,有助于优化编译器。

  • 设计引用使得编译器更容易跟踪哪个引用别名哪个变量。两个主要特征非常重要:没有“参考算术”,也没有重新分配参考文献。这些允许编译器在编译时找出哪些引用别名是哪些变量。
  • 允许引用引用没有内存地址的变量,例如编译器选择放入寄存器的变量。如果你取一个局部变量的地址,编译器很难把它放在一个寄存器中。

举个例子:

void maybeModify(int& x); // may modify x in some way

void hurtTheCompilersOptimizer(short size, int array[])
{
    // This function is designed to do something particularly troublesome
    // for optimizers. It will constantly call maybeModify on array[0] while
    // adding array[1] to array[2]..array[size-1]. There's no real reason to
    // do this, other than to demonstrate the power of references.
    for (int i = 2; i < (int)size; i++) {
        maybeModify(array[0]);
        array[i] += array[1];
    }
}

优化编译器可能会意识到我们正在访问[0]和[1]相当多的一组。它希望优化算法:

void hurtTheCompilersOptimizer(short size, int array[])
{
    // Do the same thing as above, but instead of accessing array[1]
    // all the time, access it once and store the result in a register,
    // which is much faster to do arithmetic with.
    register int a0 = a[0];
    register int a1 = a[1]; // access a[1] once
    for (int i = 2; i < (int)size; i++) {
        maybeModify(a0); // Give maybeModify a reference to a register
        array[i] += a1;  // Use the saved register value over and over
    }
    a[0] = a0; // Store the modified a[0] back into the array
}

为了进行这样的优化,需要证明在调用期间没有任何东西可以改变数组[1]。这很容易做到。我永远不会少于2,所以array [i]永远不会引用数组[1]。 maybeModify()被赋予a0作为参考(别名数组[0])。因为没有“引用”算法,编译器只需要证明maybeModify永远不会得到x的地址,并且它已经证明没有任何改变数组[1]。

它还必须证明,当我们在a0中有一个临时寄存器副本时,未来的调用没有办法读/写[0]。这通常是微不足道的,因为在很多情况下很明显,引用永远不会存储在像类实例这样的永久结构中。

现在用指针做同样的事情

void maybeModify(int* x); // May modify x in some way

void hurtTheCompilersOptimizer(short size, int array[])
{
    // Same operation, only now with pointers, making the
    // optimization trickier.
    for (int i = 2; i < (int)size; i++) {
        maybeModify(&(array[0]));
        array[i] += array[1];
    }
}

行为是一样的;只是现在更难以证明maybeModify不会修改数组[1],因为我们已经给它一个指针;这只猫已经不在了。现在它必须做更加困难的证明:对maybeModify进行静态分析以证明它永远不会写入&x + 1.它还必须证明它永远不会保存可以引用数组[0]的指针,这只是太棘手了。

现代编译器在静态分析方面越来越好,但总是很好地帮助它们并使用引用。

当然,除非进行这种巧妙的优化,否则编译器确实会在需要时将引用转换为指针。


50
2017-09-11 20:12



没错,身体必须是可见的。但是,确定一下 maybeModify 不接受与之相关的任何地址 x 比证明不会发生一堆指针算术要容易得多。 - Cort Ammon
我相信优化器已经做到了“一堆指针arithemetic不会发生”检查一堆其他原因。 - Ben Voigt
“引用与指针非常相似” - 在语义上,在适当的上下文中 - 但就生成的代码而言,仅在某些实现中而不是通过任何定义/要求。我知道你已经指出了这一点,我并不反对你的任何一篇文章的实际用语,但是我们已经有很多问题已经让人们过多地阅读简写描述,例如'引用就像/通常实现为指针' 。 - underscore_d
我有一种感觉,有人错误地标记为过时的评论 void maybeModify(int& x) { 1[&x]++; },上面的其他评论正在讨论 - Ben Voigt


参考永远不可能 NULL


32
2018-05-20 19:26



有关反例,请参阅Mark Ransom的答案。这是关于引用的最常见的神话,但这是一个神话。标准的唯一保证是,当您有NULL引用时,您立即拥有UB。但这类似于说“这辆车是安全的,它永远不会离开这条路。”(如果你把它转移到路上,我们不承担任何责任。它可能会爆炸。)“ - cmaster
@cmaster: 在一个有效的程序中,引用不能为null。但指针可以。这不是一个神话,这是一个事实。 - Mehrdad
@Mehrdad是的,有效的计划留在路上。但是,没有交通障碍来强制执行您的程序。道路的大部分区域实际上都缺少标记。因此,晚上离开这条路非常容易。对于调试此类错误至关重要 知道 这可能发生:null引用可以在崩溃程序之前传播,就像空指针一样。当它有你的代码时 void Foo::bar() { virtual_baz(); } 那段错误。如果您不知道引用可能为null,则无法将null跟踪回原点。 - cmaster
int * p = NULL; int&r = * p;引用指向NULL; if(r){} - > boOm;) - - sree
@sree int &r=*p; 是未定义的行为。此时,您没有“指向NULL的引用”,您有一个程序 不能再被推理了 一点都不。 - cdhowie