我应该总是检查nullptr成员指针吗?

Should I always check member pointers for nullptr?

本文关键字:指针 成员 nullptr 检查 我应该      更新时间:2023-10-16

这样做不好吗?(在对对象指针进行操作之前不检查draw()函数中的nullptr)

class SomeClass
{
public:
    SomeClass(Object& someValidObject)
    {
       object = &someValidObject;
    }
    void draw()
    {
        object->draw();
    }
    Object* object = nullptr;
}

或者在调用对象中的操作之前,我应该总是检查nullptr,即使构造函数肯定会使指针指向某些东西?

void draw()
{
   if(object != nullptr) 
      object->draw?
}

这取决于你想要保护什么

正如在评论中已经指出的那样,您无法可靠地检查悬空指针,因此如果传递的ObjectSomeClass之前超出范围,则会发生不好的事情。

所以你唯一可以可靠地检查的是指针是否为nullptr,但正如你自己注意到的,目前构造函数使这几乎是不可能的。但是,只要您的object成员变量是public,就像现在一样,理论上用户可以到达那里并将其设置为nullptr。您可以通过将成员设置为private并使用拒绝nullptr值的setter(或者像构造函数一样简单地接受引用)来增加难度。在这样的设计中,object != nullptr可以被认为是一个类不变量,这是一个在每次成员函数调用之前和之后(从构造之后到销毁之前)都为真的条件。

那么我们如何打破类不变量呢?作为客户端,我们可能违反函数的前提条件,从而使类进入未定义状态。对于像示例这样简单的类,这很难做到,因为函数实际上没有先决条件。但是为了便于参数,我们假设您要添加一个setter,如下所示:

// Precondition: obj must point to a valid Object.
// That object must be kept alive for the lifetime of the class,
// otherwise the behavior is undefined.
void setObject(Object* obj)
{
    object = obj;
}

这有点微妙。代码允许我们在这里传递nullptr,但文档明确禁止这样做。如果传递一个nullptr,或者传递的对象在SomeClass之前死亡,就违反了该类的契约。但是这个契约不是在代码中强制执行的,它只是在注释中。

这里要意识到的重要事情是,有些条件不能在代码中检查。我们可以检查nullptr,但不能检查悬空指针。有时检查是可能的,但由于运行时成本高(例如。检查一个范围是否为二元搜索排序)。一旦我们意识到这一点,我们作为类设计师就会有一些回旋余地。既然我们无法做到百分百的防弹,我们是否应该检查一下呢?当然,我们可以在任何地方检查nullptr,但这意味着要支付运行时开销来检查基本上是编程错误。

如果我不想在生产构建中支付这些开销怎么办?如果我在开发过程中犯了错误,也许我希望我的调试器能够捕获它,但我不希望我的客户在发布后支付检查费用。

所以你真正要找的是一个assert:

void draw()
{
    assert(object);
    object->draw();
}

决定是在断言中检查某些内容(或根本不检查),还是在契约中进行适当的运行时检查,这不是一个容易的决定。这往往是一个哲学问题。如果你想深入了解的话,John Lakos在CppCon 2014上做了一个很好的演讲。

我想这是个人喜好的问题。但是认为为了可维护性,在代码内部(和作为代码)表达期望是一个很好的实践;因此,我宁愿这样写:

void draw()
{
   if(object == nullptr) { /* throw some exception */ }
   object->draw()
}

编辑

正如我在回答的评论中建议的那样,在c++中只检查null指针通常是不够的。实际上,指针可以是非空但无效(这被称为悬空指针)。当objectdraw函数被调用之前被销毁时,就会发生这种情况。

,尽管构造函数肯定会让指针指向

对于新构造的实例来说是正确的。但是这个保证能持续多久呢?考虑:

Object o;
SomeClass s(o);
s.object = nullptr
s.draw(); // oops

某人必须保证指针不为空。当然,您可以在draw中进行检查。这工作。在这种情况下,担保人是draw本身。则draw的前提条件为:object指向有效对象,且满足Object::drawobject指向null的前提条件。

或者,您可以声明object != nullptr是一个要求(只需要求object指向一个满足Object::draw先决条件的有效对象)。在这种情况下,保证者是draw的调用者。那么你不需要检查空

前者的优点是函数有更宽松的前提条件,程序员的错误更难违反。缺点是可能存在冗余检查的性能损失(在这种情况下可能是微不足道的)。

后者的优点是在调用者已经保证了object != nullptr的情况下的运行时性能优势。缺点是程序员的错误更容易违反前提条件。这可以通过使用一个断言来减轻,该断言允许在开发时捕获错误并在生产中获得最佳性能。

后一种选择可以通过将object设置为私有来显著改进。那么object != nullptr可以被认为是一个类不变量。如果不变性得到保证,则不需要运行时检查,并且只有类的实现者可能意外地违反前提条件。使用断言和单元测试可以很容易地发现这种冲突。


在任何一种方法中违反任何前提条件的结果都是未定义行为。前提条件不能在c++中表示,但它们只是文档的一部分。

object必须指向一个有效对象的前提条件很难保证,因为SomeClass不负责object的生命周期。为了使保证更简单,考虑使用智能指针的可能性,或者完全避免间接。

这一切都归结为合同的概念。如果你的指针可以合理地为空,你必须检查它。如果指针不应该为空,不要检查它,你只是在浪费cpu周期。此外,如果指针为空,该如何处理呢?

有一个使用断言的情况。它只出现在调试代码中,并向您的用户传达合约是指针永远不应该为空。