1、請設計一個類,不能被拷貝
拷貝只會出現在兩個場景中:拷貝構造函數以及賦值運算符重載,因此想要讓一個類禁止拷貝,只需讓該類不能調用拷貝構造函數以及賦值運算符重載即可。在C++98和C++11都有相對應的方法來解決此問題,下面我們分別討論。
C++98:將拷貝構造函數與賦值運算符重載只聲明不定義,并且將其訪問權限設置為私有即可。示例如下:
class CopyBan
{
public://……
private://只聲明不定義CopyBan(const CopyBan&);//拷貝構造CopyBan& operator=(const CopyBan&);//賦值運算符重載
};
原因:
- 設置成私有:如果只聲明沒有設置成private,用戶自己如果在類外定義了,就不能禁止拷貝了。
- 只聲明不定義:不定義是因為該函數根本不會調用,定義了其實也沒有什么意義,不寫反而還簡單,而且如果定義了就不會防止成員函數內部拷貝了。
C++11:C++11拓展delete的用法,delete除了釋放new申請的資源外,如果在默認成員函數后加上=delete,即表示讓編譯器刪除掉該默認成員函數。
class CopyBan
{CopyBan(const CopyBan&) = delete;//刪除拷貝構造CopyBan& operator=(const CopyBan&) = delete;//刪除賦值運算符重載
};
庫里面如下都是經典的防拷貝:
- unique_ptr。
- thread線程 。
- mutex鎖 。
- istream 。
- ostream。
2、請設計一個類,不能被繼承
C++98:將該類的構造函數私有化即可,因為子類的構造函數被調用時,必須調用父類的構造函數初始化父類的那一部分成員,但無論是何種繼承方式,父類的私有成員在子類是不可見的,所以創建子類對象時子類就無法調用父類的構造函數對父類的成員初始化,繼而該類無法被繼承。
class NonInherit
{
public:static NonInherit GetInstance(){return NonInherit();}
private:NonInherit()//私有化構造函數{}
};
- C++98的這種方式其實不夠徹底,因為這個類仍然可以被繼承(編譯器不會報錯),只不過被繼承后無法實例化出對象而已。因此我們推出C++11的方法。
C++11:使用final關鍵字,final修飾類,表示該類不能被繼承。此時就算繼承后沒有創建對象也會編譯出錯。
class A final
{// ....
};
C++98是委婉的不能讓你繼承,C++11是直接的不能讓你繼承。
3、請設計一個類,只能在堆上創建對象
像我們平時創建對象,常見有如下三種在不同區域創建對象的方式:
class HeapOnly
{//……
};
int main()
{HeapOnly h1;static HeapOnly h2;HeapOnly* h3 = new HeapOnly;return 0;
}
既然只能在堆上創建對象,也就是只能通過new操作創建對象,有如下兩種方式。
法一:
- 將類的構造函數私有,拷貝構造聲明成私有,防止別人調用拷貝在棧上生成對象。
- 提供一個靜態的成員函數,在該靜態成員函數中完成對象的創建。
class HeapOnly
{
public://靜態成員函數完成對象的創建static HeapOnly* CreateObj(){return new HeapOnly;}
private://構造函數私有HeapOnly(){}//防拷貝//C++98,只聲明不實現,且聲明成私有HeapOnly(const HeapOnly&);//C++11HeapOnly(const HeapOnly&) = delete;
};
int main()
{HeapOnly* ph1 = HeapOnly::CreateObj();HeapOnly* ph2 = HeapOnly::CreateObj();delete ph1;delete ph2;//HeapOnly h1;//棧-錯誤//static HeapOnly h2;//靜態區-錯誤//HeapOnly copy(*ph2);//調用拷貝生成對象,在棧區,錯誤return 0;
}
注意:
- 我們沒有必要對賦值運算符重載設置為私有&&只聲明不實現或者加上=delete(禁掉),因為賦值運算符重載是兩個已經存在的對象,既然已經存在,那勢必這倆對象就已經在堆區創建好了,所以它們之間進行賦值操作并不會出錯。除非你不想用,那你可以把賦值運算符重載給禁掉。
- 而拷貝構造是拿一個已經存在的對象去構造一個對象,此對象是先前未存在的,且拷貝構造后是在棧上的,自然不符合題意,因此需要把拷貝構造給禁掉,而賦值運算符重載不需要禁掉。
法二:
- 將析構函數私有化。
class HeapOnly
{
public:
private://析構函數私有化~HeapOnly(){}
};
int main()
{//HeapOnly ph1;err//static HeapOnly ph2;errHeapOnly* ph3 = new HeapOnly;return 0;
}
為何析構函數私有化就能確保只能在堆上創建對象呢?
- C++是一個靜態綁定的語言。在編譯過程中,所有的非虛函數調用都必須分析完成。即使是虛函數,也需檢查可訪問性。因此, 當在棧上生成對象時,對象會自動調用析構函數釋放對象,也就說析構函數必須可以訪問 ,否則編譯出錯。而在堆上生成對象,由于析構函數由程序員調用(通過使用delete),所以不一定需要析構函數。
既然析構函數私有化,如何delete你new出的資源呢?
- 因為delete操作會調用析構函數,而析構函數已經被置為私有了,那就無法調用,為了解決此問題,我們只需要在類的內部提供一個靜態成員函數,既然你類外不能調用私用成員,但是類里是可以調用的,因此我們在此成員函數中調用析構函數完成delete操作。
class HeapOnly
{
public://靜態成員函數釋放new的對象static void DelObj(HeapOnly* ptr){delete ptr;}
private://析構函數私有化~HeapOnly(){}
};
int main()
{HeapOnly* ph3 = new HeapOnly;//釋放ph3HeapOnly::DelObj(ph3);return 0;
}
當然,我也可以使用delete this來釋放new出的資源:
class HeapOnly
{
public:void DelObj(){delete this;}
private://析構函數私有化~HeapOnly(){}
};
int main()
{HeapOnly* ph3 = new HeapOnly;//釋放ph3ph3->DelObj();return 0;
}
delete this–對象請求自殺,執行后不能再訪問this指針。換句話說,你不能去檢查它、將它和其他指針比較、和 NULL比較、打印它、轉換它,以及其它的任何事情。不是很推薦這種方式。
4、請設計一個類,只能在棧上創建對象
法一:
- 將構造函數設為私有,防止外部直接調用構造函數在堆上創建對象。
- 提供靜態成員函數,內部調用私有的構造函數完成對象的創建。
class StackOnly
{
public://靜態成員函數,內部調用構造函數創建對象static StackOnly CreateObj(){return StackOnly();//傳值返回 —— 拷貝構造}
private://構造函數私有StackOnly(){}
};
int main()
{StackOnly h1 = StackOnly::CreateObj();//static StackOnly h2; 錯誤//StackOnly* h3 = new StackOnly; 錯誤return 0;
}
此法有一缺陷,無法避免外部調用拷貝構造函數在靜態區、堆區……創建對象。
int main()
{StackOnly h1 = StackOnly::CreateObj();//棧區static StackOnly h2(h1);//調用拷貝構造在靜態區創建對象StackOnly* h3 = new StackOnly(h1);//調用拷貝構造在堆區創建對象return 0;
}
法二:
- 把構造函數設為公有。
- 屏蔽operator new函數和operator delete函數。
class StackOnly
{
public:StackOnly(){}
private://C++98void* operator new(size_t size);void operator delete(void* p);//C++11//void* operator new(size_t size) = delete;//void operator delete(void* p) = delete;
};
int main()
{StackOnly h1;//StackOnly* h3 = new StackOnly(h1);不能使用new在堆區創建對象return 0;
}
new和delete默認調用的是全局的operator new函數和operator delete函數,但如果一個類重載了專屬的operator new函數和operator delete函數,那么new和delete就會調用這個專屬的函數。所以只要把operator new函數和operator delete函數屏蔽掉,那么就無法再使用new在堆上創建對象了。
上述做法雖然成功避免了在堆區創建對象,但是無法避免在靜態區或全局創建對象。
class StackOnly
{
private:void* operator new(size_t size) = delete;void operator delete(void* p) = delete;
};
StackOnly h1;//全局區
int main()
{static StackOnly h2;//靜態區return 0;
}
綜上,其實無論是法一還是法二多少都會存在點瑕疵,總是會有老六的出現,只能說是不那么嚴謹的情況下來看,法一算是ok的。
5、請設計一個類,只能創建一個對象(單例模式)
設計模式:
- 設計模式(Design Pattern)是一套被反復使用、多數人知曉的、經過分類的、代碼設計經驗的總結。為什么會產生設計模式這樣的東西呢?就像人類歷史發展會產生兵法。最開始部落之間打仗時都是人拼人的對砍。后來春秋戰國時期,七國之間經常打仗,就發現打仗也是有套路的,后來孫子就總結出了《孫子兵法》。孫子兵法也是類似。
- 使用設計模式的目的:為了代碼可重用性、讓代碼更容易被他人理解、保證代碼可靠性。 設計模式使代碼編寫真正工程化;設計模式是軟件工程的基石脈絡,如同大廈的結構一樣。
現在已經總結出了23種設計模式:
- 創建型模式,共五種:工廠方法模式、抽象工廠模式、單例模式、建造者模式、原型模式。
- 結構型模式,共七種:適配器模式、裝飾器模式、代理模式、外觀模式、橋接模式、組合模式、享元模式。
- 行為型模式,共十一種:策略模式、模板方法模式、觀察者模式、迭代子模式、責任鏈模式、命令模式、備忘錄模式、狀態模式、訪問者模式、中介者模式、解釋器模式。
其中用的最多的就是單例模式。下面來展開討論。
單例模式:一個類只能創建一個對象,即單例模式,該模式可以保證系統中該類只有一個實例,并提供一個訪問它的全局訪問點,該實例被所有程序模塊共享。比如在某個服務器程序中,該服務器的配置信息存放在一個文件中,這些配置數據由一個單例對象統一讀取,然后服務進程中的其他對象再通過這個單例對象獲取這些配置信息,這種方式簡化了在復雜環境下的配置管理。
單例模式有兩種實現模式:餓漢模式和懶漢模式。下面展開討論。
餓漢模式
- 餓漢模式就是不管你將來用不用,程序啟動時就創建一個唯一的實例對象。
實現方式如下:
- 將構造函數私有化,并將拷貝構造和拷貝賦值設為私有或刪除,防止外部隨意創建對象或拷貝。
- 在類里創建一個static靜態對象的指針,在進入程序入口之前就完成單例對象的初始化。
- 提供一個static靜態成員函數,用來獲取單例對象的指針。
- 將拷貝構造和拷貝賦值私有化,防止類外調用拷貝構造創建對象。
class Singleton
{
public://靜態成員函數內部獲取單例對象的指針static Singleton* GetInstance(){return _spInst;}void print();
private:Singleton(){}//C++98 防拷貝Singleton(const Singleton&);Singleton& operator=(Singleton const&);//C++11 防拷貝//Singleton(const Singleton&) = delete;//Singleton& operator=(Singleton const&) = delete;static Singleton* _spInst;//聲明int _a = 0;
};
Singleton* Singleton::_spInst = new Singleton;//定義,在程序入口之前就完成單例對象的初始化
void Singleton::print()
{cout << _a << endl;
}
int main()
{Singleton::GetInstance()->print();//Singleton st1;err//Singleton* st2 = new Singleton;err//Singleton copy(*Singleton::GetInstance());errreturn 0;
}
再比如我現在有一個信息管理的類,需要保證進程里只有一份這樣的信息,那么就需要把它設定為單例,整體框架和上面差不多其實,具體實現細節有所變動罷了:
//InfoMgr —— 單例
class InfoMgr
{
public://靜態成員函數獲取單例對象指針static InfoMgr* GetInstacne(){return _spInst;}//修改信息void SetAddress(const string& s){_address = s;}//獲取信息string& GetAddress(){return _address;}
private://構造函數私有化InfoMgr() {}//刪除拷貝構造InfoMgr(const InfoMgr&) = delete;string _address;int _secretKey;static InfoMgr* _spInst;//聲明
};
InfoMgr* InfoMgr::_spInst = new InfoMgr;//定義
int main()
{//全局只有一個InfoMgr對象InfoMgr::GetInstacne()->SetAddress("江蘇省南京市");cout << InfoMgr::GetInstacne()->GetAddress() << endl;//江蘇省南京市return 0;
}
如果這個單例對象在多線程高并發環境下頻繁使用,性能要求較高,那么顯然使用餓漢模式來避免資源競爭,提高響應速度更好。
懶漢模式
如果單例對象構造十分耗時或者占用很多資源,比如加載插件啊, 初始化網絡連接啊,讀取文件啊等等,而有可能該對象程序運行時不會用到,那么也要在程序一開始就進行初始化,就會導致程序啟動時非常的緩慢。 所以這種情況使用懶漢模式(延遲加載)更好。
還是以上述信息管理的類為例,懶漢模式的實現方式如下:
- 將構造函數置為私有,并將拷貝構造函數和賦值運算符重載函數設為私有或刪除,防止外部創建或拷貝對象。
- 提供一個指向單例對象的static指針,并在程序入口之前先將其初始化為空。
- 提供一個static靜態成員函數,只有當static指針為空時才初始化(也就是第一次調用此成員函數才創建對象),最后返回單例對象的指針。
- 將拷貝構造和拷貝賦值私有化,防止類外調用拷貝構造創建對象。
//懶漢 -- 一開始不創建對象,第一次調用GetInstacne再創建對象
class InfoMgr
{
public://靜態成員函數獲取單例對象指針static InfoMgr* GetInstacne(){if (_spInst == nullptr){_spInst = new InfoMgr;}return _spInst;}//修改信息void SetAddress(const string& s){_address = s;}//獲取信息string& GetAddress(){return _address;}
private://構造函數私有化InfoMgr(){}//刪除拷貝構造InfoMgr(const InfoMgr&) = delete;string _address;int _secretKey;static InfoMgr* _spInst;//聲明
};
InfoMgr* InfoMgr::_spInst = nullptr;//定義
int main()
{//全局只有一個InfoMgr對象InfoMgr::GetInstacne()->SetAddress("江蘇省南京市");cout << InfoMgr::GetInstacne()->GetAddress() << endl;//江蘇省南京市return 0;
}
懶漢模式這樣寫是有問題的,還需要加鎖(雙檢查加鎖),餓漢模式不需要加鎖。