【问题标题】:Why would I want to use a smart pointer in this situation?为什么我要在这种情况下使用智能指针?
【发布时间】:2015-05-04 13:38:30
【问题描述】:

我从来没有使用过任何类型的智能指针,但是当主题是指针时,我几乎到处都在阅读它们。我确实理解在某些情况下,智能指针比原始指针更好用,因为在某种程度上它们管理指针的所有权。但是,我仍然不知道,“我不需要智能指针”和“这是智能指针的情况”之间的界限在哪里。

比方说,我有以下情况:

class A {
public:
    double get1(){return 1;}
    double get2(){return 2;}
};
class SomeUtilityClass {
public:
    SomeUtilityClass(A* a) : a(a) {}
    double getResult(){return a->get1() + a->get2();}
    void setA(A* a){a = a;}
private:
    A* a;
};
int main(int argc, char** argv) {
    A a;
    SomeUtilityClass u(&a);
    std::cout << u.getResult() << std::endl;
    A a2;
    u.setA(&a2);
    std::cout << u.getResult() << std::endl;
    return 0;
}

这当然是一个过于简单的例子。我的意思是SomeUtilityClass 不应该“拥有”A 的实例(因为它只是一个实用程序类),因此它只包含一个指针。

关于指针,我知道的唯一可能出错的是:

  • SomeUtilityClass 可以用空指针实例化
  • 指向的对象可能被删除/超出范围,SomeUtilityClass 没有注意到它

智能指针如何帮助避免这个问题?在这种情况下使用智能指针还能获得哪些其他好处?

PS:我知道有几个关于智能指针的问题(例如this one)。但是,如果您能告诉我对这个特定示例的影响,我将不胜感激。

【问题讨论】:

  • 原始指针完美地捕获了对具有范围生命周期的数据的非拥有引用的概念。你不应该在这里使用智能指针。
  • 如果您在野外使用new,这是一个非常强烈的提示,您可能需要一个智能指针。如果您使用new [],这是一个非常强烈的提示,您可能需要std::vector 或其他容器。
  • 如果nullptr 没有意义,您可以考虑参考或std::reference_wrapper
  • 顺便说一句:难道void setA(A* a){a = a;} 没有做你想做的事吗?我有时会在成员前面加上“m”;它既可以作为阅读代码时的指示符,也可以作为将成员与其他名称区分开来的自然“命名空间”。在 C# 中,它可能是一个下划线。
  • 如果你想确保指针至少在类的生命周期内是活动的,你应该使用共享指针

标签: c++ pointers smart-pointers


【解决方案1】:

这取决于参数的创建和存储方式。如果您不拥有内存并且它可以是静态或动态分配的,那么原始指针是一个非常合理的解决方案 - 特别是如果您需要支持数据交换,如您的示例中所示。另一种选择是使用std::reference_wrapper,这将消除您的nullptr 问题,同时保持相同的语义。

如果您持有指向某个共享资源的指针(即存储在某处的std::shared_ptr)并希望能够检查它是否已被删除,您可以持有std::weak_ptr

