這個話題比較大了,所以我們往簡單點說,那就是我們想要控制一個類的行為,嗯,這裡說的不是一個類對象的行為,好吧,如果接觸C++不是太深的話這可能不太好理解,因為似乎我們都不怎麼關心這個問題,我們往往關心的是一個對象的行為,但是不表示我們就不應該關心類的行為。
那麼類有哪些行為是我們可以或者說應該關心的?
首先我們來說類的構建方式,類的構建方式不都很簡單嗎?直接聲明定義或者new出來即可,那麼問題來了,直接聲明定義出來的對象都是棧對象,但是計算機棧空間遠不及堆空間大,所以可能我們會要求使用該類的人必須將對象分配在堆上面,那麼問題來了,你不知道誰在使用你所提供的類,所以你不可能對個使用你class的人都耳提面命的告訴他們你必須要new出來,否則可能會導致某種不可預期的問題,所以為了讓使用該類的人必須在堆上構建,那麼就要從根源上來解決該問題,讓他必須new出來,而打算在棧上構建該對象則無法通過編譯,要定製這樣的話很簡單,我們只需要把析構函數設定為protected或者private即可。
//+---------------------------
class Test{
public:
Test(){}
void Destroy(){
delete this;
}
protected:
~Test(){}
};
int main(){
//
// 正確的使用方式
//
Test* obj = new Test();
obj->Destroy();
//
// 下面的方式無法通過編譯
//
Test obj;
system("pause")
return 0;
}
//+-----------------------------
上面是讓類只能在堆上構建的技巧,那麼如果讓類只能在棧上構建呢?也就是我們不可以使用new來完成該行為,比如Direct3D裡面的XNA上的XMMATRIX就只能在棧上才能保證16字節對齊(好吧,幾字節對齊記得不太清,總之如果選擇堆內存會出現頻繁崩潰),那麼如何實現這樣的類呢?實現方法也是相當的簡單,我們只需要將new操作符都實現為私有的即可,既然是私有的那麼就可以不需要實現,而只需要聲明即可:
//+-----------------------------
class Test{
public:
Test(){}
~Test(){}
protected:
static void* operator new(size_t size);
static void operator delete(void* ptr, std::size_t size);
};
int main(){
//
// 錯誤的使用方式,無法通過編譯
//
Test* obj = new Test();
//
// 正確的使用方式
//
Test obj;
system("pause")
return 0;
}
//+---------------------------------
這是將類設計為只能在棧上分配的技巧,當然你可以這麼認為:使用malloc獲取內存,然後轉換為Test*:
//+-------------------------------------
Test* obj = (Test*)malloc(sizeof(Test));
//+-------------------------------------
但只是一塊raw內存而已,並沒有實質上的意義,雖然對於上面我們這個空類來說是可行的,但是當我們在構造函數中有初始化行為時這就不可行,比如:
//+--------------------------------------
class Test{
public:
Test(){
m_value = 10;
}
~Test(){}
int value() const{
return m_value;
}
protected:
static void* operator new(size_t size);
static void operator delete(void* ptr, std::size_t size);
private:
int m_value;
};
int main(){
Test* obj = (Test*)malloc(sizeof(Test));
std::cout << obj->value() << std::endl;
free(obj);
system("pause");
return 0;
}
//+-----------------------------------------
我們會看到打印的結果是一個隨機數,所以這不是正確的行為。
接下來我們設計整個進程中只存在一個類對象,其實這屬於一種設計模式——單例模式,單例簡單點說就是一個經過改進後的全局對象,然而我們不能簡單的以為是靜態數據 + 靜態函數 = 單例。
//+------------------------------------
class Test{……};
class MyOneTest{
public:
static void doSomething(Test& test);
private:
static std::queue
};
Test test;
MyOneTest::doSomething(test);
//+-----------------------------------
看上去沒啥問題,運用起來也沒啥問題,但在某些使用情況下有很多問題,比如我的行為要求有所改變,doSomething不再適合(當然如果這不是個靜態函數而是個虛函數那麼這一切都不是問題),這一切的修改需要建立在擁有源碼的基礎上才行,同時靜態數據加靜態函數的做法對數據的初始化和清理同樣帶來難題。
關於單例的實現方式有多種,這裡我就主要說說我使用的方案:
//+------------------------------------
class Singleton{
public:
static Singleton* Instance(){
if(__sPtr){
return __sPtr;
}
else{
__sPtr = new Singleton;
return __sPtr;
}
}
static void Destroy(){
if(__sPtr){
delete __sPtr;
__sPtr = nullptr;
}
}
protected:
Singleton(){};
~Singleton(){}
Singleton(const Singleton& other) = delete;
Singleton& operator(const Singleton& other) = delete;
private:
static Singleton* __sPtr;
};
Singleton* Singleton::__sPtr = nullptr;
//+---------------------------------
這種設計的好處是我不需要的時候就不會有Singleton對象的創建,這對於開銷很大的對象來說很重要,同時還有多態的性質,因為我們是動態創建,考慮一下我們為什麼不用下面的方式:
//+----------------------------------
class Singleton{
public:
static Singleton* Instance(){
return &__sObj;
}
protected:
Singleton(){};
~Singleton(){}
Singleton(const Singleton& other) = delete;
Singleton& operator(const Singleton& other) = delete;
private:
static Singleton __sObj;
};
Singleton* Singleton::__sObj;
//+-------------------------------------
當然還有為什麼Instance返回的是指針而不是引用?
關於資源的管理似乎有必要爭論一下,如果我們採用上面的實現方式那麼我們可以不用關心內存的釋放,但是帶來的問題是該單列不具有多態性,而且無論我們用與不用都得構造該對象出來,當然我們可以使用另一種方式:
//+------------------------------------
static Singleton* Instance(){
static Singleton Obj;
return &Obj;
}
//+-------------------------------------
該方式解決了內存的管理同時解決了用時才進行實例化的問題,但是引入了一個新的問題,那就是你不知道編譯器在實例化該對象的時候到底都做了些什麼,經過別人驗證該模型在某些情況下他確實會失效,所以我一直使用上面指針的模型,那麼到目前為此,我們的單例在單線程模型下工作得很好,但是一旦進入多線程就可以出現多個不同的對象出來。
//+--------------------------------------
static Singleton* Instance(){
if(__sPtr){ // 如果多條線程同時執行到該位置,那麼都會檢查到__sPtr為nullptr,自然都走到else裡面
return __sPtr;
}
else{
__sPtr = new Singleton;
return __sPtr;
}
}
//+---------------------------------------
具體問題如上,我們可以這樣來解決該問題:
//+------------------------------------------
static Singleton* Instance(){
std::mutex mtx;
std::unique_lock<:mutex> lock(mtx);
if(__sPtr){
return __sPtr;
}
else{
__sPtr = new Singleton;
return __sPtr;
}
}
//+-------------------------------------------
現在可以正常工作但是又引入了另一個問題,這是一個關於效率的問題,因為每一次進入Instance都會引起中斷,這不是一個好習慣,因為後面的都是不必要的,所以我們可以重新優化一下:
//+--------------------------------------------
static Singleton* Instance(){
if(__sPtr){
return __sPtr;
}
else{
std::mutex mtx;
std::unique_lock<:mutex> lock(mtx);
if(__sPtr){
return __sPtr;
}
else{
__sPtr = new Singleton;
return __sPtr;
}
}
}
//+-------------------------------------------
現在效率有了,安全性也有了,我們可以重新裝訂一下,讓他支持任意類實現單例模型:
//+-------------------------------------------
//
// 無論通過那個函數獲取的實例都是同一個實例對象
// 當使用完成之後需要使用 Destroy 來銷燬對象
// 可以直接使用MSingleton創建對象單例,當還可以可以使用他作為基類來讓子類具有創建單例的能力
// eg:
// 1:class A;
// A* ptr = MSingleton
// 2 : class A : public MSingleton
// A* ptr = A::Instance();
//
template
class MSingleton
{
public:
MSingleton(){}
~MSingleton(){}
MSingleton(const MSingleton&)=delete;
MSingleton& operator=(const MSingleton&)=delete;
//
// 單例普通指針類型
//
static T* Instance(){
try{
if(__sPtr == nullptr){
std::mutex mtx;
std::unique_lock<:mutex> lock(mtx);
if(__sPtr == nullptr){
__sPtr = new T;
}
}
return __sPtr;
}
catch(...){
return nullptr;
}
}
//
// 帶參數創建
//
template
static T* InstanceWithArgs(Args...args){
try{
if (__sPtr == nullptr){
std::mutex mtx;
std::unique_lock<:mutex> lock(mtx);
if (__sPtr == nullptr){
__sPtr = new T(args...);
}
}
return __sPtr;
}
catch (...){
return nullptr;
}
}
//
// 使用工廠函數進行創建對象
//
template
static T* Instance(F fun){
try{
if(__sPtr == nullptr){
std::mutex mtx;
std::unique_lock<:mutex> lock(mtx);
if(__sPtr == nullptr){
__sPtr = fun();
if(__sPtr){
__sIsFactory = true;
}
}
}
return __sPtr;
}
catch(...){
return nullptr;
}
}
static void Destroy(){
if(__sPtr && !__sIsFactory){
delete __sPtr;
__sPtr = nullptr;
}
}
//
// 使用輔助函數進行釋放對象
//
template
static void Destroy(F fun){
if(__sPtr && __sIsFactory){
fun(__sPtr);
__sPtr = nullptr;
}
}
protected:
static T* __sPtr;
static bool __sIsFactory;
};
template
T* MSingleton
template
bool MSingleton
//
// 測試代碼
//
class Test{
public:
Test(){
m_value = 10;
}
~Test(){}
int value() const{
return m_value;
}
private:
int m_value;
};
typedef MSingleton
void Destroy(){
SingleTest::Destroy();
}
int main(){
std::atexit(Destroy);
Test* obj = SingleTest::Instance();
std::cout << obj->value() << std::endl;
return 0;
}
//+-----------------------------
到此,我們實現了一個簡單的單例模型,這個單例的使用方式也相當的簡單,一如源碼裡面的說明,接下來我們再來說一個另外一種類的定製——控制類的內存分配行為,這句話不太好理解,當然是我說得不夠專業的原因,我們用代碼是說明會更好一些:
//+-----------------------------
int main(){
int num = 1e8;
for(int i=0;i int* ptr = new int; delete ptr; } return 0; } //+------------------------------- 這個例子沒有任何實際的意義,但是剛好能夠說明我們的問題,就是說在我們需要創建大量對象的時候我們想要對內存進行一些優化,眾所周知,上面的代碼執行效率相當的低,所以我們這裡說的定製就是要優化這個效率,要進行該項定製我們需要實現new操作符的重寫: //+-------------------------------- class MAllocator{ public: static void* operator new(std::size_t size); static void operator delete(void* p, std::size_t size); }; //+--------------------------------- 假如我們這個類是我們的基類,而且已經重新好了新的new操作符,那麼我們只需要繼承至該類就可以使用高效率的new操作符了:
//+----------------------------------
class Test : public MAllocator{
public:
Test(){
m_value = 10;
}
~Test(){}
int value() const{
return m_value;
}
private:
int m_value;
};
//+--------------------------------------
該技術的難點在於分配器 MAllocator 的實現上面,當然該分配器有各種實現方案,我這裡簡單的說一種:
//+--------------------------------------
struct MEmptyType{};
template
class MAllocator{
public:
static void* operator new(std::size_t size);
static void operator delete(void* p, std::size_t size);
};
/
// 分配器轉發
//
template
struct __Allocator_Dispatch__{
static void* malloc(std::size_t size){
return Allocator::Instance()->Allocator(size);
}
static void free(void* p, std::size_t size){
Allocator::Instance()->Deallocator(p, size);
}
};
template<>
struct __Allocator_Dispatch__
static void* malloc(std::size_t size){
return ::operator new(size);
}
static void free(void* p, std::size_t size){
::operator delete(p);
}
};
//
// 分配器實現
//
template
void* MAllocator
return __Allocator_Dispatch__
}
template
void MAllocator
__Allocator_Dispatch__
}
//+-------------------------------
現在我們將問題拋給了模板參數,默認情況下我們使用常規的new操作符,效率自然不會得到提高,所以我們想要提高new的效率就需要提供一個真正的分配器作為模板參數傳遞進去:
//+--------------------------------
//
// 普通內存分配器
//
template
class MPoolAllocator : public ThreadMode, public MSingleton
{
public:
enum{ value = sizeof(Type) };
void* Allocator(std::size_t size);
void Deallocator(void* p, std::size_t size);
template
struct rebind
{
typedef MPoolAllocator
};
private:
std::vector
MFixMemroy
std::deque
template
class UniquiLock{
public:
UniquiLock(CableLock* obj) :pObj(obj){
pObj->Lock();
}
~UniquiLock(){
pObj->UnLock();
}
private:
CableLock* pObj{ nullptr };
};
};
//+---------------------------------
到這裡我們可以將分配器,中間層組裝起來作為一個包裝器:
//+---------------------------------
template
class MFasWrap : public MAllocator
{
public:
typedef typename MConstParamWrape
typedef typename MParamWrape
public:
MFasWrap(const_reference_type val = Type()) :mObj(val){}
MFasWrap(const MFasWrap& other) :mObj(other.mObj){}
MFasWrap(MFasWrap&& other){
mObj = other.mObj;
other.mObj.~Type();
}
MFasWrap& operator =(const_reference_type val){
mObj = val;
return *this;
}
MFasWrap& operator =(Type&& val){
mObj = val;
val.~Type();
return *this;
}
MFasWrap& operator =(const MFasWrap& other){
mObj = other.mObj;
return *this;
}
MFasWrap& operator =(MFasWrap&& other){
mObj = other.mObj;
other.mObj.~Type();
return *this;
}
operator Type(){
return mObj;
}
reference_type toType(){
return mObj;
}
const_reference_type toType() const{
return mObj;
}
friend std::ostream& operator<
os << other.mObj;
return os;
}
friend std::wostream& operator<
os << other.mObj;
return os;
}
friend std::istream& operator>>(std::istream& is, MFasWrap& other){
is >> other.mObj;
return is;
}
friend std::wistream& operator>>(std::wistream& is, MFasWrap& other){
is >> other.mObj;
return is;
}
private:
Type mObj;
};
//+----------------------------
於是我們就可以像下面進行使用啦:
//+-----------------------------
int main(){
typedef MFasWrap
int num = 1e7;
boost::timer t;
t.restart();
for (int i = 0; i < num; ++i){
FInt* ptr = new FInt;
delete ptr;
}
std::cout << "time1 = " << t.elapsed() * 1000 << std::endl;
t.restart();
for (int i = 0; i < num; ++i){
int* ptr = new int;
delete ptr;
}
std::cout << "time2 = " << t.elapsed() * 1000 << std::endl;
system("pause");
return 0;
}
//+-----------------------------
我們可以對比一下效率,可以看出差距是相當大的:
關於類的定製就說到這裡,大家有興趣挖掘的可以繼續深入研究。
閱讀更多 IT布丁老師 的文章