C++定制刪除器與特殊類設(shè)計(jì)(餓漢和懶漢)
定制刪除器
我們?cè)谏弦黄恼轮兄v到了智能指針,相信大家都會(huì)有一個(gè)問(wèn)題,智能指針該如何辨別我們的資源是用new int開(kāi)辟的還是new int[]開(kāi)辟的呢,要知道[]必須與delete[]匹配否則會(huì)有未知錯(cuò)誤的,這個(gè)問(wèn)題我們就交給定制刪除器來(lái)解決:
int main()
{
shared_ptr<int> sp1(new int[10]);
shared_ptr<string> sp2(new string[10]);
return 0;
}比如上面的代碼,一旦運(yùn)行就會(huì)立即引發(fā)崩潰,這是因?yàn)閟hared_ptr默認(rèn)的釋放方式是delete而不是delete[]。

我們從文檔中可以看到shared_ptr有一個(gè)模板參數(shù)是del,這個(gè)參數(shù)其實(shí)就是定制刪除器,為了解決釋放資源的問(wèn)題。
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;
}我們可以看到定制刪除器非常簡(jiǎn)單,實(shí)際上就是一個(gè)仿函數(shù),當(dāng)我們將這個(gè)仿函數(shù)傳給shared_ptr,shared_ptr會(huì)在它的構(gòu)造函數(shù)中接收到這個(gè)仿函數(shù),然后析構(gòu)的時(shí)候就直接調(diào)用這個(gè)仿函數(shù)了。下面我們看看運(yùn)行結(jié)果:

可以看到是能成功釋放的,不再像之前那樣由于new和delete不匹配導(dǎo)致崩潰。當(dāng)然,這里既然是直接傳給構(gòu)造函數(shù)的,那么我們完全可以不用再寫仿函數(shù)了,直接用lambda會(huì)更加的方便,如下所示:
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;
}
可以看到這樣也是沒(méi)有問(wèn)題的如果不是要打印演示的話會(huì)比第一種方式更加的簡(jiǎn)潔,但是我們也說(shuō)過(guò)lambda表達(dá)式的底層就是仿函數(shù)所以其實(shí)也差不了多少。當(dāng)然我們不僅可以釋放資源,還可以關(guān)閉文件:
shared_ptr<FILE> sp3(fopen("test", "w"), [](FILE* fp) { fclose(fp); });
下面我們也給自己的shared_ptr搞一個(gè)定制刪除器模板:
首先我們自己是不能像庫(kù)里面那樣直接將定制刪除器傳給構(gòu)造函數(shù)的,因?yàn)閹?kù)里面的shared_ptr有好幾個(gè)類,所以可以直接給構(gòu)造函數(shù)加一個(gè)模板來(lái)傳定制刪除器。那么我們?cè)撊绾谓鉀Q呢?其實(shí)很容易因?yàn)槲覀兙鸵粋€(gè)shared_ptr類,所以只需要給這個(gè)類多一個(gè)模板參數(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)然也可以自己模板特化一個(gè)delete[],我們就不再演示了。下面我們先運(yùn)行起來(lái):