【讨论】:

    【解决方案2】:

    出于此答案的目的,我将 setA 重新定义为:

    void setA(A* new_a){a = new_a;}
    

    考虑:

    // Using your SomeUtilityClass
    
    int main() {
      A a;
      SomeUtilityClass u(&a);
      // We define a new scope, just because:
      {
        A b;
        u.setA(&b);
      }
      std::cout << u.getResult() << '\n';
      return 0;
    }
    

    作用域结束后,SomeUtilityClass 有一个悬空指针,getResult() 调用未定义行为。请注意,这不能通过参考来解决:您仍然会得到一个悬空的。

    现在考虑使用智能指针的版本:

    class SomeUtilityClass {
    public:
        SomeUtilityClass(std::shared_ptr<A>& a) : a{a} {}
        double getResult(){return a->get1() + a->get2();}
        void setA(std::shared_ptr<A>& new_a){a = new_a;}
    private:
        std::shared_ptr<A> a;
    };
    
    int main() {
      std::shared_ptr<A> a{new A};
      SomeUtilityClass u{a};
      // We define a new scope, just because:
      {
        std::shared_ptr<A> b{new A};
        u.setA(b);
      }
      std::cout << u.getResult() << '\n';
      return 0;
    }
    

    因为您拥有共享所有权,所以无法获得悬空指针。 b 指向的内存将照常删除,但只有在u 被销毁(或其指针改变)之后。

    恕我直言,在大多数情况下,您应该使用智能指针(即使起初它似乎没有多大意义)。它使维护变得更加容易。仅在实际需要它们的特定代码中使用原始指针,并尽可能封装/隔离此代码。

    【讨论】:

      【解决方案3】:

      如果SomeUtilityClass 不拥有成员变量a,则智能指针没有意义。

      你可以考虑一个引用成员,它会消除空指针的问题。

      【讨论】:

        【解决方案4】:

        在 C++ 中表示非拥有指针的默认方式是weak_ptr。要使用weak_ptr,您需要使用shared_ptr 作为所有权,因此在您的示例中,您将使用

        shared_ptr<A> owner(...)
        

        而不是

        A a
        

        然后作为 SomeUtilityClass 的私有指针成员,您使用弱指针:

        weak_ptr<A> w;
        

        并用shared_ptr初始化它:

        SomeUtilityClass(shared_ptr<A> o) : w(o) {}
        

        但是,您不能直接使用weak_ptr,因为shared_ptr 可能超出范围并且您的弱指针不能再指向任何东西。使用前需要锁定:

        shared_ptr<A> locked = w.lock();
        

        如果拥有的指针不再管理一个对象,locked 指针将为空,例如它超出了范围。如果它不为空,你可以使用它,然后它会超出范围自动释放对象的锁定。

        shared_ptrweak_ptr 在 C++11 的标准库和旧编译器的 Boost 中都可用。

        【讨论】:

        • weak_ptr 不是表达不拥有的默认方式。当使用shared_ptr 时,它只是用来中断循环。表示非拥有的默认方式是原始指针。
        • @SebastianRedl 你陈述了你的个人观点,因为它被普遍接受。但是,cppreferencestd::weak_ptr 模型临时所有权:当一个对象仅在它存在时才需要访问 和以后 此外,std::weak_ptr 用于打破循环引用,所以至少在那里 - 以及在许多其他地方 - 表达了不同的意见。
        • 我利用我的个人声誉对你的回答投了反对票。这就是 SO 的工作原理。另外,您如何阅读“模型临时所有权”来支持您声明的“表达非拥有指针的默认方式”的观点?如果有的话,cppreference 矛盾你的帖子,并且支持我的评论。
        • @SebastianRedl 一点也不。你说weak_ptr“只是为了”打破循环。 Cppreference 说这是它的附加目的。主要目的是临时所有权。如果你想使用一个指针 - 任何指针,智能或原始 - 你最好拥有它在你使用它时,否则在你使用它时你的对象可能不再存在,不能它?所以当然 weak_ptr 是非拥有的,只有当你想使用它时,你 - 有条件地 - 暂时获得所有权。这是安全的。你提出的原始指针不是。
        • 好的,“只是为了”。它也用于观察可能会消失的东西。不过,这不是默认设置。然而,“如果你想使用一个指针——任何指针,无论是智能的还是原始的——你最好在使用它的时候拥有它”是错误的——借用资源(即在不拥有它们的情况下使用它们)是非常普遍的,而且非常安全如果您使用范围来确定 someone 拥有该对象。例如,函数的指针参数通常是安全的(与非 GC 语言一样安全),如果指针不在函数调用之外保留,因为调用者可以轻松地保持对象处于活动状态。
        【解决方案5】:

        有不同类型的智能指针。在您的情况下,很明显智能指针并不是真正需要的,但它仍然可以提供一些好处。

        SomeUtilityClass 可以用空指针实例化

        这个问题可能最好通过检查构造函数来解决,在你得到一个 NULL 指针作为参数的情况下,抛出异常或以其他方式指示错误。我很难想象智能指针会有什么帮助,除非您使用不接受 NULL 的特定智能指针类,因此它已经为您进行了检查。

        指向的对象可能被删除/超出范围,没有 SomeUtilityClass 注意到它

        这实际上可以用一种特殊类型的智能指针来解决,但是需要被指向的对象以某种方式支持销毁通知。一个这样的例子是Qt库中的QPointer类,它只能指向QObject实例,当被删除时通知它,所以当对象被删除时智能指针自动变为NULL。但是,这种方法存在一些问题:

        1. 每次通过智能指针访问对象时都需要检查 NULL。
        2. 如果智能指针指向一个类的实例,比如MyClass扩展执行删除通知的类(在 Qt 案例中为QObject),你会得到奇怪的结果:它通知智能指针的 QObject 的析构函数,因此当MyClass 析构函数已经开始其脏工作时,您可能访问它,因此对象被部分破坏,但指针还不是NULL,因为破坏仍在进展。

        【讨论】:

        • 现在,我通常会忽略不赞成票,但在这种情况下,我已经清楚地回答了 OP 指出的要点,并提供了一个来自广泛使用的 C++ 库的示例。我的回答到底有什么问题,以至于有人为了告诉世界它是多么糟糕和危险而牺牲了整个声誉?也许你告诉我,我会解决它?
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-06-02
        • 2015-09-12
        相关资源
        最近更新 更多