空引用可能吗?


102

这段代码是否有效(和定义的行为)?

int &nullReference = *(int*)0;

这两个g ++以及铛++编译它没有任何警告,即使使用-Wall-Wextra-std=c++98-pedantic-Weffc++...

当然,该引用实际上不是null,因为无法访问它(这意味着取消引用null指针),但是我们可以通过检查其地址来检查其是否为null:

if( & nullReference == 0 ) // null reference

1
您能给出实际有用的任何情况吗?换句话说,这仅仅是一个理论问题吗?
cdhowie 2010年

好吧,引用是必不可少的吗?始终可以使用指针代替它们。当没有对象可以引用时,这种空引用将使您也可以使用引用。不知道它有多脏,但是在想到它之前,我对它的合法性很感兴趣。
peoro 2010年

8
我认为它不满意
默认

22
“我们可以检查”-不,您不能。有一些编译器将语句变成if (false),从而消除了检查,正是因为引用无论如何都不能为null。Linux内核中存在记录更好的版本,其中对非常类似的NULL检查进行了优化:isc.sans.edu/diary.html?storyid=6820
MSalters

2
“使用引用而不是指针的主要原因之一是使您免于必须测试以查看它是否指向有效对象的负担”,在Default的链接中,这个答案听起来不错!
peoro 2010年

Answers:


75

引用不是指针。

8.3.2 / 1:

引用必须初始化为引用有效的对象或函数。[注:特别是,空引用不能存在于定义良好的程序中,因为创建此类引用的唯一方法是将其绑定到通过解引用空指针而获得的“对象”,这会导致未定义的行为。如9.6中所述,引用不能直接绑定到位字段。]

1.9 / 4:

本国际标准将某些其他操作描述为未定义(例如,取消引用空指针的影响)

正如Johannes在删除的答案中所说的那样,是否应该将“取消引用空指针”归类为未定义的行为存在一些疑问。但这不是引起怀疑的情况之一,因为空指针肯定不会指向“有效对象或函数”,并且标准委员会内部也没有引入空引用的愿望。


我删除了答案,因为我意识到,取消引用空指针并获取引用的左值这个问题与实际绑定对它的引用不同,正如您提到的那样。尽管据说左值也可以引用对象或函数(因此,在这一点上,引用绑定确实没有什么区别),但这两件事仍然是单独的关注点。仅出于取消引用的行为,这里是链接:open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1102
Johannes Schaub-litb 2010年

1
@MSalters(回复已删除答案的评论;此处相关)我不能特别同意此处提出的逻辑。虽然它可能是方便的Elid &*pp普遍,不排除未定义行为(其性质可能“似乎工作”); 并且我不同意typeid试图确定“取消引用的空指针”类型的表达式实际上取消了对空指针的引用。我已经看到人们在认真地争论着&a[size_of_array]不能也不应该依靠,反正写起来更容易也更安全a + size_of_array
Karl Knechtel 2010年

[c ++]标记中的@Default标准应该很高。我的回答听起来像是两种行为都是一回事:)在取消引用并获取不传递的左值时,引用“无对象”可能是可行的,将其存储到引用中会逸出有限范围并突然影响更多代码。
Johannes Schaub-litb 2010年

@Karl在C ++中很好,“取消引用”并不意味着读取值。有人认为“取消引用”是指实际上访问或修改存储的值,但事实并非如此。逻辑是C ++表示左值是指“对象或函数”。如果是这样,那么问题是左值是什么*p,何时p是空指针。C ++当前不具有空左值的概念,问题232希望引入该值。
Johannes Schaub-litb 2010年

typeid作品中,基于语法而不是基于语义的对取消引用的空指针的检测。也就是说,如果你这样做typeid(0, *(ostream*)0),你做的有不确定的行为-没有bad_typeid可以保证被抛出,即使你通过从一个空指针引用语义导致左值。但是从语法上讲,它不是取消引用,而是逗号运算符表达式。
Johannes Schaub-litb 2010年

26

答案取决于您的观点:


如果根据C ++标准进行判断,则无法获取空引用,因为您首先会获得未定义的行为。在第一次出现不确定行为之后,该标准允许发生任何事情。因此,如果您编写*(int*)0,那么从语言标准的角度来看,您已经具有未定义的行为,即取消引用空指针。程序的其余部分无关紧要,一旦执行了此表达式,您就退出了游戏。


但是,实际上,可以很容易地从空指针创建空引用,直到您真正尝试访问空引用后面的值时,您才会注意到。您的示例可能有点太简单了,因为任何好的优化编译器都会看到未定义的行为,并简单地优化掉所有依赖于它的内容(甚至不会创建null引用,它也会被优化掉)。

但是,优化工作取决于编译器来证明未定义的行为,这可能是不可能做到的。考虑一下文件内的这个简单函数converter.cpp

int& toReference(int* pointer) {
    return *pointer;
}

