访问类树层次结构中的对象时避免使用空指针

Posted

技术标签:

【中文标题】访问类树层次结构中的对象时避免使用空指针【英文标题】:Avoid null pointers while accessing objects in a tree hierarchy of classes 【发布时间】:2019-09-04 15:28:56 【问题描述】:

我必须使用基于树的对象层次结构,我需要在其中访问包含应用程序所需数据的最深元素。我不确定前面的陈述是否最好地解释了问题本身,所以最好用一个例子来说明它。鉴于以下情况:

class A 
private:
    B* _b;
public:
    B* getB() return _b;
;

class B 
private:
    C* _c;
public:
    C* getC() return _c;
;

class C 
private:
    int _n;
public:
    int getN() return _n;
;

所需的操作是通过 A 访问 n。所以我会调用以下内容:

A foo;
foo.getB()->getC()->getN();

当路径的任何部分为空时,问题就出现了,我们最终会产生核心转储。在上述场景中,如果 B 为空,我们将进入核心转储场景。

因此,我就可以用来避免这种核心转储情况的任何策略或模式寻求帮助和建议。如果路径非常大,我最终会检查每个指针是否有效,并最终得到非常丑陋的代码,并且还有可能错过检查路径的一部分的风险。 注意:我无权更改类(A、B、C)的层次结构的实现,因为它们是生成的代码,我无权更改生成器。是我的首选解决方案,但不幸的是我不能。

【问题讨论】:

为什么指针一开始就无效?这些对象的生命周期是多少?谁管理这些指针?它们是任何不变量的一部分吗? 是的,很遗憾,因为您以这种方式设计架构,代码会很丑陋。 C++ 没有像 C# 那样的“安全导航运算符”。您必须自己实施。但更好的选择是显着重构您的架构。你应该尽量避免有好几层的耦合,因为处理它们总是一场噩梦。 不是答案,而是:得墨忒耳定律表明,当您发现自己不得不像这样链接多个取消引用时,是时候重构您的 API 以便您不再需要这样做了:@987654321 @ @JeremyFriesner 在“清洁代码”中称为“train wreck”。 【参考方案1】:

为了避免出现空指针,您可能希望建立一个成员永远不会为空的类不变量。这可以通过以下步骤来实现:

    封装对成员的访问,这样类外的任何东西都不能设置该成员。您已经通过使用私有访问实现了这一点。只需确保将引用或指向成员的指针从成员函数传递/返回到外部即可。 确保没有任何成员或朋友函数将成员设置为 null。 还要确保成员始终被初始化。这是通过使用自定义构造函数来实现的。示例:

class A 
    B* b;
public:
    A(B* b) : b(b) 
        if (!b) 
            // unlike indirection through null pointer, an exception can
            // be caught and (potentially) handled gracefully at runtime
            throw std::runtime_error("Naughty!");
        
    

    // following prevents passing null literal at compile time
    A(std::nullptr_t) = delete; // nullptr
    A(int)            = delete; // 0

    // since it is never null, we can safely return a reference
    B& getB() return *b;

虽然引用具有永远不会为空的良好属性,但它们作为成员却很棘手,因为它们也不可赋值。作为构造函数的参数,它们很棘手,因为通常不习惯或不期望类对象保留对传递给构造函数的对象的引用。因此,我主张在这种情况下使用指针,即使不需要 null。

注意:我无权更改类(A、B、C)的层次结构的实现,因为它们是生成的代码,我无权更改生成器。

在这种情况下,您可以用更好的类来包装生成的类:

class AWrapper 
    A a;
    // custom implementation that encapsulates A


如果空指针是无法避免的有效值,那么这样的不变量当然是不可能的。在这种情况下,您必须在间接通过它之前检查指针是否为空:

if (B* b = foo.getB())
    if (C* c = b->getC())
        c->getN();

您可能会考虑的另一件事是是否所有这些指针都是必要的。如果这些类相互包含而不是间接地相互引用,也许会更简单。

【讨论】:

【参考方案2】:

你需要一路测试:

A foo;
B* BPrt = foo.getB();
if (BPrt)

    C* CPtr = BPrt->getC();
    if (CPtr)
    
        int n = CPtr->getN();
        ...

【讨论】:

这是针对可能更大的设计问题的技术答案。有可能,OP 最好修复设计。此外,这种多重嵌套读起来很痛苦,不建议这样做。 @SergeyA 当然,但 OP 是要避免在将 nullptr 用作标记值时使用它们。这回答了这个问题。您想解决所有 OP 的问题 - 太好了!但这并不会使这个答案不正确。 你的回答没有回答。 OP 已经知道他们可以进行多次检查,问题是具体如何避免这样做。【参考方案3】:

这是我解决问题的方法:

#include <iostream>
using namespace std;

class C 
private:
    int _n;
public:
    int getN() return _n;
;

class B 
private:
    C* _c;
