C++中的单例模式

  最近遇到几道相似的笔试题:设计模式

  1. 请实现一个单例模式的类,要求线程安全。安全

  2. 用C++设计一个不能被继承的类。多线程

  3. 如何定义一个只能在堆上(栈上)生成对象的类?函数

  这些题目本质上都跟单例模式相关。测试

单例模式

  单例模式就是保证一个类只有一个实例,并提供一个访问它的全局访问点。首先,须要保证一个类只有一个实例;在类中,要构造一个实例,就必须调用类的构造函数,如此,为了防止在外部调用类的构造函数而构造实例,须要将构造函数的访问权限标记为protected或private;最后,须要提供要给全局访问点,就须要在类中定义一个static函数,返回在类内部惟一构造的实例。ui

  下边就是一个常见的单例模式程序例子:spa

 // 程序1
1
class Singleton 2 { 3 private: 4 Singleton(){} 5 ~Singleton(){} 6 static Singleton *pInstance; 7 8 public: 9 static Singleton *GetInstance() // 对GetInstance稍加修改,这个设计模板即可以适用于可变多实例状况,如一个类容许最多五个实例。 10 { 11 if (pInstance == NULL) //判断是否第一次调用 12 { 13 pInstance = new Singleton (); 14 } 15 return pInstance; 16 } 17 18 static void DestoryInstance() 19 { 20 if (pInstance != NULL) 21 { 22 delete pInstance; 23 pInstance = NULL; 24 } 25 } 26 27 }; 28 29 Singleton *Singleton ::pInstance = NULL;

  该程序保证在不调用类中的静态函数的状况下,不可以在类外建立该类的实例(由于构造函数为私有函数);另外,在非多线程模式下只能建立该类的一个实例。.net

  注:线程

  1. 由于上述构造函数析构函数为私有函数,因此该类是没法被继承的,知足文章开头提到的第二题。设计

  2. 该类的实例只能被建立在堆上(new),由于析构函数被声明为私有函数,知足文章开头提到的第三题。具体缘由摘自博文如何限制对象只能创建在堆上或者栈上:  

  “ 

  在C++中,类的对象创建分为两种,一种是静态创建,如A a;另外一种是动态创建,如A* ptr=new A;这两种方式是有区别的。

  静态创建一个类对象,是由编译器为对象在栈空间中分配内存,是经过直接移动栈顶指针,挪出适当的空间,而后在这片内存空间上调用构造函数造成一个栈对象。使用这种方法,直接调用类的构造函数。

  动态创建类对象,是使用new运算符将对象创建在堆空间中。这个过程分为两步,第一步是执行operator new()函数,在堆空间中搜索合适的内存并进行分配;第二步是调用构造函数构造对象,初始化这片内存空间。这种方法,间接调用类的构造函数。

  ... ...

  类对象只能创建在堆上,就是不能静态创建类对象,即不能直接调用类的构造函数。

  容易想到将构造函数设为私有。在构造函数私有以后,没法在类外部调用构造函数来构造类对象,只能使用new运算符来创建对象。然而,前面已经说过,new运算符的执行过程分为两步,C++提供new运算符的重载,实际上是只容许重载operator new()函数,而operator()函数用于分配内存,没法提供构造功能。所以,这种方法不能够。

  当对象创建在栈上面时,是由编译器分配内存空间的,调用构造函数来构造栈对象。当对象使用完后,编译器会调用析构函数来释放栈对象所占的空间。编译器管理了对象的整个生命周期。若是编译器没法调用类的析构函数,状况会是怎样的呢?好比,类的析构函数是私有的,编译器没法调用析构函数来释放内存。因此,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性,其实不光是析构函数,只要是非静态的函数,编译器都会进行检查。若是类的析构函数是私有的,则编译器不会在栈空间上为类对象分配内存。

  ... ...

  只有使用new运算符,对象才会创建在堆上,所以,只要禁用new运算符就能够实现类对象只能创建在栈上。将operator new()设为私有便可。代码以下:

1 class A
2 {
3 private:
4     void* operator new(size_t t){}     // 注意函数的第一个参数和返回值都是固定的
5     void operator delete(void* ptr){}  // 重载了new就须要重载delete
6 public:
7     A(){}
8     ~A(){}
9 };

  ” 

自动析构实例

  咱们知道,对于类Singleton的实例,最后咱们须要显式调用DestroyInstance函数来释放内存。那有没有一种方法可让程序自动析构实例呢?

  要自动析构实例,这里咱们须要用到C++中的RAII(Resource Acquisition Is Initialization)机制。具体地,咱们在类Singleton中在声明一个静态类(析构函数释放Singleton实例内存)并定义一个该类的静态实例。这样,在Singleton实例被析构时,该静态实例的析构函数会被自动调用,因此最终可以将Singleton实例的内存自动释放掉。具体程序以下:

 // 程序2
1
class Singleton 2 { 3 private: 4 Singleton(){} 5 ~Singleton(){} 6 static Singleton *pInstance; 7 8 class Garbo //它的惟一工做就是在析构函数中删除Singleton的实例 9 { 10 public: 11 ~Garbo() 12 { 13 if (pInstance != NULL) 14 { 15 delete pInstance; 16 pInstance = NULL; 17 cout << "Delete instance!" << endl; 18 } 19 } 20 }; 21 static Garbo garbo; //定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数 22 23 public: 24 static Singleton *GetInstance() // 对GetInstance稍加修改,这个设计模板即可以适用于可变多实例状况,如一个类容许最多五个实例。 25 { 26 if (pInstance == NULL) //判断是否第一次调用 27 { 28 pInstance = new Singleton(); 29 cout << "Create instance" << endl; 30 } 31 return pInstance; 32 } 33 34 }; 35 36 Singleton *Singleton::pInstance = NULL; 37 Singleton::Garbo Singleton::garbo;

