当我可以在析构函数中销毁对象时,为什么要使用std :: unique_ptr?

时间:2016-05-22 05:00:41

标签: c++ pointers destructor unique-ptr

说我有这个班:

class Foo
{
public:
    Foo()
    {
        bar = new Bar;
    }

    ~Foo()
    {
        if(bar)
            delete bar;
    }
private:
    Bar* bar;

};

std::unique_ptr膨胀时,为什么我要使用unique_ptr而不是原始指针?有什么优势吗?有没有我的析构函数不会被调用的情况?

3 个答案:

答案 0 :(得分:31)

您上面的代码实际上有一个错误,因为您尚未定义复制构造函数或赋值运算符。想象一下这段代码:

Foo one;
Foo two = one;

因为twoone的副本,所以使用默认的复制构造函数初始化它 - 这使得两个bar指针指向同一个对象。这意味着当two的析构函数触发时,它将解除分配one共享的同一对象,因此one的析构函数将触发未定义的行为。糟糕。

现在,如果你不想让你的对象可以复制,你可以这样明确地说:

class Foo
{
public:
    Foo()
    {
        bar = new Bar;
    }
    Foo(const Foo&) = delete;
    Foo& operator= (const Foo&) = delete;

    ~Foo()
    {
        if(bar)
            delete bar;
    }
private:
    Bar* bar;

};

这样可以解决这个问题 - 但请查看所涉及的代码量!您必须明确删除两个函数并手动编写析构函数。

除了另一个问题。假设我这样做:

Foo one;
Foo two = std::move(one);

通过将two的内容移至one来初始化two。或者是吗?不幸的是,答案是否定的,因为默认的移动构造函数将默认移动指针,这只是一个直指针复制。所以现在你得到了和以前一样的东西。糟糕。

不用担心!我们可以通过定义自定义移动构造函数和移动赋值运算符来解决这个问题:

class Foo
{
public:
    Foo()
    {
        bar = new Bar;
    }
    Foo(const Foo&) = delete;
    Foo& operator= (const Foo&) = delete;

    Foo(Foo&& rhs)
    {
        bar = rhs.bar;
        rhs.bar = nullptr;
    }

    Foo& operator= (Foo&& rhs)
    {
        if (bar != rhs.bar)
        {
            delete bar;
            bar = rhs.bar;
            rhs.bar = nullptr;
        }
    }

    ~Foo()
    {
        if(bar)
            delete bar;
    }
private:
    Bar* bar;

};

唷!这是 ton 代码,但至少它是正确的。 (或者是吗?)

另一方面,想象你写了这个:

class Foo
{
public:
    Foo() : bar(new Bar) {
    }
private:
    std::unique_ptr<Bar> bar;
};
哇,这短得多!并且它自动确保无法复制类,它使默认移动构造函数和移动赋值运算符正常工作。

因此std::unique_ptr的一个巨大优势是它会自动处理资源管理,是的,但另一个优点是它可以很好地处理复制和移动语义,并且不会以意想不到的方式工作。这是使用它的主要原因之一。你可以说出你的意思 - “我是唯一应该了解这件事的人,你不能分享它” - 编译器会为你强制执行。让编译器帮助你避免错误几乎总是一个好主意。

至于臃肿 - 我需要看到证据。 std::unique_ptr是一个指针类型的瘦包装器,一个好的优化编译器可以毫无困难地为它生成好的代码。是的,有与std::unique_ptr相关联的构造函数,析构函数等,但是合理的编译器会内联这些调用,这基本上与您最初描述的内容完全相同。

答案 1 :(得分:4)

您基本上依赖于一个类来管理指针的生命周期,但忽略了考虑指针在函数之间传递,从函数返回,并且通常存在于任何地方。如果示例中的指针需要比类更长,该怎么办?如果需要在课程被销毁之前删除该怎么办?

考虑这个功能:

Bar * doStuff(int param) {
    return new Bar(param);
}

现在您有一个动态分配的对象,如果您忘记删除它可能会泄漏。也许你没有阅读文档,或者文档缺乏。无论如何,此函数会给您带来不必要的负担,即销毁Bar的返回实例。

现在考虑:

std::unique_ptr<Bar> doStuffBetter(int param) {
    return new Bar(param);
}

返回的unique_ptr管理它包装的指针的生命周期。函数返回unique_ptr的事实消除了对所有权和生命周期的任何混淆。一旦返回的unique_ptr超出范围并调用其析构函数,Bar的实例将自动删除。

unique_ptr只是标准库提供的几种方法之一,可以使用指针进行不那么混乱的过程并表达所有权。它既轻巧又很像普通指针,除了复制外。

答案 2 :(得分:1)

使用std::unique_ptr(RAII)而不是原始指针更容易保护异常。

考虑一个类有两个成员变量在其构造函数中获取内存。

class Foo
{
public:
    Foo()
    {
        bar = new Bar;
        car = new Car;    // <- What happens when this throws exception?
    }

    ~Foo()
    {
        if(bar)
            delete bar;
        if(car)
            delete car;
    }
private:
    Bar* bar;
    Car* car;

};

如果Foo的构造函数抛出异常,则Foo未成功构造,因此不会调用其析构函数。 当new Car抛出异常时,bar不会被删除,因此会出现内存泄漏。

现在使用std::unique_ptr来考虑代码。

class Foo
{
public:
    Foo() : bar(std::make_unique<Bar>()), car(std::make_unique<Car>()) {}

private:
    std::unique_ptr<Bar> bar;
    std::unique_ptr<Car> car;

};

如果Foo的构造函数抛出异常,则Foo未成功构造,因此不会调用其析构函数。 但是,将调用成功创建的成员实例的析构函数。 即使std::make_unique<Car>()抛出异常,也会调用bar的析构函数,因此不会有内存泄漏。