程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 探究 C++ Singleton(單例模式)

探究 C++ Singleton(單例模式)

編輯:C++入門知識

一、靜態化並不是單例模式
  初學者可能會犯的錯誤, 誤以為把所有的成員變量和成員方法都用 static 修飾後, 就是單例模式了: class Singleton
{
public:
/* static method */

private:
static Singleton m_data; //static data member 在類中聲明,在類外定義
};

Singleton Singleton::m_data;   乍一看確實具備單例模式的很多條件, 不過它也有一些問題. 第一, 靜態成員變量初始化順序不依賴構造函數, 得看編譯器心情的, 沒法保證初始化順序 (極端情況: 有 a b 兩個成員對象, b 需要把 a 作為初始化參數傳入, 你的類就 必須 得要有構造函數, 並確保初始化順序).
  第二, 最嚴重的問題, 失去了面對對象的重要特性 -- "多態", 靜態成員方法不可能是 virtual 的(補充一點,靜態成員方法也不可能是 const 的. Singleton類的子類沒法享受 "多態" 帶來的便利.
  二、餓漢模式
  餓漢模式 是指單例實例在程序運行時被立即執行初始化:
class Singleton
{
public:
static Singleton& getInstance()
{
return m_data;
}

private:
static Singleton m_data; //static data member 在類中聲明,在類外定義
Singleton(){}
~Singleton(){}
};

Singleton Singleton::m_data;    這種模式的問題也很明顯, 類現在是多態的, 但靜態成員變量初始化順序還是沒保證: 假如有兩個單例模式的類 ASingleton 和 BSingleton, 某天你想在 BSingleton 的構造函數中使用 ASingleton 實例, 這就出問題了. 因為 BSingleton m_data 靜態對象可能先 ASingleton 一步調用初始化構造函數, 結果 ASingleton::getInstance() 返回的就是一個未初始化的內存區域, 程序還沒跑就直接崩掉。恩,這只是理論分析的結果,下面給出一個簡單的例子說明一下問題所在吧!
實例:ASingleton、BSingleton兩個單例類,其中 ASingleton 的構造函數中使用到 BSingleton 的單例對象。
class ASingleton
{
public:
static ASingleton* getInstance()
{
return &m_data;
}
void do_something()
{
cout<<"ASingleton do_something!"< }
protected:
static ASingleton m_data; //static data member 在類中聲明,在類外定義
ASingleton();
~ASingleton() {}
};

class BSingleton
{
public:
static BSingleton* getInstance()
{
return &m_data;
}
void do_something()
{
cout<<"BSingleton do_something!"< }
protected:
static BSingleton m_data; //static data member 在類中聲明,在類外定義
BSingleton();
~BSingleton() {}
};

ASingleton ASingleton::m_data;
BSingleton BSingleton::m_data;

ASingleton::ASingleton()
{
cout<<"ASingleton constructor!"< BSingleton::getInstance()->do_something();
}
BSingleton::BSingleton()
{
cout<<"BSingleton constructor!"< } 在這個測試例子中,我們將上述代碼放在一個 main.cpp 文件中,其中 main 函數為空。 int main()
{
return 0;
} 運行測試結果是: ASingleton constructor!
BSingleton do_something!
BSingleton constructor!
奇怪了,為什麼 BSingleton 的構造函數居然是在成員函數 do_something 之後調用的?
下面進行分析: 首先我們看到這個測試用例中,由於只有一個源文件,那麼按從上到下的順序進行編譯運行。注意到: ASingleton ASingleton::m_data;
BSingleton BSingleton::m_data; 這兩個定義式,那麼就會依次調用 ASingleton 的構造函數 和 BSingleton 的構造函數進行初始化。 一步一步來,首先是 ASingleto 的 m_data。 那麼程序就會進入 ASingleton 的構造函數中執行,即: ASingleton::ASingleton()
{
cout<<"ASingleton constructor!"< BSingleton::getInstance()->do_something();
} 首先執行 cout,然後接著要獲取 BSingleton 的單例,雖然說 BSingleton 的定義尚未執行,即 BSingleton BSingleton::m_data; 語句尚未執行到,但是 BSingleton 類中存在著其聲明,那麼還是可以調用到其 do_something 方法的。
ASingleton 的構造函數執行完畢,那麼 ASingleton ASingleton::m_data; 語句也就執行結束了,即 ASingleton 單例對象 m_data 也就初始化完成了。
接下來執行 BSingleton BSingleton::m_data; 語句,那麼也就是執行 BSingleton 的構造函數了。 所以就有了最終結果的輸出了。
那麼到此,我們或許會說:既然 ASingleton 的構造函數中要用到 BSingleton 單例對象,那麼就先初始化 BSingleton 的單例對象咯,是的,我們可以調換一下順序:
//ASingleton ASingleton::m_data;
//BSingleton BSingleton::m_data;
//修改成:
BSingleton BSingleton::m_data;
ASingleton ASingleton::m_data;
再運行一下,會發現輸出的結果就正常了。 ASingleton constructor!
BSingleton constructor!
BSingleton do_something!    問題解決了,那麼我們通過這個問題實例,我們對於 靜態成員變量 初始化順序沒有保障 有了更深刻的理解了。 在這個簡單的例子中,我們通過調換代碼位置可以保障 靜態成員變量 的初始化順序。但是在實際的編碼中是不可能的,class 文件聲明在頭文件(.h)中,class 的定義在源文件(.cpp)中。而類靜態成員變量聲明是在 .h 文件中,定義在 .cpp 文件中,那麼其初始化順序就完全依靠編譯器的心情了。所以這也就是 類靜態成員變量 實現單例模式的致命缺點。 當然,假如不出現這種:在某單例的構造函數中使用到另一個單例對象 的使用情況,那麼還是可以接受使用的。
三、懶漢模式:單例實例只在第一次被使用時進行初始化:
class Singleton
{
public:
static Singleton* getInstance()
{
if(! m_data) m_data = new Singleton();
return m_data;
}

private:
static Singleton* m_data; //static data member 在類中聲明,在類外定義
Singleton(){}
~Singleton(){}
};

Singleton* Singleton::m_data = nullptr;  getInstance() 只在第一次被調用時為 m_data 分配內存並初始化. 嗯, 看上去所有的問題都解決了, 初始化順序有保證, 多態也沒問題. 但是只是看似沒有問題而已,其實其中存在著兩個問題:
①線程不安全:我們注意到在 static Singleton* getInstance() 方法中,是通過 if 語句判斷 靜態實例變量 是否被初始化來覺得是否進行初始化,那麼在多線程中就有可能出現多次初始化的問題。比方說,有兩個多線程同時進入到這個方法中,同時執行 if 語句的判斷,那麼就會出現兩次兩次初始化靜態實例變量的情況。
②析構函數沒有被執行: 程序退出時, 析構函數沒被執行. 這在某些設計不可靠的系統上會導致資源洩漏, 比如文件句柄, socket 連接, 內存等等. 幸好 Linux / Windows 2000/XP 等常用系統都能在程序退出時自動釋放占用的系統資源. 不過這仍然可能是個隱患。   對於這個問題, 比較土的解決方法是, 給每個 Singleton 類添加一個 destructor() 方法:
virtual bool destructor()
{
// ... release resource
if (nullptr != m_data)
{
delete m_data;
m_data = nullptr;
}  
}    然後在程序退出時確保調用了每個 Singleton 類的 destructor() 方法, 這麼做雖然可靠, 但卻很是繁瑣.
   四、懶漢模式改進版:使用局部靜態變量
class Singleton {
public:
static Singleton& getInstance() {
static Singleton theSingleton;
return theSingleton;
}
/* more (non-static) functions here */

private:
Singleton(); // ctor hidden
Singleton(Singleton const&); // copy ctor hidden
Singleton& operator=(Singleton const&); // assign op. hidden
~Singleton(); // dtor hidden
};
但是這種方式也存在著很多的問題: ①任意兩個單例類的構造函數不能相互引用對方的實例,否則會導致程序崩潰。如: ASingleton& ASingleton::getInstance() {
const BSingleton& b = BSingleton::getInstance();
static ASingleton theSingleton;
return theSingleton;
}

BSingleton& BSingleton::getInstance() {
const ASingleton & b = ASingleton::getInstance();
static BSingleton theSingleton;
return theSingleton;
}
②多個 Singleton 實例相互引用的情況下, 需要謹慎處理析構函數. 如: 初始化順序為 ASingleton ?BSingleton ? CSingleton 的三個 Singleton 類, 其中 ASingleton BSingleton 的析構函數調用了CSingleton 實例的成員函數, 程序退出時, CSingleton 的析構函數 將首先被調用, 導致實例無效, 那麼後續 ASingleton BSingleton 的析構都將失敗, 導致程序異常退出.
③在局部作用域下的靜態變量在編譯時,編譯器會創建一個附加變量標識靜態變量是否被初始化,會被編譯器變成像下面這樣(偽代碼): static Singleton &Instance()
{
static bool constructed = false;
static uninitialized Singleton instance_;
if (!constructed) {
constructed = true;
new(&s) Singleton; //construct it
}
return instance_;
}
那麼,在多線程的應用場合下必須小心使用. 如果唯一實例尚未創建時, 有兩個線程同時調用創建方法, 且它們均沒有檢測到唯一實例的存在, 便會同時各自創建一個實例, 這樣就有兩個實例被構造出來, 從而違反了單例模式中實例唯一的原則. 解決這個問題的辦法是為指示類是否已經實例化的變量提供一個互斥鎖 (雖然這樣會降低效率).
加鎖如下: static Singleton &getInstance()
{
Lock();
//鎖自己實現 static
Singleton instance_;
UnLock();

return instance_;
} 但這樣每次調用instance()都要加鎖解鎖,代價略大。
五、終極方案 在前面的討論中,單例類中的靜態對象無論是作為靜態局部對象還是作為類靜態全局變量都有問題,那麼有什麼更好的解決方案呢? boost 的實現方式是:單例對象作為靜態局部變量,然後增加一個輔助類,並聲明一個該輔助類的類靜態成員變量,在該輔助類的構造函數中,初始化單例對象。 實現如下: class Singleton
{
public:
static Singleton* getInstance()
{
static Singleton instance;
return &instance;
}

protected:
struct Object_Creator
{
Object_Creator()
{
Singleton::getInstance();
}
};
static Object_Creator _object_creator;

Singleton() {}
~Singleton() {}
};
Singleton::Object_Creator Singleton::_object_creator;
在前面的方案中:餓漢模式中,使用到了類靜態成員變量,但是遇到了初始化順序的問題; 懶漢模式中,使用到了靜態局部變量,但是存在著線程安全等問題。 那麼在這個終極方案中可以說綜合了以上兩種方案。即采用到了類靜態成員變量,也采用到了靜態局部變量。
注意到其中的輔助結構體 Object_Creator (可以稱之為 proxy-class)所聲明的類靜態成員變量,初始化該靜態成員變量時,其中的構造函數 調用了單例類的 getInstance 方法。這樣就會調用到 Singleton::getInstance() 方法初始化單例對象,那麼自然 Singleton 的構造函數也就執行了。
我們可以在Singleton 和 Object_Creator 的構造函數中添加一些輸出信息: class Singleton
{
public:
static Singleton* getInstance()
{
static Singleton instance;
return &instance;
}

protected:
struct Object_Creator
{
Object_Creator()
{
cout<<"Object_Creator constructor"< Singleton::getInstance();
}
};
static Object_Creator _object_creator;

Singleton() {cout<<"Singleton constructor"< ~Singleton() {}
};
Singleton::Object_Creator Singleton::_object_creator;
運行我們會看到(在 main 函數中還未使用到該單例): Object_Creator constructor
Singleton constructor 說明,此時在main函數之前就初始化了單例對象。
對於前面的ASingleton 和 BSingleton 的例子,改進如下: class ASingleton
{
public:
static ASingleton* getInstance()
{
static ASingleton instance;
return &instance;
}
void do_something()
{
cout<<"ASingleton do_something!"< }
protected:
struct Object_Creator
{
Object_Creator()
{
ASingleton::getInstance();
}
};
static Object_Creator _object_creator;

ASingleton();
~ASingleton() {}
};


class BSingleton
{
public:
static BSingleton* getInstance()
{
static BSingleton instance;
return &instance;
}
void do_something()
{
cout<<"BSingleton do_something!"< }
protected:
struct Object_Creator
{
Object_Creator()
{
BSingleton::getInstance();
}
};
static Object_Creator _object_creator;

BSingleton();
~BSingleton() {}
};
ASingleton::Object_Creator ASingleton::_object_creator;
BSingleton::Object_Creator BSingleton::_object_creator;

ASingleton::ASingleton()
{
cout<<"ASingleton constructor!"< BSingleton::getInstance()->do_something();
}
BSingleton::BSingleton()
{
cout<<"BSingleton constructor!"< }
這樣程序就避免了 ASingleton 和 BSingleton 單例對象的初始化順序問題,使得輸出結果就始終是: ASingleton constructor!
BSingleton constructor!
BSingleton do_something!
最後,展示一下添加了模板的實現: template <typename T>
class Singleton
{
struct object_creator
{
object_creator()
{
Singleton::instance();
}
inline void do_nothing() const {}
};

static object_creator create_object;

public:
typedef T object_type;
static T& instance()
{
static T obj;
//這個do_nothing是確保create_object構造函數被調用
//這跟模板的編譯有關
create_object.do_nothing();
return obj;
}

};
template <typename T> typename Singleton::object_creator Singleton::create_object;

class QMManager
{
protected:
QMManager() {}
~QMManager() {}
friend class Singleton;
public:
void do_something() {};
};

int main()
{
Singleton::instance().do_something();
return 0;
}
boost 通過添加一個類似 proxy-class 的方式,實現了單例模式,但是顯然增加了復雜性,在實際應用中應該根據實際情況采用適當的實現方案。




  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved