【C++】特殊类的设计

2024-03-01 10:58:06 浏览数 (1)

特殊类的设计

一、设计一个不能被拷贝的类

拷贝只会放生在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。

有以下两种方法,分别是C 98和C 11;

  • C 98

将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可,如下:

代码语言:javascript复制
				class CopyBan
				{
				public:
					CopyBan()
					{}
				
				private:
					CopyBan(const CopyBan&);
					CopyBan& operator=(const CopyBan&);
				};

原因:

  1. 设置成私有:如果只声明没有设置成 private,用户自己如果在类外定义了,就不能禁止拷贝了;
  2. 只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写反而还简单,而且如果定义了就不会防止成员函数内部拷贝了。
  • C 11

C 11扩展 delete 的用法,delete 除了释放 new 申请的资源外,如果在默认成员函数后跟上 =delete ,表示让编译器删除掉该默认成员函数,如下:

代码语言:javascript复制
				class CopyBan
				{
				public:
					CopyBan()
					{}
				
				private:
					CopyBan(const CopyBan&) = delete;
					CopyBan& operator=(const CopyBan&) = delete;
				};

二、设计一个只能在堆上创建对象的类

  • 方法一:析构函数私有化

将析构函数私有化可以保证不能直接创建对象,因为不能直接调用析构函数,所以只能使用 new 在堆上申请空间。那么问题来了,该怎么释放空间呢?很简单,我们直接提供一个接口,释放 this 指针即可。如下:

代码语言:javascript复制
				class HeapOnly
				{
				public:
					void Destroy()
					{
						delete this;
					}
				
				private:
					~HeapOnly()
					{
						cout << "~HeapOnly()" << endl;
					}
				};

我们测试一下看看:

代码语言:javascript复制
				int main()
				{
					HeapOnly* hp = new HeapOnly;
					hp->Destroy();
				
					return 0;
				}
  • 方法二:构造函数私有化
  1. 将类的构造函数私有,拷贝构造声明成私有。防止别人调用拷贝在栈上生成对象。
  2. 提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建

如下:

代码语言:javascript复制
				class HeapOnly
				{
				public:
					static HeapOnly* CreateObj()
					{
						return new HeapOnly;
					}
				
				private:
					HeapOnly()
					{}
				
					HeapOnly(const HeapOnly&) = delete;
					HeapOnly& operator=(const HeapOnly&) = delete;
				};

创建对象的方法:

代码语言:javascript复制
				int main()
				{
					HeapOnly* hp = HeapOnly::CreateObj();
					return 0;
				}

三、设计一个只能在栈上创建对象的类

思路还是和上面的类似,首先我们先把构造函数私有化,并提供一个返回在栈上创建对象的接口,如下:

代码语言:javascript复制
				class StackOnly
				{
				public:
					static StackOnly CreateObj()
					{
						StackOnly obj;
						return obj;
					}
				private:
					StackOnly()
					{}
				};

但是这时候还存在另一个问题,还没有完全把 new 禁掉,例如以下代码还是可以通过的:

代码语言:javascript复制
				int main()
				{
					StackOnly st1 = StackOnly::CreateObj();
					StackOnly* st2 = new StackOnly(st1);
				
					return 0;
				}

那么如果我们把拷贝构造也禁掉呢?在这里是不行的,因为我们返回栈上的对象的接口 CreateObj() 是传值返回,需要拷贝构造函数。所以另外一个方法就是在类里面写一个 operator new 并禁掉。为什么这样可以呢?其实,如果我们使用 new,默认调的是全局的 new,但是如果我们在类中写一个 operator new,那么当我们 new 一个该类的对象时,会优先调用我们自己写的 operator new,所以我们只需要把自己写的 operator new 禁掉即可,如下:

代码语言:javascript复制
				class StackOnly
				{
				public:
					static StackOnly CreateObj()
					{
						StackOnly obj;
						return obj;
					}
				
					// 实现类专属的 operator new
					void* operator new(size_t size) = delete;
				private:
					StackOnly()
					{}
				};

四、设计一个不能被继承的类

  • C 98

C 98 中构造函数私有化,派生类中调不到基类的构造函数,则无法继承,如下:

代码语言:javascript复制
				class NonInherit
				{
				public:
					static NonInherit GetInstance()
					{
						return NonInherit();
					}
				private:
					NonInherit()
					{}
				};
  • C 11

final 关键字,final 修饰类,表示该类不能被继承。如下:

代码语言:javascript复制
				class NonInherit final
				{
				public:
					NonInherit()
					{}
				private:
					// ...
				};

五、单例模式

单例模式就是,设计一个只能创建一个对象的类。首先我们先了解一下设计模式。

设计模式:设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。

使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。

  • 单例模式

一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。

单例模式有两种实现模式:

1. 饿汉模式

