指针变量和引用变量有什么区别?

3884

指针变量和引用变量有什么区别?


133
我认为第二点应该是:“指针允许为空,但引用不允许。只有畸形代码才能创建空引用,其行为是未定义的。” - Mark Ransom
31
指针只是 C++ 中的另一种对象类型,像任何 C++ 对象一样,它们可以是变量。另一方面,引用永远不是对象,仅仅是变量。 - Kerrek SB
29
这段代码在gcc上编译没有警告:int &x = *(int*)0;,实际上引用确实可以指向NULL。 - Calmarius
28
参考文献是一个变量别名。 - Khaled.K
26
我喜欢第一句话是一个完全的谬论。参考文献有它们自己的语义学。 - Lightness Races in Orbit
显示剩余21条评论
45个回答

2197
  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 &q; // error
    int &r = x;
    
    指针变量具有自己的身份:一个唯一的、可见的内存地址,可以使用一元运算符 & 获取,并且可以使用 sizeof 运算符测量一定量的空间。在引用上使用这些运算符会返回与引用绑定的值;引用自身的地址和大小是不可见的。由于引用通过这种方式承担了原始变量的身份,因此将引用视为同一变量的另一个名称很方便。
    int x = 0;
    int &r = x;
    int *p = &x;
    int *p2 = &r;
    
    assert(p == p2); // &x == &r
    assert(&p != &p2);
    
    你可以拥有任意嵌套的指向指针的指针,提供额外的间接级别。而引用只提供一级间接。
    int x = 0;
    int y = 0;
    int *p = &x;
    int *q = &y;
    int **pp = &p;
    
    **pp = 2;
    pp = &q; // *pp is now q
    **pp = 4;
    
    assert(y == 4);
    assert(x == 2);
    
    指针可以被赋值为nullptr,而引用必须绑定到一个已存在的对象。如果你足够努力,你可以将引用绑定到nullptr,但这是未定义的,并且不会始终表现一致。
    /* the code below is undefined; your compiler may optimise it
     * differently, emit warnings, or outright refuse to compile it */
    
    int &r = *static_cast<int *>(nullptr);
    
    // prints "null" under GCC 10
    std::cout
        << (&r != nullptr
            ? "not null" : "null")
        << std::endl;
    
    bool f(int &r) { return &r != nullptr; }
    
    // prints "not null" under GCC 10
    std::cout
        << (f(*static_cast<int *>(nullptr))
            ? "not null" : "null")
        << std::endl;
    

    然而,您可以拥有一个指针的引用,其值为nullptr

    指针可以迭代数组;您可以使用++前往指针指向的下一个项目,+ 4前往第5个元素。不管指针所指向的对象大小如何,这都是无关紧要的。

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

    引用不能放入数组中,而指针可以(由用户@litb提到)

    const引用可以绑定到临时值。指针不能(没有一些间接方式):

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

    这使得在参数列表等地方使用 const & 更加方便。


34
但是取消引用 NULL 是未定义的。例如,您不能测试引用是否为 NULL(例如,&ref == NULL)。 - Pat Notz
96
第二个说法不是正确的。引用并不仅仅是“相同变量的另一个名称”。引用可以以与指针非常相似的方式传递给函数、存储在类中等等。它们存在于其所指向的变量之外。 - Derek Park
37
布莱恩,堆栈不相关。引用和指针不必在堆栈上占用空间。它们都可以分配在堆上。 - Derek Park
25
Brian,一个变量(在这种情况下是指针或引用)需要空间并不意味着它需要在栈上分配空间。指针和引用不仅可以指向堆,它们也可以在堆上分配空间。 - Derek Park
47
另一个重要的区别是:引用不能被塞进数组中。 - Johannes Schaub - litb
显示剩余41条评论

537