沒(méi)有問(wèn)題,我們?cè)僮约簜饕粋€(gè)[]的試一下:
int main()
{
sxy::shared_ptr<int,DeleteArray<int>> sp1(new int[10]);
sxy::shared_ptr<string,DeleteArray<string>> sp2(new string[10]);
return 0;
}
可以看到運(yùn)行起來(lái)也是沒(méi)有問(wèn)題的,但是我們實(shí)現(xiàn)的定制刪除器是不可以傳lambda表達(dá)式的,這是因?yàn)閘ambda表達(dá)式無(wú)法作為一個(gè)參數(shù)類型。
注意:unique_ptr也和我們自己設(shè)計(jì)的刪除器一樣,都是不可以用lambda表達(dá)式的,這里大家可以自行去驗(yàn)證。
一、設(shè)計(jì)一個(gè)只能在堆上(或棧上)創(chuàng)建的類
我們?cè)谠O(shè)計(jì)只能在堆上創(chuàng)建的類之前,先設(shè)計(jì)一個(gè)不能被拷貝的類,因?yàn)楹竺娴念惗紩?huì)用到不能被拷貝的特點(diǎn)。
拷貝只會(huì)放生在兩個(gè)場(chǎng)景中:拷貝構(gòu)造函數(shù)以及賦值運(yùn)算符重載,因此 想要讓一個(gè)類禁止拷貝, 只需讓該類不能調(diào)用拷貝構(gòu)造函數(shù)以及賦值運(yùn)算符重載即可。 C++98 將拷貝構(gòu)造函數(shù)與賦值運(yùn)算符重載只聲明不定義,并且將其訪問(wèn)權(quán)限設(shè)置為私有即可。
class BanCopy
{
// ...
private:
BanCopy(const BanCopy& bc);
BanCopy& operator=(const BanCopy& bc);
//...
};原因:
1. 設(shè)置成私有:如果只聲明沒(méi)有設(shè)置成 private ,用戶自己如果在類外定義了,就可以不 能禁止拷貝了
2. 只聲明不定義:不定義是因?yàn)樵摵瘮?shù)根本不會(huì)調(diào)用,定義了其實(shí)也沒(méi)有什么意義,不寫 反而還簡(jiǎn)單,而且如果定義了就不會(huì)防止成員函數(shù)內(nèi)部拷貝了。 C++11引入了delete關(guān)鍵字要禁用某個(gè)函數(shù)就更加的方便了:
C++11 擴(kuò)展 delete 的用法, delete 除了釋放 new 申請(qǐng)的資源外,如果在默認(rèn)成員函數(shù)后跟上 =delete ,表示讓編譯器刪除掉該默認(rèn)成員函數(shù)。
class BanCopy
{
// ...
private:
BanCopy(const BanCopy& bc) = delete;
BanCopy& operator=(const BanCopy& bc) = delete;
//...
};下面我們思考如何實(shí)現(xiàn)只能在堆上創(chuàng)建的類,首先如果只能在堆上創(chuàng)建那么我們肯定是不能直接用構(gòu)造函數(shù)創(chuàng)建對(duì)象的,所以要把構(gòu)造函數(shù)私有,其次如果將一個(gè)堆上的對(duì)象拷貝給棧上的對(duì)象也不符合要求,所以拷貝構(gòu)造肯定也要禁掉,下面我們實(shí)現(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è)為私有,這樣就不會(huì)有隨意的對(duì)象被創(chuàng)建,只能通過(guò)我們的creat函數(shù)接收在堆上開(kāi)辟的對(duì)象,注意我們的Creat接口一定是靜態(tài)的,因?yàn)槲覀儗?gòu)造封掉了沒(méi)有這個(gè)類的對(duì)象,如果不設(shè)置為靜態(tài)的那么誰(shuí)來(lái)調(diào)用這個(gè)接口呢,其次我們還有禁掉拷貝構(gòu)造,為了防止下面這種情況:
HeapOnly ht(*hp);
對(duì)于賦值重載來(lái)講我們是沒(méi)必要考慮的,因?yàn)橘x值重載一定是針對(duì)于已經(jīng)創(chuàng)建過(guò)的對(duì)象,我們將構(gòu)造函數(shù)和拷貝構(gòu)造禁掉就避免了棧對(duì)象的出現(xiàn),被創(chuàng)建的一定是堆上的,那么堆上的對(duì)象賦值還是在堆上。
當(dāng)然我們還有第二種只在堆上創(chuàng)建的類的思想:
class HeapOnly
{
public:
HeapOnly()
{}
void Destroy()
{
this->~HeapOnly();
}
private:
~HeapOnly()
{}
HeapOnly(const HeapOnly&) = delete;
};我們將析構(gòu)函數(shù)設(shè)為私有,這樣只要是棧上創(chuàng)建的對(duì)象都會(huì)編譯報(bào)錯(cuò),因?yàn)闊o(wú)法調(diào)用其析構(gòu)函數(shù),但是指針卻可以正常的開(kāi)辟空間,當(dāng)我們將析構(gòu)函數(shù)設(shè)為私有后那么該如何釋放空間呢?只需要加一個(gè)成員函數(shù),讓這個(gè)成員函數(shù)調(diào)用類內(nèi)的析構(gòu)就可以了,這里不能直接調(diào)用析構(gòu)需要用this指針指向一下否則會(huì)有編譯錯(cuò)誤。

