【重点:单例模式】特殊类设计

这篇具有很好参考价值的文章主要介绍了【重点:单例模式】特殊类设计。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

请设计一个类,只能在堆上创建对象

方式如下:

  • 将构造函数设置为私有,防止外部直接调用构造函数在栈上创建对象。
  • 向外部提供一个获取对象的static接口,该接口在堆上创建一个对象并返回。
  • 将拷贝构造函数设置为私有,并且只声明不实现,防止外部调用拷贝构造函数在栈上创建对象。
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,表示让编译器将拷贝构造函数删除,此时也能达到防拷贝的目的。

举例:

class HeapOnly
{
public:
	static HeapOnly* createObj()
	{
		return new HeapOnly;
	}
private:
	//1、将构造函数设置为私有
	HeapOnly()
		:_a(0)
	{}
	//3、将拷贝构造函数设置为私有,并且只声明不实现
	//C++98
	HeapOnly(const HeapOnly&);
	//C++11
	//HeapOnly(const HeapOnly&) = delete;
private:
	int _a;

};
int main()
{
	HeapOnly ho1;//栈上
	//HeapOnly* ptr = HeapOnly::createObj();
	//HeapOnly copy(*ptr);
	cout << ptr << endl;
	return 0;
}

【重点:单例模式】特殊类设计,C++,单例模式,java
当我们想通过HeapOnly ho1;在外部直接调用构造函数在栈上创建对象时,我们看到构造函数为私有,防止了我们在栈上创建对象。

因为构造函数私有的关系,我们只能通过提供的获取对象的static接口(createObj()),该接口在堆上创建一个对象并返回,以此来获取堆上的对象。

HeapOnly* ptr = HeapOnly::createObj();

请设计一个类,只能在栈上创建对象

方法一:同上将构造函数私有化,然后设计静态方法创建对象返回即可

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

但该方法有一个缺陷就是,无法防止外部调用拷贝构造函数创建对象。

StackOnly obj1 = StackOnly::CreateObj();
static StackOnly obj2(obj1); //在静态区拷贝构造对象
StackOnly* ptr = new StackOnly(obj1); //在堆上拷贝构造对象

但是我们不能将构造函数设置为私有,也不能用=delete的方式将拷贝构造函数删除,因为CreateObj函数当中创建的是栈上局部对象,返回局部对象的过程中势必需要调用拷贝构造函数

方法二:屏蔽operator new函数和operator delete函数。

class StackOnly
{
public:
	//2、提供一个获取对象的接口,并且该接口必须设置为静态成员函数
	static StackOnly CreateObj()
	{
		return StackOnly();
	}
	
private:
	void* operator new(size_t size) = delete;
	void operator delete(void* p) = delete;
	//1、将构造函数设置为私有
	StackOnly()
		:_a(0)
	{}
private:
	int _a;
};
	

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

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

饿汉模式

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

  • 将构造函数设置为私有,并将拷贝构造函数和赋值运算符重载函数设置为私有或删除,防止外部创建或拷贝对象。
  • 提供一个指向单例对象的static指针,并在程序入口之前完成单例对象的初始化。
  • 提供一个全局访问点获取单例对象。
class Singleton
{
public:
	//3、提供一个全局访问点获取单例对象
	static Singleton* GetInstance()
	{
		return _inst;
	}
private:
	//1、将构造函数设置为私有,并防拷贝
	Singleton()
	{}
	Singleton(const Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;
	//2、提供一个指向单例对象的static指针
	static Singleton* _inst;
};

//在程序入口之前完成单例对象的初始化
Singleton* Singleton::_inst = new Singleton;

线程安全相关问题:

  • 饿汉模式在程序运行主函数之前就完成了单例对象的创建,由于main函数之前是不存在多线程的,因此饿汉模式下单例对象的创建过程是线程安全的。
  • 后续所有多线程要访问这个单例对象,都需要通过调用GetInstance函数来获取,这个获取过程是不需要加锁的,因为这是一个读操作。
  • 当然,如果线程通过GetInstance获取到单例对象后,要用这个单例对象进行一些线程不安全的操作,那么这时就需要加锁了。

懒汉模式

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

