網(wǎng)站開發(fā)難嗎200891
一、請(qǐng)?jiān)O(shè)計(jì)一個(gè)類,不能被拷貝
拷貝只會(huì)放生在兩個(gè)場(chǎng)景中:拷貝構(gòu)造函數(shù)以及賦值運(yùn)算符重載,因此想要讓一個(gè)類禁止拷貝,
只需讓該類不能調(diào)用拷貝構(gòu)造函數(shù)以及賦值運(yùn)算符重載即可。
- C++98
????????將拷貝構(gòu)造函數(shù)與賦值運(yùn)算符重載只聲明不定義,并且將其訪問權(quán)限設(shè)置為私有即可。
class CopyBan
{// ...private:CopyBan(const CopyBan&);CopyBan& operator=(const CopyBan&);//...
};
原因:
????????1. 設(shè)置成私有:如果只聲明沒有設(shè)置成private,用戶自己如果在類外定義了,就可以不能禁止拷貝了
????????2. 只聲明不定義:不定義是因?yàn)樵摵瘮?shù)根本不會(huì)調(diào)用,定義了其實(shí)也沒有什么意義,不寫
反而還簡(jiǎn)單,而且如果定義了就不會(huì)防止成員函數(shù)內(nèi)部拷貝了。
- C++11
????????C++11擴(kuò)展delete的用法,delete除了釋放new申請(qǐng)的資源外,如果在默認(rèn)成員函數(shù)后跟上
=delete,表示讓編譯器刪除掉該默認(rèn)成員函數(shù)。
class CopyBan
{// ...CopyBan(const CopyBan&)=delete;CopyBan& operator=(const CopyBan&)=delete;//...
};
二、請(qǐng)?jiān)O(shè)計(jì)一個(gè)類,只能在堆上創(chuàng)建對(duì)象
方法一:將類的析構(gòu)函數(shù)私有。防止別人調(diào)用拷貝在棧上生成對(duì)象
編譯報(bào)錯(cuò):
棧上創(chuàng)建會(huì)自動(dòng)調(diào)用析構(gòu),這里析構(gòu)被私有了,就不能在棧上創(chuàng)建。
但這樣在堆上創(chuàng)建后要進(jìn)行delete也要調(diào)用析構(gòu)函數(shù),也就是雖然不能在棧上創(chuàng)建對(duì)象,但是同樣也不能在堆上創(chuàng)建對(duì)象了。
解決方法:在類public中實(shí)現(xiàn)一個(gè)成員函數(shù)進(jìn)行delete
class HeapOnly
{
public:void Destroy(){delete this;}
private:~HeapOnly(){//...}
};int main()
{//HeapOnly hp1;//static HeapOnly hp2;HeapOnly* hp3 = new HeapOnly;//delete hp3;hp3->Destroy();return 0;
}
編譯成功:
方法二:
實(shí)現(xiàn)方式:
1. 將類的構(gòu)造函數(shù)私有,拷貝構(gòu)造聲明成私有。防止別人調(diào)用拷貝在棧上生成對(duì)象。
2. 提供一個(gè)靜態(tài)的成員函數(shù),在該靜態(tài)成員函數(shù)中完成堆對(duì)象的創(chuàng)建。
完整代碼:
class HeapOnly
{
public:static HeapOnly* CreateObj(){return new HeapOnly;}
private:HeapOnly(){//...}HeapOnly(const HeapOnly& hp) = delete;HeapOnly& operator=(const HeapOnly& hp) = delete;
};int main()
{HeapOnly* hp3 = HeapOnly::CreateObj();//HeapOnly copy(*hp3);return 0;
}
以上代碼有兩個(gè)細(xì)節(jié):
1. 如果沒有提供靜態(tài)的成員函數(shù):
會(huì)發(fā)生報(bào)錯(cuò),調(diào)用一個(gè)類的普通成員函數(shù)是不是需要對(duì)象去調(diào)用,那我們?cè)诙焉仙暾?qǐng)的對(duì)象又正好需要調(diào)用這個(gè)函數(shù)來生成,可是我們選擇都沒有對(duì)象我們?nèi)绾稳フ{(diào)用這個(gè)函數(shù)呢?所以這就純純是先有雞還是先有蛋的問題了。
為了解決這個(gè)問題,我們可以將這個(gè)成員函數(shù)設(shè)置成靜態(tài)的。
2. 利用拷貝構(gòu)造在棧上創(chuàng)建報(bào)錯(cuò):
雖然我們不能夠直接的在棧上創(chuàng)建對(duì)象,但是我們可以在堆上申請(qǐng)一個(gè)對(duì)象之后再調(diào)用一次拷貝構(gòu)造然后間接的就在棧上申請(qǐng)對(duì)象了。
所以我們的完整代碼還要將拷貝構(gòu)造和賦值重載都聲明成delete禁掉,所以這里調(diào)用拷貝構(gòu)造報(bào)錯(cuò)
三、請(qǐng)?jiān)O(shè)計(jì)一個(gè)類,只能在棧上創(chuàng)建對(duì)象
方法:同上將構(gòu)造函數(shù)私有化,然后設(shè)計(jì)靜態(tài)方法創(chuàng)建對(duì)象返回即可。
下面先來看這種方式的實(shí)現(xiàn):
編譯報(bào)錯(cuò),說明不能在堆上創(chuàng)建。
但是我們?nèi)绻趎ew的時(shí)候調(diào)用拷貝構(gòu)造:
編譯通過:
我們發(fā)現(xiàn)又可以在堆上成功了,說明這種方法還有漏洞。
解決方法:下面我們來分析一下:new是由operator new + 構(gòu)造組成的,所以我們只要在類中重載一個(gè)new,并將new聲明成delete,就可以解決這個(gè)問題。
class StackOnly
{
public:static StackOnly CreateObj(){StackOnly st;return st;}
private:StackOnly(){//...}// 對(duì)一個(gè)類實(shí)現(xiàn)專屬operator newvoid* operator new(size_t size) = delete;
};
運(yùn)行發(fā)現(xiàn)報(bào)錯(cuò),說明不能只有new在堆上創(chuàng)建對(duì)象了。
四、請(qǐng)?jiān)O(shè)計(jì)一個(gè)類,不能被繼承
- C++98方式
將構(gòu)造函數(shù)私有:
基類的私有成員在派生類不可訪問,而派生類要繼承,需要調(diào)用基類的構(gòu)造函數(shù)。所以將構(gòu)造函數(shù)私有可以,讓類不能被繼承。
// C++98中構(gòu)造函數(shù)私有化,派生類中調(diào)不到基類的構(gòu)造函數(shù)。則無法繼承
class NonInherit
{
public:static NonInherit GetInstance(){return NonInherit();}
private:NonInherit(){}
};
- C++11方法
final關(guān)鍵字,final修飾類,表示該類不能被繼承。
class A ?final
{// ....
};
五、設(shè)計(jì)一個(gè)類,只能創(chuàng)建一個(gè)對(duì)象(單例模式)
設(shè)計(jì)模式
設(shè)計(jì)模式(Design Pattern)是一套被反復(fù)使用、多數(shù)人知曉的、經(jīng)過分類的、代碼設(shè)計(jì)經(jīng)驗(yàn)的總結(jié)。
為什么會(huì)產(chǎn)生設(shè)計(jì)模式這樣的東西呢?就像人類歷史發(fā)展會(huì)產(chǎn)生兵法。最開始部落之間打仗時(shí)都是人拼人的對(duì)砍。后來春秋戰(zhàn)國(guó)時(shí)期,七國(guó)之間經(jīng)常打仗,就發(fā)現(xiàn)打仗也是有套路的,后來孫子就總結(jié)出了《孫子兵法》。孫子兵法也是類似。
使用設(shè)計(jì)模式的目的:為了代碼可重用性、讓代碼更容易被他人理解、保證代碼可靠性。
設(shè)計(jì)模式使代碼編寫真正工程化;設(shè)計(jì)模式是軟件工程的基石脈絡(luò),如同大廈的結(jié)構(gòu)一樣。
單例模式:一個(gè)類只能創(chuàng)建一個(gè)對(duì)象,即單例模式,該模式可以保證系統(tǒng)中該類只有一個(gè)實(shí)例,并提供一個(gè)訪問它的全局訪問點(diǎn),該實(shí)例被所有程序模塊共享。比如在某個(gè)服務(wù)器程序中,該服務(wù)器的配置信息存放在一個(gè)文件中,這些配置數(shù)據(jù)由一個(gè)單例對(duì)象統(tǒng)一讀取,然后服務(wù)進(jìn)程中的其他對(duì)象再通過這個(gè)單例對(duì)象獲取這些配置信息,這種方式簡(jiǎn)化了在復(fù)雜環(huán)境下的配置管理。
對(duì)于單例模式我們首先需要解決一個(gè)問題:如果保證全局(一個(gè)進(jìn)程中)只有一個(gè)唯一實(shí)例對(duì)象
經(jīng)過上面的學(xué)習(xí)我們就可以知道,要想保證全局只有一個(gè)唯一實(shí)例對(duì)象我們需要做以下兩步操作
- 構(gòu)造函數(shù)私有定義。拷貝構(gòu)造和賦值防拷貝禁掉
- 提供一個(gè)GetInstance獲取單例對(duì)象
單例模式有兩種實(shí)現(xiàn)模式:餓漢模式與懶漢模式
餓漢模式:就是說不管你將來用不用,程序啟動(dòng)時(shí)就創(chuàng)建一個(gè)唯一的實(shí)例對(duì)象。
下面來看餓漢模式的代碼:
主要有以下三個(gè)關(guān)鍵步驟
1、構(gòu)造函數(shù)私有? ?2、提供獲取單例對(duì)象的接口函數(shù)? ?3、防拷貝
經(jīng)過上面的學(xué)習(xí)我們就可以知道,為了防止被創(chuàng)建多個(gè)對(duì)象,所以我們要1、3這倆步,構(gòu)造函數(shù)私有定義。拷貝構(gòu)造和賦值防拷貝禁掉。
然后我們創(chuàng)建單例對(duì)象,?還要給它提供獲取單例對(duì)象的接口函數(shù)。
// 餓漢模式:一開始(main函數(shù)之前)就創(chuàng)建單例對(duì)象
// 提供一個(gè)靜態(tài)指向單例對(duì)象的成員指針,初始化時(shí)new一個(gè)對(duì)象給它
// 1、如果單例對(duì)象初始化內(nèi)容很多,影響啟動(dòng)速度
// 2、如果兩個(gè)單例類,互相有依賴關(guān)系。
// 假設(shè)有A B兩個(gè)單例類,要求A先創(chuàng)建,B再創(chuàng)建,B的初始化創(chuàng)建依賴A
namespace hungry
{class Singleton{public:// 2、提供獲取單例對(duì)象的接口函數(shù)static Singleton& GetInstance(){return _sinst;}void func();void Add(const pair<string, string>& kv){_dict[kv.first] = kv.second;}void Print(){for (auto& e : _dict){cout << e.first << ":" << e.second << endl;}cout << endl;}private:// 1、構(gòu)造函數(shù)私有Singleton(){// ...}// 3、防拷貝Singleton(const Singleton& s) = delete;Singleton& operator=(const Singleton& s) = delete;map<string, string> _dict;// ...static Singleton _sinst;};Singleton Singleton::_sinst;void Singleton::func(){// _dict["xxx"] = "1111";}
}int main()
{//Singleton s1;//Singleton s2;cout << &hungry::Singleton::GetInstance() << endl;cout << &hungry::Singleton::GetInstance() << endl;cout << &hungry::Singleton::GetInstance() << endl;//Singleton copy(Singleton::GetInstance());hungry::Singleton::GetInstance().Add({ "xxx", "111" });hungry::Singleton::GetInstance().Add({ "yyy", "222" });hungry::Singleton::GetInstance().Add({ "zzz", "333" });hungry::Singleton::GetInstance().Add({ "abc", "333" });hungry::Singleton::GetInstance().Print();return 0;
}
運(yùn)行結(jié)果:
可以看到單例模式只能創(chuàng)建了一個(gè)對(duì)象
把其他的創(chuàng)建屏蔽掉再看運(yùn)行結(jié)果:
可以看到GetInstance()之后地址都是相同的,說明只創(chuàng)建了一個(gè)對(duì)象。并且能實(shí)現(xiàn)里面簡(jiǎn)單的功能
餓漢模式適用場(chǎng)景:
- 如果這個(gè)單例對(duì)象在多線程高并發(fā)環(huán)境下頻繁使用,性能要求較高,那么顯然使用餓漢模式來避免資源競(jìng)爭(zhēng),提高響應(yīng)速度更好。
餓漢模式不適用場(chǎng)景:
- 如果單例類構(gòu)造函數(shù)中,要做很多配置初始化工作,導(dǎo)致程序啟動(dòng)非常慢
- 如果兩個(gè)單例類,互相有依賴關(guān)系。?假設(shè)有A B兩個(gè)單例類,要求A先創(chuàng)建,B再創(chuàng)建,B的初始化創(chuàng)建依賴A。我們?nèi)绾伪WC先創(chuàng)建A再創(chuàng)建B呢?
這個(gè)時(shí)候我們使用餓漢就不合適了,這時(shí)我們應(yīng)該使用下面的懶漢模式。
懶漢模式
如果單例對(duì)象構(gòu)造十分耗時(shí)或者占用很多資源,比如加載插件啊, 初始化網(wǎng)絡(luò)連接啊,讀取文件啊等等,而有可能該對(duì)象程序運(yùn)行時(shí)不會(huì)用到,那么也要在程序一開始就進(jìn)行初始化,就會(huì)導(dǎo)致程序啟動(dòng)時(shí)非常的緩慢。 所以這種情況使用懶漢模式(延遲加載)更好。
下面來看這段懶漢的代碼
namespace lazy
{//懶漢模式class Singleton{public:static Singleton* GetInstance(){if (_psinst == nullptr){_psinst = new Singleton;}return _psinst;}void Print(){cout << "Print()" << _a << endl;}private:Singleton():_a(0){}//C++98 防拷貝//Singleton(const Singleton&);//Singleton& operator=(const Singleton&);//C++11 防拷貝Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;int _a;static Singleton* _psinst;};Singleton* Singleton::_psinst = nullptr;}
但是這么寫是會(huì)存在一些問題的。
在多線程場(chǎng)景下會(huì)存在線程安全的問題,因?yàn)槲覀?strong>的懶漢式要用的時(shí)候才會(huì)去調(diào)用GetInstance函數(shù)再去創(chuàng)建那個(gè)唯一的對(duì)象。
假如說現(xiàn)在有兩個(gè)線程,分別是線程A與線程B,然后我線程A與線程B都調(diào)用了GetInstance函數(shù),線程A與線程B都進(jìn)入了if條件判斷里面,此時(shí)我們線程A如果先執(zhí)行實(shí)例化對(duì)象的代碼,然后返回。因?yàn)槲覀兊?strong>線程B不知道線程A已經(jīng)實(shí)例化了唯一對(duì)象,此時(shí)線程B再去調(diào)用實(shí)例化對(duì)象的代碼,就會(huì)導(dǎo)致前面的唯一對(duì)象的地址被覆蓋掉,線程A的數(shù)據(jù)會(huì)丟失,并且會(huì)有內(nèi)存泄漏的風(fēng)險(xiǎn),更嚴(yán)重的是此時(shí)已經(jīng)產(chǎn)生了兩個(gè)實(shí)例對(duì)象,違反了單例模式的設(shè)計(jì)思想。
那我們應(yīng)如何解決上面的問題呢?我們需要進(jìn)行雙檢查加鎖。
//懶漢模式
#include <mutex>
namespace lazy
{class Singleton{public:static Singleton* GetInstance(){// 保護(hù)第一次需要加鎖,后面都不需要加鎖的場(chǎng)景,可以使用雙檢查加鎖// 特點(diǎn):第一次加鎖,后面不加鎖,保護(hù)線程安全,同時(shí)提高了效率if (_pinst == nullptr){_mtx.lock();if (_pinst == nullptr){_pinst = new Singleton;}_mtx.unlock();}return _pinst;}static void DelInstance(){_mtx.lock();if (_pinst){delete _pinst;_pinst = nullptr;}_mtx.unlock();}void Print(){cout << "Print()" << _a << endl;}private:Singleton():_a(0){// 假設(shè)單例類構(gòu)造函數(shù)中,要做很多配置初始化}~Singleton(){// 程序結(jié)束時(shí),需要處理一下,持久化保存一些數(shù)據(jù)}Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;// 實(shí)現(xiàn)一個(gè)內(nèi)嵌垃圾回收類 class CGarbo {public:~CGarbo(){if (_pinst){delete _pinst;_pinst = nullptr;}}};int _a;static Singleton* _pinst;static std::mutex _mtx;static CGarbo _gc;};Singleton* Singleton::_pinst = nullptr;std::mutex Singleton::_mtx;Singleton::CGarbo Singleton::_gc;
}
為什么是雙檢查加鎖?
我們需要知道我們想加鎖的地方只是第一次 _pinst為空的時(shí)候,我們想實(shí)例化一個(gè)唯一對(duì)象時(shí)才需要加鎖,但是如果到了后面我們已經(jīng)實(shí)例化出了對(duì)象,但是我們照樣加鎖了之后再去判斷 _pinst是否為空,然后在多線程場(chǎng)景下頻繁的加鎖解鎖會(huì)導(dǎo)致效率降低,并且也不推薦這樣做。
而我們的雙檢查加鎖里面的第二個(gè)if檢查就是我先進(jìn)行加鎖,然后進(jìn)行判斷第一次_pinst是否為空,然后實(shí)例化一個(gè)唯一對(duì)象,等到下一次某個(gè)線程再調(diào)用GetInstance的時(shí)候,我們的第一個(gè)if檢查發(fā)現(xiàn)pinst不為空,就不會(huì)進(jìn)入第一個(gè)if條件判斷的里面,因此也就不存在多線程場(chǎng)景下頻繁加鎖解鎖,保護(hù)線程安全,同時(shí)提高了效率。
?
餓漢模式和懶漢模式的優(yōu)缺點(diǎn)對(duì)比如下:
餓漢模式:
- 優(yōu)點(diǎn):實(shí)例化對(duì)象在類加載時(shí)完成,因此無須考慮多線程訪問問題,可以確保實(shí)例的唯一性。
- 缺點(diǎn):
- 餓漢式單例對(duì)象在類加載時(shí)就被創(chuàng)建,可能會(huì)造成系統(tǒng)資源的浪費(fèi),尤其是在對(duì)象占內(nèi)存較大或?qū)ο蟪跏蓟臅r(shí)較長(zhǎng)的情況下。
- 如果有多個(gè)單例對(duì)象,他們之間有初始化依賴關(guān)系,餓漢模式也會(huì)有問題。(比如有A和B兩個(gè)單例類,要求A單例先初始化,B必須在A之后初始化。那么餓漢無法保證。這種場(chǎng)景下面用懶漢就可以,懶漢可以先調(diào)用A::GetInstance(),再調(diào)用B::GetInstance().)
懶漢模式:
- 優(yōu)點(diǎn):實(shí)現(xiàn)了延遲加載,只有在第一次使用時(shí)才創(chuàng)建實(shí)例,節(jié)省了系統(tǒng)資源。(解決餓漢的缺點(diǎn)。因?yàn)樗堑谝淮握{(diào)用GetInstance時(shí)創(chuàng)建初始化單例對(duì)象)
- 缺點(diǎn):必須考慮與處理多個(gè)線程同時(shí)訪問的問題,需要進(jìn)行雙重檢查鎖定等機(jī)制及進(jìn)行控制。