可以看到是沒(méi)有問(wèn)題的。
下面我們實(shí)現(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都無(wú)法創(chuàng)建。當(dāng)然這個(gè)類是封不死的對(duì)于下面這個(gè)情況:
static StackOnly st = StackOnly::CreatObj();
這也是這個(gè)類的缺陷,當(dāng)然我們也可以直接禁掉operator new ,但是沒(méi)必要因?yàn)槲覀兘粑鰳?gòu)后就無(wú)法調(diào)用new,畢竟new是需要構(gòu)造函數(shù)的。
二、單例模式
設(shè)計(jì)模式:
設(shè)計(jì)模式( Design Pattern )是一套 被反復(fù)使用、多數(shù)人知曉的、經(jīng)過(guò)分類的、代碼設(shè)計(jì)經(jīng)驗(yàn)的 總結(jié) 。
為什么會(huì)產(chǎn)生設(shè)計(jì)模式這樣的東西呢?就像人類歷史發(fā)展會(huì)產(chǎn)生兵法。最開(kāi)始部落之間打 仗時(shí)都是人拼人的對(duì)砍。后來(lái)春秋戰(zhàn)國(guó)時(shí)期,七國(guó)之間經(jīng)常打仗,就發(fā)現(xiàn)打仗也是有 套路 的,后 來(lái)孫子就總結(jié)出了《孫子兵法》。孫子兵法也是類似。
使用設(shè)計(jì)模式的目的:為了代碼可重用性、讓代碼更容易被他人理解、保證代碼可靠性。 設(shè)計(jì)模 式使代碼編寫真正工程化;設(shè)計(jì)模式是軟件工程的基石脈絡(luò),如同大廈的結(jié)構(gòu)一樣。
單例模式: 一個(gè)類只能創(chuàng)建一個(gè)對(duì)象,即單例模式,該模式可以保證系統(tǒng)中該類只有一個(gè)實(shí)例,并提供一個(gè) 訪問(wèn)它的全局訪問(wèn)點(diǎn),該實(shí)例被所有程序模塊共享 。比如在某個(gè)服務(wù)器程序中,該服務(wù)器的配置 信息存放在一個(gè)文件中,這些配置數(shù)據(jù)由一個(gè)單例對(duì)象統(tǒng)一讀取,然后服務(wù)進(jìn)程中的其他對(duì)象再 通過(guò)這個(gè)單例對(duì)象獲取這些配置信息,這種方式簡(jiǎ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è)對(duì)象,所以我們必須將構(gòu)造函數(shù)設(shè)為私有,上面的場(chǎng)景是一個(gè)工資管理表用map存放,既然只有一個(gè)類我們就直接加了一個(gè)私有靜態(tài)成員,這個(gè)成員就是我們唯一使用的類,注意我們還需要將這個(gè)靜態(tài)成員在類外初始化:
InfoSingleton InfoSingleton::_sin;
然后我們?cè)O(shè)計(jì)一個(gè)getinstance的接口可以返回這個(gè)私有對(duì)象,注意返回的是引用因?yàn)橹挥幸粋€(gè)類不能產(chǎn)生拷貝。insert和print接口是為了演示所以設(shè)計(jì)出來(lái)了,然后我們要將拷貝構(gòu)造和賦值都禁掉,還是那個(gè)原因只有一個(gè)對(duì)象。接下來(lái)我們運(yùn)行起來(lái)看看:
int main()
{
InfoSingleton::GetInstance().insert("張三", 20);
InfoSingleton& sl = InfoSingleton::GetInstance();
sl.insert("李四", 200);
sl.insert("王五", 600);
sl.insert("趙六", 3100);
sl.print();
return 0;
}上面的代碼中我們給出了兩種拿到這個(gè)類的唯一對(duì)象的方法,一種是利用靜態(tài)成員函數(shù)的特性直接使用,另一個(gè)是引用接收,下面我們看看結(jié)果:

結(jié)果也是沒(méi)有問(wèn)題的,下面我們說(shuō)一說(shuō)餓漢模式的缺點(diǎn):
首先因?yàn)殪o態(tài)成員變量的原因,我們的類會(huì)在main函數(shù)開(kāi)始之前就創(chuàng)建對(duì)象,這么做就會(huì)有一些缺點(diǎn),比如:?jiǎn)卫龑?duì)象初始化的時(shí)候數(shù)據(jù)太多,會(huì)導(dǎo)致啟動(dòng)慢。2.多個(gè)單例類如果有初始化依賴關(guān)系的話,那么餓漢模式是無(wú)法保證哪個(gè)類先初始化的(比如:A和B都是單例類,我們要求B先初始化然后再初始化A,但是餓漢模式是無(wú)法控制順序的)。
2.懶漢模式
懶漢模式和餓漢模式的區(qū)別在于,懶漢模式是等需要用到對(duì)象的時(shí)候再創(chuàng)建,下面我們寫一下代碼:
class InfoSingleton
{
public:
static InfoSingleton* GetInstance()
{
//第一次獲取單例對(duì)象的時(shí)候創(chuàng)建對(duì)象
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)成員變量指針,并且懶漢模式只有第一次獲取單列對(duì)象的時(shí)候才創(chuàng)建對(duì)象,這就解決了剛剛餓漢模式啟動(dòng)的時(shí)候初始化數(shù)據(jù)太多導(dǎo)致啟動(dòng)慢的問(wèn)題,并且懶漢模式如果在有依賴關(guān)系的情況下是可以控制先初始化某個(gè)類再初始化某個(gè)類的。
不知道大家有沒(méi)有發(fā)現(xiàn),我們現(xiàn)在寫的懶漢模式是有一個(gè)很明顯的問(wèn)題的,那就是如果是多線程的情況下第一次獲取單例對(duì)象的時(shí)候有可能多new了對(duì)象,面對(duì)這個(gè)情況我們可以直接加鎖解決問(wèn)題。
//懶漢模式
class InfoSingleton
{
public:
static InfoSingleton* GetInstance()
{
//第一次獲取單例對(duì)象的時(shí)候創(chuàng)建對(duì)象
//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;首先我們定義鎖的時(shí)候必須是靜態(tài)的,這是因?yàn)間etinstance這個(gè)函數(shù)就是靜態(tài)的,這個(gè)函數(shù)里是無(wú)法調(diào)用普通成員函數(shù)的,只能調(diào)用靜態(tài)成員函數(shù),并且我們的鎖只需要保護(hù)第一次來(lái)判斷是否需要?jiǎng)?chuàng)建對(duì)象的情況,如果寫成下面這樣的代碼就會(huì)造成資源浪費(fèi):
static InfoSingleton* GetInstance()
{
lock_guard<mutex> lock(_mtx);
if (_psin == nullptr)
{
return new InfoSingleton;
}
return _psin;
}為什么會(huì)造成資源浪費(fèi)呢?因?yàn)榈谝淮蝿?chuàng)建后后面的幾次我們只需要返回這個(gè)對(duì)象的指針,這個(gè)時(shí)候是不需要加鎖的,而像上面的代碼我們即使已經(jīng)創(chuàng)建過(guò)一次對(duì)象了進(jìn)來(lái)后還是要加鎖解鎖消耗資源,這就造成了資源的浪費(fèi),所以我們多做一個(gè)判斷就能解決這個(gè)問(wèn)題。注意:餓漢模式是沒(méi)有這個(gè)線程安全的問(wèn)題的,因?yàn)轲I漢模式在main函數(shù)之前就創(chuàng)建好對(duì)象了,main函數(shù)之前是不會(huì)有兩個(gè)線程去創(chuàng)建對(duì)象的。
當(dāng)然一般情況下我們的單例對(duì)象是不考慮釋放的,不過(guò)如果需要釋放該如何釋放呢?其實(shí)和創(chuàng)建的時(shí)候一樣,寫一個(gè)靜態(tài)的接口即可,代碼如下:
static void DelInstance()
{
lock_guard<mutex> lock(_mtx);
if (_psin != nullptr)
{
delete _psin;
_psin = nullptr;
}
}當(dāng)然也有人想到如果有些人就是忘記手動(dòng)釋放資源那就麻煩了,所以又出現(xiàn)了一個(gè)自動(dòng)的釋放資源的方法:
class GC
{
public:
~GC()
{
if (_psin)
{
cout << "~GC()" << endl;
DelInstance();
}
}
};GC是這個(gè)類對(duì)象的內(nèi)部類,這個(gè)類的析構(gòu)函數(shù)就是如果我們沒(méi)有手動(dòng)釋放單例類的資源,那么GC這個(gè)對(duì)象出了作用域會(huì)自動(dòng)幫我們進(jìn)行銷毀。

