C++进阶:特殊类

目录

  • [1. 不能被拷贝的类](#1. 不能被拷贝的类)
  • [2. 只能在堆上创建的类](#2. 只能在堆上创建的类)
  • [3. 只能在栈上创建的类](#3. 只能在栈上创建的类)
  • [4. 不能被继承的类](#4. 不能被继承的类)
  • [5. 类的设计模式(单例模式)](#5. 类的设计模式(单例模式))
    • [5.1 饿汉模式设计](#5.1 饿汉模式设计)
    • [5.2 懒汉模式设计](#5.2 懒汉模式设计)

特殊类的概念: 特殊类是一些具有特殊行为、用途,用特殊方法设计而出的类。


1. 不能被拷贝的类

创建一个类其不能够被拷贝,不能使用其拷贝构造其他对象,不能用其为其他对象赋值,C++库中就存在着这样的类,诸如,istream、ostream、unique_ptr。C++98及之前都采用私有化拷贝构造与赋值重载的方式来达成,C++11后新增了关键字delete的使用方式可以直接禁止拷贝构造与赋值重载的生成,具体实现细节如下:

cpp 复制代码
//不能被拷贝的类
class NoCopy
{
public:
	NoCopy(int x = 0)
		:_x(x)
	{}

	NoCopy(const NoCopy&) = delete;
	NoCopy& operator=(const NoCopy&) = delete;

private:
	int _x;
};

2. 只能在堆上创建的类

创建出一个只能在堆上的类,这就意味着只能使用new/malloc这样在堆上动态开辟空间的操作。因此,我们不能直接使用构造函数创建出一个对象,具体的设计方式有两种:

  • 方法1:将构造函数设为私有,再专门设置一个对外接口提供在堆上创建对象的方法
cpp 复制代码
//只能在堆上开辟的类
class HeapOnly
{
public:
	template<class ...Args>
	static HeapOnly* CreateObj(Args&&... args)//可变模板参数,可以直接匹配类的构造函数
	{
		return new HeapOnly(args...);
	}

	HeapOnly(const HeapOnly&) = delete;
	HeapOnly& operator=(const HeapOnly&) = delete;

private:
	HeapOnly(int x = 0, int y = 0)
		:_x(x)
		,_y(y)
	{}

	int _x;
	int _y;
};

对外返回new出对象的函数必须要设定为静态成员函数 ,普通成员函数的调用需要this指针即要用这个类的对象来调用。此类的拷贝构造必须设置其禁止生成 ,否则会出现HeapOnly n2(*pn1);以此种方式拷贝构造出栈上对象的情况。此类的赋值重载最好也设置为禁止生成,默认生成的赋值重载为浅拷贝。

  • 方法2: 禁止析构函数的生成,使得用构造函数创建的对象无法自动销毁,程序结束时产生报错,因此,就只能使用动态开辟new的方式去创建对象(new不会自动调用析构)
cpp 复制代码
class HeapOnly
{
public:
	HeapOnly(int x = 0, int y = 0)
		:_x(x)
		,_y(y)
	{}

	//默认生成的拷贝构造

	//默认生成的赋值重载

	//手动释放
	void Destory()
	{
		delete this;
	}

private:
	~HeapOnly()
	{
		cout << "~HeapOnly" << endl;
	}

	int _x;
	int _y;
};

私有化析构设计的只能堆上开辟的类,其对象不能够自动释放自己的资源,但new申请出的资源还是需要释放的。对象的析构函数被私有化,delete时会调用析构,所以,直接delete对象的指针此种方法是不可行的。可以提供一个专门释放资源的成员函数,成员函数本身就可以使用类内私有的方法 。但我们手动释放可能会存在遗忘疏忽的可能,因此,此处可以使用只能指针对对象资源进行管理并配合以定值删除器,shared_ptr<HeapOnly> ptr(new HeapOnly, [](HeapOnly* ptr) { ptr->Destroy(); });

3. 只能在栈上创建的类

设计一个只能在栈上创建的类,也可以说设计一个不能使用new来创建的类。这里可以采取重载声明一个专属的operator new,然后使用delete禁用的方法来实现 。当没有重载operator new时,类会默认使用调用全局的operator new,当有了自己的会默认使用自己。而与operator new相对象的operator delete视情况而定是否要禁用。

同时,此类的构造函数也必须要设置为私有,对外提供一个专门创建对象的静态成员函数。拷贝构造函数不能禁用,返回对象的对外接口为传值返回,在这一过程中需要调用拷贝构造。赋值重载根据使用场景最好也用delete设置为进行生成。

cpp 复制代码
class StackOnly
{
private:
	template<class ...Args>
	static StackOnly CreateObj(Args ...args)
	{
		return StackOnly(args...);
	}

	//不能禁用拷贝构造,CreateObj接口本身就是传值返回

	StackOnly& operator=(const StackOnly&) = delete;

	//重载一个类专属的operator new
	void* operator new(size_t n) = delete;//返回值为void*

private:
	StackOnly()
	{}

	StackOnly(int x, int y)
		:_x(x)
		,_y(y)
	{}

	int _x;
	int _y;
};

4. 不能被继承的类

C++98即之前是采用了私有化构造函数的方式来使得类本身不能够被继承,因为子类继承父类,在构造时需要调用父类的构造函数对父类进行初始化。而C++11后,则添加了关键字final,被此关键字修饰的类代表其为最终类,不可被继承。

cpp 复制代码
//C++98及之前
class A
{
private:
	A()
	{}
};

//C++11
class B final
{};

5. 类的设计模式(单例模式)

类存在着多种多样的设计模式,我们上面所了解到的各种特殊类就可以算作此中。但对于类的设计模式并不是官方的标准,而是在工作与日常中被反复使用,使用性很高的一些设计模式。接下来,就来了解其中一种很常见实用的设计模式,单例模式。单例模式的类常用于存储一些一个进程内只需要一份的资源或信息,诸如内存池、配置信息等。

  • 单例模式: 一个进程的全局范围内,只能够去创建一个对象 ,单例模式存在两种设计方法,两者各有优劣,其中之一被称作饿汉模式 ,另一种被称为懒汉模式

5.1 饿汉模式设计

饿汉模式的类创建一个自己类型的静态成员变量,这个变量属于整个类,并不属于某一个对象。(类内无法定义自己类型的成员变量)这个静态成员变量实际上就相当于是全局变量,其存储在静态区,只不过在类内声明,受类域限制。而需要懒汉模式的对象去存储的资源或信息就存储在这个静态成员变量中,全局的静态变量会在main函数运行之前就创建好并初始化

具体设计时,将此类的构造函数私有化,专门设计一个对外的接口去返回存储资源或信息的静态成员变量。因为此模式下设计的类整个程序的全局范围内只允许有一份资源,所以,此类的构造函数与赋值重载需要使用delete禁止其生成。

想要修改或访问单例模式对象中的资源信息,就可以在设计时提供相应的成员函数。

cpp 复制代码
//饿汉模式
namespace hunger
{
	class Singleton
	{
	public:
		static Singleton* GetInstance()
		{
			return &_sint;
		}

		Singleton(const Singleton&) = delete;
		Singleton& operator=(Singleton&) = delete;
		
		//修改访问内部资源(修改访问场景:增删改)
		void setVal(int x, int y)
		{
			_x = x;
			_y = y;
		}

	private:
		Singleton(int x = 0, int y = 0)
			:_x(x)
			,_y(y)
		{}

		int _x;
		int _y;

		static Singleton _sint;//类内声明
	};

	Singleton Singleton::_sint = { 1, 1 };//类外定义
}

懒汉模式的优点:

  • 1. 全局静态成员变量的创建先于main函数之前,没有线程安全的问题

懒汉模式的缺点:

  • 1. 当单例对象数据较多时,构造初始化很慢成本较高,拖慢整个程序启动的速度
  • 2. 多个单例模式有初始化以来关系时,饿汉模式设计的对象无法控制初始化的顺序

5.2 懒汉模式设计

设计方法1:

懒汉模式的第一种设计模式也是采用全局的静态成员变量的方式去存储资源,但与饿汉模式不同的是,此时的静态成员变量为指针类型。在程序启动时只需要main函数运行前,创建此静态成员变量时,只需要创建一个指针,待到需要调用此对象时,再正式去new出一个对象

具体设计时,因为懒汉模式同样也是单例模式设计的一种,所以,同样也需要禁止拷贝构造与赋值重载的生成。再将构造函数私有化,提供一个对外获取对象的静态成员函数接口。存储资源的静态成员变量指针应被初始化为nullptr。调用GetInstance接口时,当指针为nullptr时,new出一个新对象,当指针不为nullptr时,返回现有的指针

除此之外,还需要创建一个专门用来释放静态成员变量资源的函数接口DelInstance,此接口也要设置为静态成员函数,用于手动释放new出的资源。因为手动释放的不确定性,这里提供两种解决方案:

  • 方法1:定义一个私有的内部类(防止外部去访问),在懒汉模式的类中设置一个此内部类类型的静态成员变量。在其析构函数中,调用懒汉模式对象的资源释放接口。静态成员变量的声明周期随进程,当进程结束时,自动析构同时释放资源。
  • 方法2:使用智能指针帮忙管理,在定值删除器中传入相应的资源释放接口
cpp 复制代码
namespace lazy
{
	class Singleton
	{
	public:
		static Singleton* GetInstance()
		{
			if (_psint == nullptr)
			{
				return new Singleton();
			}
		}

		static void DelInstance()
		{
			if (_psint)
			{
				delete _psint;
				_psint = nullptr;
			}
		}

		Singleton(Singleton const&) = delete;
		Singleton& operator=(Singleton const&) = delete;

	private:
		Singleton(int x = 0, int y = 0)
			:_x(x)
			,_y(y)
		{}

		~Singleton()
		{
			//析构时,可能会需要将数据写到文件中去
			cout << "Singleton" << endl;
		}		

		int _x;
		int _y;

		static Singleton* _psint;

		//内部类,自动控制资源释放
		class GC
		{
		public:
			~GC()
			{
				Singleton::DelInstance();
			}
		};

		static GC gc;//类内声明
	};

	Singleton* Singleton::_psint = nullptr;
	Singleton::GC Singleton::gc;//类外定义
}

懒汉模式的优点:

  • 1. 不会影响程序的启动速度
  • 2. 可以手动控制单例对象的创建顺序

懒汉模式的缺点:

  • 1. 存在线程安全问题,需要加锁

设计方法2:

GetInstance接口中创建一个局部的静态成员变量用来存储资源,局部的静态成员变量声明周期是全局的。因为是局部的静态成员变量,所以,只有当第一次调用GetIsntance接口时才会创建对象。此方法在C++11前时无法使用的,在C++11以前此方法无法保证线程安全,C++11标准后才解决了这里的线程安全问题

cpp 复制代码
namespace lazy
{
	class Singleton
	{
	public:
		static Singleton* GetInstance()
		{
			static Singleton _sinst;

			return &_sinst;
		}

		Singleton(Singleton const&) = delete;
		Singleton& operator=(Singleton const&) = delete;

	private:
		Singleton(int x = 0, int y = 0)
			:_x(x)
			,_y(y)
		{}

		~Singleton()
		{
			cout << "Singleton" << endl;
		}

		int _x;
		int _y;
	};
}