C++引用是什么(适用于C程序员

可以将一个引用视为一个常量指针(不要与指向常量值的指针混淆!)具有自动间接性,即编译器将为您应用*运算符。

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

C程序员可能不喜欢C ++引用,因为除非查看函数签名,否则将不再明显地发生间接引用或参数是通过值还是通过指针传递。

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

考虑以下来自C++ FAQ的声明:

即使引用通常使用底层汇编语言中的地址实现,请不要将引用视为指向对象的奇怪指针。引用就是对象。它不是对象的指针,也不是对象的副本。它就是对象。

但如果引用确实是对象,那么为什么会存在悬空引用?在非托管语言中,引用不可能比指针更“安全” - 通常没有一种可靠地跨作用域边界别名值的方法!

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

从C背景出发,C ++引用可能看起来像一个有点愚蠢的概念,但应尽可能使用它们而不是指针:自动间接寻址确实很方便,而且当涉及RAII时,引用变得特别有用 - 但并不是因为任何感知的安全优势,而是因为它们使编写惯用代码不那么笨拙。

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


22
不,你也可以通过返回引用自动变量来得到悬空引用。 - Ben Voigt
20
在一般情况下编译器几乎不可能检测到这种情况。考虑一个返回类成员变量引用的成员函数:这是安全的,编译器不应禁止它。然后,一个自动实例化该类的调用程序调用该成员函数并返回引用。瞬间就会出现悬空引用问题。是的,这会带来麻烦,@kriss: 这就是我的观点。很多人认为引用比指针的优势在于引用总是有效的,但事实并非如此。 - Ben Voigt
7
“@kriss:不,一个指向自动存储期的对象的引用与临时对象是非常不同的。无论如何,我只是提供一个反例来回应你关于只有通过对无效指针进行间接引用才能获得无效引用的说法。Christoph 是正确的 - 引用并不比指针更安全,完全使用引用的程序仍然可能破坏类型安全。” - Ben Voigt
11
参考文献不是指针类型,它们是现有对象的新名称。 - catphive
29
如果按语言语义来看,@catphive所说的是正确的,但如果实际查看实现情况,则不正确;相比C语言,C++是一种更加“神奇”的语言,如果去除引用的魔法,最终你会得到一个指针。 - Christoph
显示剩余12条评论

228
如果你想非常严谨,参考与指针有一个不同点:可以延长临时对象的生命周期。在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 引用绑定到临时对象,也不能获取它的地址。

7
那这个有什么用途? - Ahmad Mushtaq
26
s3_copy 会创建一个临时对象,然后将其复制构造到 s3_copy 中,而 s3_reference 直接使用该临时对象。如果要非常严谨,您需要考虑返回值优化,编译器可以省略第一种情况下的复制构造。 - Matt Price
7
@digitalSurgeon: 那里的魔法非常强大。通过const &绑定,对象的生命周期得以延长,只有当引用超出范围时,才会调用_actual_引用类型的析构函数(与可能是基类的引用类型相比)。由于它是一个引用,因此不会在其间发生切片。 - David Rodríguez - dribeas
11
C++11 更新:最后一句话应该改为“你不能将非 const 的左值引用绑定到临时对象上”,因为你可以将非 const 的右值引用绑定到临时对象上,并且它会有相同的生命周期延长行为。 - Oktalist
9
这句话的关键用途是派生类。如果没有涉及继承,你可以使用值语义,由于返回值优化/移动构造函数,它将非常便宜或免费。但是,如果你有Animal x = fast ? getHare() : getTortoise(),那么x将面临经典的切割问题,而Animal& x = ...将可以正常工作。 - Arthur Tacca
显示剩余4条评论

177
除了语法糖之外,引用是一个const指针(而不是指向const的指针)。在声明引用变量时,必须确定它所引用的内容,并且以后不能更改。
更新:现在我再想一想,有一个重要的区别。
const指针的目标可以通过取其地址并使用const转换来替换。
引用的目标无法以任何方式替换,除非是未定义行为。
这应该允许编译器对引用进行更多优化。

16
我认为这是迄今为止最好的答案。其他人谈论引用和指针时好像它们是不同的东西,然后又说明它们在行为上有什么区别。在我看来,这并没有让事情变得更容易。我一直把引用理解为带有不同语法糖(碰巧可以从代码中消除大量*和&)的 T* const - Carlo Wood
10
通过取其地址并使用 const 强制转换,可以替换常量指针的目标。这样做是未定义的行为。有关详细信息,请参见 https://dev59.com/aILba4cB1Zd3GeqPc1A6。 - dgnuff
2
试图更改引用的指向或常量指针(或任何常量标量)的值是完全非法的。你可以这样做:删除由隐式转换添加的const限定符:int i; int const *pci = &i; /* implicit conv to const int* */ int *pi = const_cast<int*>(pci); 是可以的。 - curiousguy
3
这里的区别在于 UB 与字面上的不可能。在 C++ 中没有语法可以让您更改引用所指向的内容。 - user3458
1
不是不可能,只是更难而已。你可以访问指针所模拟的引用的内存区域并更改其内容。这当然是可行的。 - Nicolas Bousquet
显示剩余3条评论

138

与普遍观点相反,有可能存在一个为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,因此从编译器的角度来看,比较始终为false,并且可以消除if子句作为死代码 - 这是未定义行为的本质。
避免解引用空指针以创建引用的正确方法是避免这种情况。以下是自动完成此操作的方法。
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的Null References

另一个例子是当尝试将代码移植到另一个平台时,暴露未定义行为,请注意避免对空指针进行引用。


71
所涉及的代码存在未定义的行为。从技术上讲,你不能对空指针进行除设置和比较之外的任何操作。一旦你的程序引发了未定义的行为,它可以做任何事情,包括在向大老板演示之前似乎正常工作。 - KeithB
12
Mark有一个有效的论点。指针可能为NULL并且因此必须进行检查的论点也不是真实的:如果你说一个函数需要非NULL,那么调用者就必须这样做。所以,如果调用者没有这样做,他就会引发未定义的行为。就像Mark使用错误引用一样。 - Johannes Schaub - litb
18
描述有误。这段代码可能会创建一个空引用(NULL),也可能不会。它的行为是未定义的,可能会创建一个完全有效的引用,也可能根本不会创建任何引用。 - David Schwartz
12
@David Schwartz,如果我在谈论按照标准运行的方式,那么你是正确的。但这不是我在谈论的—— 我正在谈论一个非常流行编译器的实际观察行为,并根据我对典型编译器和CPU架构的了解进行推断,以预测可能会发生什么。如果您认为引用比指针更安全,并且没有考虑到引用也可能出现问题,那么总有一天您会被一个简单的问题难住,就像我当时遇到的情况一样。 - Mark Ransom
8
对空指针进行解引用是错误的。即使为了初始化引用而这样做,任何这样的程序都是错误的。如果您正在从指针初始化引用,则应始终检查指针是否有效。即使这样成功,底层对象也可能随时被删除,导致引用引用不存在的对象,对吗?你说的是好东西。我认为这里的真正问题是,在看到引用时不需要检查“nullness”,但至少应该断言指针。 - t0rakka
显示剩余13条评论

130
您忘记了最重要的部分:
使用指针进行成员访问使用->, 使用引用进行成员访问使用.foo.bar 显然比 foo->bar 更好,就像 vi 显然比 Emacs 更好 :-)。

6
@Orion Edwards >使用指针的成员访问需要使用->,使用引用的成员访问需要使用. 这并非完全正确。你可以有一个指向指针的引用。在这种情况下,您将使用->访问取消引用指针的成员struct Node { Node *next; }; Node first; // p是指向指针的引用 void foo(Node&p) { p->next = first; } Node *bar = new Node; foo(bar); -- OP:您是否熟悉rvalue和lvalue的概念? - user6105
4
智能指针既有“.”(作用于智能指针类)又有“->”(作用于基础类型)的方法。 - JBRWilkinson
2
@user6105 Orion Edwards 的说法实际上是完全正确的。 “解引用指针的成员”指的是指针本身没有任何成员,指针所指向的对象才有成员,而->正是提供了对指针引用的对象成员的访问方式,就像对指针本身一样。 - Max Truxa
3
为什么 .-> 与 vi 和 emacs 有关系呢 :) (注:.-> 是 C/C++ 等编程语言中用于访问结构体成员和指针成员的运算符,vi 和 emacs 则是两种 Unix/Linux 下的文本编辑器) - artm
14
@artM - 这只是个玩笑,对于非英语母语的人来说可能没有意义。我很抱歉。简单解释一下,vi是否比emacs更好完全取决于主观判断。有些人认为vi远胜于emacs,而另一些人则持截然相反的观点。同样,我认为使用“.”比使用“->”更好,但就像vi和emacs一样,这完全是主观的,无法证明任何事情。 - Orion Edwards
显示剩余3条评论

