假设我有这个类:
class Foo
{
public:
Foo()
{
bar = new Bar;
}
~Foo()
{
if(bar)
delete bar;
}
private:
Bar* bar;
};
在使用unique_ptr
时,相比于裸指针,为什么我要选择std::unique_ptr
?它有哪些优势呢?是否存在析构函数不被调用的情况?
假设我有这个类:
class Foo
{
public:
Foo()
{
bar = new Bar;
}
~Foo()
{
if(bar)
delete bar;
}
private:
Bar* bar;
};
在使用unique_ptr
时,相比于裸指针,为什么我要选择std::unique_ptr
?它有哪些优势呢?是否存在析构函数不被调用的情况?
您上面的代码实际上有一个错误,因为您没有定义复制构造函数或赋值运算符。想象一下这段代码:
Foo one;
Foo two = one;
因为two
是one
的一个副本,所以它使用默认的拷贝构造函数进行初始化 - 这将使得两个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);
这将通过将one
的内容移动到two
来初始化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;
};
哎呀!这是一堆代码,但至少它是正确的。(或者不是吗?)
另一方面,想象一下你写了这个:
class Foo
{
public:
Foo() : bar(new Bar) {
}
private:
std::unique_ptr<Bar> bar;
};
哇,那样子就简短多了!它自动确保该类无法被复制,而且能使默认的移动构造函数和移动赋值运算符正常工作。
因此,std::unique_ptr
的一个巨大优势在于它自动处理资源管理,但另一个优势是它与复制和移动语义相协调,不会出现意外行为。这是使用它的主要原因之一。您可以明确表达自己的意思——“只有我应该知道这个东西,你不能共享它”——编译器会为您执行强制性规定。让编译器帮助您避免错误几乎总是一个好主意。
至于代码膨胀问题——我需要看到证据。 std::unique_ptr
是对指针类型的轻薄封装,良好的优化编译器应该可以为其生成良好的代码。确实,与 std::unique_ptr
相关联的构造函数、析构函数等,但合理的编译器将内联这些调用,从根本上做的事情与最初描述的一样。
std::vector
和std::map
这样最初是作为STL库的一部分,但现在已经(在一定程度上)合并到语言中的东西。我不会真的将std::unique_ptr
称为STL的一部分)。 - templatetypedefchar *
并且每次需要时都会编写自己的动态数组实现。而你使用标准库,因为它们是已经解决的问题。为什么要每次写新程序时都要重复解决这些问题呢? - Miles BudnekBar * 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
只是标准库提供的几种方法之一,使使用指针的过程变得更加简洁,并表达所有权。它既轻量级又基本上像普通指针一样工作,除了拷贝。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
的析构函数也会被调用,因此不会发生内存泄漏。
std::unique_ptr
为什么被认为是臃肿的?它只是对原始指针的一个简单包装而已。 - chrisstd::unique_ptr
。 - Galik