融會(huì)貫通C++智能指針教程
一、基礎(chǔ)知識(shí)介紹
裸指針常出現(xiàn)以下幾個(gè)問(wèn)題:
- 忘記釋放資源,導(dǎo)致資源泄露(常發(fā)生內(nèi)存泄漏問(wèn)題)
- 同一資源釋放多次,導(dǎo)致釋放野指針,程序崩潰
- 寫(xiě)了釋放資源的代碼,但是由于程序邏輯滿足條件,執(zhí)行中間某句代碼時(shí)程序就退出了,導(dǎo)致釋放資源的代碼未被執(zhí)行到
- 代碼運(yùn)行過(guò)程中發(fā)生異常,隨著異常棧展開(kāi),導(dǎo)致釋放資源的代碼未被執(zhí)行到
template<typename T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr):_ptr(ptr) {}
~SmartPtr() {delete _ptr;}
private:
T* _ptr;
};
int main(){
SmartPtr<int> ptr(new int);
return 0;
}
上面這段代碼就是一個(gè)非常簡(jiǎn)單的智能指針,主要用到了這兩點(diǎn):
手動(dòng)實(shí)現(xiàn)智能指針體現(xiàn)在把裸指針進(jìn)行了面向?qū)ο蟮姆庋b,在構(gòu)造函數(shù)中初始化資源地址,在析構(gòu)函數(shù)中負(fù)責(zé)釋放資源
利用棧上的對(duì)象出作用域自動(dòng)析構(gòu)這個(gè)特點(diǎn),在智能指針的析構(gòu)函數(shù)中保證釋放資源。
所以,智能指針一般都是定義在棧上的
面試官:能不能在堆上定義智能指針?
答:不能。就好比SmartPtr<int>* ptr = new SmartPtr<int>();這段代碼中,在堆空間定義一個(gè)智能指針,
這依然需要我們手動(dòng)進(jìn)行delete,否則無(wú)法堆空間的對(duì)象無(wú)法釋放,因?yàn)槎芽臻g的對(duì)象無(wú)法自動(dòng)調(diào)用析構(gòu)函數(shù)。
一般而言智能指針還需要提供裸指針常見(jiàn)的* 和 ->兩種運(yùn)算符的重載函數(shù):
const T& operator*() const{return *_ptr;}
T& operator*(){return *_ptr;}
const T operator->() const{return _ptr;}
T operator->(){return _ptr;}
二、不帶引用計(jì)數(shù)的智能指針
int main(){
SmartPtr<int> ptr1(new int);
SmartPtr<int> ptr2(ptr1);
return 0;
}
以上代碼運(yùn)行時(shí),由于ptr2拷貝構(gòu)造時(shí)默認(rèn)是淺拷貝,會(huì)出現(xiàn)同一資源釋放兩次的錯(cuò)誤(釋放野指針),這里需要解決兩個(gè)問(wèn)題:
智能指針的淺拷貝
多個(gè)智能指針指向同一個(gè)資源的時(shí)候,怎么保證資源只釋放一次,而不是每個(gè)智能指針都釋放一次
不帶引用計(jì)數(shù)的智能指針主要包括
auto_ptr,scoped_ptr,unique_ptr
(1)auto_ptr源碼
auto_ptr(auto_ptr& _Right) noexcept : _Myptr(_Right.release()) {}
_Ty* release() noexcept {
_Ty* _Tmp = _Myptr;
_Myptr = nullptr;
return _Tmp;
}
使用auto_ptr
auto_ptr<int> ptr1(new int);
auto_ptr<int> ptr2(ptr1);
從源碼可以看到,auto_ptr底層先是將ptr1置空,然后將指向的資源再給ptr2, auto_ptr所做的就是使最后一個(gè)構(gòu)造的指針指向資源,以前的指針全都置空,如果再去訪問(wèn)以前的指針就是訪問(wèn)空指針了,這很危險(xiǎn)。所以一般不使用auto_ptr
(2)scoped_ptr
該智能指針底層私有化了拷貝構(gòu)造函數(shù)和operator=賦值函數(shù),
從根本上杜絕了智能指針淺拷貝的發(fā)生,所以scoped_ptr也是不能用在容器當(dāng)中的。
- 如果容器互相進(jìn)行拷貝或者賦值,就會(huì)引起
scoped_ptr對(duì)象的拷貝構(gòu)造和賦值,這是不允許的,代碼會(huì)提示編譯錯(cuò)誤。 auto_ptr和scoped_ptr這一點(diǎn)上的區(qū)別,有些資料上用所有權(quán)的概念來(lái)描述,道理是相同的。auto_ptr可以任意轉(zhuǎn)移資源的所有權(quán),而scoped_ptr不會(huì)轉(zhuǎn)移所有權(quán)(因?yàn)榭截悩?gòu)造和賦值被禁止了)
一般也不推薦使用scoped_ptr
(3)unique_ptr源碼
template <class _Dx2 = _Dx, enable_if_t<is_move_constructible_v<_Dx2>, int> = 0>
unique_ptr(unique_ptr&& _Right) noexcept
: _Mypair(_One_then_variadic_args_t{}, _STD forward<_Dx>(_Right.get_deleter()), _Right.release()) {}
unique_ptr(unique_ptr<_Ty2, _Dx2>&& _Right) noexcept
: _Mypair(_One_then_variadic_args_t{}, _STD forward<_Dx2>(_Right.get_deleter()), _Right.release()) {}
// 拷貝構(gòu)造或者賦值運(yùn)算符的時(shí)候,用于將以前的智能指針置空
pointer release() noexcept {
return _STD exchange(_Mypair._Myval2, nullptr);
}
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
從源碼可以看到,unique_ptr直接delete了拷貝構(gòu)造函數(shù)和operator=賦值重載函數(shù)
禁止用戶對(duì)unique_ptr進(jìn)行顯示的拷貝構(gòu)造和賦值,防止智能指針淺拷貝問(wèn)題的發(fā)生
但是unique_ptr提供了帶右值引用參數(shù)的拷貝構(gòu)造和賦值
即unique_ptr智能指針可以通過(guò)右值引用進(jìn)行拷貝構(gòu)造和賦值操作
unique_ptr<int> ptr1(new int);
unique_ptr<int> ptr2(std::move(ptr1));// 使用右值引用的拷貝構(gòu)造,由于執(zhí)行了release,ptr1已經(jīng)被置空
cout << (ptr1 == nullptr) << endl; // true
ptr2 = std::move(ptr1); // 使用右值引用的operator=賦值重載函數(shù)
cout << (ptr2 == nullptr) << endl; // true
用臨時(shí)對(duì)象構(gòu)造新的對(duì)象時(shí),也會(huì)調(diào)用帶右值引用參數(shù)的函數(shù)
unique_ptr<int> get_unique_ptr() {
unique_ptr<int> tmp(new int);
return tmp;
}
int main(){
unique_ptr<int> ptr = get_unique_ptr(); // 調(diào)用帶右值引用參數(shù)的拷貝構(gòu)造函數(shù),由tmp直接構(gòu)造ptr
return 0;
}
unique_ptr從名字就可以看出來(lái),最終也是只能有一個(gè)智能指針引用資源,其他智能指針全部置空
三、帶引用計(jì)數(shù)的智能指針
#include <iostream>
#include <memory>
using namespace std;
template<typename T>
class RefCnt {
public:
RefCnt(T* ptr = nullptr){
mcount = (mptr == nullptr) ? 0 : 1;
}
void addRef() {
mcount++;
}
int subRef() {
return --mcount;
}
private:
int mcount; // mptr指向某個(gè)資源的引用計(jì)數(shù),線程不安全。使用atomic_int線程安全
T* mptr; //指向智能指針內(nèi)部指向資源的指針,間接指向資源
};
template<typename T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr) :_ptr(ptr) {
cout << "SmartPtr()" << endl;
mpRefCnt = new RefCnt<int>(_ptr); // 用指向資源的指針初始化引用計(jì)數(shù)對(duì)象
}
~SmartPtr() {
if (0 == mpRefCnt->subRef()) {
cout << "釋放資源析構(gòu)~SmartPtr()" << endl;
delete _ptr;
}
else {
cout << "空析構(gòu)~SmartPtr()" << endl;
}
}
const T& operator*() const { return *_ptr; }
T& operator*() { return *_ptr; }
const T operator->() const { return _ptr; }
T operator->() { return _ptr; }
SmartPtr(const SmartPtr<T>& src) :_ptr(src._ptr), mpRefCnt(src.mpRefCnt) {
cout << "SmartPtr(const SmartPtr<T>& src)" << endl;
if (_ptr != nullptr) {
// 用于拷貝的對(duì)象已經(jīng)引用了資源
mpRefCnt->addRef();
}
}
SmartPtr<T>& operator=(const SmartPtr<T>& src) {
cout << "SmartPtr<T>& operator=" << endl;
if (this == &src) {
return *this;
}
// 當(dāng)前智能指針指向和src相同的資源,考慮是否釋放之前的資源
if (0 == mpRefCnt->subRef()) {
// 若之前指向的資源引用計(jì)數(shù)為1,釋放之前的資源
delete _ptr;
}
_ptr = src._ptr;
mpRefCnt = src.mpRefCnt;
mpRefCnt->addRef();
return *this;
}
private:
T* _ptr; // 指向資源的指針
RefCnt<T>* mpRefCnt; // 指向該資源引用計(jì)數(shù)的指針
};
int main(){
SmartPtr<int> ptr1(new int);
SmartPtr<int> ptr2(ptr1);
SmartPtr<int> ptr3;
ptr3 = ptr2;
*ptr2 = 100;
cout << *ptr2 << " " << *ptr3 << endl;
return 0;
}

