剑指offer面试题(2)——实现Singleton模式

转自:我想有个长长的名字的博客,剑指offer 面试题2 Singleton模式 C++实现

题目:实现Singleton模式

​ 以下内容是我在看《剑指offer》的面试题2时,遇到的问题,因为书中使用C#实现,所以想用C++重新实现一下,Test方法不够全,后续还要完善。C++实现过程主要参考:C++设计模式——单例模式

​ 代码中的注释一般是我的笔记,或一些发现。

​ PS: 感谢勤劳的慵懒君~~ @亦余心之所向兮

1 解法一:单线程解法

缺点:多线程情况下,每个线程可能创建出不同的Singleton实例

 
  1. // 剑指offer 面试题2 实现Singleton模式

  2. #include <iostream>

  3. using namespace std;

  4.  
  5. class Singleton

  6. {

  7. public:

  8. static Singleton* getInstance()

  9. {

  10. // 在后面的Singleton实例初始化时,若后面是new Singleton(),则此处不必new;(废话)

  11. // 若后面是赋值成NULL,则此处需要判断,需要时new

  12. // 注意!然而这两种方式并不等价!后面的Singleton实例初始化时,new Singleton(),其实是线程安全的,因为static初始化是在主函数main()之前,那么后面的方法岂不是很麻烦。。。。这也是我测试的时候想到的

  13. /*

  14. if(m_pInstance == NULL)

  15. {

  16. m_pInstance = new Singleton();

  17. }

  18. */

  19. return m_pInstance;

  20. }

  21.  
  22. static void destroyInstance()

  23. {

  24. if(m_pInstance != NULL)

  25. {

  26. delete m_pInstance;

  27. m_pInstance = NULL;

  28. } }

  29.  
  30. private:

  31. Singleton(){}

  32. static Singleton* m_pInstance;

  33. };

  34.  
  35. // Singleton实例初始化

  36. Singleton* Singleton::m_pInstance = new Singleton(); // 前面不能加static,会和类外全局static混淆

  37.  
  38. // 单线程获取多次实例

  39. void Test1(){

  40. // 预期结果:两个实例指针指向的地址相同

  41. Singleton* singletonObj = Singleton::getInstance();

  42. cout << singletonObj << endl;

  43.  
  44. Singleton* singletonObj2 = Singleton::getInstance();

  45. cout << singletonObj2 << endl;

  46.  
  47. Singleton::destroyInstance();

  48. }

  49.  
  50. int main(){

  51. Test1();

  52. return 0;

  53. }

2 解法二:多线程+加锁

​ 解法1是最简单,也是最普遍的实现方式,也是现在网上各个博客中记述的实现方式,但是,这种实现方式,有很多问题,比如:没有考虑到多线程的问题,在多线程的情况下,就可能创建多个Singleton实例,以下版本是改善的版本。 
​ 注意:下面的代码涉及互斥锁以及多线程测试,使用了C++11的多线程库,std::thread,,std::mutex,请使用支持C++11多线程的编译器,并确认开启了C++11的编译选项,具体方法见:http://blog.csdn.net/huhaijing/article/details/51753085

 
  1. #include <iostream>

  2. #include <mutex>

  3. #include <thread>

  4. #include <vector>

  5. using namespace std;

  6.  
  7. class Singleton

  8. {

  9. private:

  10. static mutex m_mutex; // 互斥量

  11.  
  12. Singleton(){}

  13. static Singleton* m_pInstance;

  14.  
  15. public:

  16. static Singleton* getInstance(){

  17. if(m_pInstance == NULL){

  18. m_mutex.lock(); // 使用C++11中的多线程库

  19. if(m_pInstance == NULL){ // 两次判断是否为NULL的双重检查

  20. m_pInstance = new Singleton();

  21. }

  22. m_mutex.unlock();

  23. }

  24. return m_pInstance;

  25. }

  26.  
  27. static void destroyInstance(){

  28. if(m_pInstance != NULL){

  29. delete m_pInstance;

  30. m_pInstance = NULL;

  31. }

  32. }

  33. };

  34.  
  35. Singleton* Singleton::m_pInstance = NULL; // 所以说直接new 多好啊,可以省去Lock/Unlock的时间

  36. mutex Singleton::m_mutex;

  37.  
  38.  
  39. void print_singleton_instance(){

  40. Singleton *singletonObj = Singleton::getInstance();

  41. cout << singletonObj << endl;

  42. }

  43.  
  44. // 多个进程获得单例

  45. void Test1(){

  46. // 预期结果,打印出相同的地址,之间可能缺失换行符,也属正常现象

  47. vector<thread> threads;

  48. for(int i = 0; i < 10; ++i){

  49. threads.push_back(thread(print_singleton_instance));

  50. }

  51.  
  52. for(auto& thr : threads){

  53. thr.join();

  54. }

  55. }

  56.  
  57. int main(){

  58. Test1();

  59. Singleton::destroyInstance();

  60. return 0;

  61. }

