最近遇到几道类似的笔试题:
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;
参考资料