88

引用(References)与指针(Pointers)非常相似,但它们是专门为优化编译器而设计的。

  • 引用是这样设计的,以使得编译器更容易跟踪哪个引用别名了哪些变量。两个重要的特性非常重要:没有“引用算术”和不允许重新分配引用。这使得编译器可以在编译时确定哪些引用别名了哪些变量。
  • 引用可以引用没有内存地址的变量,例如编译器选择放入寄存器中的变量。如果您获取局部变量的地址,编译器很难将其放入寄存器中。

举个例子:

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

一个优化器可能会意识到我们经常访问a[0]和a[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
}
为了进行这样的优化,需要证明在调用期间没有任何东西可以改变array[1]。这很容易做到。i从未小于2,因此array[i]永远不可能指向array[1]。给maybeModify()一个引用a0(别名为array [0])。由于没有“引用”算术,编译器只需证明maybeModify永远不会获得x的地址,并且已经证明没有任何东西会更改array [1]。
此外,还必须证明在我们暂时将其存储在a0中的情况下,在未来的调用中不存在任何读/写a [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 没有修改 array[1],因为我们已经给了它一个指针;猫已经被放出来了。现在必须进行更加困难的证明:对 maybeModify 进行静态分析以证明它从不写入 &x + 1。还必须证明它从未保存可以引用到 array[0] 的指针,这同样棘手。

现代编译器在静态分析方面越来越好,但使用引用可以帮助它们更好地完成工作。

当然,在没有这样聪明的优化时,编译器确实会将引用转换为指针。

编辑:在发布此答案五年后,我发现了一个实际上引用与仅仅是一种不同视角下的地址概念之间的技术差异。引用可以以指针无法做到的方式修改临时对象的生命周期。

F createF(int argument);

void extending()
{
    const F& ref = createF(5);
    std::cout << ref.getArgument() << std::endl;
};

通常类似于对createF(5)的调用创建的临时对象,将在表达式结束时被销毁。然而,通过将该对象绑定到引用ref,C++将延长该临时对象的生命周期直到ref超出范围。


确实,函数体需要可见。然而,确定maybeModify没有获取与x相关的任何地址要容易得多,而不是证明一堆指针算术不会发生。 - Cort Ammon
我相信优化器已经为许多其他原因做了“一堆指针算术不会发生”检查。 - Ben Voigt
“引用在语义上与指针非常相似,但在生成的代码方面,只有在某些实现中才是如此,并不通过任何定义/要求。我知道你已经指出了这一点,在实际情况下我不反对你的任何帖子,但我们已经有太多问题了,人们会过度解读简短描述,比如‘引用通常被实现为指针’。” - underscore_d
1
我有一种感觉,有人错误地标记了一个注释,类似于void maybeModify(int& x) { 1[&x]++; },其他上面的注释正在讨论它。 - Ben Voigt

83

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

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

当您创建一个引用时,您只是告诉编译器将另一个名称分配给指针变量;这就是为什么引用不能“指向空”,因为变量既不能存在也不能不存在。

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

现在解释一些真实代码:

int a = 0;
int& b = a;

在这里,您不是创建指向a的另一个变量;您只是为保存a的内存内容添加了另一个名称。 这块内存现在有两个名称:ab,可以使用任何一个名称来访问它。

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

int a;
increment(a);

调用函数时,编译器通常会生成内存空间以便将参数复制到该空间中。函数签名定义了应该创建的空间,并给出了应使用这些空间的名称。将参数声明为引用只是告诉编译器在方法调用期间使用输入变量的内存空间,而不是分配新的内存空间。可能感觉奇怪的是,您的函数将直接操作在调用作用域中声明的变量,但请记住,在执行编译后的代码时,没有更多的作用域; 只有纯粹的平面内存,您的函数代码可以操作任何变量。

现在可能会有一些情况,在编译时编译器可能无法知道引用的位置,例如当使用外部变量时。因此,在底层代码中,引用可能会或可能不会被实现为指针。但在我给您的示例中,它很可能不会被实现为指针。


2
引用是对左值的引用,不一定是对变量的引用。因此,它更接近指针而不是真正的别名(编译时构造)。可以引用的表达式的例子包括 *p 或甚至 *p++。 - user3458
6
没错,我只是在指出一个事实:引用并不总是像新指针一样将一个新的变量推到堆栈上。 - Vincent Robert
4
如果函数被内联,引用和指针都将被优化掉,其行为与指针相同。如果有函数调用,对象的地址需要传递给函数。 - Ben Voigt
1
int *p = NULL; int &r=*p; 引用指向NULL; if(r){} -> boOm ;) - uss
2
这种关注编译阶段的方法似乎很好,但是当你记住引用可以在运行时传递时,静态别名就不再适用了。(然后,引用通常被实现为指针,但标准并不要求使用这种方法。) - underscore_d
显示剩余3条评论

