C++中的单例模式

  最近遇到几道类似的笔试题:

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

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

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

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

单例模式

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

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

 // 程序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;

  该程序保证在不调用类中的静态函数的情况下,不能够在类外创建该类的实例(因为构造函数为私有函数);另外,在非多线程模式下只能创建该类的一个实例。

  注:

  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++设计模式——单例模式

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

 

posted @ 2015-09-04 14:34  峰子_仰望阳光  阅读(4361)  评论(0编辑  收藏  举报