智能指針專題
- 1.普通指針的問題
- 2.智能指針是什么
- 什么是所有權
- 3.智能指針三個好處:
- 4.C++11提供的智能指針
- 4.1 shared_ptr(共享所有權指針)
- 4.1.1 分配內存
- 4.1.2 成員函數
- 4.1.3 計數情況匯總:
- 4.1.4 示例代碼(計數)
- 4.1.5 示例代碼(reset)
- 4.1.6 自定義刪除器
- 4.1.7 shared_ptr的陷阱
- 4.2 unique_ptr(獨占所有權指針)
- 4.2.1 分配內存
- 4.2.2 成員函數
- 4.2.3 拷貝賦值
- 4.2.4 release
- 4.2.5 reset
- 4.2.6 自定義刪除器
- 4.2.7 unique_ptr的陷阱
- 不要與裸指針混用
- 5.性能
- 6.選擇指針
- 6.1 unique_ptr場景示例:
- 6.1.1 作為參數
- 6.1.2 作為返回值
- 6.2 shared_ptr場景示例:
- 6.2.1 作為參數
- 參考資料
1.普通指針的問題
-
1.訪問失敗:如果一塊內存被多個指針引用,但其中的一個指針釋放且其余的指針并不知道,這樣的情況下,就發生了訪問失敗。
-
2.內存泄漏:從堆中申請了內存后不釋放回去,就會引發內存泄漏。
2.智能指針是什么
在構造的時候分配內存,當離開作用域的時候,自動釋放分配的內存,這樣的話開發人員就可以從手動動態管理內存的繁雜內容中解放出來。
每種智能指針都是以類模板的方式實現的,shared_ptr
也不例外。
什么是所有權
https://blog.csdn.net/weixin_39722329/article/details/96301534
-
單一所有權:所有權擁有著有義務去釋放或轉移所有權,同一時刻只會有一個所有權擁有者
-
共享所有權:和使用裸指針一樣,所有權的使用者不必釋放內存,引用計數器會負責釋放內存,同一時刻可以有多個所有權擁有者。
3.智能指針三個好處:
-
1.明確資源的所屬權
-
2.避免忘記delete,這種比較容易犯錯誤
-
3.更好的處理異常
//函數結束后shared_ptr自動釋放內存
void f(){shared_ptr<int> sp(new int(11));//假設拋出了異常,而且在f中未捕獲
}//函數結束后ip所指向的內存沒有被釋放。
void f1(){int* ip = new int(12);//假設delete語句前拋出了異常,而且在f中未捕獲delete ip;
}
重要性:1>>2>3
4.C++11提供的智能指針
4.1 shared_ptr(共享所有權指針)
??共享指針shared_ptr
是具有共享所有權語義的智能指針。 每當共享指針shared_ptr的最后一個所有者被銷毀時,關聯對象都將被刪除(或關聯資源被清除)。
4.1.1 分配內存
? std::make_shared
// make_shared<int>分配一塊int類型大小的內存,并值初始化為100(返回值是shared_ptr類型,因此可以直接賦值給sp)
shared_ptr<int> sp = std::make_shared<int>(100);
? new 接受指針參數的智能指針構造函數是explicit的,直接初始化形式
// 錯誤! 不會進行隱式轉換,類型不符合
shared_ptr<int> sp1 = new int(100);
// 正確,直接初始化調用構造函數
shared_ptr<int> sp2(new int(100000));
4.1.2 成員函數
p.get()
p.get()的返回值就相當于一個裸指針的值,使用遵守以下幾個約定
1.不要保存p.get()的返回值
2.無論是保存為裸指針還是shared_ptr都是錯誤的
3.保存為裸指針不知什么時候就會變成空懸指針
4.保存為shared_ptr則產生了獨立指針
5.不要delete p.get()的返回值
6.會導致對一塊內存delete兩次的錯誤swap(p,q)
? 交換p、q中保存的指針shared_ptr<T> p(q)
? p是q的拷貝,它們指向同一塊內存,互相關聯p = q
? 用q為p賦值,之后p、q指向同一塊內存,q引用計數+1,p(原來內存空間的)引用計數-1p.use_count()
? 返回與p共享對象的智能指針數量shared_ptr<T> p(q,d)
? q是一個可以轉換為T*的指針,d是一個可調用對象(作為刪除器),p接管q所指對象的所有權,用刪除器d代替delete釋放內存p.reset()
? 將p重置為空指針p.reset(p)
? 將p重置為p(的值)p.reset(p,d)
? 將p重置為p(的值)并使用d作為刪除器shared_ptr為什么沒有release()
? 對于shared_ptr,是可能存在多個shared_ptr指向同一塊內存,如果提供了release可能會造成錯誤的釋放,導致其他shared_ptr出現錯誤。
4.1.3 計數情況匯總:
賦值(增加)
auto sp = make_shared<int>(1024); // sp的引用計數為1
#include <iostream>
#include <memory>using namespace std;
// compile:g++ test.cpp -o a.exe -std=c++11int main()
{{auto sp1 = make_shared<string>("obj1");auto sp2(sp1);auto sp3 = make_shared<string>("obj2");cout << "before sp2->use_count() = " << sp2.use_count() << '\n';cout << "before sp3->use_count() = " << sp3.use_count() << '\n';sp1 = sp3; // 該操作會減少sp2的引用計數,增加sp3的引用計數cout << "after sp2->use_count() = " << sp2.use_count() << '\n';cout << "after sp3->use_count() = " << sp3.use_count() << '\n';}return 0;
}
該操作會減少sp2的引用計數,增加sp3的引用計數。(sp1、sp2指向對象obj1,sp3指向對象obj2,那么賦值之后,sp1也會指向obj2,那就是說指向obj1的就少了,指向obj2的就會多。)
拷貝(增加)
auto sp2 = make_shared<int>(1024);
auto sp1(sp2);
該操作會使得sp1和sp2都指向同一個對象。
傳參(拷貝)(增加)
而關于拷貝比較容易忽略的就是作為參數傳入函數:
auto sp2 = make_shared<int>(1024);
func(sp2); // func的執行會增加其引用計數
reset(減少)
釋放sp指向的對象( 而如果sp是唯一指向該對象的,則該對象被銷毀 )
sp.reset()
4.1.4 示例代碼(計數)
參考:https://en.cppreference.com/w/cpp/memory/shared_ptr
#include <iostream>
#include <memory>// g++ test.cpp -o a.exe -std=c++11 class Base
{
public:Base() { std::cout << " Base::Base()\n"; }~Base() { std::cout << " Base::~Base()\n"; }
};class Derived: public Base
{
public:Derived() { std::cout << " Derived::Derived()\n"; }~Derived() { std::cout << " Derived::~Derived()\n"; }
};void test(std::shared_ptr<Base> p)增加引用計數
{ std::cout << "local pointer in a function:\n"<< " p.get() = " << p.get()<< ", p.use_count() = " << p.use_count() << '\n';std::shared_ptr<Base> lp = p; std::cout << "local pointer in a function:\n"<< " lp.get() = " << lp.get()<< ", lp.use_count() = " << lp.use_count() << '\n';
}//銷毀ptr,減少引用計數int main()
{std::shared_ptr<Base> p = std::make_shared<Derived>();std::cout << "func before:\n"<< " p.get() = " << p.get()<< ", p.use_count() = " << p.use_count() << '\n';test(p);std::cout << "func after:\n"<< " p.get() = " << p.get()<< ", p.use_count() = " << p.use_count() << '\n';
}
運行結果
Base::Base()
Derived::Derived()
func before:p.get() = 0x1028c30, p.use_count() = 1
local pointer in a function:p.get() = 0x1028c30, p.use_count() = 2
local pointer in a function:lp.get() = 0x1028c30, lp.use_count() = 3
func after:p.get() = 0x1028c30, p.use_count() = 1
Derived::~Derived()
Base::~Base()
4.1.5 示例代碼(reset)
重置共享指針,減少計數
#include<memory>
#include<iostream>
// 編譯: g++ test.cpp -o a.exe -std=c++11using namespace std;
class A {
public:int i ;A() { cout << "construct\n"; }~A() { cout << "delete "<< i <<"\n"; }
};
int main()
{// 共享指針a,b,c都指向堆內存new A的位置shared_ptr<A> a( new A);shared_ptr<A> b(a);shared_ptr<A> c(b);shared_ptr<A> d(new A);a->i = 10;cout << a.use_count() << endl;cout << b.use_count() << endl;d->i = 30;// 錯:不要用p.get()的返回值為shared_ptr賦值,因為返回的是裸指針,很容易被共享指針重復釋放,造成錯誤//a.reset(d.get())// 令a釋放指向的空間 A//a.reset();// 令a釋放指向的空間 A,指向新空間a.reset(new A);a->i = 20;cout << b.use_count() << endl;cout << "end" <<endl;return 0;
}
4.1.6 自定義刪除器
??如果用shared_ptr管理非new對象或是沒有析構函數的類時,應當為其傳遞合適的刪除器,原理是:當刪除器的指針Deleter
傳給shared_ptr/unique_ptr時,shared_ptr/unique_ptr不會使用默認的delete val
來釋放其管理的資源,而是使用Deleter(val)
來釋放資源,這樣就調用了Deleter來釋放管理的資源。
1.普通刪除函數定義類似于:
void Deleter(T *val){// 其他代碼// 釋放val的內存delete val;// 或者(如果val是數組)delete[] val;
}
#include <iostream>
#include <memory>
#include <string>
// 編譯: g++ test.cpp -o a.exe -std=c++11
using namespace std;class Connection{
public:string _name;explicit Connection(string name):_name(name){}string get_name() const {return _name;}
};void close(Connection* connection){cout << string("關閉") + connection->get_name() + "管理的連接中..." << endl;// 關閉連接的代碼// .....cout << "關閉完成。" << endl;
}// 函數式刪除器
void Deleter(Connection *connection){close(connection);delete connection;
}int main(){// 新建管理連接Connection的智能指針shared_ptr<Connection> sp(new Connection("shared_ptr"), Deleter);sp->_name = "hello";
}
- 自定義釋放規則,用于申請的動態數組
??對于申請的動態數組來說,shared_ptr 指針默認的釋放規則是不支持釋放數組的,只能自定義對應的釋放規則,才能正確地釋放申請的堆內存。釋放規則可以使用 C++11 標準中提供的 default_delete 模板類,我們也可以自定義釋放規則:
//1.指定 default_delete 作為釋放規則
std::shared_ptr<int> p6(new int[10], std::default_delete<int[]>());//2.自定義釋放規則
void deleteInt(int*p) {delete []p;
}
//初始化智能指針,并自定義釋放規則
std::shared_ptr<int> p7(new int[10], deleteInt);//3.lambda方式構造和釋放
std::shared_ptr<int> p7(new int[10], [](int* p) {delete[]p; });
4.1.7 shared_ptr的陷阱
1.不要與裸指針混用
錯誤場景1:
int *x(new int(10));
shared_ptr<int> sp1(x);
shared_ptr<int> sp2(x);//x隨時可能變成空懸指針而無從知曉
錯誤場景2:
int *x(new int(10));
//創建了一個指向x指針所指內存的共享指針,引用計數為1,是引用這塊內存的唯一共享指針func(shared_ptr<int> (x));
//離開函數即離開共享指針的作用域,這塊內存即被刪除
2.謹慎使用p.get()的返回值
shared_ptr<int> sp1(new int(10));
shared_ptr<int> sp2(sp1), sp3;
sp3 = sp1;//一個典型的錯誤用法
shared_ptr<int> sp4(sp1.get());
cout << sp1.use_count() << " " << sp2.use_count() << " " << sp3.use_count() << " " << sp4.use_count() << endl;//輸出:
3
3
3
1(獨立)
sp1,sp2,sp3是相互關聯的共享指針,共同控制所指內存的生存期,sp4雖然指向同樣的內存,卻是與sp1,sp2,sp3獨立的,sp4按自己的引用計數來關聯內存的釋放。
4.2 unique_ptr(獨占所有權指針)
兩個unique_ptr不能指向同一個對象,不能進行復制操作,只能進行移動操作。
4.2.1 分配內存
? 與shared_ptr不同,unique_ptr沒有定義類似make_shared的操作,因此只可以使用new來分配內存,并且由于unique_ptr不可拷貝和賦值,初始化unique_ptr必須使用直接初始化的方式。
unique_ptr<int> up1(new int()); // okay:直接初始化std::unique_ptr<int> p4(new int);
std::unique_ptr<int> p5(std::move(p4)); // okay:調用移動構造函數,p5 將獲取 p4 所指堆空間的所有權,而 p4 將變成空指針(nullptr)unique_ptr<int> up2 = new int(); // error! 避免隱式轉換
unique_ptr<int> up3(up1); // error! 不允許拷貝
4.2.2 成員函數
-
up.release()
? up放棄對它所指對象的控制權,并返回保存的指針,將up置為空,不會釋放內存
-
up.reset()
參數可以為空,內置指針,先將up所指對象釋放,然后重置up的值
4.2.3 拷貝賦值
??前面說了unique_ptr不可拷貝和賦值,那要怎樣傳遞unique_ptr參數和返回unique_ptr呢? 事實上不能拷貝unique_ptr的規則有一個例外:我們可以拷貝或賦值一個將要被銷毀的unique_ptr
// 從函數返回一個unique_ptr
unique_ptr<int> func1(int a)
{return unique_ptr<int> (new int(a));
}// 返回一個局部對象的拷貝
unique_ptr<int> func2(int a)
{unique_ptr<int> up(new int(a));return up;
}
或者是引用
void func1(unique_ptr<int> &up){cout<<*up<<endl;
}unique_ptr<int> func2(unique_ptr<int> up){cout<<*up<<endl;return up;
}// 使用up作為參數
unique_ptr<int> up(new int(10));// 傳引用,不拷貝,不涉及所有權的轉移
func1(up);// 暫時轉移所有權,函數結束時返回拷貝,重新收回所有權
up = func2(unique_ptr<int> (up.release()));
// 如果不用up重新接受func2的返回值,這塊內存就泄漏了
4.2.4 release
釋放方法:注意!注意!注意!這里的釋放并不會摧毀其指向的對象,而且將其指向的對象釋放出去。
#include <iostream>
#include <memory>//編譯:g++ test.cpp -o a.exe -std=c++11int main () {std::unique_ptr<int> auto_pointer(new int);int * manual_pointer;*auto_pointer=10;manual_pointer = auto_pointer.release();// (auto_pointer is now empty)std::cout << "manual_pointer points to " << *manual_pointer << '\n';delete manual_pointer;return 0;
}
執行結果為:
manual_pointer points to 10
4.2.5 reset
??重置方法,銷毀由該智能指針管理的任何可能存在的對象,該智能指針被指為空
#include <iostream>
#include <memory>//編譯:g++ test.cpp -o a.exe -std=c++11int main () {std::unique_ptr<int> up; // emptyup.reset (new int); // takes ownership of pointer*up=5;std::cout << *up << '\n';up.reset (new int); // deletes managed object, acquires new pointer*up=10;std::cout << *up << '\n';up.reset(); // deletes managed objectreturn 0;
}
執行結果:
5
10
4.2.6 自定義刪除器
#include <iostream>
#include <memory>
#include <string>
// 編譯: g++ test.cpp -o a.exe -std=c++11
using namespace std;class Connection{
public:string _name;explicit Connection(string name):_name(name){}string get_name() const {return _name;}
};void close(Connection* connection){cout << string("關閉") + connection->get_name() + "管理的連接中..." << endl;// 關閉連接的代碼// .....cout << "關閉完成。" << endl;
}// 函數式刪除器
void Deleter(Connection *connection){close(connection);delete connection;
}int main(){// 新建管理連接Connection的智能指針unique_ptr<Connection, decltype(Deleter)*> up(new Connection("unique_ptr"), Deleter);up->_name = "hello";
}
4.2.7 unique_ptr的陷阱
不要與裸指針混用
錯誤做法
int *x(new int());
unique_ptr<int> up1,up2;
// 會使up1 up2指向同一個內存
up1.reset(x);
up2.reset(x);
??unique_ptr不允許兩個獨占指針指向同一個對象,在沒有裸指針的情況下,我們只能用release獲取內存的地址,同時放棄對對象的所有權,這樣就有效避免了多個獨占指針同時指向一個對象。
正確做法
unique_ptr<int> up1(new int()); // okay,直接初始化
unique_ptr<int> up2;
up2.reset(up1.release());
5.性能
- 內存占用高
shared_ptr 的內存占用是裸指針的兩倍。因為除了要管理一個裸指針外,還要維護一個引用計數。
因此相比于 unique_ptr, shared_ptr 的內存占用更高 - 原子操作性能低
考慮到線程安全問題,引用計數的增減必須是原子操作。而原子操作一般情況下都比非原子操作慢。 - 使用移動優化性能
shared_ptr 在性能上固然是低于 unique_ptr。而通常情況,我們也可以盡量避免 shared_ptr 復制。
如果,一個 shared_ptr 需要將所有權共享給另外一個新的 shared_ptr,而我們確定在之后的代碼中都不再使用這個 shared_ptr,那么這是一個非常鮮明的移動語義。對于此種場景,我們盡量使用 std::move,將 shared_ptr 轉移給新的對象。因為移動不用增加引用計數,性能比復制更好。
6.選擇指針
? 不是說任何地方都要使用智能指針,比如說你想傳遞一個對象到一個函數里,那你就可以使用引用或者普通指針(raw ptr), 這里的引用和普通指針體現的是沒有所屬權(ownership),也就是說函數本身不負責這個對象的生命周期。只有需要體現所屬權(ownership)的創立和變動的時候,采用智能指針。參考
選擇條件:
在使用智能指針的時候,優先選用unique_ptr,原因如下:
1.語義簡單,即當你不確定使用的指針是不是被分享所有權的時候,默認選unique_ptr獨占式所有權,當確定要被分享的時候可以轉換成shared_ptr;
2.unique_ptr效率比shared_ptr高,不需要維護引用計數和背后的控制塊;
3.unique_ptr用起來更順暢,選擇性更多,可以轉換成shared_ptr和通過get和release定制化智能指針(custom smart pointer)。
如果有多個指針指向同一對象的話,你應該使用shared_ptr;
如果一個對象只需要一個智能指針,那你應該是用unique_ptr,它非常適合于返回值類型為unique_ptr的函數
6.1 unique_ptr場景示例:
6.1.1 作為參數
因為不能被拷貝,所以傳遞裸指針或者引用或者外部不需要了直接轉移,但是要注意不能傳遞值(拷貝)
// 裸指針
#include<iostream>
#include<memory>
void test(int *p)
{*p = 10;
}
int main()
{std::unique_ptr<int> up(new int(42));test(up.get());//傳入裸指針作為參數std::cout<<*up<<std::endl;//輸出10return 0;
}// 引用
#include<iostream>
#include<memory>
void test(std::unique_ptr<int> &p)
{*p = 10;
}
int main()
{std::unique_ptr<int> up(new int(42));test(up);std::cout<<*up<<std::endl;//輸出10return 0;
}// 轉移
#include<iostream>
#include<memory>
void test(std::unique_ptr<int> p)
{*p = 10;
}
int main()
{std::unique_ptr<int> up(new int(42));test(std::unique_ptr<int>(up.release()));//test(std::move(up));//這種方式也可以return 0;
}
6.1.2 作為返回值
返回可以用unique_ptr(偽代碼),這里可以理解為返回值是拷貝了指向的地方,相當于std::move,獲取唯一的所有權
unique_ptr<int> make_init(int n)
{return unique_ptr<int> (new int(n));
}int main()
{
···vector<unique_ptr<int>> vp(size);for(int i = 0; i < vp.size(); i++)vp[i] = make_init(rand() % 1000)
···
}
6.2 shared_ptr場景示例:
6.2.1 作為參數
#include<iostream>
#include<memory>
void func0(std::shared_ptr<int> sp)
{std::cout<<"fun0:"<<sp.use_count()<<std::endl;
}void func1(std::shared_ptr<int> sp)
{std::cout<<"fun1:"<<sp.use_count()<<std::endl;
}void func2(std::shared_ptr<int> &sp)
{std::cout<<"fun1:"<<sp.use_count()<<std::endl;
}int main()
{auto sp = std::make_shared<int>(1024);func0(sp); // 拷貝方式(這種方式unique不可以)func1(sp); // 拷貝方式(這種方式unique不可以)func2(sp); // 引用方式return 0;
}
這里建議傳參使用引用,免拷貝。
參考資料
主要參考:https://www.yanbinghu.com/categories/Cpp/
C++11智能指針: https://www.jianshu.com/p/e4919f1c3a28
C++11 shared_ptr智能指針:http://c.biancheng.net/view/7898.html
shared_ptr官方講解:http://www.cplusplus.com/reference/memory/shared_ptr/shared_ptr/
unique_ptr官方講解:http://www.cplusplus.com/reference/memory/unique_ptr/unique_ptr/
智能指針的選擇:https://blog.csdn.net/qq_22533607/article/details/82318595