下面我們將程序運(yùn)行起來(lái)看看能否釋放:

上面是我們自己手動(dòng)釋放后,GC就沒(méi)有幫我們釋放,下面我們?cè)倏纯醋约翰皇謩?dòng)釋放GC是否會(huì)幫我們釋放:

所以我們總結(jié)一下,GC有下面兩個(gè)好處:
可以手動(dòng)調(diào)用主動(dòng)回收,也可以讓它在程序結(jié)束時(shí)自動(dòng)回收。
當(dāng)然下面還有一種更簡(jiǎn)單的懶漢模式的實(shí)現(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;
};為什么這種方法也被稱為懶漢模式呢?這是因?yàn)間etinstance這個(gè)函數(shù)內(nèi)部定義的靜態(tài)變量sin是一個(gè)局部靜態(tài)變量,一個(gè)局部的靜態(tài)成員變量在初始化的時(shí)候是在main函數(shù)后初始化,所以和我們new的效果一樣,并且還不用加鎖保護(hù)。這個(gè)方式就利用了一個(gè)知識(shí)點(diǎn):靜態(tài)的局部變量是在main函數(shù)之后才創(chuàng)建初始化的。但是對(duì)于這樣的寫法有一個(gè)點(diǎn)需要注意:

在C++11之前,上面紅框的部分是不能保證sin的初始化是線程安全的,在C++11之后,可以保證sin的初始化是線程安全的。
總結(jié)
設(shè)計(jì)模式中最重要的就是單例模式了,這個(gè)模式在面試中是高頻考點(diǎn)大家一定要學(xué)會(huì)。
到此這篇關(guān)于C++定制刪除器與特殊類設(shè)計(jì)(餓漢和懶漢)的文章就介紹到這了,更多相關(guān)C++定制刪除器與特殊類內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C++實(shí)現(xiàn)順序表的常用操作(插入刪出查找輸出)
實(shí)現(xiàn)順序表的插入,刪除,查找,輸出操作在C語(yǔ)言中經(jīng)常用到。下面小編給大家整理實(shí)現(xiàn)代碼,一起看下吧2016-08-08
詳談C++何時(shí)需要定義賦值/復(fù)制構(gòu)造函數(shù)
C++實(shí)現(xiàn)學(xué)校人員管理系統(tǒng)
C語(yǔ)言入門篇--sizeof與strlen基礎(chǔ)理論
OpenCV實(shí)現(xiàn)二值圖像的邊緣光滑處理

