C++ - 一些特殊类的设计

前言

我们在日常写项目的过程当中,肯定会遇到各种各样的需求,那么也就要求我们要写各种各样的类。本篇博客当中,就一些常用的特殊类进行介绍和实现。

不能被拷贝的类

 关于实例化类拷贝(对象的拷贝)一般就是两个场景,第一个是 拷贝构造函数;第二个是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 出来再堆上开辟的空间的话就会报错:
 

C++ - 一些特殊类的设计_第1张图片

如果不是在堆上 存储的对象,编译器是会自动调用这个对象的析构函数来 释放这个对象的,但是以为 类当中的 析构函数是 私有的,所以就不能调用到。

但是 在堆上存储的对象是 不会自动 释放的,需要手动释放,这也是内存泄漏的一大原因 之一,所以这种情况下,只有 在堆上 存储的对象才能 正常使用。

但是现在的问题是,因为 析构函数是 私有的,所以,此时我们进行手动释放也是释放不了的。

C++ - 一些特殊类的设计_第2张图片

 解决方式就是提供接口,因为 私有是 类外不能访问,但是在 类内 是能访问的,所以,我们可以提供一个接口,在外部调用这个接口,在接口当中调用析构函数。

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);
}

报错:
 

C++ - 一些特殊类的设计_第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;
};

此时:
 

C++ - 一些特殊类的设计_第4张图片

 此时也是可以实现的。

但是需要注意的是,我们还是要把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;
};

 报错:

C++ - 一些特殊类的设计_第5张图片

 写一个不能被继承的类

 第一种就是使用 "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;
}

结果:
 

C++ - 一些特殊类的设计_第6张图片

 使用本类当中创建一个 静态的本类对象 方法实现:
 

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;
}

结果和上述是一样的。

看上述的例子,发现,在本类当中是可以创建一个 静态的本类对象的。但是不能在 本类当中是可以创建一个静态的本类对象。

这样的话就会陷入死循环了,编译器也会直接报错不会让我们这样做的:

C++ - 一些特殊类的设计_第7张图片

 除了上述,我们还有防止拷贝构造:
 

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;
}

你可能感兴趣的:(c++,开发语言)