为什么C++引用被认为比指针更安全?

13

当C++编译器为引用和指针生成非常相似的汇编代码时,为什么使用引用比使用指针更受欢迎(并被认为更安全)?

我看到了C++中指针变量和引用变量之间的区别,讨论了它们之间的区别。

编辑-1:

我正在查看由g++为此小程序生成的汇编代码:

int main(int argc, char* argv[])
{
  int a;
  int &ra = a;
  int *pa = &a;
}

@YouKnowWho:我会对此持保留态度 --> 编译器可能会出现一些错误,但它们通常会产生非常有趣的效果 :) - Matthieu M.
2
你没有提供“使用引用(相对于指针)更受青睐(也被认为更安全)”这一论点的引文。我敢打赌,论坛上很多人可能并不同意这种说法。 - Lightness Races in Orbit
1
@Tomalak:我不知道任何具体的参考资料。这就是我通过这个问题想要澄清的事情。 - yasouser
9个回答

22

许多人认为使用引用更加安全,因为许多人“听说”过它比指针更加安全,并将这一信息传递给了其他人,现在这些人也“听说”过引用更加安全。

但是,任何理解引用和指针的人都不会告诉你它们比指针更加安全,它们具有相同的缺陷和可能失效的潜力。

例如:

#include <vector>

int main(void)
{
    std::vector<int> v;
    v.resize(1);

    int& r = v[0];
    r = 5; // ok, reference is valid

    v.resize(1000);
    r = 6; // BOOM!;

    return 0;
}

编辑:由于一些人对于引用是对象的别名还是绑定到内存位置存在困惑,这里有一段标准草案(第3225版,章节[basic.life]),明确说明了引用是绑定到存储空间的,并且可以超过创建引用时所存在的对象的生命周期:

如果在一个对象的生命周期结束后,在该对象占用的存储空间被重用或释放之前,在该原始对象所占用的存储位置上创建了一个新对象,则指向原始对象的指针、引用原始对象的引用或原始对象的名称将自动引用到新对象,并且一旦新对象的生命周期开始,就可以用来操作新对象,如果:

  • 新对象的存储正好覆盖了原始对象占用的存储位置,并且
  • 新对象与原始对象具有相同的类型(忽略顶层cv限定符),并且
  • 原始对象的类型没有const限定符,并且,如果是类类型,则不包含任何类型为const限定符或引用类型的非静态数据成员,并且
  • 原始对象是类型T的最派生对象,而新对象是类型T的最派生对象(即它们不是基类子对象)。

3
仅仅因为这个原因我点赞(+1):“很多人听说过它更安全,然后告诉了其他人,现在这些人也听说过它更安全,所以它被认为更安全。” - yasouser
7
“了解指针和引用的人都会告诉你,它们并没有比指针更安全。”---> 我理解它们(我相信其他人也一样),但我仍然认为它们更安全。请注意,“更安全”并不意味着“安全”... 你仍然可能会搞砸,只是更困难而已。 - user541686
“引用指向原始对象,或者原始对象的名称将自动指向新对象。” 这句话支持我一直在说的话,即对对象的引用不是“地址”,就像对象本身一样,也就是说,引用和名称都是地址。它们都是内存中数据对象地址的符号。但是指针的“名称”表示的是地址的地址,而不是任何数据对象的地址。 - ThomasMcLeod
1
@ThomasMcLoed:也许你忽略了指针也属于同一类别的事实——指针、引用和变量名,都指向存储位置,而不是对象本身。当标准明确描述不同的生命周期时,请不要将“名称”等同于“对象本身”。 - Ben Voigt
1
太遗憾了,他在解释过程中犯的错误和他所揭穿的“事实”一样多。我认为将if (ptr)称为“容错处理”就是荒谬的。但是,确实有其他人注意到编译器可以使引用变得安全,它们必须通过正确的代码使其变得安全——就像指针一样。 - Ben Voigt

8
这取决于你如何定义“更安全”。
编译器不允许你创建未初始化的引用,或者指向空值的引用,并且在使用引用时不会让你意外地将其引用到其他位置。这些更严格的规则也意味着你的编译器可以给你一些关于常见错误的警告,而对于指针来说,它从来不确定你是否有意要做你所做的事情。
另一方面,语法的透明性——即Alexandre C.提到的在调用时将参数作为引用传递的样子——使得很容易意识不到你正在传递一个引用。因此,你可能没有意识到你应该维护参数的所有权和生命周期,或者你的参数可能会被永久修改。

