从C语言到C++_37(特殊类设计和C++类型转换)单例模式(上):https://developer.aliyun.com/article/1522499
1.5 只能创建一个对象的类(单例模式)(重点)
单例模式是一种设计模式。
设计模式: 设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打 仗时都是人拼人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后来孙子就总结出了《孙子兵法》。孙子兵法也是类似。
使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模 式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。
面试会有时会问到什么是单例模式:
单例模式:一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。
单例模式有两种实现模式:饿汉模式和懒汉模式
1.5.1 饿汉模式
① 饿汉模式:就是说不管你将来用不用,程序启动时就创建一个唯一的实例对象。
class Singleton { public: static Singleton* GetInstance() // 获取单例对象接口 { return &m_instance; } private: Singleton() // 构造函数私有化 {} Singleton(const Singleton& s) = delete; // 禁止使用拷贝构造 Singleton& operator=(const Singleton& s) = delete; // 禁止使用赋值运算符重载 //保证单例对象在静态区且只有一个 static Singleton m_instance;//单例对象 }; Singleton Singleton::m_instance;//在程序入口之前就完成单例对象初始化 int main() { Singleton* s = Singleton::GetInstance();//获取单例对象 return 0; }
静态成员变量只能在类域外进行定义初始化。所以在main函数之前就将单例对象定义初始化,此时该单例对象创建在静态区上,而且仅有一个,后面就无法再创建。
想要获取该单例对象只能通过静态成员函数GetInstance()来获取。
静态成员函数可以直接访问静态成员变量m_instance。
为什么称之为饿汉模式呢?不管将来会不会使用到这个单例对象,但是在程序一启动还没有进入main函数之前就创建一个唯一的实例对象。就像一个饿汉一样,一上来就先吃(创建单例对象)。
缺点:
可能会导致进程启动较慢,如过实例对象很复杂,在创建实例对象时就会花费很多时间。
实例顺序不确定,如果有多个单例对象,并且对象之间存在互相依赖关系,由于对象的实例对象不确定(和代码顺序无关,由编译器决定),此时就会发生错误。
优点:简单、没有线程安全问题。
如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避 免资源竞争,提高响应速度更好。
1.5.2 懒汉模式
懒汉模式:
class Singleton { public: static Singleton* GetInstance() { // 如果单例对象没有创建则在堆区创建 if (m_pInstance == nullptr) { m_pInstance = new Singleton; } return m_pInstance; } private: Singleton() // 构造函数 {} Singleton(const Singleton& s) = delete; // 禁止拷贝 Singleton& operator=(const Singleton& s) = delete; // 禁止赋值 // 静态单例对象指针 static Singleton* m_pInstance; // 单例对象指针 }; Singleton* Singleton::m_pInstance = nullptr; // 初始化为空 int main() { Singleton* ps = Singleton::GetInstance();//获取单例对象 return 0; }
同样将构造函数私有,拷贝构造和赋值运算符重载函数禁止调用,用来保证单例模式的唯一性。
增加静态单例对象指针成员变量。在类外实例化静态指针变量的时候,并没有创建单例对象,而是将其初始化为空。
在获取单例对象的时候,如果是第一次使用,那么就会在堆区上new一个单例对象,并且将所在地址赋值给静态的成员指针变量。
为什么称之为懒汉模式呢?
懒汉模式又叫做延时加载模式,如果单例对象构造十分耗时或者占用很多资源,比如加载插件啊, 初始化网络连接啊,读取文件啊等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载)更好。
优点:
① 第一次使用单例对象时才创建对象,进程启动过程中无负载。
② 多个互相依赖的单例可以控制启动顺序(通过代码顺序)。
缺点:
① 相对复杂。(线程安全问题没讲,后面学了Linux系统期间再讲)
② 线程安全问题要处理好
单例对象释放问题:
① 一般情况下,单例对象不需要释放的。因为一般整个程序运行期间都可能会用它。
单例对象在进程正常结束后,也会资源释放。
② 有些特殊场景需要释放,比如单例对象析构时,要进行一些持久化(往文件、数据库写)操作。
2. 类型转换
在C语言中,如果赋值运算符(=)两边的类型不同,或者形参和实参类型不匹配,或者返回值类型和接收值类型不一致,就需要发生类型转换。
C语言中有两种类型转换:
- 隐式类型转换:编译器在编译阶段自动进行,能转就转,不能转就编译失败。
- 显式类型转换:需要用户自己处理。
double d = i是发生了隐式类型转换,将整形转换成了double类型。
隐式类型转换只发生在相近类型,比如整形家族直接,或者这些int,double等表示数值直接的变量类型。
int address = (int)p是发生了显式类型转换,将int * 类型的变量转换为int类型。
显式类型需要用户自己维护,在两种类型没有任何关系的时候需要进行显式类型转换,比如将指针类型转换成普通类型等等。
C语言的类型转换存在缺陷:
隐式类型转换有些情况下会出现问题,比如数据精度发生丢失(整形提升等)。
显式类型转换将所有情况混合在一起,代码不够清晰。
所以C++提出了自己的类型转换风格,但是仍然可以使用C语言的转换风格,因为要兼容C语言。标准C++为了加强类型转换的可视性,引入了四种命名的强制类型转换操作符:
static_cast、reinterpret_cast、const_cast、dynamic_cast。下面我们一一了解。
2.1 static_cast
C语言的隐式类型转换(两个相近的类型转换)在C++中就可以使用static_cast来转换,但是不能用于两个不相关的类型进行转换。
int main() { double a = 7.14; int b = static_cast<int>(a); cout << a << " " << b << endl; return 0; }
double类型转int类型,在C语言中是隐式类型转换,在C++中为了更加明确使用了static_cast。
static_cast后的<>里放要转换的类型,()里放被转换的类型。
如果将static_cast看成是类模板,就是在实例化,(d)就是在拷贝构造,siatic_cast(d)就是在用d构建一个匿名对象。
2.2 reinterpret_cast
reinterpret_cast操作符通常为操作数的位模式提供较低层次的重新解释,用于将一种类型转换为另一种不同的类型。C语言的显式类型转换在C++中就可以reinterpret_cast。
int main() { int a = 7; int* pa = &a; int address = reinterpret_cast<int>(pa); cout << a << " " << pa << " " << address << endl; return 0; }
int*类型转换为int类型,在C语言中是显式类型转换,在C++中为了不混乱使用了reinterpret_cast。
这里如果使用static_cast进行类型转换的话会报错,必须使用reinterpret_cast:
2.1反过来用也会报错:
2.3 const_cast
const_cast最常用的用途就是删除变量的const属性,方便赋值:
int main() { const int a = 2; int* p = const_cast<int*>(&a); *p = 3; cout << a << " " << *p << endl; return 0; }
变量a原本是const属性的,不能被修改,使用了const_cast以后去除了常量属性,可以修改了,如*p = 3。
F11:
在调试窗口中可以看到,成功修改了原本是const属性的变量a。
为什么a在调试窗口看到的是3,打印出来的是2?这就涉及到了以前提到的汇编里的寄存器的内容
此时代码已经被优化了,不同编译器的优化不一样,有些编译器(VS)会把a = 2放到寄存器(认为const类型的变量不会被修改,每次去内存取的话很慢),那么读的时候去寄存器取就好了,打印的时候不会去内存取,所以打印出来的是3,监视窗口去内存取的,是2。
如果加上一个volatile关键字就告诉编译器不要优化:
int main() { //const int a = 2; //int* p = const_cast<int*>(&a); //*p = 3; //cout << a << " " << *p << endl; // 2 3 volatile const int a = 2; int* p = const_cast<int*>(&a); *p = 3; cout << a << " " << *p << endl; // 3 3 return 0; }
可以用C语言的强制类型转化:
这里用reinterpret_cast就不行:
const_cast更多的是一种警示,表示去除了const属性,要谨慎操作。
已经讲了三种C++的类型转化,应该也知道了是不能混着用的,
static_cast对标的是C语言的隐式类型转化,reinterpret_cast和const_cast对标的都是C语言的强制类型转化,增加就是为了建议你规范的用。
下面的dynamic_cast是C++特有的。
从C语言到C++_37(特殊类设计和C++类型转换)单例模式(下):https://developer.aliyun.com/article/1522503?spm=a2c6h.13148508.setting.20.50c04f0ef94tTt