public:
    C* getC() return _c;
;


class A 
private:
    B* _b;
public:
    B* getB() return _b;
;

int main(void);
int main() 
    static B b;
    static C c;
    static A foo; 
    unsigned int n;
    B *bPtr; C *cPtr;

    /* --RECODE (CHAIN-CALL): foo.getB()->getC()->getN();-- */
    bPtr = (B *) (foo.getB());
    cPtr = (C *) (bPtr ? bPtr->getC() : 0);
    n    = (int) (cPtr ? cPtr->getN() : 0);
    /* --USE (CAST) and (TERNARY) instead of (CHAIN-CALL)-- */

    cout << n << endl;
    return n;

【讨论】:

不需要那些演员表。 在这种情况下,这是正确的。但是呈现的形式保留了从调用链到转换三元的代码反转,可以将其扩展为处理n 调用链的情况。 statics 在这里添加了什么?我无法想象main 中的局部变量会受益于static 的场景。 这里没有必要——只是我使用的原始代码的剩余部分。【参考方案4】:

如果类不能更改,则可以通过模板进行检查:

template <typename T, typename A, typename ...Args>
auto recurse(T t, A a, Args... args)

    if (!t)
        throw std::exception;
    auto next = (t->*a)();
    if constexpr (sizeof...(Args) > 0)
        return recurse(next, args...);
    else
        return next;

Then call as follows:

recurse(&foo, &A::getB, &B::getC, &C::getN);

【讨论】:

【参考方案5】:

问题不在于指针为null。这实际上很好,因为它大多在运行时崩溃。如果它不是 null 但之前已被释放/删除怎么办?使用可能会导致未定义的行为。

如果可以的话,更好的方法是使用引用:

class A

private:
  B& _b;
public:
  A(B& b): _bb 
  B& getB ()
  
    return _b;
  
;

或者类似的东西。然后你至少没有任何悬空的东西(除非你也在某处使用指针)。

如果您必须使用指针,请使用其中一个智能指针 - 看看 std::unique_ptr 是否可以为您解决问题。如果不是,那么对于共享所有权使用std::shared_ptr 等等。还要确保初始化对象的方式不会导致那里出现默认的 null。

【讨论】:

引用类型的数据成员比较麻烦。因为它们不能被重新安置,所以很难实现赋值和移动语义。此外,尚不清楚其中一个指针是否打算nullptr 这通常是不明智的,也不一定适用于 OP(没有说明空指针的来源)。成员引用确实使处理类变得复杂,它们应该保留用于真正必要的情况。通常,非拥有指针是更好的选择。 公平地说,我错误地认为 OP 想要确保它不应该是 null 或这样的设计。我误解了,好像您正在检查是否为空,那么您可能不确定何时应该使用它,然后它可能不是空的,而是垃圾。【参考方案6】:

如果可能的话,你可以确保你的指针总是被初始化:

class C 
private:
    int _n = 0;
public:
    int getN() return _n;
;


class B 
private:
    static C default_c;
    C* _c = &default_c;
public:
    C& getC() return *_c;
;

C B::default_c; // An out-of-line static member definition is required.

class A 
private:
    static B default_b;
    B* _b = &default_b;
public:
    B& getB() return *_b;
;

B A::default_b; // An out-of-line static member definition is required.

int main() 
    A a;
    std::cout << a.getB().getC().getN() << '\n';

请注意,指针比引用更能成为成员,因为引用破坏了值语义并使您的类不可赋值。

【讨论】:

我认为假设指针总是可以有意义地初始化是不合理的。并且它没有考虑到稍后可能会出现nullptr 的情况。 @FrançoisAndrieux 我注意到您的评论是否与我的特定代码相关。 这可能是一个很好且有价值的建议,假设默认对象对 OP 有意义。不幸的是,OP 并没有真正告诉我们期望的行为到底是什么。 它专门针对这个特定的帖子。您的答案是始终初始化指针,但我不相信有任何迹象表明 OPs 的情况是可能的,当然这通常不是有意义的事情。对象表示概念、信息、行为等。此解决方案假定每个对象的全局非const 实例是有意义的,但通常情况并非如此。 @FrançoisAndrieux 在我看来,您的 cmets 放错地方了。您可能想发布您自己的答案。随意不同意。【参考方案7】:

您是否考虑过:

try 
foo.getB()->getC()->getN();

catch(...)

    //Here you know something is Null

在处理现有代码时,这似乎是最简单最安全的。

【讨论】:

取消引用空指针会导致未定义的行为。不需要抛出异常。

以上是关于访问类树层次结构中的对象时避免使用空指针的主要内容,如果未能解决你的问题,请参考以下文章

Java中避免空指针查昂见的方法

测试数据时如何避免空指针异常

优雅的避免空指针的示例

C# - 避免 LINQ ToDictionary 中的空指针

规避空指针异常

野指针空指针