剑指offer 面试题2 Singleton模式 C++实现
2016-06-24 22:55
399 查看
参考:
1、题目来源《剑指offer 名企面试官精讲典型编程题 纪念版-何海涛 著》
2、C++设计模式——单例模式: http://www.jellythink.com/archives/82
3、C++11多线程教程:/article/4525725.html
题目:实现Singleton模式
以下内容是我在看《剑指offer》的面试题2时,遇到的问题,因为书中使用C#实现,所以想用C++重新实现一下,Test方法不够全,后续还要完善。C++实现过程主要参考:C++设计模式——单例模式。
代码中的注释一般是我的笔记,或一些发现。
PS: 感谢勤劳的慵懒君~~ @亦余心之所向兮
注意:下面的代码涉及互斥锁以及多线程测试,使用了C++11的多线程库,std::thread,,std::mutex,请使用支持C++11多线程的编译器,并确认开启了C++11的编译选项,具体方法见:/article/11891447.html
此处进行了两次m_pInstance == NULL的判断,是借鉴了Java的单例模式实现时,使用的所谓的“双检锁”机制。因为进行一次加锁和解锁是需要付出对应的代价的,而进行两次判断,就可以避免多次加锁与解锁操作,同时也保证了线程安全。但是,如果进行大数据的操作,加锁操作将成为一个性能的瓶颈;为此,一种新的单例模式的实现也就出现了。
因为静态初始化在程序开始时,也就是进入主函数之前,由主线程以单线程方式完成了初始化,所以静态初始化实例保证了线程安全性。在性能要求比较高时,就可以使用这种方式,从而避免频繁的加锁和解锁造成的资源浪费。由于上述三种实现,都要考虑到实例的销毁,关于实例的销毁,待会在分析。
以上就是四种主流的单例模式的实现方式。
我们一般的编程观念是,new操作是需要和delete操作进行匹配的;是的,这种观念是正确的。在上述的实现中,是添加了一个destoryInstance的static函数,这也是最简单,最普通的处理方法了;但是,很多时候,我们是很容易忘记调用destoryInstance函数,就像你忘记了调用delete操作一样。由于怕忘记delete操作,所以就有了智能指针;那么,在单例模型中,没有“智能单例”,该怎么办?怎么办?
在实际项目中,特别是客户端开发,其实是不在乎这个实例的销毁的。因为,全局就这么一个变量,全局都要用,它的生命周期伴随着软件的生命周期,软件结束了,它也就自然而然的结束了,因为一个程序关闭之后,它会释放它占用的内存资源的,所以,也就没有所谓的内存泄漏了。
但是,有以下情况,是必须需要进行实例销毁的:
在类中,有一些文件锁了,文件句柄,数据库连接等等,这些随着程序的关闭而不会立即关闭的资源,必须要在程序关闭前,进行手动释放;
具有强迫症的程序员。
在代码实现部分的第四种方法能满足第二个条件,但是无法满足第一个条件。好了,接下来,就介绍一种方法,这种方法也是我从网上学习而来的,代码实现如下:
在程序运行结束时,系统会调用Singleton的静态成员GC的析构函数,该析构函数会进行资源的释放,而这种资源的释放方式是在程序员“不知道”的情况下进行的,而程序员不用特别的去关心,使用单例模式的代码时,不必关心资源的释放。
那么这种实现方式的原理是什么呢?由于程序在结束的时候,系统会自动析构所有的全局变量,系统也会析构所有类的静态成员变量,因为静态变量和全局变量在内存中,都是存储在静态存储区的,所有静态存储区的变量都会被释放。
由于此处使用了一个内部GC类,而该类的作用就是用来释放资源,而这种使用技巧在C++中是广泛存在的,参见《C++中的RAII机制》。
运行结果:
1、题目来源《剑指offer 名企面试官精讲典型编程题 纪念版-何海涛 著》
2、C++设计模式——单例模式: http://www.jellythink.com/archives/82
3、C++11多线程教程:/article/4525725.html
题目:实现Singleton模式
以下内容是我在看《剑指offer》的面试题2时,遇到的问题,因为书中使用C#实现,所以想用C++重新实现一下,Test方法不够全,后续还要完善。C++实现过程主要参考:C++设计模式——单例模式。
代码中的注释一般是我的笔记,或一些发现。
PS: 感谢勤劳的慵懒君~~ @亦余心之所向兮
1 解法一:单线程解法
缺点:多线程情况下,每个线程可能创建出不同的Singleton实例// 剑指offer 面试题2 实现Singleton模式 #include <iostream> using namespace std; class Singleton { public: static Singleton* getInstance() { // 在后面的Singleton实例初始化时,若后面是new Singleton(),则此处不必new;(废话) // 若后面是赋值成NULL,则此处需要判断,需要时new // 注意!然而这两种方式并不等价!后面的Singleton实例初始化时,new Singleton(),其实是线程安全的,因为static初始化是在主函数main()之前,那么后面的方法岂不是很麻烦。。。。这也是我测试的时候想到的 /* if(m_pInstance == NULL) { m_pInstance = new Singleton(); } */ return m_pInstance; } static void destroyInstance() { if(m_pInstance != NULL) { delete m_pInstance; m_pInstance = NULL; } } private: Singleton(){} static Singleton* m_pInstance; }; // Singleton实例初始化 Singleton* Singleton::m_pInstance = new Singleton(); // 前面不能加static,会和类外全局static混淆 // 单线程获取多次实例 void Test1(){ // 预期结果:两个实例指针指向的地址相同 Singleton* singletonObj = Singleton::getInstance(); cout << singletonObj << endl; Singleton* singletonObj2 = Singleton::getInstance(); cout << singletonObj2 << endl; Singleton::destroyInstance(); } int main(){ Test1(); return 0; }
2 解法二:多线程+加锁
解法1是最简单,也是最普遍的实现方式,也是现在网上各个博客中记述的实现方式,但是,这种实现方式,有很多问题,比如:没有考虑到多线程的问题,在多线程的情况下,就可能创建多个Singleton实例,以下版本是改善的版本。 注意:下面的代码涉及互斥锁以及多线程测试,使用了C++11的多线程库,std::thread,,std::mutex,请使用支持C++11多线程的编译器,并确认开启了C++11的编译选项,具体方法见:/article/11891447.html
#include <iostream> #include <mutex> #include <thread> #include <vector> using namespace std; class Singleton { private: static mutex m_mutex; // 互斥量 Singleton(){} static Singleton* m_pInstance; public: static Singleton* getInstance(){ if(m_pInstance == NULL){ m_mutex.lock(); // 使用C++11中的多线程库 if(m_pInstance == NULL){ // 两次判断是否为NULL的双重检查 m_pInstance = new Singleton(); } m_mutex.unlock(); } return m_pInstance; } static void destroyInstance(){ if(m_pInstance != NULL){ delete m_pInstance; m_pInstance = NULL; } } }; Singleton* Singleton::m_pInstance = NULL; // 所以说直接new 多好啊,可以省去Lock/Unlock的时间 mutex Singleton::m_mutex; void print_singleton_instance(){ Singleton *singletonObj = Singleton::getInstance(); cout << singletonObj << endl; } // 多个进程获得单例 void Test1(){ // 预期结果,打印出相同的地址,之间可能缺失换行符,也属正常现象 vector<thread> threads; for(int i = 0; i < 10; ++i){ threads.push_back(thread(print_singleton_instance)); } for(auto& thr : threads){ thr.join(); } } int main(){ Test1(); Singleton::destroyInstance(); return 0; }
此处进行了两次m_pInstance == NULL的判断,是借鉴了Java的单例模式实现时,使用的所谓的“双检锁”机制。因为进行一次加锁和解锁是需要付出对应的代价的,而进行两次判断,就可以避免多次加锁与解锁操作,同时也保证了线程安全。但是,如果进行大数据的操作,加锁操作将成为一个性能的瓶颈;为此,一种新的单例模式的实现也就出现了。
3 解法三:const static 型实例
#include <iostream> #include <thread> #include <vector> using namespace std; class Singleton { private: Singleton(){} static const Singleton* m_pInstance; public: static Singleton* getInstance(){ return const_cast<Singleton *>(m_pInstance); // 去掉“const”特性 // 注意!若该函数的返回值改为const static型,则此处不必进行const_cast静态转换 // 所以该函数可以改为: /* const static Singleton* getInstance(){ return m_pInstance; } */ } static void destroyInstance(){ if(m_pInstance != NULL){ delete m_pInstance; m_pInstance = NULL; } } }; const Singleton* Singleton::m_pInstance = new Singleton(); // 利用const只能定义一次,不能再次修改的特性,static继续保持类内只有一个实例 void print_singleton_instance(){ Singleton *singletonObj = Singleton::getInstance(); cout << singletonObj << endl; } // 多个进程获得单例 void Test1(){ // 预期结果,打印出相同的地址,之间可能缺失换行符,也属正常现象 vector<thread> threads; for(int i = 0; i < 10; ++i){ threads.push_back(thread(print_singleton_instance)); } for(auto& thr : threads){ thr.join(); } } int main(){ Test1(); Singleton::destroyInstance(); return 0; }
因为静态初始化在程序开始时,也就是进入主函数之前,由主线程以单线程方式完成了初始化,所以静态初始化实例保证了线程安全性。在性能要求比较高时,就可以使用这种方式,从而避免频繁的加锁和解锁造成的资源浪费。由于上述三种实现,都要考虑到实例的销毁,关于实例的销毁,待会在分析。
4 解法四:在get函数中创建并返回static临时实例的引用
PS:该方法不能人为控制单例实例的销毁#include <iostream> #include <thread> #include <vector> using namespace std; class Singleton { private: Singleton(){} public: static Singleton* getInstance(){ static Singleton m_pInstance; // 注意,声明在该函数内 return &m_pInstance; } }; void print_singleton_instance(){ Singleton *singletonObj = Singleton::getInstance(); cout << singletonObj << endl; } // 多个进程获得单例 void Test1(){ // 预期结果,打印出相同的地址,之间可能缺失换行符,也属正常现象 vector<thread> threads; for(int i = 0; i < 10; ++i){ threads.push_back(thread(print_singleton_instance)); } for(auto& thr : threads){ thr.join(); } } // 单个进程获得多次实例 void Test2(){ // 预期结果,打印出相同的地址,之间换行符分隔 print_singleton_instance(); print_singleton_instance(); } int main(){ cout << "Test1 begins: " << endl; Test1(); cout << "Test2 begins: " << endl; Test2(); return 0; }
以上就是四种主流的单例模式的实现方式。
5 实例的销毁
在上述的四种方法中,除了第四种没有使用new操作符实例化对象以外,其余三种都使用了; 我们一般的编程观念是,new操作是需要和delete操作进行匹配的;是的,这种观念是正确的。在上述的实现中,是添加了一个destoryInstance的static函数,这也是最简单,最普通的处理方法了;但是,很多时候,我们是很容易忘记调用destoryInstance函数,就像你忘记了调用delete操作一样。由于怕忘记delete操作,所以就有了智能指针;那么,在单例模型中,没有“智能单例”,该怎么办?怎么办?
在实际项目中,特别是客户端开发,其实是不在乎这个实例的销毁的。因为,全局就这么一个变量,全局都要用,它的生命周期伴随着软件的生命周期,软件结束了,它也就自然而然的结束了,因为一个程序关闭之后,它会释放它占用的内存资源的,所以,也就没有所谓的内存泄漏了。
但是,有以下情况,是必须需要进行实例销毁的:
在类中,有一些文件锁了,文件句柄,数据库连接等等,这些随着程序的关闭而不会立即关闭的资源,必须要在程序关闭前,进行手动释放;
具有强迫症的程序员。
在代码实现部分的第四种方法能满足第二个条件,但是无法满足第一个条件。好了,接下来,就介绍一种方法,这种方法也是我从网上学习而来的,代码实现如下:
#include <iostream> #include <thread> #include <vector> using namespace std; class Singleton { private: Singleton(){} static Singleton* m_pInstance; // **重点在这** class GC // 类似Java的垃圾回收器 { public: ~GC(){ // 可以在这里释放所有想要释放的资源,比如数据库连接,文件句柄……等等。 if(m_pInstance != NULL){ cout << "GC: will delete resource !" << endl; delete m_pInstance; m_pInstance = NULL; } }; }; // 内部类的实例 static GC gc; public: static Singleton* getInstance(){ return m_pInstance; } }; Singleton* Singleton::m_pInstance = new Singleton(); Singleton::GC Singleton::gc; void print_singleton_instance(){ Singleton *singletonObj = Singleton::getInstance(); cout << singletonObj << endl; } // 多个进程获得单例 void Test1(){ // 预期结果,打印出相同的地址,之间可能缺失换行符,也属正常现象 vector<thread> threads; for(int i = 0; i < 10; ++i){ threads.push_back(thread(print_singleton_instance)); } for(auto& thr : threads){ thr.join(); } } // 单个进程获得多次实例 void Test2(){ // 预期结果,打印出相同的地址,之间换行符分隔 print_singleton_instance(); print_singleton_instance(); } int main(){ cout << "Test1 begins: " << endl; Test1(); cout << "Test2 begins: " << endl; Test2(); return 0; }
在程序运行结束时,系统会调用Singleton的静态成员GC的析构函数,该析构函数会进行资源的释放,而这种资源的释放方式是在程序员“不知道”的情况下进行的,而程序员不用特别的去关心,使用单例模式的代码时,不必关心资源的释放。
那么这种实现方式的原理是什么呢?由于程序在结束的时候,系统会自动析构所有的全局变量,系统也会析构所有类的静态成员变量,因为静态变量和全局变量在内存中,都是存储在静态存储区的,所有静态存储区的变量都会被释放。
由于此处使用了一个内部GC类,而该类的作用就是用来释放资源,而这种使用技巧在C++中是广泛存在的,参见《C++中的RAII机制》。
6 我的解法
就是在解法1中我注释里写的那样,直接是类内的static 实例指针,然后将在类外初始化(也只能在类外初始化),保证线程安全。在类内实现垃圾回收器GC类,满足完美强迫症。。。#include <iostream> #include <thread> #include <vector> using namespace std; class Singleton{ private: Singleton(){}; static Singleton* m_pInstance; public: static Singleton* getInstance(){ return m_pInstance; } class GC{ public: ~GC(){ if(m_pInstance != NULL){ cout << "GC: delete Singleton instance" << endl; delete m_pInstance; m_pInstance = NULL; } }; }; static GC gc; }; Singleton* Singleton::m_pInstance = new Singleton(); Singleton::GC Singleton::gc; void print_instance(){ Singleton* obj1 = Singleton::getInstance(); cout << obj1 << endl; } // 多线程获取单例 void Test1(){ // 预期输出:相同的地址,中间可能缺失换行符,属于正常现象 vector<thread> threads; for(int i = 0; i < 10; ++i){ threads.push_back(thread(print_instance)); } for(auto& thr : threads){ thr.join(); } } // 单线程获取单例 void Test2(){ // 预期输出:相同的地址,换行符分隔 print_instance(); print_instance(); print_instance(); print_instance(); print_instance(); } int main() { cout << "Test1 begins: " << endl; cout << "预期输出:相同的地址,中间可以缺失换行(每次运行结果的排列格式通常不一样)。" << endl; Test1(); cout << "Test2 begins: " << endl; cout << "预期输出:相同的地址,每行一个。" << endl; Test2(); return 0; }
运行结果:
相关文章推荐
- ubuntu下的c/c++环境搭建
- c++知识点2
- c++学习知识点
- c语言学习笔记36
- C++ Primer Plus 第六版_编程练习(1)(Chapter_two 1-7)
- RAII惯用法:C++资源管理的利器
- C/C++ 多线程编程方法
- 268. Missing Number
- 提高项目9.1-歌手大奖赛计分系列2
- 提高项目9-歌手大奖赛计分系列1
- 提高项目8-k次方之和
- C++设计模式系列之二结构型模式
- 提高项目7-太乐
- GeekBand c++學習筆記之八(泛型算法)
- C++引用与指针/值传递的比较
- OPTICS算法的C语言实现
- C++类构造函数初始化列表
- C++拷贝构造函数(深拷贝,浅拷贝)
- C++ const关键字
- ASCII码对照表