  • 将构造函数设置为私有,并将拷贝构造函数和赋值运算符重载函数设置为私有或删除,防止外部创建或拷贝对象。
  • 提供一个指向单例对象的static指针,并在程序入口之前先将其初始化为空。
  • 提供一个全局访问点获取单例对象。
class Singleton
{
public:
	//3、提供一个全局访问点获取单例对象
	static Singleton* GetInstance()
	{
		//双检查
		if (_inst == nullptr)
		{
			_mtx.lock();
			if (_inst == nullptr)
			{
				_inst = new Singleton;
			}
			_mtx.unlock();
		}
		return _inst;
	}
private:
	//1、将构造函数设置为私有,并防拷贝
	Singleton()
	{}
	Singleton(const Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;
	//2、提供一个指向单例对象的static指针
	static Singleton* _inst;
	static mutex _mtx; //互斥锁
};

//在程序入口之前先将static指针初始化为空
Singleton* Singleton::_inst = nullptr;
mutex Singleton::_mtx; //初始化互斥锁

线程安全相关问题:

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

双检查加锁:

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

饿汉模式和懒汉模式对比

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

其他版本的懒汉

懒汉模式还有一种比较经典的实现方式:

  • 将构造函数设置为私有,并将拷贝构造函数和赋值运算符重载函数设置为私有或删除,防止外部创建或拷贝对象。
  • 提供一个全局访问点获取单例对象。
class Singleton
{
public:
	//2、提供一个全局访问点获取单例对象
	static Singleton* GetInstance()
	{
		static Singleton inst;
		return &inst;
	}
private:
	//1、将构造函数设置为私有,并防拷贝
	Singleton()
	{}
	Singleton(const Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;
};

在单例类的GetInstance函数中定义一个静态的单例对象并返回。

  • 由于实际只有第一次调用GetInstance函数时才会定义这个静态的单例对象,这也就保证了全局只有这一个唯一实例。
  • 并且这里单例对象的定义过程是线程安全的,因为现在的C++标准保证多线程初始化static变量不会发生数据竞争,可以视为原子操作。
  • 该方法属于懒汉模式,因为局部静态变量不是在程序运行主函数之前初始化的,而是在第一次调用GetInstance函数时初始化的。

这种版本的懒汉主要有如下两个缺点:

  • 单例对象定义在静态区,因此太大的单例对象不适合使用这种方式。
  • 单例对象创建在静态区后没办法主动释放。

单例对象的释放

单例对象创建后一般在整个程序运行期间都可能会使用,所以我们可以不考虑单例对象的释放,程序正常结束时会自动将资源归还给操作系统。

如果要考虑单例对象的释放,可以参考以下两种方式:

1.在单例类中编写一个DelInstance函数,在该函数中进行单例对象的释放动作,当不再需要该单例对象时就可以主动调用DelInstance释放单例对象。

static void DelInstance()
{
	_mtx.lock();
	if (_inst != nullptr)
	{
		delete _inst;
		_inst = nullptr;
	}
	_mtx.unlock();
}

2.在单例类中实现一个内嵌的垃圾回收类,在垃圾回收类的析构函数中完成单例对象的释放。在单例类中定义一个静态的垃圾回收类对象,当该对象被消耗时就会调用其析构函数,这时便对单例对象进行了释放。文章来源地址https://www.toymoban.com/news/detail-606208.html

//垃圾回收类
class CGarbo
{
public:
	~CGarbo()
	{
		if (_inst != nullptr)
		{
			delete _inst;
			_inst = nullptr;
		}
	}
};

到了这里,关于【重点:单例模式】特殊类设计的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【C++高阶(八)】单例模式&特殊类的设计

