前言
我们在日常写项目的过程当中,肯定会遇到各种各样的需求,那么也就要求我们要写各种各样的类。本篇博客当中,就一些常用的特殊类进行介绍和实现。
不能被拷贝的类
关于实例化类拷贝(对象的拷贝)一般就是两个场景,第一个是 拷贝构造函数;第二个是operaoto=()赋值重载运算符函数,当然大多数情况 ,赋值重载运算符函数是复用 的 拷贝构造函数,我们实现也非常简单,就是让 这个类的使用者 不能调用到这两个函数。
思路清楚了,如果我们不想 这个类的使用者 调用到某个函数的话,我们有三种方法来实现:
前两种是 C++98 当中使用的方式:
- 把这两个函数设置为 private (私有)的,这样这个函数就不能再类外被调用了。
- 只声明 不 定义这两个函数。这种方式虽然可以实现不能调用的效果,但是,在类当中只声明不定义的函数,在类外是可以再被重新定义的。意思就是,类的使用者可以在类外定义这个函数,达到赋值的效果。
// private 修饰 和 只声明不定义
class CopyBan
{// ...
private:CopyBan(const CopyBan&);CopyBan& operator=(const CopyBan&);//...
};
对于上述两种方式,推荐使用 private 修饰,只声明不定义的方式不建议做。
在 C++11 当中就又更新了一种方式,就是在这个函数后面写上 "= delete" 表示这个函数已经被删除(这种方式是极力推荐使用的):
class CopyBan
{// ...CopyBan(const CopyBan&) = delete;CopyBan& operator=(const CopyBan&) = delete;//...
};
当我们在外部调用这个两个函数的时候就会报错(如下例子所示):
class CopyBan
{
public:CopyBan(){}// ...CopyBan(const CopyBan&) = delete;CopyBan& operator=(const CopyBan&) = delete;//...
};int main()
{CopyBan CB;CopyBan CB1(CB);CopyBan CB2 = CB;return 0;
报错:
error C2280: “CopyBan::CopyBan(const CopyBan &)”: 尝试引用已删除的函数message : “CopyBan::CopyBan(const CopyBan &)”: 已隐式删除函数
需要注意的是:我们必须要写,因为,如果我们不写,那么编译器就会自动生成一个浅拷贝的 拷贝构造函数 和 operator=()函数。所以我们必须要声明一下。或者像上述一样,删除这个函数。
只能在 堆 上创建对象的类
对于一个普通类,我可以在栈上创建一个对象,也可以在静态区当中创建一个 静态的对象;还可以在堆上创建一个 对象:
class A
{
public:A(int a):_a(a){}private:int _a = 1;
};void main()
{A a1(1);static A a2(2);A* pa2 = new A(3);
}
私有化 析构函数 的 方式
就是把 类当中的 析构函数定义出来,并且,最重要的是要 用 private
class A
{
public:A(int a):_a(a){}private:~A(){// ......}int _a = 1;
};void main()
{A a1(1);static A a2(2);A* pa2 = new A(3);
}
此时,如果不是 new 出来再堆上开辟的空间的话就会报错:
如果不是在堆上 存储的对象,编译器是会自动调用这个对象的析构函数来 释放这个对象的,但是以为 类当中的 析构函数是 私有的,所以就不能调用到。
但是 在堆上存储的对象是 不会自动 释放的,需要手动释放,这也是内存泄漏的一大原因 之一,所以这种情况下,只有 在堆上 存储的对象才能 正常使用。
但是现在的问题是,因为 析构函数是 私有的,所以,此时我们进行手动释放也是释放不了的。
解决方式就是提供接口,因为 私有是 类外不能访问,但是在 类内 是能访问的,所以,我们可以提供一个接口,在外部调用这个接口,在接口当中调用析构函数。
class A
{
public:A(int a):_a(a){}void DeleteFunc(){delete this;}private:~A(){// ......}int _a = 1;
};void main()
{//A a1(1);//static A a2(2);A* pa2 = new A(3);pa2->DeleteFunc();
}
此时是可以的。
私有化 构造函数 的 方式
除了可以把 析构函数私有化来实现,我们私有化 构造函数也是可以的。
但是,如直接把 构造函数私有化,我们甚至连 new 在堆上 创建的对象也是不能构造的:
class A
{
public:private:A(int a):_a(a){}int _a = 1;
};void main()
{A a1(1);static A a2(2);A* pa2 = new A(3);
}
报错:
所以,此时就要再提供一个接口,我们虽然不能在类外调用构造函数但是可以在类内调用构造函数。在这个接口当中我们就写死,只写new 这种方式构建对象的方式,也就是在堆上构造对象。
但是,我们知道,要想调用类当中非静态的成员函数,是需要对象这个媒介的,但是我们又是要创建一个对象,所以,此时这个接口应该是 静态的。
因为只有静态的函数才可以在 类外 使用 "类名::静态函数名" 的方式调用类当中的静态成员函数。
如下例子所示:
class A
{
public:static A* CreatObj(int a){return new A(a);}private:A(int a):_a(a){}int _a = 1;
};void main()
{A* pa2 = A::CreatObj(1);
}
拷贝构造函数也可以创建 非堆上对象问题
上述解决了 直接构造的问题,但是,编译器自己实现的 拷贝构造函数 也是可以在 非堆上构造对象的。所以,此时我们要对 拷贝构造函数 来进行改造。
如果我们对拷贝构造函数没有需求的话,可以直接把 拷贝构造函数 和 operator=()赋值重载运算符函数 直接 delete 删除掉也行。
class A
{
public:static A* CreatObj(int a){return new A(a);}// 删除掉 两函数A(const A& sa) = delete;A& operator=(const A& sa) = delete;private:A(int a):_a(a){}int _a = 1;
};
如果对这两个函数有需求的话,也可以像上述一样,对这两个函数进行特殊处理。
只能在 栈 上创建对象的类
此时从析构函数方向已经不能解决这个问题了 ,只能在 构造函数一样像上述一样 私有 构造函数,然后提供一个接口,在这个接口当中 直接写死 用 栈的方式构造对象。
class StackOnly
{
public:static StackOnly CreateObj(){return StackOnly();}private:StackOnly():_a(0){}
private:int _a;
};
此时:
此时也是可以实现的。
但是需要注意的是,我们还是要把operator new() 和 operator delete()两个运算符重载函数给delete 删除了。
因为,new 和 delete 两个运算符,在执行的时候是分两步的,比如:new,会先去调用 operator new () 这个全区当中函数(在类当中没有重写 operator new ()函数的情况下),然后 再去调用 构造函数(包括拷贝构造函数),虽然构造函数是封了的,但是 拷贝构造函数是没有封的,所以new 依然可以调用。
此时有人就想了,那么我向上述一样,直接把 拷贝构造函数删除不就行了吗?肯定是不行的,在上述可以在不使用 拷贝构造函数的情况 删除拷贝构造函数,但是我们在 写 构造函数的接口的时候,就是用的传值返回,这里是一定要用到 拷贝构造函数的。
此时又有人想了,那么我使用现代写法,直接跳过拷贝构造行不行呢?也是不行的,因为删除拷贝构造函数不是长久的方法,一个类没有拷贝构造函数是非常麻烦的事情,所以,此时就有了一个更好的方式。
我们知道,如果一个类当中没有冲写 operator new () 和 operator delete()函数的话,默认是调用全局当中实现的 operator new() 和 operator delete()函数;但是,当在类当中重写了 operator new() 和 operator delete()函数 的话,默认就去调用 类当中重写的 这两个函数了,我们可以利用这个特性,在 类当中声明不定义,可以实现;当时上述说过 只声明不实现是不好的,直接删除掉这个函数是最好选择,因为在这个类当中我们不会使用到 operator new() 和 operator delete()函数 这两个函数。
在 类当中重写是是实现 这个类的专属 的 operator new() 和 operator delete()函数 。
class StackOnly
{
public:static StackOnly CreateObj(){return StackOnly();}// 禁掉operator new可以把下面用new 调用拷贝构造申请对象给禁掉// StackOnly obj = StackOnly::CreateObj();// StackOnly* ptr3 = new StackOnly(obj);void* operator new(size_t size) = delete;void operator delete(void* p) = delete;
private:StackOnly():_a(0){}
private:int _a;
};
报错:
写一个不能被继承的类
第一种就是使用 "final" 关键词修饰,final 有最终的意思,修饰这个类,说这个类是最终的一个类,就不能被继承了。
第二种是 构造函数私有化。
第三种是析构函数私有化。
这不在过多介绍,具体情况请看下述文章的当中对于 “防止某个类被继承的方法” 这个章节的介绍:C++ - 继承-CSDN博客
// C++98中构造函数私有化,派生类中调不到基类的构造函数。则无法继承
class NonInherit
{
public:static NonInherit GetInstance(){return NonInherit();}
private:NonInherit(){}
};class A final
{// ....
};
只能创建一个对象 (单例模式) 的类
在类的设计历史当中会发现,有些了的设计是经常要用的,有人就总结出了 23 种设计模式:
23 种设计模式详解(全23种)-CSDN博客
此处的 单例模式就是其中的一种,而所谓单例模式就是:在这个进程当中,这里有且最多只能创建 一个 对象。
比如:在某一服务程序当中,有关于这个程序的ip 等等配置信息,用类对象来存储的话,那么这些信息都是只有一份配置文件就行了,我们只需要对这个配置文件进行 写或者 访问就行了;还有内存池的设计当中,也可以按照单例的方式去写。
那么,我们如何让一个类,只能创建一个对象呢?
- 首先第一步是要把 构造函数私有化,方式随便创建对象。
- 然后,使用一个接口,来实现只能创建一个对象的效果;有两种方式可以实现,一种是 创建一个全局的对象,然后在 接口函数当中直接返回这个全局对象即可;第二种是在本类当中创建一个 静态的本类对象,然后接口返回 这个 静态对象的 引用 就行了。
全局对象实现:
static B b;class B
{
public:static B& GetInstance(){return b;}private:// 构造函数私有化B(){}
};int main()
{B s1 = B::GetInstance();static B s1;B* s1 = new B;return 0;
}
结果:
使用本类当中创建一个 静态的本类对象 方法实现:
class B
{
public:static B& GetInstance(){return b;}private:// 构造函数私有化B(){}static B b;
};int main()
{B s1 = B::GetInstance();static B s1;B* s1 = new B;return 0;
}
结果和上述是一样的。
看上述的例子,发现,在本类当中是可以创建一个 静态的本类对象的。但是不能在 本类当中是可以创建一个 非静态的本类对象。
这样的话就会陷入死循环了,编译器也会直接报错不会让我们这样做的:
除了上述,我们还有防止拷贝构造:
B(const B& s) = delete;
B& operator=(const B& s) = delete;
同样的如果不控制拷贝构造函数的话,编译器自己生成的 拷贝构造函数 和 赋值重载运算符函数都是可以实现在 栈 上的浅拷贝的。
饿汉模式 和 懒汉模式
除了像上述一样自己实现一个了 单例模式的类,我们还可以控制已经创建的类在 整个进程当中只能创建一个对象:
如下所示,我们在一个类当中定义一个map 的成员变量,这个对象是 单例类,这个单例类只能创建一个 对象,而这个对象当中有一个 map ,假设我们现在把这个类 命名为 Singleton_map,那么 这个 Singleton_map 类就只能创建一个 对象,而且这个类的底层是用 map 来实现的;
这种方式就和 map 和 set 的底层实现是红黑树一样的,两者是差不错的实现方式,只不过 map 和 set 当中的实现更加复杂:
namespace hungry
{class Singleton_map{public:// 2、提供获取单例对象的接口函数static Singleton& GetInstance(){return _sinst;}// 随便写一些接口// 直接覆盖 map 当中 的 valuevoid Add(const pair<string, string>& kv){_dict[kv.first] = kv.second;}// 打印 mapvoid Print(){for (auto& e : _dict){cout << e.first << ":" << e.second << endl;}cout << endl;}private:// 1、构造函数私有Singleton(){// ...}// 3、防拷贝Singleton(const Singleton& s) = delete;Singleton& operator=(const Singleton& s) = delete;map<string, string> _dict;// ...static Singleton _sinst;};Singleton Singleton::_sinst;
}
我们把上述这种方式称之为 -- 饿汉模式。
饿汉模式:在main函数之前就 创建单例对象。我们把这种实现单例模式 方式称之为 饿汉模式。
像上述的 静态成员实现的方法就是 一种饿汉模式,因为 静态变量是在静态区当中,静态区当中的数据要先被生成,才会去调用 main函数。
饿汉模式的启动问题:
问题一:因为 是在main 函数之前就需要创建单例对象,程序的启动是在main 函数当中启动的;如果 需要创建的 单例对象要初始化的内容特别多,那么,程序的启动速度将会受到很大的影响。
如果程序的启动速度慢的话,对于我们调试这个程序,有很大影响。而且,程序在启动之时,比如在 linux 当中运用 某 可执行程序,我们运行程序的时候,要启动大半天,那么这个程序是 挂掉了(比如死循环),还是只是启动比较慢呢?
问题二:举例:假设现在有两个单例模式的类,有强耦合关系(依赖关系),比如 必须创建完 类A,才能创建 类 B,因为是在 main函数开始之前 就要创建的,我们如何实现 两个类的创建先后关系呢?
其实上述的根本上都是在main 函数 之前创建对象导致的,那么我们就在想,能不能不止main函数之前创建对象,在main函数当中,其他人想创建的时候再去创建呢?
当然是可以的。
对于上述控制的静态成员对象,我们可以替换为 静态成员变量指针,那么这个指针会在 main 函数执行之前就 创建,但是此时是没有创建 对象空间的,只是创建了一个指针,创建一个指针的开销非常小了,不就是 4/8 字节嘛。
如下所示:
namespace hungry
{class Singleton_map{public:// ......private:static Singleton* _sinst;};Singleton* Singleton::_sinst = nullptr;
最主要 的就是在 构造函数的接口函数当中实现,只在第一次创建:
// 2、提供获取单例对象的接口函数static Singleton& GetInstance(){if (_psinst == nullptr){// 第一次调用GetInstance的时候创建单例对象_psinst = new Singleton;}return *_psinst;}
像上述这种方式就可以解决 饿汉模式的启动问题。我们把这种方式称之为 懒汉模式。
一般单例对象是不用释放的,一个一个单例对象不是只是当前要使用的,一般是一直在使用的,但是,在一些特殊场景当中是需要进行释放的。比如:需要显示释放的对象,还有是在程序结束时候需要做一些特殊动作(比如持久化)。
我们注意到,上述开空间的方式是使用 new 在堆上 创建的,所以我们需要对这个对象进行显示的释放。
// 显示释放空间static void DelInstance(){if (_psinst){delete _psinst;_psinst = nullptr;}}~Singleton(){cout << "~Singleton()" << endl;// map数据写到文件中FILE* fin = fopen("map.txt", "w");for (auto& e : _dict){fputs(e.first.c_str(), fin);fputs(":", fin);fputs(e.second.c_str(), fin);fputs("\n", fin);}// 然后进行释放操作}
这时就有一个问题,如果,当前的程序有多个结束方式(此时你不知道 程序什么时候结束),但是像上述一样的释放对象的方式是显示释放。比如有 好几十种程序结束方式,难道我们就在每一个结束位置都显示的调用 释放函数吗?
这显然太挫了。
所以,我们可以延用 智能指针当中的思想(但是这里不是智能指针实现,智能指针也不好实现),创建一个类 GC,在这个类当中的 析构函数,就把 单例模式对象 的 释放函数给调用了,这样在这个 GC 类对象结束作用域的时候,调用析构函数就会释放 单例模式 对象。
这个 GC 类可以写在单例模式类外,但是最好写在 单例模式类当中,在单例模式类当中,创建这个 GC类对象,完整代码演示:
namespace lazy
{class Singleton{public:// 2、提供获取单例对象的接口函数static Singleton& GetInstance(){if (_psinst == nullptr){// 第一次调用GetInstance的时候创建单例对象_psinst = new Singleton;}return *_psinst;}// 一般单例不用释放。// 特殊场景:1、中途需要显示释放 2、程序结束时,需要做一些特殊动作(如持久化)static void DelInstance(){if (_psinst){delete _psinst;_psinst = nullptr;}}void Add(const pair<string, string>& kv){_dict[kv.first] = kv.second;}void Print(){for (auto& e : _dict){cout << e.first << ":" << e.second << endl;}cout << endl;}class GC{public:~GC(){lazy::Singleton::DelInstance();}};private:// 1、构造函数私有Singleton(){// ...}~Singleton(){cout << "~Singleton()" << endl;// map数据写到文件中FILE* fin = fopen("map.txt", "w");for (auto& e : _dict){fputs(e.first.c_str(), fin);fputs(":", fin);fputs(e.second.c_str(), fin);fputs("\n", fin);}}// 3、防拷贝Singleton(const Singleton& s) = delete;Singleton& operator=(const Singleton& s) = delete;map<string, string> _dict;// ...static Singleton* _psinst;static GC _gc;};Singleton* Singleton::_psinst = nullptr;Singleton::GC Singleton::_gc;
}