<C++11> 特殊类设计

目录

一、设计一个类,只允许在堆上创建对象

二、设计一个类,只允许在栈上创建对象

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

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

五、设计一个类,只允许存在一个对象--单例模式

饿汉单例模式

懒汉单例模式


一、设计一个类,只允许在堆上创建对象

只能在堆上创建对象,也就是只能通过new操作符创建对象,方式如下:

  1. 将构造函数设置为私有,防止外部直接调用构造函数在栈上创建对象。
  2. 向外部提供一个获取对象的static接口,该接口在堆上创建一个对象并返回。
  3. 将拷贝构造函数设置为私有(C++98)并且只声明不实现,或者将拷贝构造函数后加上 = delete(C++11),防止外部调用拷贝构造函数在栈上创建对象。

代码如下:

class HeapOnly
{
public:
	//2、提供一个获取对象的接口,并且该接口必须设置为静态成员函数
	static HeapOnly* CreateObj()
	{
		return new HeapOnly;
	}
private:
	//1、将构造函数设置为私有
	HeapOnly()
	{}
	//3、将拷贝构造函数设置为私有,并且只声明不实现
	//C++98
	HeapOnly(const HeapOnly&);
	//C++11
	//HeapOnly(const HeapOnly&) = delete;
};

说明一下:

  • 向外部提供的CreateObj函数必须设置为静态成员函数,因为外部调用该接口就是为了获取对象的,而非静态成员函数必须通过对象才能调用,这就变成鸡生蛋蛋生鸡的问题了。
  • C++98通过将拷贝构造函数声明为私有以达到防拷贝的目的,C++11可以在拷贝构造函数后面加上=delete,表示让编译器将拷贝构造函数删除,此时也能达到防拷贝的目的。

二、设计一个类,只允许在栈上创建对象

方法一

方式如下:

  1. 将构造函数设置为私有,防止外部直接调用构造函数在堆上创建对象。
  2. 向外部提供一个获取对象的static接口,该接口在栈上创建一个对象并返回
class StackOnly
{
public:
	//2、提供一个获取对象的接口,并且该接口必须设置为静态成员函数
	static StackOnly CreateObj()
	{
		return StackOnly();
	}
private:
	//1、将构造函数设置为私有
	StackOnly()
	{}
};

问题:new的时候可以调用拷贝构造,此时new底层调用的就是拷贝构造函数,而编译器默认生成的拷贝构造函数会逐字节拷贝,所以会拷贝构造出一个栈上的对象

StackOnly st1 = StackOnly::CreateObj();
StackOnly* st2 = new StackOnly(st1);

因为new的底层是operator new + 构造函数,而现在我们调用的是拷贝构造,所以该类的设计有这么一个漏洞。

那么我们直接把拷贝构造封了,是否可行?

不可行,不能封拷贝构造,因为CreateObj返回值的是一个栈上的对象,返回的时候需要调用拷贝构造。因为new默认调用的是全局的operator new,所以我们可以在类中重载一个该类专属的operator new

方法2.0:

  • 在类中重载一个该类专属的operator new
class StackOnly
{
public:
	StackOnly()
	{}
private:
	//C++98
	void* operator new(size_t size);
	void operator delete(void* p);
	//C++11
	//void* operator new(size_t size) = delete;
	//void operator delete(void* p) = delete;
};

 new和delete的原理:

  • new在堆上申请空间实际分为两步,第一步是调用operator new函数申请空间,第二步是在申请的空间上执行构造函数,完成对象的初始化工作。
  • delete在释放堆空间也分为两步,第一步是在该空间上执行析构函数,完成对象中资源的清理工作,第二步是调用operator delete函数释放对象的空间。

new和delete默认调用的是全局的operator new函数和operator delete函数(会先检查类域,没有的话再去全局找),但如果一个类重载了专属的operator new函数和operator delete函数,那么new和delete就会调用这个专属的函数。所以只要把operator new函数和operator delete函数屏蔽掉,那么就无法再使用new在堆上创建对象了。

但该方法也有一个缺陷,就是无法防止外部在静态区创建对象,防不住static对象。

static StackOnly obj; //在静态区创建对象

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

要让一个类不能被拷贝,就要让该类不能调用拷贝构造函数和赋值运算符重载函数,因此直接将该类的拷贝构造函数和赋值运算符重载函数设置为私有,或者用C++11的方式将这两个函数删除即可。

class CopyBan
{
public:
	CopyBan()
	{}
private:
	//C++98
	CopyBan(const CopyBan&);
	CopyBan& operator=(const CopyBan&);
	//C++11
	//CopyBan(const CopyBan&) = delete;
	//CopyBan& operator=(const CopyBan&) = delete;
};

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