1
当然,你可以声明一个常量指针(我不是指指向常量的指针),这将要求你对其进行初始化,并防止使其引用其他地方。 - Ben Voigt
3
我只是在说使用引用并不是一种独有的好处,你也可以通过声明指针为const来获得相同的行为。 - Ben Voigt
关于未初始化的引用,int *const p 也具有该特性。关于空值,[[gnu::nonnull]] int *p 也具有该特性。顺便说一下,如果您将(可能为NULL的)指针分配给非空指针或引用,则编译器不会发出警告。因此,最终,引用与 [[gnu::nonnull]] int *const p 一样不安全(并且在我看来更加丑陋)。 - alx - recommends codidact
关于“而使用指针时,你所做的事情是否有意义并不确定”的回复:除非您使用Clang的_Nonnull,否则使用指针可以获得比使用引用更多的警告。引用是伪装成[[gnu::nonnull]] * const指针的东西。 - alx - recommends codidact

7

引用是其他变量的别名,按照定义不能为NULL,从而提供了内在的安全层。


9
禁止使用NULL并不能使引用更加安全。事实上,引用并不比指针更安全,隐藏的间接性使它们更加危险。 - Ben Voigt
2
@Alexandre:它们可以被创建为空(取消引用指针),但最重要的一点是它们也可能引用垃圾。 - Matthieu M.
5
除非您之前触发了未定义行为,否则它们不可能为NULL,此时所有的赌注都是无效的,因此这并不是引用的错误。 :) - Lightness Races in Orbit
2
@Matthie - 不行,它们不能。解引用空指针是未定义的行为。这样做的程序已经是不良形式了。因此,需要学习的教训是不要解引用无效或空指针。 - Edward Strange
4
@Ben: 不是一样的。这个“函数”(注意单位)有保证会正常工作:void f(int& i) { i = 0; } 无论何时都是如此,不管发生什么情况。如果我遇到了错误,我永远不需要考虑这个函数是导致错误的原因。相反,这个函数是可能会出错的: void f(int* i) { *i = 0; },并且当传递一个空指针给这个函数时,我必须在面对错误时要考虑这种情况。引用的作用是使我的函数自包含,并将正确处理任何可能拥有的指针的责任放在函数的“用户”身上。 - GManNickG
显示剩余50条评论

2

引用始终从现有对象初始化,因此它永远不会为NULL,而指针变量允许为NULL。

编辑:感谢所有回复。是的,引用确实可以指向垃圾,我忘记了悬挂引用。


2
一个引用可以从指针初始化,因此它并不更安全。 - Ben Voigt
1
这并不一定是真的。引用可以从被取消引用的指针初始化,如果指针为空,那么会抛出异常。 - Beanz
1
@Alexandre C.:不是的,这是未定义行为 - Lightness Races in Orbit
1
只有让一个对象超出其作用域,才能解决这个问题!是的,很容易留下悬空引用。 - Lightness Races in Orbit
2
实际上(即在所有主要编译器上),通过取消引用空指针来绑定引用不会引起访问冲突、分段错误、空指针异常或与将空指针复制到另一个指针不同的任何行为。只有通过引用进行访问才会发生不良事情。 引用基本上是自动取消引用的指针,至少在现实生活中是这样。 这完全符合标准... UB 意味着程序可能立即失败、工作或以某种难以预料的晦涩方式稍后失败。 - Ben Voigt
显示剩余5条评论

2

这样做相对安全一些,但并不完全相同。请注意,您仍然存在“悬空引用”的问题,就像“悬空指针”一样。例如,从作用域对象返回引用会产生未定义的行为,与指针完全相同:

int& f() { int x = 2; return x; }

唯一的好处是你无法 创建 一个空引用。即使你很努力:
int& null_ref = *((int*)0); // Dereferencing a null pointer is undefined in C++
                            // The variable null_ref has an undefined state.

