我们在日常写项目的过程当中,肯定会遇到各种各样的需求,那么也就要求我们要写各种各样的类。本篇博客当中,就一些常用的特殊类进行介绍和实现。
关于实例化类拷贝(对象的拷贝)一般就是两个场景,第一个是 拷贝构造函数;第二个是operaoto=()赋值重载运算符函数,当然大多数情况 ,赋值重载运算符函数是复用 的 拷贝构造函数,我们实现也非常简单,就是让 这个类的使用者 不能调用到这两个函数。
思路清楚了,如果我们不想 这个类的使用者 调用到某个函数的话,我们有三种方法来实现:
前两种是 C++98 当中使用的方式:
// 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 当中 的 value
void Add(const pair& kv)
{
_dict[kv.first] = kv.second;
}
// 打印 map
void 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 _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& 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 _dict;
// ...
static Singleton* _psinst;
static GC _gc;
};
Singleton* Singleton::_psinst = nullptr;
Singleton::GC Singleton::_gc;
}