当编译器看到此函数时,它不知道指针是否为空指针。因此,它只是生成将任何指针转换为相应引用的代码。(顺便说一句,这是一个小问题,因为指针和引用在汇编程序中是完全相同的野兽。)现在,如果您还有另一个user.cpp包含代码的文件

#include "converter.h"

void foo() {
    int& nullRef = toReference(nullptr);
    cout << nullRef;    //crash happens here
}

编译器不知道 toReference()将取消对传递的指针的引用,并假定它返回有效的引用,实际上,该引用将为空引用。调用成功,但是当您尝试使用引用时,程序崩溃。希望。该标准允许发生任何事情,包括出现粉红色大象。

您可能会问为什么这是相关的,毕竟,内部已经触发了未定义的行为toReference()。答案是调试:空引用可能像空指针一样传播和扩散。如果您不知道可以存在空引用,并学会避免创建空引用,则可能要花费大量时间来弄清楚为什么成员函数在尝试读取普通旧int成员时似乎崩溃了(答案:实例在该成员的调用中,有一个空引用,this一个空指针也是如此,并且您的成员被计算为位于地址8)。


那么如何检查空引用呢?你给了电话

if( & nullReference == 0 ) // null reference

在你的问题。嗯,那是行不通的:按照标准,如果取消引用空指针,您将具有未定义的行为,并且不能不引用空指针而创建空引用,因此,空引用仅存在于未定义行为的领域内。由于编译器可能假定您未触发未定义的行为,因此可以假定不存在空引用(即使它很容易发出生成空引用的代码!)。这样,它看到了if()条件,得出结论说它不能成立,只是丢弃了整个if()语句。随着链接时间优化的引入,以健壮的方式检查空引用已变得毫无可能。


TL; DR:

空引用有些可怕地存在:

它们的存在似乎是不可能的(按标准而言),
但是它们存在(按生成的机器代码来确定),
但是您看不到它们是否存在(=您的尝试将被优化),
但是无论如何它们可能会杀死您(=您的程序在怪异的点崩溃甚至更糟)。
您唯一的希望是它们不存在(=编写您的程序以不创建它们)。

我希望那不会困扰您!


2
什么是“坪象”?
法拉普

2
@Pharap我不知道,这只是一个错字。但是无论如何,C ++标准都不会在乎它是粉红色的大象还是Ping的大象;-)
cmaster-恢复莫妮卡

9

如果您打算找到一种在单例对象枚举中表示空值的方法,那么(取消)引用空值(C ++ 11,nullptr)是一个坏主意。

为什么不按如下所示在类中声明表示NULL的静态单例对象,并添加一个返回nullptr的强制转换指针运算符?

编辑:更正了几种错误类型,并在main()中添加了if语句,以测试实际使用的强制转换指针操作符(我忘了..我的错)-2015年3月10日-

// Error.h
class Error {
public:
  static Error& NOT_FOUND;
  static Error& UNKNOWN;
  static Error& NONE; // singleton object that represents null

public:
  static vector<shared_ptr<Error>> _instances;
  static Error& NewInstance(const string& name, bool isNull = false);

private:
  bool _isNull;
  Error(const string& name, bool isNull = false) : _name(name), _isNull(isNull) {};
  Error() {};
  Error(const Error& src) {};
  Error& operator=(const Error& src) {};

public:
  operator Error*() { return _isNull ? nullptr : this; }
};

// Error.cpp
vector<shared_ptr<Error>> Error::_instances;
Error& Error::NewInstance(const string& name, bool isNull = false)
{
  shared_ptr<Error> pNewInst(new Error(name, isNull)).
  Error::_instances.push_back(pNewInst);
  return *pNewInst.get();
}

Error& Error::NOT_FOUND = Error::NewInstance("NOT_FOUND");
//Error& Error::NOT_FOUND = Error::NewInstance("UNKNOWN"); Edit: fixed
//Error& Error::NOT_FOUND = Error::NewInstance("NONE", true); Edit: fixed
Error& Error::UNKNOWN = Error::NewInstance("UNKNOWN");
Error& Error::NONE = Error::NewInstance("NONE");

// Main.cpp
#include "Error.h"

Error& getError() {
  return Error::UNKNOWN;
}

// Edit: To see the overload of "Error*()" in Error.h actually working
Error& getErrorNone() {
  return Error::NONE;
}

int main(void) {
  if(getError() != Error::NONE) {
    return EXIT_FAILURE;
  }

  // Edit: To see the overload of "Error*()" in Error.h actually working
  if(getErrorNone() != nullptr) {
    return EXIT_FAILURE;
  }
}

因为它很慢
wandalen

6

clang ++ 3.5甚至对此发出警告:

/tmp/a.C:3:7: warning: reference cannot be bound to dereferenced null pointer in well-defined C++ code; comparison may be assumed to
      always evaluate to false [-Wtautological-undefined-compare]
if( & nullReference == 0 ) // null reference
      ^~~~~~~~~~~~~    ~
1 warning generated.
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.