C++定制刪除器與特殊類設(shè)計(餓漢和懶漢)
定制刪除器
我們在上一篇文章中講到了智能指針,相信大家都會有一個問題,智能指針該如何辨別我們的資源是用new int開辟的還是new int[]開辟的呢,要知道[]必須與delete[]匹配否則會有未知錯誤的,這個問題我們就交給定制刪除器來解決:
int main() { shared_ptr<int> sp1(new int[10]); shared_ptr<string> sp2(new string[10]); return 0; }
比如上面的代碼,一旦運行就會立即引發(fā)崩潰,這是因為shared_ptr默認(rèn)的釋放方式是delete而不是delete[]。
我們從文檔中可以看到shared_ptr有一個模板參數(shù)是del,這個參數(shù)其實就是定制刪除器,為了解決釋放資源的問題。
template <class T> struct DeleteArray { void operator()(const T* ptr) { delete[] ptr; cout << "delete[]" <<ptr<< endl; } }; int main() { shared_ptr<int> sp1(new int[10],DeleteArray<int>()); shared_ptr<string> sp2(new string[10],DeleteArray<string>()); return 0; }
我們可以看到定制刪除器非常簡單,實際上就是一個仿函數(shù),當(dāng)我們將這個仿函數(shù)傳給shared_ptr,shared_ptr會在它的構(gòu)造函數(shù)中接收到這個仿函數(shù),然后析構(gòu)的時候就直接調(diào)用這個仿函數(shù)了。下面我們看看運行結(jié)果:
可以看到是能成功釋放的,不再像之前那樣由于new和delete不匹配導(dǎo)致崩潰。當(dāng)然,這里既然是直接傳給構(gòu)造函數(shù)的,那么我們完全可以不用再寫仿函數(shù)了,直接用lambda會更加的方便,如下所示:
int main() { shared_ptr<int> sp1(new int[10], [](const int* ptr) { delete[] ptr; cout << "delete[] ptr :" << ptr << endl; }); shared_ptr<string> sp2(new string[10], [](const string* ptr) { delete[] ptr; cout << "delete[] ptr(string) :" << ptr << endl; }); return 0; }
可以看到這樣也是沒有問題的如果不是要打印演示的話會比第一種方式更加的簡潔,但是我們也說過lambda表達(dá)式的底層就是仿函數(shù)所以其實也差不了多少。當(dāng)然我們不僅可以釋放資源,還可以關(guān)閉文件:
shared_ptr<FILE> sp3(fopen("test", "w"), [](FILE* fp) { fclose(fp); });
下面我們也給自己的shared_ptr搞一個定制刪除器模板:
首先我們自己是不能像庫里面那樣直接將定制刪除器傳給構(gòu)造函數(shù)的,因為庫里面的shared_ptr有好幾個類,所以可以直接給構(gòu)造函數(shù)加一個模板來傳定制刪除器。那么我們該如何解決呢?其實很容易因為我們就一個shared_ptr類,所以只需要給這個類多一個模板參數(shù)就可以了。
namespace sxy { template <class T> struct Delete { void operator()(T* ptr) { delete ptr; } }; template <class T, class D = Delete<T>> class shared_ptr { public: //保存資源 shared_ptr(T* ptr = nullptr) :_ptr(ptr) , _pcount(new int(1)) , _pmtx(new mutex) { } //拷貝構(gòu)造 shared_ptr(const shared_ptr<T>& sp) :_ptr(sp._ptr) , _pcount(sp._pcount) , _pmtx(sp._pmtx) { _pmtx->lock(); ++(*_pcount); _pmtx->unlock(); } void Release() { bool flag = false; _pmtx->lock(); if (--(*_pcount) == 0) { _del(_ptr); delete _pcount; flag = true; } _pmtx->unlock(); if (flag) { delete _pmtx; } } //賦值重載 //.......... //釋放資源 ~shared_ptr() { Release(); } //像指針一樣 //.............. private: T* _ptr; int* _pcount; mutex* _pmtx; D _del; }; }
我們這里默認(rèn)的是delete,當(dāng)然也可以自己模板特化一個delete[],我們就不再演示了。下面我們先運行起來:
沒有問題,我們再自己傳一個[]的試一下:
int main() { sxy::shared_ptr<int,DeleteArray<int>> sp1(new int[10]); sxy::shared_ptr<string,DeleteArray<string>> sp2(new string[10]); return 0; }
可以看到運行起來也是沒有問題的,但是我們實現(xiàn)的定制刪除器是不可以傳lambda表達(dá)式的,這是因為lambda表達(dá)式無法作為一個參數(shù)類型。
注意:unique_ptr也和我們自己設(shè)計的刪除器一樣,都是不可以用lambda表達(dá)式的,這里大家可以自行去驗證。
一、設(shè)計一個只能在堆上(或棧上)創(chuàng)建的類
我們在設(shè)計只能在堆上創(chuàng)建的類之前,先設(shè)計一個不能被拷貝的類,因為后面的類都會用到不能被拷貝的特點。
拷貝只會放生在兩個場景中:拷貝構(gòu)造函數(shù)以及賦值運算符重載,因此 想要讓一個類禁止拷貝, 只需讓該類不能調(diào)用拷貝構(gòu)造函數(shù)以及賦值運算符重載即可。 C++98 將拷貝構(gòu)造函數(shù)與賦值運算符重載只聲明不定義,并且將其訪問權(quán)限設(shè)置為私有即可。
class BanCopy { // ... private: BanCopy(const BanCopy& bc); BanCopy& operator=(const BanCopy& bc); //... };
原因:
1. 設(shè)置成私有:如果只聲明沒有設(shè)置成 private ,用戶自己如果在類外定義了,就可以不 能禁止拷貝了
2. 只聲明不定義:不定義是因為該函數(shù)根本不會調(diào)用,定義了其實也沒有什么意義,不寫 反而還簡單,而且如果定義了就不會防止成員函數(shù)內(nèi)部拷貝了。 C++11引入了delete關(guān)鍵字要禁用某個函數(shù)就更加的方便了:
C++11 擴(kuò)展 delete 的用法, delete 除了釋放 new 申請的資源外,如果在默認(rèn)成員函數(shù)后跟上 =delete ,表示讓編譯器刪除掉該默認(rèn)成員函數(shù)。
class BanCopy { // ... private: BanCopy(const BanCopy& bc) = delete; BanCopy& operator=(const BanCopy& bc) = delete; //... };
下面我們思考如何實現(xiàn)只能在堆上創(chuàng)建的類,首先如果只能在堆上創(chuàng)建那么我們肯定是不能直接用構(gòu)造函數(shù)創(chuàng)建對象的,所以要把構(gòu)造函數(shù)私有,其次如果將一個堆上的對象拷貝給棧上的對象也不符合要求,所以拷貝構(gòu)造肯定也要禁掉,下面我們實現(xiàn)一下:
class HeapOnly { public: static HeapOnly* CreatObj() { return new HeapOnly; } void print() { cout << "print()" << endl; } private: HeapOnly() {} HeapOnly(const HeapOnly&) = delete; }; int main() { HeapOnly* hp = HeapOnly::CreatObj(); hp->print(); return 0; }
可以看到我們將構(gòu)造函數(shù)設(shè)為私有,這樣就不會有隨意的對象被創(chuàng)建,只能通過我們的creat函數(shù)接收在堆上開辟的對象,注意我們的Creat接口一定是靜態(tài)的,因為我們將構(gòu)造封掉了沒有這個類的對象,如果不設(shè)置為靜態(tài)的那么誰來調(diào)用這個接口呢,其次我們還有禁掉拷貝構(gòu)造,為了防止下面這種情況:
HeapOnly ht(*hp);
對于賦值重載來講我們是沒必要考慮的,因為賦值重載一定是針對于已經(jīng)創(chuàng)建過的對象,我們將構(gòu)造函數(shù)和拷貝構(gòu)造禁掉就避免了棧對象的出現(xiàn),被創(chuàng)建的一定是堆上的,那么堆上的對象賦值還是在堆上。
當(dāng)然我們還有第二種只在堆上創(chuàng)建的類的思想:
class HeapOnly { public: HeapOnly() {} void Destroy() { this->~HeapOnly(); } private: ~HeapOnly() {} HeapOnly(const HeapOnly&) = delete; };
我們將析構(gòu)函數(shù)設(shè)為私有,這樣只要是棧上創(chuàng)建的對象都會編譯報錯,因為無法調(diào)用其析構(gòu)函數(shù),但是指針卻可以正常的開辟空間,當(dāng)我們將析構(gòu)函數(shù)設(shè)為私有后那么該如何釋放空間呢?只需要加一個成員函數(shù),讓這個成員函數(shù)調(diào)用類內(nèi)的析構(gòu)就可以了,這里不能直接調(diào)用析構(gòu)需要用this指針指向一下否則會有編譯錯誤。
可以看到是沒有問題的。
下面我們實現(xiàn)只在棧上創(chuàng)建的類:
只在棧上創(chuàng)建我們只需要禁掉operator new和operator delete以及禁止static變量即可。
class StackOnly { public: static StackOnly CreatObj() { return StackOnly(); } void print() { cout << "print()" << endl; } private: StackOnly() {} };
一旦我們將構(gòu)造函數(shù)禁掉,那么static變量和stackOnly* = new都無法創(chuàng)建。當(dāng)然這個類是封不死的對于下面這個情況:
static StackOnly st = StackOnly::CreatObj();
這也是這個類的缺陷,當(dāng)然我們也可以直接禁掉operator new ,但是沒必要因為我們禁掉析構(gòu)后就無法調(diào)用new,畢竟new是需要構(gòu)造函數(shù)的。
二、單例模式
設(shè)計模式:
設(shè)計模式( Design Pattern )是一套 被反復(fù)使用、多數(shù)人知曉的、經(jīng)過分類的、代碼設(shè)計經(jīng)驗的 總結(jié) 。
為什么會產(chǎn)生設(shè)計模式這樣的東西呢?就像人類歷史發(fā)展會產(chǎn)生兵法。最開始部落之間打 仗時都是人拼人的對砍。后來春秋戰(zhàn)國時期,七國之間經(jīng)常打仗,就發(fā)現(xiàn)打仗也是有 套路 的,后 來孫子就總結(jié)出了《孫子兵法》。孫子兵法也是類似。
使用設(shè)計模式的目的:為了代碼可重用性、讓代碼更容易被他人理解、保證代碼可靠性。 設(shè)計模 式使代碼編寫真正工程化;設(shè)計模式是軟件工程的基石脈絡(luò),如同大廈的結(jié)構(gòu)一樣。
單例模式: 一個類只能創(chuàng)建一個對象,即單例模式,該模式可以保證系統(tǒng)中該類只有一個實例,并提供一個 訪問它的全局訪問點,該實例被所有程序模塊共享 。比如在某個服務(wù)器程序中,該服務(wù)器的配置 信息存放在一個文件中,這些配置數(shù)據(jù)由一個單例對象統(tǒng)一讀取,然后服務(wù)進(jìn)程中的其他對象再 通過這個單例對象獲取這些配置信息,這種方式簡化了在復(fù)雜環(huán)境下的配置管理。
1.餓漢模式
我們先寫出代碼然后進(jìn)行講解:
#include <map> class InfoSingleton { public: static InfoSingleton& GetInstance() { return _sin; } void insert(string name,int salary) { _info[name] = salary; } void print() { for (auto& e : _info) { cout << e.first << " : " << e.second << endl; } cout << endl; } private: InfoSingleton() {} InfoSingleton(const InfoSingleton&) = delete; InfoSingleton& operator=(const InfoSingleton&) = delete; map<string, int> _info; static InfoSingleton _sin; };
首先單例模式是只能創(chuàng)建一個對象,所以我們必須將構(gòu)造函數(shù)設(shè)為私有,上面的場景是一個工資管理表用map存放,既然只有一個類我們就直接加了一個私有靜態(tài)成員,這個成員就是我們唯一使用的類,注意我們還需要將這個靜態(tài)成員在類外初始化:
InfoSingleton InfoSingleton::_sin;
然后我們設(shè)計一個getinstance的接口可以返回這個私有對象,注意返回的是引用因為只有一個類不能產(chǎn)生拷貝。insert和print接口是為了演示所以設(shè)計出來了,然后我們要將拷貝構(gòu)造和賦值都禁掉,還是那個原因只有一個對象。接下來我們運行起來看看:
int main() { InfoSingleton::GetInstance().insert("張三", 20); InfoSingleton& sl = InfoSingleton::GetInstance(); sl.insert("李四", 200); sl.insert("王五", 600); sl.insert("趙六", 3100); sl.print(); return 0; }
上面的代碼中我們給出了兩種拿到這個類的唯一對象的方法,一種是利用靜態(tài)成員函數(shù)的特性直接使用,另一個是引用接收,下面我們看看結(jié)果:
結(jié)果也是沒有問題的,下面我們說一說餓漢模式的缺點:
首先因為靜態(tài)成員變量的原因,我們的類會在main函數(shù)開始之前就創(chuàng)建對象,這么做就會有一些缺點,比如:單例對象初始化的時候數(shù)據(jù)太多,會導(dǎo)致啟動慢。2.多個單例類如果有初始化依賴關(guān)系的話,那么餓漢模式是無法保證哪個類先初始化的(比如:A和B都是單例類,我們要求B先初始化然后再初始化A,但是餓漢模式是無法控制順序的)。
2.懶漢模式
懶漢模式和餓漢模式的區(qū)別在于,懶漢模式是等需要用到對象的時候再創(chuàng)建,下面我們寫一下代碼:
class InfoSingleton { public: static InfoSingleton* GetInstance() { //第一次獲取單例對象的時候創(chuàng)建對象 if (_psin == nullptr) { return new InfoSingleton; } return _psin; } void insert(string name, int salary) { _info[name] = salary; } void print() { for (auto& e : _info) { cout << e.first << " : " << e.second << endl; } cout << endl; } private: InfoSingleton() {} InfoSingleton(const InfoSingleton&) = delete; InfoSingleton& operator=(const InfoSingleton&) = delete; map<string, int> _info; static InfoSingleton* _psin; }; InfoSingleton* InfoSingleton::_psin = nullptr;
我們可以看到餓漢模式和懶漢模式代碼上的區(qū)別是:
餓漢模式用的是靜態(tài)成員變量,懶漢模式用的靜態(tài)成員變量指針,并且懶漢模式只有第一次獲取單列對象的時候才創(chuàng)建對象,這就解決了剛剛餓漢模式啟動的時候初始化數(shù)據(jù)太多導(dǎo)致啟動慢的問題,并且懶漢模式如果在有依賴關(guān)系的情況下是可以控制先初始化某個類再初始化某個類的。
不知道大家有沒有發(fā)現(xiàn),我們現(xiàn)在寫的懶漢模式是有一個很明顯的問題的,那就是如果是多線程的情況下第一次獲取單例對象的時候有可能多new了對象,面對這個情況我們可以直接加鎖解決問題。
//懶漢模式 class InfoSingleton { public: static InfoSingleton* GetInstance() { //第一次獲取單例對象的時候創(chuàng)建對象 //lock_guard<mutex> lock(_mtx); if (_psin == nullptr) { lock_guard<mutex> lock(_mtx); if (_psin == nullptr) { return new InfoSingleton; } } return _psin; } private: InfoSingleton() {} InfoSingleton(const InfoSingleton&) = delete; InfoSingleton& operator=(const InfoSingleton&) = delete; map<string, int> _info; static InfoSingleton* _psin; static mutex _mtx; }; InfoSingleton* InfoSingleton::_psin = nullptr; mutex InfoSingleton::_mtx;
首先我們定義鎖的時候必須是靜態(tài)的,這是因為getinstance這個函數(shù)就是靜態(tài)的,這個函數(shù)里是無法調(diào)用普通成員函數(shù)的,只能調(diào)用靜態(tài)成員函數(shù),并且我們的鎖只需要保護(hù)第一次來判斷是否需要創(chuàng)建對象的情況,如果寫成下面這樣的代碼就會造成資源浪費:
static InfoSingleton* GetInstance() { lock_guard<mutex> lock(_mtx); if (_psin == nullptr) { return new InfoSingleton; } return _psin; }
為什么會造成資源浪費呢?因為第一次創(chuàng)建后后面的幾次我們只需要返回這個對象的指針,這個時候是不需要加鎖的,而像上面的代碼我們即使已經(jīng)創(chuàng)建過一次對象了進(jìn)來后還是要加鎖解鎖消耗資源,這就造成了資源的浪費,所以我們多做一個判斷就能解決這個問題。注意:餓漢模式是沒有這個線程安全的問題的,因為餓漢模式在main函數(shù)之前就創(chuàng)建好對象了,main函數(shù)之前是不會有兩個線程去創(chuàng)建對象的。
當(dāng)然一般情況下我們的單例對象是不考慮釋放的,不過如果需要釋放該如何釋放呢?其實和創(chuàng)建的時候一樣,寫一個靜態(tài)的接口即可,代碼如下:
static void DelInstance() { lock_guard<mutex> lock(_mtx); if (_psin != nullptr) { delete _psin; _psin = nullptr; } }
當(dāng)然也有人想到如果有些人就是忘記手動釋放資源那就麻煩了,所以又出現(xiàn)了一個自動的釋放資源的方法:
class GC { public: ~GC() { if (_psin) { cout << "~GC()" << endl; DelInstance(); } } };
GC是這個類對象的內(nèi)部類,這個類的析構(gòu)函數(shù)就是如果我們沒有手動釋放單例類的資源,那么GC這個對象出了作用域會自動幫我們進(jìn)行銷毀。
下面我們將程序運行起來看看能否釋放:
上面是我們自己手動釋放后,GC就沒有幫我們釋放,下面我們再看看自己不手動釋放GC是否會幫我們釋放:
所以我們總結(jié)一下,GC有下面兩個好處:
可以手動調(diào)用主動回收,也可以讓它在程序結(jié)束時自動回收。
當(dāng)然下面還有一種更簡單的懶漢模式的實現(xiàn)方法:
class InfoSingleton { public: static InfoSingleton& GetInstance() { static InfoSingleton sin; return sin; } void insert(string name, int salary) { _info[name] = salary; } void print() { for (auto& e : _info) { cout << e.first << " : " << e.second << endl; } cout << endl; } private: InfoSingleton() {} InfoSingleton(const InfoSingleton&) = delete; InfoSingleton& operator=(const InfoSingleton&) = delete; map<string, int> _info; };
為什么這種方法也被稱為懶漢模式呢?這是因為getinstance這個函數(shù)內(nèi)部定義的靜態(tài)變量sin是一個局部靜態(tài)變量,一個局部的靜態(tài)成員變量在初始化的時候是在main函數(shù)后初始化,所以和我們new的效果一樣,并且還不用加鎖保護(hù)。這個方式就利用了一個知識點:靜態(tài)的局部變量是在main函數(shù)之后才創(chuàng)建初始化的。但是對于這樣的寫法有一個點需要注意:
在C++11之前,上面紅框的部分是不能保證sin的初始化是線程安全的,在C++11之后,可以保證sin的初始化是線程安全的。
總結(jié)
設(shè)計模式中最重要的就是單例模式了,這個模式在面試中是高頻考點大家一定要學(xué)會。
到此這篇關(guān)于C++定制刪除器與特殊類設(shè)計(餓漢和懶漢)的文章就介紹到這了,更多相關(guān)C++定制刪除器與特殊類內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

詳談C++何時需要定義賦值/復(fù)制構(gòu)造函數(shù)

C++實現(xiàn)學(xué)校人員管理系統(tǒng)

C語言入門篇--sizeof與strlen基礎(chǔ)理論