C++——單例模式的原理及實現
(一)定義
單例模式,屬於創建類型的一種常用的軟件設計模式。通過單例模式的方法創建的類在當前進程中只有一個實例(根據需要,也有可能一個線程中屬於單例,如:僅線程上下文內使用同一個實例)。
(二)簡介
單例模式是設計模式中最簡單的形式之一。這一模式的目的是使得類的一個對象成為系統中的唯一實例。要實現這一點,可以從客戶端對其進行實例化開始。因此需要用一種只允許生成對象類的唯一實例的機制,“阻止”所有想要生成對象的訪問。使用工廠方法來限制實例化過程。這個方法應該是靜態方法(類方法),因為讓類的實例去生成另一個唯一實例毫無意義。這是百度的解釋,以我個人的觀點來說的話,其實就是在整個程序中整個類只能實例化出一個對象。通俗來講,就是在某些場景下,我們之能有一個對象,例如,一個系統中可以存在多個打印任務,但是只能有一個正在工作的任務;一個系統只能有一個窗口管理器或文件系統;一個系統只能有一個計時工具或ID(序號)生成器。如在Windows中就只能打開一個任務管理器。如果不使用機制對窗口對象進行唯一化,將彈出多個窗口,如果這些窗口顯示的內容完全一致,則是重復對象,浪費內存資源;如果這些窗口顯示的內容不一致,則意味着在某一瞬間系統有多個狀態,與實際不符,也會給用戶帶來誤解,不知道哪一個才是真實的狀態。因此有時確保系統中某個對象的唯一性即一個類只能有一個實例非常重要。
(三)具體實現
首先,我們先大致講下我們要用到的知識:靜態成員變量、靜態成員函數
1.靜態成員變量
靜態變量(Static Variable)在計算機編程領域指在程序執行前系統就為之靜態分配(也即在運行時中不再改變分配情況)存儲空間的一類變量。與之相對應的是在運行時只暫時存在的自動變量(即局部變量)與以動態分配方式獲取存儲空間的一些對象,其中自動變量的存儲空間在調用棧上分配與釋放。
我們用一段代碼演示一下:
1 #include<iostream> 2 using namespace std; 3 class Person 4 { 5 public: 6 int a; //定義兩個變量,一個普通變量,一個靜態變量 7 static int b; 8 }; 9 int Person::b = 111; //靜態變量只能在類外復制,並且需要聲明作用域 10 void test01() 11 { 12 Person p1,p2; //定義兩個類p1和p2 13 p1.a = 100; //p1的a賦值100 14 cout<<"p1.b: "<<p1.b<<" p2.b: "<<p2.b<<endl; //通過該步的輸出,我們發現p1和p2的b同時被9行的代碼給賦值了 15 p1.b = 20; //我們修改p1.b 16 p2.a = 3; 17 cout<<"p1.a: "<<p1.a<<" p2.a: "<<p2.a<<endl; 18 cout<<"p1.b: "<<p1.b<<" p2.b: "<<p2.b<<endl; //修改p1.b后,我們注意觀察p1和p2的b 19 p2.b = 123; 20 cout<<"p1.b: "<<p1.b<<" p2.b: "<<p2.b<<endl; //修改p2.b后,我們注意觀察p1和p2的b 21 } 22 int main(int argc, char const *argv[]) 23 { 24 test01(); 25 return 0; 26 }
運行結果:
這段代碼是為了給大家演示一下靜態變量的情況,首先我們是在類中定義了兩個變量,一個是普通變量a,一個是靜態變量b。接着我們要注意一下靜態變量的賦值:
①靜態變量不能在類內賦值。
②靜態變量在全局變量賦值時要聲明作用域。
按照上面的約束,我們在給b賦值111之后,在test01()中先實例化出兩個對象p1和p2,並把p1.a賦值100,接着輸出p1.b和p2.b,我們發現兩個值都是111,接着我們又修改p1.b=20,再輸出我們會發現p1.b和p2.b都等於20,而p1.a和p2.a我們可以對比看出,兩者互不影響。再接着,我們修改p2.b=123,再輸出我們又發現兩者結果又同時被修改,這就是靜態變量,從這個類實例出的對象的靜態成員變量是共享的。
小總結一下:
①靜態變量不能在類內賦值。
②靜態變量在全局變量賦值時要聲明作用域。
③這個類實例出的對象的靜態成員變量是共享。
④靜態成員變量在類內聲明,聲明的作用只是限制靜態成員變量作用域。
⑤靜態成員變量存放在靜態全局區。
2.靜態成員函數
靜態成員函數主要用來訪問靜態數據成員而不訪問非靜態數據成員,我們只需要記住下面三點即可:
①靜態成員函數只能訪問靜態成員變量,不能訪問非靜態成員變量。
②可以通過類的作用域訪問靜態成員變量。
③可以通過對象訪問靜態成員變量。
上代碼
1 #include<iostream> 2 using namespace std; 3 class Person 4 { 5 public: 6 static void fun() 7 { 8 cout<<"靜態成員函數"<<b<<endl; 9 } 10 int a; //定義兩個變量,一個普通變量,一個靜態變量 11 static int b; 12 }; 13 int Person::b = 111; //靜態變量只能在類外復制,並且需要聲明作用域 14 void test01() 15 { 16 Person p1,p2; //定義兩個類p1和p2 17 p1.fun(); 18 p2.fun(); 19 } 20 int main(int argc, char const *argv[]) 21 { 22 test01(); 23 return 0; 24 }
運行結果
這段代碼應該沒什么可講的,就是通過fun()函數訪問靜態成員變量b,只需要記住上面的幾點即可。
后面將進入我們今天的主菜。
3.單例模式
在文章的開頭我已經給大家介紹了單例模式,大家看了定義之后,覺得應該如何去讓類只能實例化出一個對象呢?大家首先想到的肯定是限制它的構造函數吧,沒錯,第一步就是限制構造函數,這樣我們就無法使用普通的實例化方法去創建新的對象了(這一步的代碼就不用我寫了吧,把構造函數私有化即可)。接着第二步呢?很明顯,我們要到前面講的靜態成員變量和靜態成員函數吧。我們定義一個靜態指針變量同時再提供一個唯一的接口函數來訪問這個靜態變量。代碼如下:
#include<iostream> using namespace std; #include<string.h> class Person { public: int age; string name; void show() { cout<<age<<" "<<name<<endl; } static Person* Createobj() { if(single == nullptr) { single = new Person; } return single; } ~Person() { cout<<"析構函數"<<endl; } private: Person() { cout<<"構造函數"<<endl; } static Person* single; }; Person* Person::single = nullptr; void test01() { Person *p1 = Person::Createobj(); Person *p2 = Person::Createobj(); p1->age = 10; p1->name = "stronger"; p2->age = 20; p2->name = "zjf"; p1->show(); p2->show(); } int main(int argc, char const *argv[]) { test01(); return 0; }
運行結果:
通過上面的運行結果,我們發現只調用一次構造函數,並且兩個對象內容都是一直的,這是實現單例模式最簡單的方式,但會造成內存泄漏的問題,因為沒有調用析構函數,要想釋放的話,我們還要手動去釋放,太麻煩了,並且在線程安全的時候也會遇到問題。那么我們如何解決這個問題呢?大家看下下面這段代碼:
1 #include<iostream> 2 using namespace std; 3 #include<string.h> 4 class Person 5 { 6 public: 7 int age; 8 string name; 9 void show() 10 { 11 cout<<age<<" "<<name<<endl; 12 } 13 static Person* Createobj() 14 { 15 static Person obj; 16 return &obj; 17 } 18 ~Person() 19 { 20 cout<<"析構函數"<<endl; 21 } 22 private: 23 Person() 24 { 25 cout<<"構造函數"<<endl; 26 } 27 }; 28 void test01() 29 { 30 Person *p1 = Person::Createobj(); 31 Person *p2 = Person::Createobj(); 32 p1->age = 10; 33 p1->name = "stronger"; 34 p2->age = 20; 35 p2->name = "zjf"; 36 p1->show(); 37 p2->show(); 38 } 39 int main(int argc, char const *argv[]) 40 { 41 test01(); 42 return 0; 43 }
運行結果:
通過運行結果,我們可以看到,這次的方法成功了,既只有一個對象,也發生了析構。怎么做到的呢?我們是通過在接口函數中創建了一個靜態對象,然后返回這個對象的地址,這樣只要后面調用接口函數,都會是對這個靜態對象操作,所以滿足單例模式,也不會有內存泄漏的問題。但是這樣做仍有問題,用戶可能會用delete p1的方法來提前銷毀對象,但我們的對象不是new 出來的,如果用了delete 程序會出錯,那怎么辦嘞?
1 #include<iostream> 2 using namespace std; 3 #include<string.h> 4 class Person 5 { 6 public: 7 int age; 8 string name; 9 void show() 10 { 11 cout<<age<<" "<<name<<endl; 12 } 13 static Person& Createobj() 14 { 15 static Person obj; 16 return obj; 17 } 18 ~Person() 19 { 20 cout<<"析構函數"<<endl; 21 } 22 private: 23 Person() 24 { 25 cout<<"構造函數"<<endl; 26 } 27 }; 28 void test01() 29 { 30 Person& p1 = Person::Createobj(); 31 Person& p2 = Person::Createobj(); 32 p1.age = 10; 33 p1.name = "stronger"; 34 p2.age = 20; 35 p2.name = "zjf"; 36 p1.show(); 37 p2.show(); 38 } 39 int main(int argc, char const *argv[]) 40 { 41 test01(); 42 return 0; 43 }
我們對上面的代碼做一下改動,我們不用指針來接了,而是讓接口函數返回對象的引用,然后創建時也用引用來接。這樣用戶就不能用delete來刪除了。但是這樣的話,用戶還能通過另外一種類似bug的方法再次新建一個對象。
1 #include<iostream> 2 using namespace std; 3 #include<string.h> 4 class Person 5 { 6 public: 7 int age; 8 string name; 9 void show() 10 { 11 cout<<age<<" "<<name<<endl; 12 } 13 static Person& Createobj() 14 { 15 static Person obj; 16 return obj; 17 } 18 ~Person() 19 { 20 cout<<"析構函數"<<endl; 21 } 22 Person(const Person &obj) 23 { 24 cout<<"拷貝構造"<<endl; 25 } 26 private: 27 Person() 28 { 29 cout<<"構造函數"<<endl; 30 } 31 }; 32 void test01() 33 { 34 Person& p1 = Person::Createobj(); 35 Person p2 = Person::Createobj(); 36 p1.age = 10; 37 p1.name = "stronger"; 38 p2.age = 20; 39 p2.name = "zjf"; 40 p1.show(); 41 p2.show(); 42 } 43 int main(int argc, char const *argv[]) 44 { 45 test01(); 46 return 0; 47 }
注意看第35行代碼,我們不用引用去接創建函數了,而是用拷貝函數的方法又新建出一個對象了,所以說要想消除這個bug,我們需要將拷貝構造也私有化。這樣用戶就無法再用拷貝函數了。或者也可以不用私有,我們可以將拷貝構造改成下面的樣子。
1 Person(const Person &obj) = delete;
這樣即使用了拷貝構造,也會被delete,就不會再創建出第二個對象了。
上面便是實現單例模式的方法。
(四)總結
要想實現單例模式,有下面幾步:
①私有化構造函數和拷貝函數。
②創建一個靜態對象,並提供接口函數,返回該靜態變量的引用,這樣在引用該對象創建對象時都是針對的該對象,就無法再創建第二個了。