四、shared_ptr 和 weak_ptr
- shared_ptr:強(qiáng)智能指針,可以改變資源的引用計(jì)數(shù)
- weak_ptr:弱智能指針,不可改變資源的引用計(jì)數(shù)
weak_ptr -> shared_ptr -> 資源
智能指針的交叉引用問(wèn)題
#include <iostream>
#include <memory>
using namespace std;
class B;
class A {
public:
A() {
cout << "A()" << endl;
}
~A() {
cout << "~A()" << endl;
}
shared_ptr<B> _ptrb;
};
class B {
public:
B() {
cout << "B()" << endl;
}
~B() {
cout << "~B()" << endl;
}
shared_ptr<A> _ptra;
};
int main() {
shared_ptr<A> pa(new A());
shared_ptr<B> pb(new B());
pa->_ptrb = pb;
pb->_ptra = pa;
cout << pa.use_count() << endl;
cout << pb.use_count() << endl;
return 0;
}
/*
A()
B()
2
2
*/

解決辦法: 定義對(duì)象時(shí)用shared_ptr,引用對(duì)象時(shí)用weak_ptr
class B;
class A {
public:
A() {
cout << "A()" << endl;
}
~A() {
cout << "~A()" << endl;
}
weak_ptr<B> _ptrb;
};
class B {
public:
B() {
cout << "B()" << endl;
}
~B() {
cout << "~B()" << endl;
}
weak_ptr<A> _ptra;
};
/*
A()
B()
1
1
~B()
~A()
*/
weak_ptr只是用于觀察資源,不能夠使用資源,并沒(méi)有實(shí)現(xiàn)operator*和operator->。可以使用weak_ptr對(duì)象的lock()方法返回shared_ptr對(duì)象,這個(gè)操作會(huì)增加資源的引用計(jì)數(shù)。
五、多線程訪問(wèn)共享對(duì)象的線程安全問(wèn)題
線程A和線程B訪問(wèn)一個(gè)共享對(duì)象,如果線程A已經(jīng)析構(gòu)這個(gè)對(duì)象
線程B又要調(diào)用該共享對(duì)象的成員方法,線程B再去訪問(wèn)該對(duì)象,就會(huì)發(fā)生不可預(yù)期的錯(cuò)誤
#include <iostream>
#include <memory>
#include <thread>
using namespace std;
class A {
public:
A() {
cout << "A()" << endl;
}
~A() {
cout << "~A()" << endl;
}
void test() {
cout << "test()" << endl;
}
};
void handler01(A* q) {
// 睡眠2s,使得主線程進(jìn)行delete
std::this_thread::sleep_for(std::chrono::seconds(2));
q->test();
}
int main() {
A* p = new A();
thread t1(handler01, p);
delete p;
t1.join();
return 0;
}