饿汉模式就是说不管你将来用不用,程序启动时就创建一个唯一的实例对象。

首先我们必须将构造函数、拷贝构造和赋值重载私有化。饿汉模式是在程序启动时就创建并初始化一个唯一的对象,所以我们可以使用一个全局静态变量,全局变量是进入 main 函数之前就完成初始化的,所以设为全局变量;而静态是为了能在私有化构造函数的类中创建对象,我们在类和对象部分也讲过,static 的类成员不算该类的成员,静态成员变量属于所有类的对象,属于整个类,即属于整个 Singleton 类。 我们将 GetInstance() 函数也设为静态成员函数,因为在类外只需要突破类域就能访问该函数。饿汉模式的单例模式设计如下:

代码语言:javascript复制
				class Singleton
				{
				public:
					static Singleton* GetInstance()
					{
						return &_inst;
					}
				
				private:
					static Singleton _inst;  // 声明
				
					Singleton()
					{}
					Singleton(const Singleton&) = delete;
					Singleton& operator=(const Singleton&) = delete;
				};
				
				// 定义
				Singleton Singleton::_inst;

我们也可以返回它的引用,并写一个 Print() 函数进行测试:

代码语言:javascript复制
				class Singleton
				{
				public:
					static Singleton& GetInstance()
					{
						return _inst;
					}
				
					void Print()
					{
						cout << "void Print()" << endl;
					}
				
				private:
					static Singleton _inst;  // 声明
				
					Singleton()
					{}
					Singleton(const Singleton&) = delete;
					Singleton& operator=(const Singleton&) = delete;
				};
				
				// 定义
				Singleton Singleton::_inst;
  • 饿汉模式的优点:实现简单
  • 饿汉模式的缺点:可能会导致进程启动慢;如果两个单例有启动先后顺序,那么饿汉无法控制

如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避免资源竞争,提高响应速度更好。因为在程序启动前需要进行初始化,如果需要初始化的资源很多,就会降低程序的启动速度。

2. 懒汉模式

如果单例对象构造十分耗时或者占用很多资源,比如加载插件, 初始化网络连接,读取文件等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载)更好。懒汉模式就是在我们需要使用时,才给我们创建对象。

懒汉模式不用在程序启动时就准备好对象,所以我们只需要初始化一个空指针即可,因为空指针不费资源,初始化也很快。在用户调用 GetInstance() 函数时,说明需要创建对象,我们再在函数内部判断是否已经初始化过该对象,即判断 _inst 是否为空,如果为空则 new 一个对象给它并返回,否则直接返回。这样就完成了单例模式的懒汉模式,代码如下:

代码语言:javascript复制
				class Singleton
				{
				public:
					static Singleton* GetInstance()
					{
						if (_inst == nullptr)
						{
							_inst = new Singleton;
						}
						return _inst;
					}
				private:
					static Singleton* _inst;
				
					Singleton()
					{}
					Singleton(const Singleton&) = delete;
					Singleton& operator=(const Singleton&) = delete;
				};
				Singleton* Singleton::_inst = nullptr;

注意,new 的懒汉对象一般不需要释放,进程正常结束会释放资源;但是如果需要做一些动作,比如持久化(持久化:要求把数据写到文件),那么可以利用 gcstatic 对象搞定。如下代码:

代码语言:javascript复制
				class Singleton
				{
				public:
					static Singleton* GetInstance()
					{
						if (_inst == nullptr)
						{
							_inst = new Singleton;
						}
						return _inst;
					}
				
					// 使用 delete 调用析构函数
					static void DelInstance()
					{
						if (_inst)
						{
							delete _inst;
							_inst = nullptr;
						}
					}
				private:
					static Singleton* _inst;
				
					Singleton()
					{}
					Singleton(const Singleton&) = delete;
					Singleton& operator=(const Singleton&) = delete;
				
					~Singleton()
					{
						cout << "持久化动作..." << endl;
					}
				
					class gc
					{
					public:
						~gc()
						{
							DelInstance();
						}
					};
					// 该对象会在 main 函数结束后自动调用析构函数
					static gc _gc;
				};
				Singleton* Singleton::_inst = nullptr;
				Singleton::gc Singleton::_gc;

上述代码中,_gc 对象在 main 函数结束后会自动调用它自己的析构函数,所以我们在它的析构函数调用 DelInstance() 函数,而 DelInstance() 函数是 Singleton 类的一个静态成员函数,我们在 DelInstance() 函数中使用 delete _inst,使它调用 Singleton 类的析构函数,这样我们就可以在析构函数里面做持久化的动作。

  • 懒汉模式的优点:第一次使用实例对象时,创建对象。进程启动无负载。多个单例实例启动顺序自由控制
  • 懒汉模式的缺点:复杂

有关懒汉模式还有线程安全问题需要解决,我们后面再解决…

0 人点赞