1.单例模式
(1)设计模式是什么?
简单来说,被反复使用,多数人知晓、经过分类的代码设计经验的总结就叫设计模式 ,它建立在特殊类的设计之上,实现特殊的功能,运用的知识也十分综合。如迭代器模式就是一种常见的设计模式。 下面再分享一个常见的设计模式,单例模式。
(2)单例模式
单例模式: 全局中定义的一个类只能创建一个对象,即单例模式可以保证系统中该类只有一个实例 ,并提供一个访问该对象的全局访问点,该实例化的对象被所有程序模块共享,即能在不同模块访问这个唯一的对象。
单例模式又分为饿汉模式和懒汉模式,下面分别介绍
(3)饿汉模式
先看一下下面的代码,会输出什么呢?
cpp
#include <iostream>
using namespace std;
class A
{
public:
static A& constructor()
{
return a;
}
A(const A& a) = delete;//移动版本也不会生成
A& operator=(const A& a) = delete;//移动版本也不会生成
private:
A(int m = 10)
:_m(m)
{
cout << "私有化A()" << endl;
}
~A()
{
cout << "私有化~A()" << endl;
}
int _m;
static A a;
};
A A::a(5);
int main()
{
cout << sizeof(A) << endl;
return 0;
}
结果是
相信看到这个结果会有很多疑惑,下面将逐一回答
①为什么A的静态成员变量类型是A还没有导致无限递归?
我们先来看看无限递归的情况是怎样的
在这种情况下编译会直接报错,但是为什么加了static就没事了呢?
类中的static函数和变量虽然和其它成员函数和变量都是在同一个类下面声明的,但其实有很大差别。类中的static对象属于整个类而不是单个对象,当实例化类的时候并不会为static对象单独开辟空间。 只有当要使用的时候直接到类里面取,我们可以将类里面的static对象当作公共区域,每个实例化对象按需来取。
但使用公共区域来解释类中类又不太严谨。 还有一种理解方式可以更好地解释为什么A不会无限递归。我们先忽略所有的静态对象,剩下的就是类A该有的东西。 注意我们所忽略的静态成员变量A a中,A对应的内容也是如此,没有静态对象。按照这样理解,我们这个时候就能发现**静态成员变量A a中并没有另一个A a,因为A中我们先是忽略了所有静态对象。**最后我们将这些忽略的对象还原回去。
这就像在一座房子里有一个很小的房子模型,这个模型非常精妙,包含了这个房子的一切,就是模型里没有另一个房子模型,不然的话就会无限递归了。其实我们再进一步,这个小模型放在房子外面和房子里面都长一样。模型在外面的话那么这个模型和房子就一模一样,造出来的房子也一模一样。模型在房子里面的话房子虽然多了一个小模型,但小模型的样子没有变,而后面我们造房子是按照模型来造的,根本不会受到影响。
这里需要我们仔细体会,找到自己的理解方式。总的来说静态成员变量可以说是一个仅受类域限制的对象,这个对象在类里还是类外都不会对这个类造成任何影响,只有调用时才会感受到影响。
用已有的知识解释下面的现象
我们发现计算类的大小时,静态成员变量都不会算在其中,这也进一步验证了刚刚的解释。
②为什么使用static函数?
关于这点,我在特殊类中也讲过。static函数属于整个类而不是单个对象。类的所有成员函数(包括static)都会在编译或链接时存入代码段。不管实不实例化这些函数都只会有一份,所以static在唯一性上体现不出不同 ,但像我前面所说,static对象只是受类域限制,其他东西和类没有关系,所以static函数没有this,避免了先有鸡还是先有蛋的问题。
③为什么可以在全局定义类中的私有成员?
要注意私有成员保护的是调用,即不能在类外面调用
只要是成员函数,非静态成员变量都能在全局定义(能够随便调用类里面的私有对象),注意格式,static定义时不写
④为什么什么都不做就会调用A的构造和析构?
static A a在全局中定义也就意味着它的生命周期和全局对象、静态对象一样,在调用main函数前创建,在程序结束时销毁。在开始执行main函数的时候,a已经调用了构造函数创建了。
⑤如何实现单例模式的?
和特殊类一样,我们要根据需求来处理,由于我们需要单例,把这个单例放在类里面作为static成员变量是最好的选择 ,因为这样我们就可以直接私有化构造函数,将拷贝赋值全部封死,完全失去了任何创建新对象的机会。唯一的一份对象只能通过static来获取 ,这里其实也借助了static作为成员变量的一个特点。因此我们只能通过接口获取A的唯一的实例化对象,符合单例模式需求。
⑥为什么叫做饿汉模式?
当调用main函数之前,static A a就在全局中定义出来了,也就是说,我们将这个对象准备好了才开始执行main函数的代码。 就像亲人在家早早做好饭,饥饿的人一回家就能吃饭。这个代码也是同样的意思,先把这些特殊类的对象准备好,当我需要的时候直接拿来用就可以了。
⑦这个模式有什么缺点吗?
多个饿汉模式的单例会导致启动慢 ,在main函数前不存在多线程的概念,main函数前的处理也不会有什么反馈,有的时候让人无法分辨到底是启动慢还是程序陷入死循环了。并且某些对象初始化内容较多(有的程序启动要读很多文件)。 还有一个致命的问题:A和B两个饿汉,如果对象初始化存在依赖关系,很难控制顺序(A和B在不同文件中) ,这个时候就需要懒汉模式来处理了。
(4)懒汉模式
理解了饿汉模式,懒汉模式就很简单了。总的来说就是在第一次调用的时候才会创建对象,并且因为能够显式创建对象,创建顺序的问题也能解决。
看一下下面的代码
cpp
#include <iostream>
using namespace std;
class A
{
public:
static A* constructor(int m = 10)//模拟构造函数
{
if (a)
return a;
return new A(m);
}
A(const A& a) = delete;//移动版本也不会生成
A& operator=(const A& a) = delete;//移动版本也不会生成
private:
A(int m = 10)
:_m(m)
{
cout << "私有化A()" << endl;
}
~A()
{
cout << "私有化~A()" << endl;
}
int _m;
static A* a;
};
class B
{
public:
static B* constructor(int m = 10)//模拟构造函数
{
if (b)
return b;
return new B(m);
}
B(const B& b) = delete;//移动版本也不会生成
B& operator=(const B& b) = delete;//移动版本也不会生成
private:
B(int m = 10)
:_m(m)
{
cout << "私有化B()" << endl;
}
~B()
{
cout << "私有化~B()" << endl;
}
int _m;
static B* b;
};
A* A::a = nullptr;
B* B::b = nullptr;
int main()
{
B* p1 = B::constructor(0);
A* p2 = A::constructor(5);
return 0;
}
结果是
注意堆区的空间回收的时候都不会调用析构,和生命周期结束调用析构存在区别。
懒汉模式依然是借助static变量来写的 ,只不过先用nullptr初始化保证程序启动快 ,后续需要的时候先检查是不是nullptr,是的话就创建变量,不是的话就直接用现有的对象返回数据。我们显式调用可以保证严格控制创建对象顺序,并且整体运行速度也有上升。
之所以叫懒汉模式,是因为这类似于饿了才去做饭,需要的时候才去干的生活模式。我们写代码的时候要慎用饿汉模式,懒汉模式在很多方面都有优势。
2.类型转换
在C/C++中,我们难免会遇到类型转换的问题,如int和double之间的转换,short和int之间转换。注意权限的缩小和放大仅限于指针和引用,其余的都叫类型转换 ,**类型转换只要是自定义类型都要手动写转换的构造函数。**接下来就讲讲C++中类型转换的规则。
类型转换分为隐式类型转换 和显式(强制)类型转换 。后面我会讲哪些场景必须使用强制类型转换。C++有四种显式类型转换:static_cast、reinterpret_cast、const_cast、dynamic_cast
注意:这四个操作符都是以static_cast<int>(a)方式写,但并不是仿函数,它们和仿函数有很大区别
(1)static_cast<T>
①内置类型之间
static_cast<T> 适用于大多数类型转换的场景。它必要时能调用构造函数,专门进行安全转换。
这种转换方式都能使用 隐式类型转换(不意味着隐式类型转换只支持这一种) ,我们写不写都是能运行的,写了相当于给别人一个更明显的标志。这类转换方式有个很大的特点就是数据的意义不会发生大的改变 ,如我前面int转short,也可以说是内置类型之间的转换
指针类型和void*之间,这种只能进行强转,不能隐式类型转换
编译器认为像int*和char*之间的转换不够安全,所以就不支持static_cast强转
针对这种有互相关联的类型,我们可以选择不写,走隐式类型转换也是可以的。
②内置类型 -> 自定义类型
这类转换我们应该见过很多了,单参数构造函数支持隐式类型转换
如果不想隐式类型转换可用explicit,但显式转换不受影响
多参数比较特殊,它不支持使用static_cast强转
多参数构造要么直接写()构造函数,要么使用列表初始化,static_cast识别不了列表,不能使用
③自定义类型 -> 内置类型
这应该我第一次提到用自定义类型转内置类型。如果有一个A a,我想转为int,那么我应该会写作int b = (int)a,由于这个(int),我们应该写一个重载函数出来,C++中这个重载函数是operator int,其实operator()也很合理,但这有弊端,不仅和仿函数的冲突了,还无法确定类型。因此重载一个int函数其实也很清晰。
重载函数形式:operator 类型
我们看到,a直接隐式转换成了int,因为我们写了重载函数,cout的operator<<函数又有int版本,于是就能隐式类型转换
但如果写两个重载就不明确了,会报错
自定义类型转内置类型一般建议强转,这样至少看上去一目了然。
④自定义类型之间
我们只需要在类中写对应的构造函数即可,会自动匹配上的。
⑤父子类转换
继承体系下,父子类指针都能进行互指,父类指针可以指向子类,子类也能指向父类
如果不是指针,仅支持父类指向子类的空间,下面图中a2不是赋值兼容转换,而是强制类型转换(效果一样)
⑥非指针内置类型的const与不带const的对象转换
这只是赋值操作,不涉及什么权限等,了解即可
(2)reinterpret_cast<T>
和运算符的名字一样,reinterpret,重新解释。也就是说使用这种类型转换的操作数在转换前后的意义是有一个比较大的转变的,需要我们慎重考虑使用,它并不安全。
不仅如此,reinterpret_cast<T>并不聪明,它不会调用构造也不会截断数据,一般只负责按位重解释,改变某个空间的解释含义,这也导致它的使用场景很低。
①任意指针之间转换
reinterpret_cast可以强转static_cast不能强转的对象。
我们需要强制类型转换,可以使用(int*)p这种方式,也可以使用reinterpret_cast来操作
②指针和内置类型的转换
前面说的,reinterpret_cast<T>适合不改变数据改变它的解释的操作,这就使得我们可以在指针和内置类型之间做转换。
一般来说不建议使用reinterpret_cast<T>,它经常做出一些未定义的事
(3)const_cast<T>
const_cast是用来给const修饰的指针或引用进行强转的。注意const修饰的和无const修饰的对象是不能通过这个强转的(const修饰的迭代器之间转换也是如此),必须要写转换的构造函数,由static_cast转换。
这个结果说明了const的风险,有的编译器在编译阶段会像宏替换那样将部分变量直接替换成常量,就算对应空间的值被修改,也不会看出来
(4)dynamic_cast
这是专门针对有虚函数的继承体系的更安全的转换方式(仅指针和引用)。支持 RTTI(运行时类型识别),如typeid、dynamic_cast、decltype都有,dynamic_cast通过检查虚函数表里面的标识,以此来判断是父还是子
dynamic_cast会自动根据虚函数表判断谁指谁,如果是子类指向父类返回空,可以用这个来进行安全检查
向下转型(父类开辟的空间交给子去管理):编译器认为不安全,返回空
向上转型:如果显式写dynamic_cast,就是强制类型转换,否则就是赋值兼容转换
我们可多方位验证不安全时返回的是空
我们平时很少自己显式写出这四个操作符,理解各自的用途即可。
相比之下,知道什么时候该自己去写转换函数(const迭代器转换),怎么写,以及知道什么时候是隐式类型转换,什么时候是赋值兼容转换。