​ 此处进行了两次m_pInstance == NULL的判断,是借鉴了Java的单例模式实现时,使用的所谓的“双检锁”机制。因为进行一次加锁和解锁是需要付出对应的代价的,而进行两次判断,就可以避免多次加锁与解锁操作,同时也保证了线程安全。但是,如果进行大数据的操作,加锁操作将成为一个性能的瓶颈;为此,一种新的单例模式的实现也就出现了。

3 解法三:const static 型实例

 
  1. #include <iostream>

  2. #include <thread>

  3. #include <vector>

  4. using namespace std;

  5.  
  6. class Singleton

  7. {

  8. private:

  9. Singleton(){}

  10. static const Singleton* m_pInstance;

  11. public:

  12. static Singleton* getInstance(){

  13.  
  14. return const_cast<Singleton *>(m_pInstance); // 去掉“const”特性

  15. // 注意!若该函数的返回值改为const static型,则此处不必进行const_cast静态转换

  16. // 所以该函数可以改为:

  17. /*

  18. const static Singleton* getInstance(){

  19. return m_pInstance;

  20. }

  21. */

  22. }

  23.  
  24. static void destroyInstance(){

  25. if(m_pInstance != NULL){

  26. delete m_pInstance;

  27. m_pInstance = NULL;

  28. }

  29. }

  30. };

  31. const Singleton* Singleton::m_pInstance = new Singleton(); // 利用const只能定义一次,不能再次修改的特性,static继续保持类内只有一个实例

  32.  
  33. void print_singleton_instance(){

  34. Singleton *singletonObj = Singleton::getInstance();

  35. cout << singletonObj << endl;

  36. }

  37.  
  38. // 多个进程获得单例

  39. void Test1(){

  40. // 预期结果,打印出相同的地址,之间可能缺失换行符,也属正常现象

  41. vector<thread> threads;

  42. for(int i = 0; i < 10; ++i){

  43. threads.push_back(thread(print_singleton_instance));

  44. }

  45.  
  46. for(auto& thr : threads){

  47. thr.join();

  48. }

  49. }

  50.  
  51. int main(){

  52. Test1();

  53. Singleton::destroyInstance();

  54. return 0;

  55. }

​ 因为静态初始化在程序开始时,也就是进入主函数之前,由主线程以单线程方式完成了初始化,所以静态初始化实例保证了线程安全性。在性能要求比较高时,就可以使用这种方式,从而避免频繁的加锁和解锁造成的资源浪费。由于上述三种实现,都要考虑到实例的销毁,关于实例的销毁,待会在分析。

4 解法四:在get函数中创建并返回static临时实例的引用

PS:该方法不能人为控制单例实例的销毁

 
  1. #include <iostream>

  2. #include <thread>

  3. #include <vector>

  4. using namespace std;

  5.  
  6. class Singleton

  7. {

  8. private:

  9. Singleton(){}

  10.  
  11. public:

  12. static Singleton* getInstance(){

  13. static Singleton m_pInstance; // 注意,声明在该函数内

  14. return &m_pInstance;

  15. }

  16. };

  17.  
  18. void print_singleton_instance(){

  19. Singleton *singletonObj = Singleton::getInstance();

  20. cout << singletonObj << endl;

  21. }

  22.  
  23. // 多个进程获得单例

  24. void Test1(){

  25. // 预期结果,打印出相同的地址,之间可能缺失换行符,也属正常现象

  26. vector<thread> threads;

  27. for(int i = 0; i < 10; ++i){

  28. threads.push_back(thread(print_singleton_instance));

  29. }

  30.  
  31. for(auto& thr : threads){

  32. thr.join();

  33. }

  34. }

  35.  
  36. // 单个进程获得多次实例

  37. void Test2(){

  38. // 预期结果,打印出相同的地址,之间换行符分隔

  39. print_singleton_instance();

  40. print_singleton_instance();

  41. }

  42.  
  43. int main(){

  44. cout << "Test1 begins: " << endl;

  45. Test1();

  46. cout << "Test2 begins: " << endl;

  47. Test2();

  48. return 0;

  49. }

以上就是四种主流的单例模式的实现方式。

