zoukankan      html  css  js  c++  java
  • 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++设计模式——单例模式

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

  • 相关阅读:
    check事件
    JAVA----泛型
    JAVA集合的遍历for循环、Iterator迭代器
    JAVA 集合的定义分类
    JAVA面向对象---方法的重写与重载
    JAVA 面向对象---继承
    JAVA 面向对象
    JAVA ArrayList集合
    JAVA基础测试
    JAVA中 引用数据类型(类)
  • 原文地址:https://www.cnblogs.com/xiehongfeng100/p/4781013.html
Copyright © 2011-2022 走看看