C++內存管理——unique_ptr


1. 概述

本想將unique_ptr, shared_ptr和weak_ptr寫在同一篇文章中,無奈越(廢)寫(話)越(連)長(篇),本着不給自己和讀者太大壓力的原則,最終決定分為三篇去描述它們(不是惡意湊文章數哦)。
本篇文章主要描述了unique_ptr,在此之前先給出了auto_ptr的介紹,廢話不說,直入正題。

2. 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"語義的實現有以下三大問題:

  1. auto_ptr采用拷貝構造和拷貝賦值構造去實現"Move"語義,若將auto_ptr采用值傳遞作為函數的參數,當函數執行結束時會導致資源被釋放,若之后的代碼再次訪問此auto_ptr則會是nullptr;
  2. 由於auto_ptr總是使用"non-array delete",所以它不能用於管理array類的動態內存;
  3. auto_ptr不能和STL容器和算法配合工作,因為STL中的"Copy"真的是"Copy",而不是"Move"。

由於auto_ptr有諸多問題,需要一個更加完美的"Smart Point",unique_ptr也就應運而生了。

3. unqiue_ptr

3.1 Smart Points簡介

Smart Points是什么,或者說它是用來干什么的?它是用來管理動態分配的內存的,它能夠動態地分配資源且能夠在適當的時候釋放掉曾經動態分配的內存。
此時對智能指針來說就有兩條原則:

  1. 智能指針本身不能是動態分配的,否則它自身有不被釋放的風險,進而可能導致它所管理對象不能正確地被釋放;
  2. 在棧上分配智能指針,讓它指向堆上動態分配的對象,這樣就能保證智能指針所管理的對象能夠合理地被釋放。

3.2 unique_ptr的實現

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禁用了拷貝構造和拷貝賦值構造,僅僅實現了移動構造和移動賦值構造,這也就使得它是獨占式的。

3.3 unique_ptr的使用

3.3.1 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就會調用移動賦值構造函數實現所有權的轉移。

3.3.2 訪問管理的對象

unique_ptr有重載的"operator*"和"operator->",即它和普通的指針具有相似的訪問對象的方法。 其中"operator*"返回它管理對象的引用,"operator->"返回一個指向它管理對象的指針。 

3.3.3 unique_ptr和array

不同於auto_ptr只能有"delete",unique_ptr可以有"delete"和"array delete"。其中,unique_ptr對於std::array, std::vector和std::string的支持比較友好。

3.3.4 make_unique

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。

3.3.5 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。

3.3.6 unique_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

3.3.7 unique_ptr作為類的成員變量

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(); ... } 

3.3.8 其它用法

unique_ptr的其它用法如下:

3.3.9 unique_ptr的誤用

常見的誤用有兩種:

  1. 多個智能指針對象管理同一個資源:
Resource* res{ new Resource() }; std::unique_ptr<Resource> res1{ res }; std::unique_ptr<Resource> res2{ res }; 

unique_ptr是獨占的,另外res1和res2的生命周期結束后都會釋放同一塊資源,從而導致未定義的錯誤。

  1. unique_ptr管理資源后,又自定義了delete資源:
Resource* res{ new Resource() }; std::unique_ptr<Resource> res1{ res }; delete res; 

在res1的生命周期結束時會去釋放已經被delete釋放過的資源,從而導致未定義的錯誤。

4. 總結

本文通過對auto_ptr的介紹引出了unique_ptr,總結了unique_ptr的實現以及一些常用的方法,並給出了常見的錯誤使用。

5. 參考文獻

  1. Move語義和Smart Pointers先導(以一個例子說明),https://www.jianshu.com/p/0c9b4e1e7b9f
  2. Move constructors 和 Move assignment constructors簡介,https://www.jianshu.com/p/f97e211fdc2d
  3. c++ 之智能指針:盡量使用std::make_unique和std::make_shared而不直接使用new,https://blog.csdn.net/p942005405/article/details/84635673
  4. C++智能指針作為成員變量,https://www.jianshu.com/p/3402d90a5647

 

歡迎大家批評指正、評論和轉載(請注明源出處),謝謝!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM