C++ 特殊类设计以及单例模式

目录

[1 不能被拷贝](#1 不能被拷贝)

[2 只能在堆上创建对象](#2 只能在堆上创建对象)

[3 只能在栈上创建对象](#3 只能在栈上创建对象)

[4 禁止在堆上创建对象](#4 禁止在堆上创建对象)

[5 不能被继承的类](#5 不能被继承的类)

[6 单例类](#6 单例类)


特殊类就是一些有特殊需求的类。

1 不能被拷贝

要设计一个防拷贝的类,C++98之前我们只需要将拷贝构造以及拷贝赋值设为私有,同时只声明不实现,就能防止拷贝。

class A
{
public:
	A() {}
private:
	A(const A& );
	A& operator=(A&);
};

而C++11新增了关键字delete之后,我们就可以直接删除这两个成员函数来达到防拷贝的目的。

class A
{
public:
	A() {}
private:
	A(const A& ) = delete;
	A& operator=(A&) =delete;
};

2 只能在堆上创建对象

要设计这样的类我们必须把构造函数私有,防止用户自己去创建对象,然后提供一个接口专门用来给用户创建堆上的对象返回,用户只有这一种方法能够获得对象,相当于从源头上杜绝在栈上创建对象。

要注意的是,我们的这个返回堆上的对象的接口必须是公有且静态的。

class A
{
public:
	static A* getA()
	{
		return new A();
	}
private:
	A(){};
	int _a = 0;
};

但是这样写的话还有一个漏洞,就是拷贝构造和拷贝赋值没有禁止,不禁止的话用户可能会利用这个漏洞来拷贝构造出栈上的对象,或者使用拷贝赋值玩出栈上的对象。

所以还是必须禁止掉拷贝构造和拷贝赋值

class A
{
public:
	static A* getA()
	{
		return new A();
	}
private:
	A(const A&) = delete;
	A& operator=(const A&) = delete;
	A(){};
	int _a = 0;
};

其实还有一种方法:就是直接将析构函数私有,而不管构造函数

这时候如果是在栈上创建的对象,由于析构函数是私有的,所以无法析构,这时候会在编译时就报错。

那么与此同时,我们就需要提供一个接口destroy用来销毁堆上的对象,销毁的方法也很简单,我们可以在里面调用delete this 来析构和释放 ,也可以直接显式调用析构函数。 注意析构函数要显式调用的话必须显式用this来调用。

class A
{
public:
	A() {}
	static A* getA()
	{
		return new A();
	}
	void destroy()
	{
		//this->~A();  //也可以显式调用析构函数
		delete this;
	}
private:
	A(const A&) = delete;
	A& operator=(const A&) = delete;
	~A() {}
private:
	int _a = 0;
};

3 只能在栈上创建对象

首先还是要把构造函数私有,那么new的时候编译器就调用不了构造函数了,也就无法在堆上创建对象。但是如何获得栈上的对象呢?提供一个接口,创建一个对象并且返回,因为我们外面要接受的话,肯定是要发生拷贝,所以拷贝构造我们必须实现,但是如此一来,我们使用new的时候就可以调用拷贝构造了,所以单纯把构造函数私有是没有达到目标。

所以我们好像必须将拷贝构造和拷贝赋值私有,但是这样一来我们怎么获取栈上的对象呢?那么就不获取了,直接通过函数的返回值来充当临时对象来调用内部的方法。

class A 
{
public:
	static A getA()
	{
		return A();
	}
	void func() { cout << "func" << endl; }

private:
	A(){}
	A(const A& a){}
	A& operator=(const A&){}
};

如果我们觉得每次都要调用getA函数才能调用类的方法麻烦,我们也可以直接用一个const左值引用来接收返回值拷贝出来的临时对象,被const 左值引用之后,这个临时对象的生命周期就延长了,我们可以把它当作栈的对象来用。

	A::getA().func();
	const A& ra = A::getA();
	ra.func();

同时,我们把拷贝构造和拷贝赋值私有之后,也防止了在静态区创建对象,因为他要创建对象也只能通过 getA 函数的返回值来构造,但是我们已经把构造和拷贝构造都死有了,所以他也没办法创建对象。

4 禁止在堆上创建对象

最简单的办法就是将 operator new 和operator delete 删除。

	void* operator new(size_t size) = delete;
	void operator delete(void*) = delete;

因为 new 对象的时候是调用 operator new 和构造函数来在堆上申请对象的,同时在delete的时候也是调用 析构函数 和 operator delete 来释放对象的,那么我们只需要吧这两个接口删除,就无法创建在堆上的对象了。

5 不能被继承的类

C++11之前,我们可以将 所有构造函数设为私有 ,因为子类的构造函数中必须显式调用父类的构造函数,如果父类的构造函数是私有的话,子类是访问不到的。

第二种方法,就是在类的声明后面加上修饰符 final ,表示这是一个最终类,不能被继承。

6 单例类

单例就是该类只能有一个对象。同时这也涉及到了一个设计模式:单例模式

单例模式: 一个类只能创建一个对象,即单例模式,该模式可以保证系统中只存在该类的一个实例,并提供一个访问它的全局访问点,该实例被所有程序模板共享。

也就是全局只有一个对象,这个对象必须很容易就能访问到。

其实设计起来就跟我们上面设计的只能在栈上创建对象的类有点类型,只能通过类提供的静态的接口来获取对象,然后通过这个对象来调用成员方法。

单例模式有两种实现方式,饿汉模式和懒汉模式

1 饿汉模式

指的是不管当前或者未来用不用这个对象,在程序或者服务器启动的时候,都先把对象创建出来。

要保证这个类只有一个对象,我们可以用一个静态的对象来表示这个唯一对象。这个静态对象当然可以设置为公有的,但是公有的太过随便,不安全,最好还是设为私有然后提供一个接口来返回这个对象的指针,外部通过返回值来进行调用。

同时,为了保证单例,我们必须将构造函数设为私有,然后拷贝构造和拷贝赋值直接删除。

class A
{
public:
	//获取单例的方法
	static const A* GetSingle()
	{
		return &single;
	}
	//类的其他的成员函数 ...
	void func()
	{
		cout << "func" << endl;
	}

private:
	A() {};
	A(const A& a) = delete;
	A& operator=(const A& a)=delete;
	
private:
	//类的成员

	//类的唯一实例
	static A single;
};
//初始化
A A::single;

这里大家可能会有两个疑惑?

1 类里面怎么能包含类自身的对象,计算类大小的时候不会出有问题吗?

因为这是静态成员,是整个类所共享的,他不是存在对象中,所有静态成员的大小并不也会算在类的大小中。

2 为什么能够在类外调用构造函数初始化 single ?

这是因为我们指明了类域,这其实是在类域中调用构造函数进行初始化。

饿汉模式是程序启动的时候对象就创建了,也就是在main函数执行之前就有了,我们上面将其设置为了全局的对象(作用域是类域,但是生命周期是从该对象被定义到程序结束),该对象我们在全局就定义好了,而全局对象是在main函数开始之前就已经创建好了,所以符合饿汉的条件。

同时为什么我们上面的getsingle不传值返回而是要指针返回呢?

因为我们把拷贝构造删除了,而传值返回是需要调用拷贝构造来构造一个临时对象的。不过除了传指针返回,我们更推荐传引用返回,因为这个对象是一直存在的,我们在外面使用的时候也可以用一个引用接收返回值,接收之后就不用每次都调用这个函数了。

	//返回引用
	static const A& GetSingle()
	{
		return single;
	}

	A::GetSingle().func();
	const A& ra = A::GetSingle();
	ra.func();

饿汉模式的单例是线程安全的,因为对象在程序加载的时候就创建出来了,外界每一次调用返回的都是这一个对象。

饿汉模式的缺点:

1 如果 单例对象初始化时数据太多 ,会导致程序或者说服务器启动慢。

比如说这个单例的创建还需要去网络中和数据库中拿数据来进行构造,那么就会导致启动速度很慢,因为不管怎么样,只有构造完这个对象之后才能进入main函数执行

2 如果多个单例类有初始化的依赖关系,饿汉模式无法控制顺序。

因为单例都是在main函数之前进行初始化,而如果有多个单例对象需要初始化的时候,当他们不在同一个文件中,我们是无法保证哪个单例对象先被创建的,我们无法控制他们初始化的顺序。那么就会导致有依赖关系的单例对象的初始化出现问题。

所以饿汉模式在有些场景下就很不合适,于是又提出了一种新的方式:懒汉模式

懒汉模式的特点:在第一次获取对象调用的时候才初始化

class A
{
public:
	//获取单例的方法
	//返回指针
	static const A* GetSingle()
	{
		//第一次调用这个函数的时候才初始化单例对象
		if(single==nullptr)
		single = new A();

		return single;
	}
	//类的其他的成员函数 ...
	void func()const
	{
		cout << "func" << endl;
	}

private:
	A() {};
	A(const A& a) = delete;
	A& operator=(const A& a)=delete;
	
private:
	//类的成员

	//类的唯一实例
	static A* single;
};

A* A::single = nullptr;

饿汉模式的优点:

1 对象在main函数之后才创建,不会影响启动速度

2 可以主动控制多个单例对象的创建顺序

我们可以通过调用的顺序来控制创建的顺序。

但是创建对象的时候是有线程安全问题的,所以我么需要锁来保证只有一个线程能创建对象。

class A
{
public:
	//获取单例的方法
	//返回指针
	static const A* GetSingle()
	{
		//第一次调用这个函数的时候才初始化单例对象
		mtx.lock();
		if(single==nullptr)
		single = new A();
		mtx.unlock();

		return single;
	}
	//类的其他的成员函数 ...
	void func()const
	{
		cout << "func" << endl;
	}

private:
	A() {};
	A(const A& a) = delete;
	A& operator=(const A& a)=delete;
	
private:
	//类的成员

	//类的唯一实例
	static A* single;
	static mutex mtx;
};

A* A::single = nullptr;
mutex A::mtx;

但是这样一来,每个线程在进入判断之前都要加锁才能判断,那么效率就低了,我们可以用双重判断来提高效率。

	static const A* GetSingle()
	{
		//第一次调用这个函数的时候才初始化单例对象
		if (single == nullptr)
		{
			mtx.lock();
			if (single == nullptr)
				single = new A();
			mtx.unlock();
		}
		return single;
	}

第一个判断是为了判断是不是第一次调用,那么如果已经存在对象了,我们就不需要进去了,也就不需要加锁和解锁了。

而加锁之后的 if 是用来判断是否需要创建对象,因为多线程的场景下,这个if可能会被多个线程同时执行到。 但是我们加锁之后,就可以避免多个线程同时进入这个if,就能保证只会创建一次对象。

最后还有一个问题就是,new的时候是可能会出错抛异常的,那么我们就需要捕获异常,并完成解锁。

	static const A* GetSingle()
	{
		//第一次调用这个函数的时候才初始化单例对象
		if (single == nullptr)
		{
			try 
			{
				mtx.lock();
				if (single == nullptr)
					single = new A();
				mtx.unlock();
			}
			catch (...)
			{
				mtx.unlock();
			}
		}
		return single;
	}

但是这样写的话代码不够美观。我们可以搞成 RAII 风格的锁。

			try
			{
				lock_guard<mutex> lock(mtx);
				if (single == nullptr)
					single = new A();

			}
			catch (...) { throw; }

懒汉模式我们还可以完善一下他的析构,不过一般单例是不需要释放的,因为他的生命周期一般是从创建开始到进程结束的,而进程结束的时候会自动释放所有资源,所以一般是不需要我们主动去销毁这个单例的。

但是考虑在有的场景下需要提前手动释放这个对象,那么我们可以提供一个destroy接口来释放这个单例对象,那么与此同时就必须要提供析构函数。

懒汉模式还有一种写法:

class A
{
public:
	static A& getsingle()
	{
		static A a;
		return a;
	}
private:
};

就是返回一个局部静态对象。

如果静态对象是局部对象的话,那么会在第一次定义的时候创建和初始化,也就是会在main函数之后的第一次调用该函数时进初始化。

但是这种静态的局部对象会出现线程安全问题吗?

在C++11之前,这里是不能保证这个局部的静态对象的初始化四线程安全的,所以C++11之前我们不是用这种方式。但是C++11之后,可以保证局部静态对象的创建是安全的。

相关推荐
一只小bit28 分钟前
数据结构之栈,队列,树
c语言·开发语言·数据结构·c++
沐泽Mu2 小时前
嵌入式学习-QT-Day05
开发语言·c++·qt·学习
szuzhan.gy3 小时前
DS查找—二叉树平衡因子
数据结构·c++·算法
火云洞红孩儿3 小时前
基于AI IDE 打造快速化的游戏LUA脚本的生成系统
c++·人工智能·inscode·游戏引擎·lua·游戏开发·脚本系统
FeboReigns4 小时前
C++简明教程(4)(Hello World)
c语言·c++
FeboReigns4 小时前
C++简明教程(10)(初识类)
c语言·开发语言·c++
zh路西法4 小时前
【C++决策和状态管理】从状态模式,有限状态机,行为树到决策树(二):从FSM开始的2D游戏角色操控底层源码编写
c++·游戏·unity·设计模式·状态模式
.Vcoistnt5 小时前
Codeforces Round 994 (Div. 2)(A-D)
数据结构·c++·算法·贪心算法·动态规划
小k_不小5 小时前
C++面试八股文:指针与引用的区别
c++·面试
沐泽Mu5 小时前
嵌入式学习-QT-Day07
c++·qt·学习·命令模式