5 解法五:最终方案,最简&显式控制实例销毁

​ 在上述的四种方法中,除了第四种没有使用new操作符实例化对象以外,其余三种都使用了;

​ 我们一般的编程观念是,new操作是需要和delete操作进行匹配的;是的,这种观念是正确的。在上述的实现中,是添加了一个destoryInstance的static函数,这也是最简单,最普通的处理方法了;但是,很多时候,我们是很容易忘记调用destoryInstance函数,就像你忘记了调用delete操作一样。由于怕忘记delete操作,所以就有了智能指针;那么,在单例模型中,没有“智能单例”,该怎么办?怎么办?

​ 在实际项目中,特别是客户端开发,其实是不在乎这个实例的销毁的。因为,全局就这么一个变量,全局都要用,它的生命周期伴随着软件的生命周期,软件结束了,它也就自然而然的结束了,因为一个程序关闭之后,它会释放它占用的内存资源的,所以,也就没有所谓的内存泄漏了。

​ 但是,有以下情况,是必须需要进行实例销毁的:

  1. 在类中,有一些文件锁了,文件句柄,数据库连接等等,这些随着程序的关闭而不会立即关闭的资源,必须要在程序关闭前,进行手动释放;
  2. 具有强迫症的程序员。

​ 在代码实现部分的第四种方法能满足第二个条件,但是无法满足第一个条件。好了,接下来,就介绍一种方法,这种方法也是我从网上学习而来的,代码实现如下:

 
  1. #include <iostream>

  2. #include <thread>

  3. #include <vector>

  4. using namespace std;

  5.  
  6. class Singleton

  7. {

  8. private:

  9. Singleton(){}

  10. static Singleton* m_pInstance;

  11.  
  12. // **重点在这**

  13. class GC // 类似Java的垃圾回收器

  14. {

  15. public:

  16. ~GC(){

  17. // 可以在这里释放所有想要释放的资源,比如数据库连接,文件句柄……等等。

  18. if(m_pInstance != NULL){

  19. cout << "GC: will delete resource !" << endl;

  20. delete m_pInstance;

  21. m_pInstance = NULL;

  22. }

  23. };

  24. };

  25.  
  26. // 内部类的实例

  27. static GC gc;

  28.  
  29. public:

  30. static Singleton* getInstance(){

  31. return m_pInstance;

  32. }

  33. };

  34.  
  35.  
  36. Singleton* Singleton::m_pInstance = new Singleton();

  37. Singleton::GC Singleton::gc;

  38.  
  39. void print_instance(){

  40. Singleton* obj1 = Singleton::getInstance();

  41. cout << obj1 << endl;

  42. }

  43.  
  44. // 多线程获取单例

  45. void Test1(){

  46. // 预期输出:相同的地址,中间可能缺失换行符,属于正常现象

  47. vector<thread> threads;

  48. for(int i = 0; i < 10; ++i){

  49. threads.push_back(thread(print_instance));

  50. }

  51.  
  52. for(auto& thr : threads){

  53. thr.join();

  54. }

  55. }

  56.  
  57. // 单线程获取单例

  58. void Test2(){

  59. // 预期输出:相同的地址,换行符分隔

  60. print_instance();

  61. print_instance();

  62. print_instance();

  63. print_instance();

  64. print_instance();

  65. }

  66.  
  67. int main()

  68. {

  69. cout << "Test1 begins: " << endl;

  70. cout << "预期输出:相同的地址,中间可以缺失换行(每次运行结果的排列格式通常不一样)。" << endl;

  71. Test1();

  72. cout << "Test2 begins: " << endl;

  73. cout << "预期输出:相同的地址,每行一个。" << endl;

  74. Test2();

  75. return 0;

  76. }

​ 在程序运行结束时,系统会调用Singleton的静态成员GC的析构函数,该析构函数会进行资源的释放,而这种资源的释放方式是在程序员“不知道”的情况下进行的,而程序员不用特别的去关心,使用单例模式的代码时,不必关心资源的释放。

​ 那么这种实现方式的原理是什么呢?由于程序在结束的时候,系统会自动析构所有的全局变量,系统也会析构所有类的静态成员变量,因为静态变量和全局变量在内存中,都是存储在静态存储区的,所有静态存储区的变量都会被释放。

​ 由于此处使用了一个内部GC类,而该类的作用就是用来释放资源,而这种使用技巧在C++中是广泛存在的,参见《C++中的RAII机制》

运行结果: 
这里写图片描述

 

转载于:https://my.oschina.net/u/4000302/blog/3016942

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值