方法一:C++98

将该类的构造函数设置为私有即可,因为子类的构造函数被调用时,必须调用父类的构造函数初始化父类的那一部分成员,但父类的私有成员在子类当中是不可见的,所以在创建子类对象时子类无法调用父类的构造函数对父类的成员进行初始化,因此该类被继承后子类无法创建出对象。

代码如下:

class NonInherit
{
public:
	static NonInherit CreateObj()
	{
		return NonInherit();
	}
private:
	//将构造函数设置为私有
	NonInherit()
	{}
};

方法二:C++11

C++98的这种方式其实不够彻底,因为这个类仍然可以被继承(编译器不会报错),只不过被继承后无法实例化出对象而已。于是C++11中提供了final关键字,被final修饰的类叫做最终类,最终类无法被继承,此时就算继承后没有创建对象也会编译出错。

代码如下:

class NonInherit final
{
	//...
};

五、设计一个类,只允许存在一个对象--单例模式

什么是单例模式?

  • 单例模式是一种设计模式(Design Pattern),设计模式就是一套被反复使用、多数人知晓的、经过分门别类的、代码设计经验的总结。使用设计模式的目的就是为了可重用代码、让代码更容易被他人理解、保证代码可靠性程序的重用性。
  • 单例模式指的就是一个类只能创建一个对象,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
  • 比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象同一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。 

单例模式有两种实现方式,分别是饿汉模式和懒汉模式:

饿汉单例模式

饿汉模式

单例模式的饿汉实现方式如下:

  1. 将构造函数设置为私有,并将拷贝构造函数和赋值运算符重载函数设置为私有或删除,防止外部创建或拷贝对象。
  2. 提供一个单例的static对象,并在程序入口之前完成单例对象的初始化。
  3. 提供一个全局访问点获取单例对象。

代码如下:

// 饿汉单例模式类
namespace hungry
{
	class Singleton
	{
	public:
		// 2. 获取单例对象的接口函数
		static Singleton& GetInstance()
		{
			return _sinst;
		}

		void Add(const pair<string, string>& kv)
		{
			// 覆盖形式
			_dict[kv.first] = kv.second;
		}