在多線程訪問(wèn)共享對(duì)象的時(shí)候,往往需要lock檢測(cè)一下對(duì)象是否存在。
開(kāi)啟一個(gè)新線程,并傳入共享對(duì)象的弱智能指針。
#include <iostream>
#include <memory>
#include <thread>
using namespace std;
class A {
public:
A() {
cout << "A()" << endl;
}
~A() {
cout << "~A()" << endl;
}
void test() {
cout << "test()" << endl;
}
};
void handler01(weak_ptr<A> pw) {
shared_ptr<A> ps = pw.lock();
// lock方法判定資源對(duì)象是否析構(gòu)
if (ps != nullptr) {
ps->test();
}else {
cout << "A對(duì)象已經(jīng)析構(gòu),無(wú)法訪問(wèn)" << endl;
}
}
int main() {
{
shared_ptr<A> p(new A());
//開(kāi)啟一個(gè)新線程,并傳入共享對(duì)象的弱智能指針
thread t1(handler01, weak_ptr<A>(p));
// 將子線程和主線程的關(guān)聯(lián)分離,也就是說(shuō)detach()后子線程在后臺(tái)獨(dú)立繼續(xù)運(yùn)行,
// 主線程無(wú)法再取得子線程的控制權(quán),即使主線程結(jié)束,子線程未執(zhí)行也不會(huì)結(jié)束。
t1.detach();
}
// 讓主線程等待,給時(shí)間子線程執(zhí)行,否則main函數(shù)最后會(huì)調(diào)用exit方法結(jié)束進(jìn)程
std::this_thread::sleep_for(std::chrono::seconds(2));
return 0;
}

