本想将unique_ptr, shared_ptr和weak_ptr写在同一篇文章中,无奈越(废)写(话)越(连)长(篇),本着不给自己和读者太大压力的原则,最终决定分为三篇去描述它们(不是恶意凑文章数哦)。
本篇文章主要描述了unique_ptr,在此之前先给出了auto_ptr的介绍,废话不说,直入正题。
auto_ptr是在C++ 98中引入的,在C++ 17中被移除掉。它的引入是为了管理动态分配的内存,它的移除是因为本身有严重的缺陷,并且已经有了很好的替代者(unique_ptr)。
auto_ptr采用"Copy"语义,期望实现"Move"语义,有诸多的问题。标准库中的auto_ptr和《Move语义和Smart Pointers先导(以一个例子说明)》中的AutoPtr2十分类似,此处再次给出代码并分析它的问题。
template<typename T> struct AutoPtr2 { AutoPtr2(T* ptr = nullptr) : ptr(ptr) { } ~AutoPtr2() { if(this->ptr != nullptr) { delete this->ptr; this->ptr = nullptr; } } AutoPtr2(AutoPtr2& ptr2) // not const { this->ptr = ptr2.ptr; ptr2.ptr = nullptr; } AutoPtr2& operator=(AutoPtr2& ptr2) // not const { if(this == &ptr2) { return *this; } delete this->ptr; this->ptr = ptr2.ptr; ptr2.ptr = nullptr; return *this; } T& operator*() const { return *this->ptr; } T* operator->() const { return this->ptr; } bool isNull() const { return this->ptr == nullptr; } private: T* ptr; };
以上采用"Copy"语义,期望实现"Move"语义的实现有以下三大问题:
由于auto_ptr有诸多问题,需要一个更加完美的"Smart Point",unique_ptr也就应运而生了。
Smart Points是什么,或者说它是用来干什么的?它是用来管理动态分配的内存的,它能够动态地分配资源且能够在适当的时候释放掉曾经动态分配的内存。
此时对智能指针来说就有两条原则:
unique_ptr是独占式的,即完全拥有它所管理对象的所有权,不和其它的对象共享。标准库中的实现和《Move constructors 和 Move assignment constructors简介》中的AutoPtr4十分相似,代码如下:
template<typename T> struct AutoPtr4 { AutoPtr4(T* ptr = nullptr) : ptr(ptr) { } ~AutoPtr4() { if(this->ptr != nullptr) { delete this->ptr; this->ptr = nullptr; } } AutoPtr4(const AutoPtr4& ptr4) = delete; // disable copying AutoPtr4(AutoPtr4&& ptr4) noexcept // move constructor : ptr(ptr4) { ptr4.ptr = nullptr; } AutoPtr4& operator=(const AutoPtr4& ptr4) = delete; // disable copy assignment AutoPtr4& operator=(AutoPtr4&& ptr4) noexcept // move assignment { if(this == &ptr4) { return *this; } delete this->ptr; this->ptr = ptr4.ptr; ptr4.ptr = nullptr; return *this; } T& operator*() const { return *this->ptr; } T* operator->() const { return this->ptr; } bool isNull() const { return this->ptr == nullptr; } private: T* ptr; };
从中可以看到,unique_ptr禁用了拷贝构造和拷贝赋值构造,仅仅实现了移动构造和移动赋值构造,这也就使得它是独占式的。
下面是一个unique_ptr的例子,此处的res是在栈上的局部变量,在main()结束时会被销毁,它管理的资源也会被释放掉。
#include <iostream> #include <memory> // for std::unique_ptr struct Resource { Resource() { std::cout << "Resource acquired\n"; } ~Resource() { std::cout << "Resource destroyed\n"; } }; int main() { // allocate a Resource object and have it owned by std::unique_ptr std::unique_ptr<Resource> res{ new Resource() }; return 0; } // the allocated Resource is destroyed here
以下的代码讲解unique_ptr和"Move"语义:
#include <iostream> #include <memory> // for std::unique_ptr #include <utility> // for std::move struct Resource { Resource() { std::cout << "Resource acquired" << std::endl; } ~Resource() { std::cout << "Resource destroyed" << std::endl; } }; int main() { std::unique_ptr<Resource> res1{ new Resource{} }; std::unique_ptr<Resource> res2{}; std::cout << "res1 is " << (static_cast<bool>(res1) ? "not null" : "null") << std::endl; std::cout << "res2 is " << (static_cast<bool>(res2) ? "not null" : "null") << std::endl; // res2 = res1; // Won't compile: copy assignment is disabled res2 = std::move(res1); // res2 assumes ownership, res1 is set to null std::cout << "Ownership transferred" << std::endl; std::cout << "res1 is " << (static_cast<bool>(res1) ? "not null" : "null") << std::endl; std::cout << "res2 is " << (static_cast<bool>(res2) ? "not null" : "null") << std::endl; return 0; } // Resource destroyed here
以上代码的运行结果如下:
Resource acquired res1 is not null res2 is null Ownership transferred res1 is null res2 is not null Resource destroyed
由于unique_ptr禁止了"Copy"语义,所以"res2 = res1;"不能编译通过。如果我们想转移unique_ptr管理的一个对象的所有权怎么办?可以采用"Move"语义,即通过move()将res1转化为一个右值,此时再将它赋值给res2就会调用移动赋值构造函数实现所有权的转移。
unique_ptr有重载的"operator*"和"operator->",即它和普通的指针具有相似的访问对象的方法。 其中"operator*"返回它管理对象的引用,"operator->"返回一个指向它管理对象的指针。
不同于auto_ptr只能有"delete",unique_ptr可以有"delete"和"array delete"。其中,unique_ptr对于std::array, std::vector和std::string的支持比较友好。
std::make_unique是C++ 14才引入的(详见参考文献3,此处不详细展开),它能够创建并返回 unique_ptr 至指定类型的对象。它完美传递了参数给对象的构造函数,从一个原始指针构造出一个std::unique_ptr,返回创建的std::unique_ptr。其大概的实现如下:
template<typename T, typename... Ts> std::unique_ptr<T> make_unique(Ts&&... params) { return std::unique_ptr<T>(new T(std::forward<Ts>(params)...)); }
此处需要记住优选std::make_unique(),而不是自己去创建一个std::unique_ptr。
unique_ptr可以作为函数的返回值,如下的代码:
struct Resource { ... }; std::unique_ptr<Resource> createResource() { return std::make_unique<Resource>(); } int main() { auto ptr{ createResource() }; ... return 0; }
可以看到unique_ptr作为值在createResource()函数中返回,并在main()函数中通过"Move"语义将所有权转移给ptr。
若要函数接管指针的所有权,可以通过值传递unique_ptr,且要采用"Move"语义。
#include <iostream> #include <memory> #include <utility> struct Resource { Resource() { std::cout << "Resource acquired" << std::endl; } ~Resource() { std::cout << "Resource destroyed" << std::endl; } friend std::ostream& operator<<(std::ostream& out, const Resource& res) { out << "I am a resource"; return out; } }; void takeOwnership(std::unique_ptr<Resource> res) { if (res) { std::cout << *res << std::endl; } } // the Resource is destroyed here int main() { auto ptr{ std::make_unique<Resource>() }; takeOwnership(std::move(ptr)); // move semantics std::cout << "Ending program" << std::endl; return 0; }
以上的代码输出如下:
Resource acquired I am a resource Resource destroyed Ending program
从中可以看到,所有权被函数takeOwnership()接管,当函数执行完毕时资源即被释放。
然而大多数时候我们只是想通过函数调用去改变智能指针管理的对象,而不是让函数接管所有权。此时我们可以通过传递原始的指针或者引用来实现,如下:
#include <iostream> #include <memory> struct Resource { Resource() { std::cout << "Resource acquired" << std::endl; } ~Resource() { std::cout << "Resource destroyed" << std::endl; } friend std::ostream& operator<<(std::ostream& out, const Resource& res) { out << "I am a resource"; return out; } }; void useResource(const Resource* res) { if (res) { std::cout << *res << std::endl; } } int main() { auto ptr{ std::make_unique<Resource>() }; useResource(ptr.get()); // get(): get a pointer to the Resource std::cout << "Ending program" << std::endl; return 0; } // The Resource is destroyed here
以上代码的输出如下:
Resource acquired I am a resource Ending program Resource destroyed
unique_ptr还可以作为类的成员变量,以下代码中的普通指针怎么用unique_ptr替换?详见参考文献4。
普通指针版本:
struct Device { ... }; struct Settings { Settings(Device* device) { this->device = device; } Device* getDevice() { return device; } private: Device* device; }; int main() { Device* device = new Device(); Settings settings(device); ... Device* myDevice = settings.getDevice(); ... delete device; }
unique_ptr版本:
#include <memory> struct Device { ... }; struct Settings { Settings(std::unique_ptr<Device> d) { device = std::move(d); } Device& getDevice() { return *device; } private: std::unique_ptr<Device> device; }; int main() { std::unique_ptr<Device> device(new Device()); Settings settings(std::move(device)); ... Device& myDevice = settings.getDevice(); ... }
unique_ptr的其它用法如下:
常见的误用有两种:
Resource* res{ new Resource() }; std::unique_ptr<Resource> res1{ res }; std::unique_ptr<Resource> res2{ res };
unique_ptr是独占的,另外res1和res2的生命周期结束后都会释放同一块资源,从而导致未定义的错误。
Resource* res{ new Resource() }; std::unique_ptr<Resource> res1{ res }; delete res;
在res1的生命周期结束时会去释放已经被delete释放过的资源,从而导致未定义的错误。
本文通过对auto_ptr的介绍引出了unique_ptr,总结了unique_ptr的实现以及一些常用的方法,并给出了常见的错误使用。
欢迎大家批评指正、评论和转载(请注明源出处),谢谢!