    💓博主CSDN主页:杭电码农-NEO💓   ⏩专栏分类:C++从入门到精通⏪   🚚代码仓库:NEO的学习日记🚚   🌹关注我🫵带你学习C++   🔝🔝 在实际场景中,总会遇见一些特殊情况, 比如设计一个类,只能在堆上开辟空间, 亦或者是设计一个类只能实例化一个对象 在实际需求的场景

    2024年02月04日
    浏览(47)
  • C++中特殊类的设计与单例模式的简易实现

    对于这种特殊类的设计我们一般都是优先考虑私有构造函数。 然后对于一些特殊要求就直接通过静态成员函数的实现来完成。  这里选择禁掉拷贝构造函数和拷贝函数是为了防止将已创建的对象去拷贝构造新的对象。  这里如果没有禁掉operator new和operator delete的话就会导致以

    2024年01月18日
    浏览(46)
  • 从C语言到C++_37(特殊类设计和C++类型转换)单例模式

    目录 1. 特殊类设计 1.1 不能被拷贝的类 1.2 只能在堆上创建的类 1.3 只能在栈上创建的类 1.4 不能被继承的类 1.5 只能创建一个对象的类(单例模式)(重点) 1.5.1 饿汉模式 1.5.2 懒汉模式 2. 类型转换 2.1 static_cast 2.2 reinterpret_cast 2.3 const_cast 2.4 dynamic_cast 3. RTTI(了解)和类型转换常见面

    2024年02月10日
    浏览(48)
  • C++:特殊类和单例模式

    设计一个不能被拷贝的类,通常来说方法就是把拷贝构造和赋值重载都设置为私有,这样就不能进行拷贝了 对于C++11来说,新增了delete的用法,可以直接删除这个函数,也能做到相同的效果 这是一个比较奇怪的要求,但是也有对应实现的方法: 把类的构造函数设置为私有 提

    2024年01月18日
    浏览(59)
  • Java设计模式-单例模式

    单例模式是一种设计模式,它确保一个类只能创建一个实例,并提供一种全局访问这个实例的方式。在Java中,单例模式可以通过多种方式来实现,其中最常见的是使用私有构造函数和静态方法实现 在Java中,实现单例模式的方式有多种,其中最常见的实现方式包括以下几种:

    2024年02月01日
    浏览(43)
  • 【java】设计模式——单例模式

    单例模式要点 : 一个类只需要一个实例化对象; 必须自行创建实例; 必须自行向整个系统提供这个实例 实现 : 只提供 私有 构造方法; 有一个该类的 静态 私有对象; 提供一个静态 公有 方法用于创建、获取静态私有对象; 分析: 私有构造方法-不能随意创建实例; 静态

    2024年02月13日
    浏览(44)
  • Java设计模式【单例模式】

    单例模式(Singleton Pattern)是一种创建型设计模式,其主要目的是确保一个类只有一个实例,并提供对该实例的唯一访问点。 优点 : 提供了对唯一实例的受控访问。 由于在系统内存中只存在一个对象,因此可以节约系统资源。 缺点 : 单例类的扩展有很大的困难。 单例类的

    2024年02月04日
    浏览(59)
  • JAVA设计模式——单例模式

    单例模式是应用最广的设计模式之一,也是程序员最熟悉的一个设计模式,使用单例模式的类必须保证只能有创建一个对象。 今天主要是回顾一下单例模式,主要是想搞懂以下几个问题 为什么要使用单例? 如何实现一个单例? 单例存在哪些问题? 单例对象的作用域的范围

    2024年02月16日
    浏览(41)
  • Java 设计模式——单例模式

    (1)单例模式 (Singleton Pattern) 是 Java 中最简单的设计模式之一。它提供了一种创建对象的最佳方式。 这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的

    2024年02月13日
    浏览(59)
  • 设计模式篇(Java):单例模式

    上一篇:设计模式篇(Java):前言(UML类图、七大原则) 所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。 构造器私有化 (防止 new ) 类的内部创建对象 向外暴露一个静

    2024年02月11日
    浏览(59)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包