六、自定義刪除器
通常我們使用智能指針管理的資源是堆內(nèi)存,當(dāng)智能指針出作用域的時(shí)候,
在其析構(gòu)函數(shù)中會(huì)delete釋放堆內(nèi)存資源,但是除了堆內(nèi)存資源,智能指針還可以管理其它資源,
比如打開(kāi)的文件,此時(shí)對(duì)于文件指針的關(guān)閉,就不能用delete了,
這時(shí)我們需要自定義智能指針釋放資源的方法。
template<typename T>
class ArrDeletor {
public:
// 對(duì)象刪除的時(shí)候需要調(diào)用對(duì)應(yīng)刪除器的()重載函數(shù)
void operator()(T* ptr) const {
cout << "ArrDeletor::operator()" << endl;
delete[] ptr;
}
};
template<typename T>
class FileDeletor {
public:
// 對(duì)象刪除的時(shí)候需要調(diào)用對(duì)應(yīng)刪除器的()重載函數(shù)
void operator()(T* fp) const {
cout << "FileDeletor::operator()" << endl;
fclose(fp);
}
};
int main() {
unique_ptr<int, ArrDeletor<int>> ptr1(new int[100]);
unique_ptr<FILE, FileDeletor<FILE>> ptr2(fopen("1.cpp", "w"));
// 使用lambda表達(dá)式
// function<返回值(參數(shù))>
// []叫做捕獲說(shuō)明符,表示一個(gè)lambda表達(dá)式的開(kāi)始。接下來(lái)是參數(shù)列表,即這個(gè)匿名的lambda函數(shù)的參數(shù)
unique_ptr<int, function<void(int*)>> ptr1(
new int[100],
[](int* p)->void {
cout << "call lambda release new int[]" << endl;
delete[] p;
}
);
unique_ptr<FILE, function<void(FILE*)>> ptr2(
fopen("1.cpp", "w"),
[](FILE* p)->void {
cout << "call lambda release fopen(\"1.cpp\", \"w\")" << endl;
fclose(p);
}
);
return 0;
}
到此這篇關(guān)于C++智能指針教程的文章就介紹到這了,更多相關(guān)C++智能指針內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C++實(shí)現(xiàn)LeetCode(74.搜索一個(gè)二維矩陣)
這篇文章主要介紹了C++實(shí)現(xiàn)LeetCode(74.搜索一個(gè)二維矩陣),本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-07-07
VS中scanf為何會(huì)報(bào)錯(cuò)詳解
在我們剛使用vs時(shí),在使用scanf函數(shù)時(shí)常會(huì)遇到報(bào)錯(cuò)提醒,下面這篇文章主要給大家介紹了關(guān)于VS中scanf為何會(huì)報(bào)錯(cuò)的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-02-02
Qt學(xué)習(xí)之QListWidget控件的使用教程詳解
這篇文章主要為大家詳細(xì)介紹了Qt中QListWidget控件的使用教程,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)Qt有一定的幫助,需要的可以參考一下2022-12-12
C++?RBTree紅黑樹(shù)的性質(zhì)與實(shí)現(xiàn)
紅黑樹(shù)是一種二叉搜索樹(shù),但在每個(gè)結(jié)點(diǎn)上增加一個(gè)存儲(chǔ)位表示結(jié)點(diǎn)的顏色,可以是Red或Black;通過(guò)對(duì)任何一條從根到葉子的路徑上各個(gè)結(jié)點(diǎn)著色方式的限制,紅黑樹(shù)確保沒(méi)有一條路徑會(huì)比其他路徑長(zhǎng)出倆倍,因而是平衡的2023-03-03
C++簡(jiǎn)明講解類型轉(zhuǎn)換的使用與作用
類型轉(zhuǎn)換(type?cast),是高級(jí)語(yǔ)言的一個(gè)基本語(yǔ)法。它被實(shí)現(xiàn)為一個(gè)特殊的運(yùn)算符,以小括號(hào)內(nèi)加上類型名來(lái)表示,接下來(lái)讓我們一起來(lái)詳細(xì)了解2022-04-04
C語(yǔ)言中find_package()的搜索路徑的實(shí)現(xiàn)
本文主要介紹了C語(yǔ)言中find_package()的搜索路徑的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-12-12
Qt使用QPainter實(shí)現(xiàn)自定義圓形進(jìn)度條
這篇文章主要介紹了Qt如何使用QPainter實(shí)現(xiàn)自定義圓形進(jìn)度條功能,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)Qt有一定的幫助,需要的可以參考一下2022-06-06

