# std::unique_ptr 獨佔指標
create : 2024/11/28
last update : 2024/12/19
[std::unique_ptr<T/>](https://ithelp.ithome.com.tw/articles/10214031)
[C++11智能指针(六):unique_ptr介绍与例子-CSDN博客](https://blog.csdn.net/lijinqi1987/article/details/79005794)
這篇寫得很好:
[【C++高阶】:智能指针的全面解析_智能指针详解-CSDN博客](https://blog.csdn.net/island1314/article/details/140936746?spm=1001.2014.3001.5501)
deleter(我不會) : [【C++高阶】:自定义删除器的全面探索-CSDN博客](https://island.blog.csdn.net/article/details/140993248)
google 4.1. 所有權與智慧指標 [4. 來自 Google 的奇技 — Google 開源專案風格指南](https://tw-google-styleguide.readthedocs.io/en/latest/google-cpp-styleguide/magic.html)
如何使用智慧指標:在這裡如果用raw指標有什麼缺點?可以用智慧指標去除這些缺點?可以的話就改成智慧指標。比如可以不用手動delete。或是當物件B的指標屬於某一物件A,A負責銷毀它,那就可用智慧指標將B指標傳給A。
## what is smart ptr
智慧指標是被設計來處裡"動態配置"的物件/變數的生命週期,目的是讓寫程式的人不用花很多時間去煩惱該怎們管理動態配置的物件的生命週期 = 到底是誰需要負責銷毀它 = 建立一個自動銷毀該物件的機制 = 建立垃圾回收機制 = 避免記憶體洩漏。
達成此目的的做法就是把raw指標包成一個類,並靜態配置/宣告該智慧指標物件去間接使用該raw指標。因為靜態配置的物件符合RAII = 當程式離開scope或exception發生,該物件的解構子會被自動呼叫。
對於靜態配置的物件,不應該也不能用智慧指標。因為當unique_ptr out of scope時,他的解構子會delete該raw pointer。就會編譯錯誤。因為靜態物件指標不能被delete。delete只能用於heap的物件 = 只能用於動態配置的物件。
raw指標的使用情境可二分為唯一所有權(只能給一個物件使用)跟共享所有權(可同時給多的物件使用),因此智慧指標分unique_ptr跟shared_ptr。weak_ptr(用於shared_ptr)是為了補足shared_ptr的缺點而被發明的。
```cpp=
#include <memory>
int main() {
class MyClass {};
MyClass obj;
std::unique_ptr<MyClass> ptr(&obj);
// 這行本身就有問題,不應該傳raw指標(&obj)給unuque_ptr。應該只能(new MyClass)
return 0;
}
```
有哪些常見的情況是會將raw指標用於靜態配置的物件?
比如在main中定義一個container(比如list)並將他丟到一個call by value/ref的函數使用。因為container本身很大,所以不用call by value(耗資源)。
[cplusplus.com/reference/memory/unique_ptr/](https://cplusplus.com/reference/memory/unique_ptr/)
## unique_ptr
可想成raw指標管理器,它控制/處裡的東西是raw pointer。
它裡面主要的資料成員是raw指標跟deleter物件。
## 用法
在獨佔指標的使用過程中,是不會看到raw指標的。new字眼也應該只有在建立獨佔指標物件的時候才會看到
重點 : unique_ptr不支持拷貝
補充:智慧指標的宣告就是靜態配置的物件的宣告方式(他就是靜態配置的物件)
new跟std::make_unique(c++14)區別
1. new不能用auto,make_unique可以
2. make_unique有exception保護 避免記憶體洩漏,new沒有
### 初始化獨佔指標
#### case1 : 直接初始化獨佔指標的方式
```cpp=
std::unique_ptr<int> up(new int()); // correct:用raw指標初始化
// std::unique_ptr<int> up = new int(); // error:等號右邊的變數的type必須跟等號左邊的type一樣。因為當你這樣寫(type_name obj_name = )時,代表你呼叫拷貝建構子或是移動建構子,這兩種建構子的參數的type必須跟該class本身的type一樣(這是規定)。
// auto up(new int()); // complier把up推論成raw指標,不是獨佔指標
std::unique_ptr<int> ptr2 = std::make_unique<int>(3); // better
std::unique_ptr<int> ptr2(std::make_unique<int>(3));
// same as : MyClass obj(obj2);
// 跟上一個例子一樣用到移動建構子。因為std::make_unique<int>(3)返回的是臨時物件。
auto ptr3 = std::make_unique<int>(3); // common practice (c++ primer),用auto更簡潔。
```
1. c++ primer在共享指標中說std::make_shared()比用new更安全,但沒解釋原因。
2. std::make_unique()是c++14
3. 為何"auto ptr3 = std::make_unique\<int>(3);"是對的:
1. std::make_unique()的樣子:
```cpp=
template<typename T, typename... Ts>
std::unique_ptr<T> make_unique(Ts&&... params)
{
return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
}
```
[C++11新特性之十三:std::make_unique和std::make_shared-CSDN博客](https://blog.csdn.net/caoshangpa/article/details/79178639)
就是在該函數模板內建立一個獨佔指標的臨時物件並返回他(return by value)
2. 對於臨時物件,接收者會優先用移動建構子或移動指派運算符。因此這裡是呼叫移動建構子。
#### case2 : 用已存在的獨佔指標做初始化的方式 = 將一個獨佔指標的raw的指標的所有權轉移給另一個獨佔指標 : std::move() 跟 .reset()+.release()
##### std::move()
```cpp=
auto ptr = std::make_unique<int>();
std::unique_ptr<int> ptr2(ptr); // error
// same as MyClass obj(obj2);
// error原因:不支持拷貝:這裡是呼叫他的拷貝建構子。但拷貝建構子是delete的。為何不是呼叫一般的建構子?因為他丟進去的引數是同type的物件,就不會是一般的建構子。為何不是呼叫移動建構子?因為沒有std::move()
std::unique_ptr<int> ptr3 = ptr; // error 不支持拷貝
std::unique_ptr<int> ptr4 = std::move(ptr3); // correct
std::unuqie_ptr<int> ptr5(std::move(ptr4)); // correct
```
```cpp=
auto up = std::make_uniue<int>(0);
std::unique_ptr<int> ptr;
ptr = up; // error 不支持拷貝
ptr = std::move(up); // correct
```
##### .reset()+.release() : from c++primer
方法2 : 用release()、reset() c++primer
```cpp=
auto ptr = std::make_unique<MyClass>();
std::unique_ptr<MyClass> ptr2(ptr.release()); // correct
// auto ptr(ptr.release()); // ptr被編譯器推論為raw指標
```
```cpp=
auto ptr = std::make_unique<MyClass>();
std::unique_ptr<MyClass> ptr2;
ptr2.reset(ptr1.release()); // correct
```
### 建立指向某一物件的獨佔指標
```cpp=
#include <memory>
#include <iostream>
class B {
public:
B() : a_(0), f_(0.0) {}
B(int a, float f) : a_(a), f_(f) {}
int a_;
float f_;
};
int main() {
auto up = std::make_unique<B>();
auto up2 = std::make_unique<B>(10, 2.5);
return 0;
}
```
## unique_ptr作為某class的資料成員的相關code寫法
### 在class的初始化列表初始化獨佔指標成員
直接在初始化列表初始化最好
```cpp=
#include <iostream>
#include <memory>
class A {
public:
A() : up_(std::make_unique<int>(0)) {}
private:
std::unique_ptr<int> up_;
};
```
```cpp=
class A {
public:
A() : up_(nullptr) {
up_ = std::make_unique<int>(0);
}
private:
std::unique_ptr<int> up_;
};
```
```cpp=
#include <memory>
#include <iostream>
class B {
public:
B() : a_(0), f_(0.0) {}
B(int a, float f) : a_(a), f_(f) {}
int a_;
float f_;
};
class A {
public:
A() : up_(std::make_unique<B>()) {}
A(int a, float f) : up_(std::make_unique<B>(a, f)) {}
private:
std::unique_ptr<B> up_;
};
int main() {
A obj(10, 1.1);
return 0;
}
```
### 在class內提供public修改獨佔指標的方法
```cpp=
#include <memory>
#include <iostream>
class B {
public:
B() : a_(0), f_(0.0) {}
B(int a, float f) : a_(a), f_(f) {}
int a_;
float f_;
};
class A {
public:
A() : up_(std::make_unique<B>()) {}
A(int a, float f) : up_(std::make_unique<B>(a, f)) {}
void set_up(std::unique_ptr<B> up) {
up_ = std::move(up);
}
void Show() {
std::cout << up_->a_ << " " << up_->f_ << std::endl;
}
private:
std::unique_ptr<B> up_;
};
int main() {
A obj(10, 1.1);
obj.Show();
auto up = std::make_unique<B>(25, 7.5);
obj.set_up(std::move(up));
/*
or
obj.set_up(std::make_unique<B>(25, 7.5));
*/
if (up == nullptr) {
std::cout << "up is nullptr" << std::endl;
} else {
std::cout << "up is not nullptr" << std::endl;
}
obj.Show();
return 0;
}
```
### 錯誤用法
用智慧指標的目的是符合modern c++ design:對於動態配置的物件,不使用raw ptr,只用smart ptr,因此不該像下面這樣用:
```cpp=
#include <memory>
int main() {
int* raw_ptr = new int(3);
std::unique_ptr<int> smart_ptr(raw_ptr);
}
```
上述例子還有可能導致未定義行為。比如在智慧指標前或後delete raw_ptr;
## exclusive ownership 獨佔/唯一所有權
任何時刻該動態配置的物件的記憶體位置只會被唯一一個unique_ptr物件持有,該raw指標的所有權可轉移
= 一個raw指標只能被唯一一個unique_ptr擁有
= 不會被兩個或以上的unique_ptr持有。
它避免了多個指標指向同一個物件,導致的潛在問題。比如他被其中一個delete,另一各卻還用它。或是double delete(未定義行為)。
他的做法:不允許copy = 將copy constructor跟copy assigment operator 設為delete。
delete是c++11的,在這之前可以把拷貝建構子跟拷貝指派運算符設為private達成相同目的。
```cpp=
#include <memory>
class MyClass {
public:
MyClass(int num) : num_(num) {}
private:
int num_;
};
int main() {
std::unique_ptr<MyClass> obj_ptr(new MyClass(2));
// std::unique_ptr<MyClass> obj2_ptr(obj_ptr); //error
// std::unique_ptr<MyClass> obj3_ptr = obj_ptr; //error
return 0;
}
```
### 當一個類有獨佔指標資料成員又希望該類的物件可被拷貝的寫法
假設你有一個class A,裡面有物件成員B objb;如果class B不能拷貝(class B的拷貝建構子跟拷貝指派運算符 = delete;最常見的例子應該就是unique_ptr一樣)。那當你用編譯器自動生成的拷貝建構子跟拷貝指派運算符就會編譯錯誤。需要自己做deep copy = 自己實現class A的拷貝建構子跟拷貝指派運算符。這是常見用法。
實做"拷貝建構子跟拷貝指派運算符"的方式就是在"拷貝建構子跟拷貝指派運算符"的{}裡面建一個新的物件並把該物件的資料成員一個個複製過去。
補充:在實做之前要確認需求,到底該需求是否真的需要用deep copy的方式處裡獨佔指標?或是可以用共享指標?或是她確實是用共享指標,但該類的物件不該被拷貝?
e.g.
```cpp=
#include <iostream>
#include <memory>
class A {
public:
A() : up_(std::make_unique<int>(0)) {}
private:
std::unique_ptr<int> up_;
};
int main() {
A obj1;
A obj2(obj1); // error
}
```
correct:
```cpp=
#include <iostream>
#include <memory>
class A {
public:
A(int num) : up_(std::make_unique<int>(num)) {}
A(const A& other) {
up_ = std::make_unique<int>(*other.up_);
}
A& operator=(const A& other) {
if (this != &other) {
up_ = std::make_unique<int>(*other.up_);
}
return *this;
}
std::unique_ptr<int> up_;
};
int main() {
A obj1(3);
A obj2(obj1);
std::cout << *obj2.up_ << std::endl;
A obj3(1);
obj3 = obj2;
std::cout << *obj3.up_ << std::endl;
return 0;
}
```
如果獨佔指標指向的是一個物件(of class B), 那就要在拷貝建構子跟拷貝指派運算符中使用std::make_unique\<B>(xxx);去呼叫建構子把該物件的所有資料成員複製過去。
假設class B只有預設建構子,但他有3個資料成員。那只能先用std::make_unique\<B>();再把那三個資料成員一個一個assign過去。
recall : std::make_unique<class_name>()是呼叫建構子,不能放該type的物件。
## 補充:delete用法
https://hackmd.io/@S-gwYZv8RkKi_mnmtTIOCw/Bk2JSyhX1g