  这个程序可能会显得麻烦臃肿,咱们能够改进成这个样子:

 // 程序3
1
class Singleton 2 { 3 private: 4 Singleton(){} // 构造函数是私有的 5 // ~Singleton(){} // 在这里不能够声明为private。由于咱们在函数GetInstance声明定义了位于栈上的变量, 6 // 这样程序结束时会自动调用析构函数(为private则调用不了,编译不经过). 7 8 public: 9 static Singleton& GetInstance() 10 { 11 static Singleton instance; // 局部静态变量 12 return instance; 13 } 14 }; 15 16 int main() 17 { 18 Singleton singleton1 = Singleton::GetInstance(); 19 Singleton singleton2 = singleton1; 20 cout << &singleton1 << endl; 21 cout << &singleton2 << endl; 22 23 return 0; 24 }

  这一下,程序简洁又可以在程序运行结束时自动释放实例内存。但咱们发现,在测试(main函数)时,咱们发现singleton1和singleton2的地址并不同,也就是说,这个程序存在漏洞,即经过默认拷贝函数能够生成不止一个类的实例。不过咱们能够考虑将默认拷贝函数和默认赋值函数权限设定为private或protect:

 // 程序4
1
class Singleton 2 { 3 private: 4 Singleton(){} // 构造函数是私有的 5 Singleton(const Singleton& orig){}; 6 Singleton& operator=(const Singleton& orig){}; 7 // ~Singleton(){} // 在这里不能够声明为private。由于咱们在函数GetInstance声明定义了位于栈上的变量, 8 // 这样程序结束时会自动调用析构函数(为private则调用不了,编译不经过). 9 10 public: 11 static Singleton& GetInstance() 12 { 13 static Singleton instance; // 局部静态变量 14 return instance; 15 } 16 }; 17 18 int main() 19 { 20 Singleton singleton1 = Singleton::GetInstance(); // 通不过编译,实际会调用默认拷贝函数 21 Singleton singleton2 = singleton1; // 通不过编译,由于会调用默认拷贝函数 22 cout << &singleton1 << endl; 23 cout << &singleton2 << endl; 24 25 return 0; 26 }

  接下来,咱们继续改进这个程序:

 // 程序5
1
class Singleton 2 { 3 private: 4 Singleton(){} // 构造函数是私有的 5 // ~Singleton(){} // 在这里不能够声明为private。由于咱们在函数GetInstance声明定义了位于栈上的变量, 6 // 这样程序结束时会自动调用析构函数(为private则调用不了,编译不经过). 7 8 public: 9 ~Singleton(){ cout << "~Singleton is called!" << endl; } 10 static Singleton* GetInstance() 11 { 12 static Singleton instance; // 局部静态变量 13 return &instance; 14 } 15 }; 16 17 int main() 18 { 19 Singleton *singleton1 = Singleton::GetInstance(); 20 Singleton *singleton2 = singleton1; 21 Singleton *singleton3 = Singleton::GetInstance(); 22 cout << singleton1 << endl; 23 cout << singleton2 << endl; 24 cout << singleton3 << endl; 25 26 return 0; 27 }

  程序运行结果以下:

  

  结果证实了最后改进的这个程序可以只生成一个类的实例,并且在程序结束时可以自动调用析构函数释放内存。

考虑多线程

   对于程序5而言,不存在线程竞争的问题;但对程序1和程序2而言是存在这个问题的。这里以程序2为例来讲明如何避免线程竞争:

 1 class Singleton
 2 {
 3 private:
 4     Singleton(){}
 5     ~Singleton(){}
 6     static Singleton *pInstance;
 7 
 8     class Garbo                           //它的惟一工做就是在析构函数中删除Singleton的实例  
 9     {
10     public:
11         ~Garbo()
12         {
13             if (pInstance != NULL)
14             {
15                 Lock();
16                 if (pInstance != NULL)
17                 {
18                     delete pInstance;
19                     pInstance = NULL;
20                     cout << "Delete instance!" << endl;
21                 }
22                 Unlock();
23             }
24         }
25     };
26     static Garbo garbo;                  //定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数 
27 
28 public:
29     static Singleton *GetInstance()        // 对GetInstance稍加修改,这个设计模板即可以适用于可变多实例状况,如一个类容许最多五个实例。
30     {
31         if (pInstance == NULL)            //判断是否第一次调用
32         {
33             Lock();
34             if (pInstance == NULL)        // 此处进行了两次m_Instance == NULL的判断,是借鉴了Java的单例模式实现时,
35                                           // 使用的所谓的“双检锁”机制。由于进行一次加锁和解锁是须要付出对应的代价的,
36                                           // 而进行两次判断,就能够避免屡次加锁与解锁操做,同时也保证了线程安全。
37             {
38                 pInstance = new Singleton();
39                 cout << "Create instance" << endl;
40             }
41             Unlock();
42         }
43         return pInstance;
44     }
45 
46 };
47 
48 Singleton *Singleton::pInstance = NULL;
49 Singleton::Garbo Singleton::garbo;

参考资料

  C++中的单例模式

  C++设计模式——单例模式

  如何限制对象只能创建在堆上或者栈上

相关文章
相关标签/搜索