		void Print()
		{
			for (const auto& kv : _dict)
			{
				cout << kv.first << " " << kv.second << 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函数之前是不存在多线程的,因此饿汉模式下单例对象的创建过程是线程安全的。
  • 后续所有多线程要访问这个单例对象,都需要通过调用GetInstance函数来获取,这个获取过程是不需要加锁的,因为这是一个读操作,没有涉及修改,不会引起数据不一致问题。(例如读者写者模型的读者)
  • 当然,如果线程通过GetInstance获取到单例对象后,要用这个单例对象进行一些线程不安全的操作,那么这时就需要加锁了。

在一个类中,不能用本类类型定义一个普通的成员变量,但是如果加上static就可以定义,原因如下: 

在C++中,类的 静态成员变量static 成员变量)与普通成员变量有本质的区别。静态成员变量属于类本身,而不是类的某个实例。因此,静态成员变量的存储和生命周期与类的实例无关,这解决了普通成员变量导致的无限递归问题。

为什么普通成员变量不行?

普通成员变量是类的实例的一部分,每个类的实例都会包含这些成员变量的存储空间。如果类中包含一个本类类型的普通成员变量,就会导致无限递归的定义问题,因为编译器无法确定类的大小。

例如:

class MyClass {
    MyClass member;  // 错误:无限递归
};

这里,MyClass 的每个实例都需要包含一个 MyClass 类型的成员变量,而这个成员变量又需要包含另一个 MyClass 类型的成员变量,依此类推,导致无限递归。

为什么静态成员变量可以?

静态成员变量不属于类的某个实例,而是属于类本身。它在程序的生命周期内只有一份存储空间,所有类的实例共享这个静态成员变量。因此,静态成员变量不会影响类实例的大小,也不会导致无限递归。

例如:

class MyClass {
    static MyClass member;  // 允许:静态成员变量
};

这里,MyClass::member 是一个静态成员变量,它不属于任何 MyClass 的实例,而是属于类本身。编译器只需要为 MyClass::member 分配一份存储空间,而不会影响 MyClass 实例的大小。

所以sizeof(类类型)的结果不受static成员大小影响

静态成员变量不会影响 sizeof 类类型的结果。sizeof 计算的是类的实例(对象)的大小,而静态成员变量不属于类的实例,因此不会被计算在内。

为什么静态成员变量不影响 sizeof

  1. 存储位置不同

    • 普通成员变量存储在类的实例中,每个实例都有自己的一份拷贝。

    • 静态成员变量存储在全局数据区,所有实例共享同一份拷贝。

  2. 生命周期不同

    • 普通成员变量的生命周期与类的实例绑定。

    • 静态成员变量的生命周期与程序的生命周期绑定。

  3. 内存分配不同

    • 普通成员变量是类实例的一部分,sizeof 会计算它们的大小。

    • 静态成员变量是类的一部分,但不属于类的实例,因此 sizeof 不会计算它们的大小。


示例代码

#include <iostream>

class MyClass {
public:
    int x;                // 普通成员变量
    static int y;          // 静态成员变量
};

int MyClass::y = 0;        // 静态成员变量的定义和初始化

int main() {
    std::cout << "Size of MyClass: " << sizeof(MyClass) << std::endl;
    return 0;
}
输出:
Size of MyClass: 4
解释:
  • MyClass 中有一个普通成员变量 int x,它占 4 个字节(假设 int 是 4 字节)。

  • 静态成员变量 static int y 不占用 MyClass 实例的空间,因此 sizeof(MyClass) 只计算 int x 的大小,结果是 4。

懒汉单例模式

单例模式的懒汉实现方式如下:

  1. 将构造函数设置为私有,并将拷贝构造函数和赋值运算符重载函数设置为私有或删除,防止外部创建或拷贝对象。
  2. 提供一个指向单例对象的static指针,并在程序入口之前先将其初始化为空。
  3. 提供一个全局访问点获取单例对象。

代码如下:

// 懒汉,因为_psinst定义的是指针,没有初始化
namespace lazy
{
	class Singleton
	{
	public:
		// 2. 获取单例对象的接口函数
		static Singleton* GetInstance()
		{
			if (_psinst == nullptr)
			{
				_psinst = new Singleton;
			}
			return _psinst;
		}

		static void DelInstance()
		{
			if (_psinst)
			{
				delete _psinst;
				_psinst = nullptr;
			}
		}
		void Add(const pair<string, string>& kv)
		{
			// 覆盖形式
			_dict[kv.first] = kv.second;
		}

		void Print()
		{
			for (const auto& kv : _dict)
			{
				cout << kv.first << " " << kv.second << endl;
			}
		}

	private:
		// 1. 构造函数私有
		Singleton()
		{}

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

		// 3. 防拷贝
		Singleton(const Singleton& s) = delete;
		Singleton& operator=(const Singleton& s) = delete;

		map<string, string> _dict;
		// 定义一个唯一的静态的类对象
		static Singleton* _psinst;
	};

	// 类内声明,类外定义
	Singleton* Singleton::_psinst = nullptr;
}

懒汉采用指针形式,所以在初始化时没有初始化对象,当程序主动调用GetInstance获取单例资源时,我们才new空间资源,而饿汉是在main函数调用前就已经初始化对象了(因为是static对象)

线程安全相关问题:

  • 懒汉模式在程序运行之前没有进行单例对象的创建,而是等到某个线程需要使用这个单例对象时再进行创建,也就是GetInstance函数第一次被调用时创建单例对象。
  • 因此在调用GetInstance函数获取单例对象时,需要先判断这个static指针是否为空,如果为空则说明这个单例对象还没有创建,此时需要先创建这个单例对象然后再将单例对象返回。
  • GetInstance函数第一次调用时需要对static指针进行写入操作,这个过程不是线程安全的,因为多个线程可能同时调用GetInstance函数,如果不对这个过程进行保护,此时这多个线程就会各自创建出一个对象。

双检查加锁:

  • 对GetInstance函数中创建单例对象的过程进行保护,本质就是需要引入互斥锁,最简单的加锁方式就是在进行if判断之前加锁,在整个if语句之后进行解锁。
  • 但实际只有GetInstance函数第一次被调用,创建单例对象时需要使用互斥锁进行保护,而后续调用GetInstance函数获取单例对象只是一个读操作,是不需要使用互斥锁进行保护的。
  • 如果简单的将加锁解锁操作放到if语句前后,那么在后续调用GetInstance函数获取已经创建好的单例对象时,就会进行大量无意义的加锁解锁操作,导致线程不断切入切出,进而影响程序运行效率。
  • 对于这种只有第一次需要加锁保护的场景可以使用双检查加锁,双检查就是在当前加锁和解锁的外面再进行一次if判断,判断static指针是否为空。
  • 这样一来,后续调用GetInstance函数获取已经创建好的单例对象时,外层新加的if判断就会起作用,这样就避免了后续无意义的加锁解锁操作。

饿汉模式和懒汉模式对比

  • 饿汉模式的优点就是简单,但是它的缺点也比较明显。饿汉模式在程序运行主函数之前就会创建单例对象,如果单例类的构造函数中所做的工作比较多,就会导致程序迟迟无法进入主函数,在外部看来就好像是程序卡住了。
  • 此外,如果有多个单例类需要创建单例对象,并且它们之间的初始化存在某种依赖关系,比如单例对象A的创建必须在单例对象B之后,此时饿汉模式也会存在问题,因为我们无法保证这多个单例对象中的哪个对象先创建。
  • 而懒汉模式就能很好的解决上述饿汉模式的缺点,因为懒汉模式并不是一开始就完成单例对象的创建,因此不会导致程序迟迟无法进入主函数,并且懒汉模式中各个单例对象创建的顺序是由各个单例类中的GetInstance函数第一次被调用的顺序决定,因此是可控制的。
  • 懒汉模式的缺点就是,在编码上比饿汉模式复杂,在创建单例对象时需要考虑线程安全的问题。

单例对象的释放

单例模式的资源,一般来说都是一直使用到进程退出,当进程退出后,进程所掌握的资源会被OS释放,所以不用担心没有delete而造成内存泄漏,当然了我们也可以使用智能指针来管理单例模式的资源。

如果我们想在main函数中显示的释放单例模式的资源,该怎么办?

很明显,如果采用智能指针的形式,是不可行的,因为如果手动释放资源后,当程序出了main函数作用域,智能指针会自动调用析构函数,造成资源的二次析构!

所以我们需要手动写一个DelInstance函数,函数内delete资源,当 delete_psinst 后,delete底层会先调用该资源的析构函数,再释放空间。

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

持久化:在进程退出前,程序需要进行一些收尾工作,例如将数据写入文件中,所以此时就更需要进程退出前进行显示的delete,在析构之前进行收尾工作

 如何在进程退出之前,自动调用析构,有两种方案,仿照智能指针利用生命周期来销毁

方法一:类外定义一个类

class GC
{
public:
	~GC()
	{
		lazy::Singleton::DelInstance();
	}
};

GC gc;

int main()
{
	// 饿汉
	//hungry::Singleton::GetInstance().Add({ "xxx", "111" });
	//hungry::Singleton::GetInstance().Add({ "yyy", "222" });
	//hungry::Singleton::GetInstance().Add({ "zzz", "333" });
	//hungry::Singleton::GetInstance().Add({ "abc", "444" });

	//hungry::Singleton::GetInstance().Print();

	// 懒汉
	lazy::Singleton::GetInstance()->Add({ "xxx", "111" });
	lazy::Singleton::GetInstance()->Add({ "yyy", "222" });
	lazy::Singleton::GetInstance()->Add({ "zzz", "333" });
	lazy::Singleton::GetInstance()->Add({ "abc", "444" });

	//lazy::Singleton::GetInstance()->Print();
	// 进程退出后,new出来的空间自动释放,但是如果我们想在main函数中,手动控制释放,就需要显示的写Del函数
	// 持久化:在进程退出之前还需要一些收尾工作,所以不能采取上面进程退出空间资源自动释放的策略,因为new出来的空间不会自动delete
	// 所以有两种方法,外面套一个类(注意如果使用智能指针,那么我们不能手动的析构资源,否则智能指针除了作用域会造成二次析构)

	return 0;
}

很明显,gc是一个全局变量,在main函数退出前会自用调用gc的析构函数,从而回调DelInstance函数,完成资源的delete,收尾工作一般在单例类的析构函数中。

方法二:内部类

在单例类中定义GC内部类,再定义一个static类型的GC对象,GC对象在main函数退出前会自动析构,调用DelInstance函数

// 懒汉,因为_psinst定义的是指针,没有初始化
namespace lazy
{
	class Singleton
	{
	public:
		// 2. 获取单例对象的接口函数
		static Singleton* GetInstance()
		{
			if (_psinst == nullptr)
			{
				_psinst = new Singleton;
			}
			return _psinst;
		}

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

		void Add(const pair<string, string>& kv)
		{
			// 覆盖形式
			_dict[kv.first] = kv.second;
		}

		void Print()
		{
			for (const auto& kv : _dict)
			{
				cout << kv.first << " " << kv.second << endl;
			}
		}

		// 方法二:内部类,直接定义static的GC对象
		class GC
		{
		public:
			~GC()
			{
				lazy::Singleton::DelInstance();
			}
		};

	private:
		// 1. 构造函数私有
		Singleton()
		{}

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值