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

时间:2016-09-27 03:24:23

标签: c++ c++11

做这样的事情不好吗? (在使用对象指针进行操作之前,不检查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?
}

4 个答案:

答案 0 :(得分:4)

取决于您要保护的内容。

正如已经指出的那样in the comments,你无法可靠地检查悬空指针,所以如果传递的Object超出你SomeClass之前的范围,就会发生不好的事情。

所以你唯一能够可靠检查的是指针是否为nullptr,但正如你已经注意到的那样,目前构造函数使得它几乎不可能。但是,只要您的object成员变量现在是public,用户就可以在那里到达并将其设置为nullptr。您可以通过创建成员private并使用拒绝nullptr值的setter(或简单地使用引用,如构造函数)来更难。在这样的设计中,object != nullptr可以被认为是类不变,这是在每个成员函数调用之前和之后(从构造之后到破坏之前)为真的条件。 / p>

那么我们如何打破一个类不变量呢?作为客户端,我们可能会违反函数的前提条件,从而使类处于未定义状态。使用像示例一样简单的类,这很难做到,因为函数实际上没有前提条件。但是,为了论证,我们假设您要添加这样的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,但我们无法检查悬空指针。有时检查是可能的,但由于高运行时间成本(例如,检查范围是否为二进制搜索排序)是不可取的。一旦我们意识到这一点,很明显我们作为班级设计师在这里有一些摆动空间。既然我们无法做到100%防弹,我们应该检查一下吗?当然,我们可以在任何地方检查nullptr,但这意味着支付运行时开销来检查基本上是编程错误。

如果我不想在生产版本中支付这笔费用,该怎么办?如果我在开发过程中犯了错误,也许我希望我的调试器能够抓住它,但我不希望我的客户在发布后不得不支付支票。

所以你真正想要的是一个断言:

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

决定是否在断言中检查某些内容(或根本不检查)或进行适当的运行时检查是合同的一部分并非易事。它往往不是一个哲学问题。如果你想深入挖掘,约翰拉科斯给了一个很好的talk about this at CppCon 2014

答案 1 :(得分:1)

我想这是一个品味问题。但是认为为了可维护性而在代码中表达期望(以及代码)是一种好的做法;因此我宁愿写:

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

   object->draw()
}

修改

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

答案 2 :(得分:0)

  

即使构造函数肯定会使指针指向

对于新构建的实例来说,这是真的。但这保证持续多久?请考虑以下事项:

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

某人必须保证指针不为空。当然,您可以随时查看draw。这样可行。在这种情况下,保证者本身是draw。然后draw的先决条件是:object指向有效对象,并满足Object::drawobject点为空的前提条件。

或者,您可以声明object != nullptr是必需的(只需要object指向满足Object::draw前提条件的有效对象)。在这种情况下,保证者是draw的来电者。然后你不需要检查空值。

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

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

通过将object设为私有,可以显着改善后一种选择。然后object != nullptr可以被视为类不变量。如果保证不变性,则不需要运行时检查,只有类的实现者可能会意外违反前提条件。使用断言和单元测试可以很容易地捕获到这种违规行为。

违反任何一种方法中的任何先决条件的结果是未定义的行为。前提条件不能用C ++表示,只是文档的一部分。

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

答案 3 :(得分:0)

这一切都归结为合同的概念。如果你的指针可以合理地为空,你必须检查它。如果指针永远不应为null,请不要检查它,只是浪费cpu周期。此外,如果指针为空,行为应该是什么。

有使用断言的情况。它只出现在调试代码中并传达给你的用户tgat合同是指针永远不应为null。