作为类成员,指针更受欢迎,因为它们具有更好的赋值语义:当引用被初始化后,您无法重新分配引用。如果类中存在引用成员,则编译器将无法提供默认的赋值运算符。
因此,C++不能摆脱指针,您可以自由地使用它们:通过将参数作为指针而不是(非const)引用传递,您在调用站点清楚地表明对象将被修改。这可以增加一些安全性,因为您可以肉眼看到哪些函数确实修改了对象。
我有点反驳,但引用很容易被滥用。

我不认为第一段中的故事会使它们更加"安全",只会让人更加恼火。 - Lightness Races in Orbit
@Tomalak:是的,这有点离题,但我必须承认我很快地阅读了问题。 - Alexandre C.
在C++中取消引用空指针是未定义的,因此程序不正确。这是错误的,没有任何意义。该行完全是形式良好的(在编译时对源代码进行测量),但其结果是未定义的(在运行时环境中进行测量)。运行时测量与编译时测量无关,除非你提出了“在UB中任何事情都可能发生”的想法(时光机?)。 - GManNickG
@GMan:你说得对,但是null_ref的内容是未定义的。 - Alexandre C.
@Alexandre:你可以这么说,但未定义的行为是最终结果。null_ref的值是未知的,当然,整个程序的状态也是未知的。 - GManNickG

2

因为引用必须始终初始化,且必须引用现有对象,所以相比于未初始化/悬空指针,悬空引用的出现更难(但并非不可能)。此外,操作引用更容易,因为您无需担心获取地址和解除引用。

但是,仅凭引用本身并不能使您的程序100%安全,请考虑以下情况:

int *p = NULL;
int &r = *p;
r = 10; /* bad things will happen here */

或者这个:
int &foo() {
  int i;
  return i;
}

...

int &r = foo();
r = 10; /* again, bad things will happen here */

0

好的,你指出的答案回答了这个问题。从“安全”的角度来看,我认为基本上很难编写像这样的代码:

int* i;
// ...
cout << *i << endl; // segfault

由于引用始终被初始化,因此

MyObject* po = new MyObject(foo);
// ...
delete po;
// ...
po->doSomething(); // segfault

但正如你提到的问题中所说,不仅因为它们更安全,引用才被使用...

我的个人看法


0
假设您在语言中没有引用运算符<type>&。那么,每当您想要将对象的引用传递给函数时,您必须执行C中所做的操作,将参数作为&<myobject>传递,并将其作为指针参数<type>* p在函数中接收。然后,您必须使自己不要像p++这样做。
这就是安全性的出处。 按引用传递非常有用,具有引用运算符可以避免您修改指针的风险。
(也许const指针会达到相同的效果,但您必须承认&更加简洁。而且它可以应用于除参数之外的其他变量。)

-1
指针是一个独立的变量,可以被重新赋值指向另一个数据项、未初始化的内存,或者根本没有指向任何地方(NULL)。指针可以递增、递减、从同类型的另一个指针中减去等操作。引用与现有变量绑定,仅仅是该变量名称的别名。

2
@ThomasMcLeod:认为引用是别名是一个美好的梦想,但也是一个令人悲伤和常见的误解。 - Lightness Races in Orbit
@ThomasMcLeod:当然,引用指向内存。否则,怎么可能在类成员中使用引用,然后在程序的完全不同部分使用它们呢? - Ben Voigt
@Ben:一些编译器设计者可能会选择将引用作为内部的存储地址来表示,这需要解除引用,但这是编译器设计者的方便性选择。(显然这是提问者的编译器情况。) 即使如此,这也不会改变引用的功能或用途。它们仍然在功能上表现为数据对象而不是指针。因此,我认为说引用指向内存是不正确的。 - ThomasMcLeod
@Tomalak:引用作为别名的概念并不是我提出来的,而是来自 Dietel & Dietel 的流行 C++ 文本。该书指出:“引用变量可以在函数内用作本地别名……所有操作都被认为是在变量上执行的。”那么这种说法何以成为“令人遗憾和常见的误解”呢? - ThomasMcLeod
很明显,引用并不像数据对象一样起作用。当数据对象离开范围时,会调用析构函数。当引用离开范围时,没有析构函数被调用(除了临时变量因const引用而导致的生命周期延长的特殊情况)。 - Ben Voigt
显示剩余12条评论

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