48

引用永远不可能是NULL


12
请参考Mark Ransom的回答,这是关于引用最常被提到的谬论,但它是一个谬论。根据标准,你唯一能得到的保证是当你有一个NULL引用时,立即会出现未定义行为。但这就像说“这辆车安全,永远不会撞车。(如果你无论如何驾驶它驶离路面,我们不会对可能发生的任何事情负责。它可能会爆炸。)” - cmaster - reinstate monica
20
在一个有效的程序中,一个引用不能为null。但是一个指针可以为null。这不是一个神话,而是事实。 - user541686
9
@Mehrdad,是的,有效的程序仍然在路上运行。但是没有交通障碍来强制执行你的程序确实如此做。道路的大部分实际上缺少标记。因此,在晚上离开道路变得非常容易。对于调试这样的错误来说,知道这种情况可能会发生非常重要:空引用可能会在崩溃程序之前传播,就像空指针一样。当它发生时,你就会有像 void Foo::bar() { virtual_baz(); } 这样的代码导致段错误。如果你没有意识到引用可能为空,你就无法将空追溯到其起源。 - cmaster - reinstate monica
4
int *p = NULL; int &r=*p; 引用指向NULL; if(r){} -> boOm ;) – - uss
13
int &r=*p; 是未定义行为。此时,你不是拥有一个“指向NULL的引用”,而是拥有一个完全无法推理的程序。 - cdhowie
显示剩余6条评论

38

如果你不熟悉以抽象甚至学术的方式研究计算机语言,那么可能会出现一些看似深奥的语义差异。

在最高级别上,引用的想法是它们是透明的“别名”。你的计算机可能使用地址使它们工作,但你不必担心这个问题:你应该把它们想象成为现有对象的“另一个名称”,并且语法反映了这一点。它们比指针更严格,因此编译器可以更可靠地警告你即将创建挂起的引用,而不是即将创建挂起的指针。

除此之外,指针和引用之间当然也存在一些实际差异。使用它们的语法显然是不同的,并且你不能“重新设置”引用,也不能有对无物的引用或指向引用的指针。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接