特殊类的设计

目录

1、禁止拷贝

2、只在堆上创建对象

3、只在栈上创建对象

4.不能被继承

5、设计一个类,只能创建一个对象(单例模式)

饿汉模式:

懒汉模式:


1、禁止拷贝

这种特殊类的需求就是不让进行拷贝。

设计的核心就是禁止调用拷贝构造和赋值运算符重载即可。

c++98中就是对这两个只声明不定义,且放在私有里。

(如果没有放私有,用户还是可以在类外进行定义)

(只声明放私有里,用户就不能调用赋值或拷贝,这样的话不管有没有定义,都无法使用)

cpp 复制代码
class pl {
private:
	pl(const pl& s);
	pl& operator=(const pl& s);
};

c++11中增加了默认成员函数后加=delete,可以禁止调用该函数

cpp 复制代码
class pl {
public:
	pl(const pl& s) = delete;
	pl& operator=(const pl& s) = delete;
};

推荐用c++11的用法

2、只在堆上创建对象

cpp 复制代码
class pl {
public:
	template<class ...Args>//这个是可变参数模板,可不加,不加的话有多少种构造就要多少种createpl
	//重点是写一个静态的成员函数,用来在外面调用,函数内部强制new对象即可。
	static pl* createpl(Args&& ...args)
	{
		return new pl(args...);
	}
	//注意,还要禁掉拷贝和赋值,因为拷贝和赋值的时候,不管里面是浅拷贝还是深拷贝
	//对象都是开辟在栈上的。
	pl(const pl& s) = delete;
	pl& operator=(const pl& s) = delete;
private:
	pl(int a)//将构造私有化,这样外面的人就不能通过各种各样的方式直接构造对象了
	{
		//....
	}
	pl() {

	}
};

int main()
{
	//如果不写静态的话,没有对象就不能调用成员函数,没有成员函数又不能创建对象,所以必须静态
	pl*s1 = pl::createpl(3);
	return 0;
}

特殊写法

cpp 复制代码
class pl {
public:
	pl(int a)
	{
		//....
	}
	pl() {

	}
	//封掉析构函数
	~pl() = delete;

private:

};

int main()
{
	//这样就可以
	//pl s1;这样的写法就会报错,因为栈上的对象都会生命周期结束都会自动调用析构函数
	//而析构函数被禁,就会出问题,所以编译器会直接报错。
	pl *s1 = new pl();
	return 0;
}

但这一种不能释放资源

所以下面再改下

cpp 复制代码
class pl {
public:
	pl(int a)
	{
		//....
	}
	pl() {

	}
	void Destory() {
		delete this;
	}

private:
	//不封了,而是放私有,通过成员函数delete this的方式来调用析构
	~pl() {
		cout << 1;
	}

};

int main()
{

	pl *s1 = new pl();
	s1->Destory();
	return 0;
}

如果再配合智能指针

cpp 复制代码
class pl {
public:
	pl(int a)
	{
		//....
	}
	pl() {

	}
	void Destory() {
		delete this;
	}

private:
	//不封了,而是放私有,通过成员函数delete this的方式来调用析构
	~pl() {
		cout << 1;
	}

};

int main()
{
	pl *s1 = new pl();
	//这种情况下必须手动给个删除器,不然默认的删除器是直接delete ptr,但此时的析构是私有的
	//所以必须通过下面的方式调用对象的成员函数,再通过成员函数间接调用析构函数。
	shared_ptr<pl>s2(new pl(), [](pl* ptr) {ptr->Destory(); });
	return 0;
}

3、只在栈上创建对象

cpp 复制代码
class pl {
public:
	template<class ...Args>
	static pl createpl(Args&& ...args)
	{
		return pl(args...);
	}
	//注意,这里跟堆的不一样,拷贝构造不能封住,因为传值返回,编译器优化之后也是要经过一次拷贝构造的。
	//pl(const pl& s) = delete;
	pl& operator=(const pl& s) = delete;
	//注意,因为没有封住拷贝构造,所以外面仍然可以通过拷贝的方式构造函数
	//当然不是pl s2(s1)这种,这种还是在栈上的,防的是这种pl s2=new pl(s1);
	// 所以重载new即可,默认是调用全局std的new,我们只要重载该类的new,并让这个new禁言掉即可。
	void* operator new(size_t n) = delete;
private:
	pl(int a)
	{
		//....
	}
	pl() {

	}
};

int main()
{
	pl s1 = pl::createpl(3);
	return 0;
}

4.不能被继承

第一种,c++98,父类构造私有化

cpp 复制代码
class pl {
public:




private:
	//构造私有化即可,因为派生类继承父类的时候,父类私有成员是无法被继承的
	//(或者说虽然被继承下来了,但是派生类也没法调用,相当于隐藏了)
	//
	pl() {

	}

};

第二种,利用c++11特性,final,加了final的类无法被继承

cpp 复制代码
class pl final 
{
public:




private:


};

5、设计一个类,只能创建一个对象(单例模式)

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

目的:代码可重用性、代码易读性、代码可靠性。

让代码编写真的工程化,是软件工程的基石

**单例模式:**一个类在当前进程中只能创建一个对象,即单例模式,该模式保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如内存池(希望一个进程都只访问这一个内存池)、配置信息(服务器配置信息放在一个文件中,这些配置数据都一个单例对象统一读取,然后服务器进程中其他对象再通过这个单例对象获取这些配置信息)。旨在简化复杂环境下的配置管理。

饿汉模式:

cpp 复制代码
//饿汉:main之前,就创建出对象
class Singleton
{
public:
	//静态成员函数
	static Singleton* GetInstance() {
		return &_ps;
	}
	void OutPut()
	{
		cout << a1 << endl;
		cout << a2 << endl;
		cout << str << endl;
	}
	void Adds(const string& s)
	{
		str += s;
	}
	//记得封拷贝构造和赋值
	Singleton(const Singleton& s) = delete;
	Singleton& operator=(const Singleton& s) = delete;

private:
	//先构造私有化
	Singleton(int a11=0,int a22=0,const string&s="wdawdad")
		:a1(a11)
		,a2(a22)
		,str(s)
	{}
	int a1;
	int a2;
	string str;
	
	//注意,静态成员,不存在对象中,而是在静态区,
	//相当于全局,声明在类中,定义在类外,受类域限制。
	//所以是可以在类里面写个同样类的静态对象的。
	static Singleton _ps;
};
Singleton Singleton::_ps(3,4,"wwwwwww");
int main()
{
	//这样就能用静态成员函数,访问静态成员(对象),再调用相应的成员函数
	Singleton::GetInstance()->OutPut();
	auto s1 = Singleton::GetInstance();
	s1->OutPut();
	s1->Adds("dwadwa");
	s1->OutPut();

	return 0;
}

饿汉自动释放,对象是存在静态区的。

饿汉的问题:

1、如果单例数据太多,构造初始化的成本较高,会影响程序的启动速度。

比如有好多个单例,每个单例都有一堆信息要存储。这样的话速度就很慢,导致迟迟进不了

main()函数。在进main()函数前,是单线程进行工作的,只有在进了main()函数之后才是多线程工作,这样的话,如果因为单例加载太慢,导致一直堵在main()之前,那程序的启动速度就会非常慢。

2、多个单例类有初始化启动依赖关系,饿汉无法控制。

对于多个单例类的情况,如果对其中一些对象有初始化顺序的要求,比如a对象要求比b对象先初始化,饿汉无法保证。

懒汉模式:

cpp 复制代码
class Singleton 
{
public:
	//调用对象
	static Singleton* GetInstance() {
		//第一次调用才会开辟空间
		if (_ps == nullptr)
		{
			_ps = new Singleton;
		}
		return _ps;
	}
	//释放对象
	static void DelInstance() {
		if (_ps)
		{
			delete _ps;
			_ps = nullptr;
			//考虑到可能中途释放了单例对象后,
			//还要重新有一个单例对象,要把指针重新设为空指针
			//这样下次调用GetInstance就能重新开辟空间了。

			//这个空指针,还要考虑到如果重复调用了DelInstance,防止多次释放空间
			//防止释放空指针
		}
	}

	void OutPut()
	{
		cout << a1 << endl;
		cout << a2 << endl;
		cout << str << endl;
	}
	void Adds(const string& s)
	{
		str += s;
	}
	Singleton(const Singleton& s) = delete;
	Singleton& operator=(const Singleton& s) = delete;

private:

	Singleton(int a11=0,int a22=0,const string&s="wdawdad")
		:a1(a11)
		,a2(a22)
		,str(s)
	{}
	~Singleton() {
		cout << "~";
		//可以做很多事情,比如释放的时候把数据写入某个文件等
	}
	int a1;
	int a2;
	string str;
	static Singleton*_ps;
	//考虑到加入了DelInstance之后,如果不显示调用,因为空间是new出来的,不会自动释放
	//所以加入下面的内部类
	class DS {
	public:
		~DS() {
			Singleton::DelInstance();
		}
	};
	//注意,如果是放在内部的话,ds一定得开静态。
	//因为如果是普通的成员的话,只有单例对象释放的时候才会调用ds的析构
	//但为了让单例对象释放,我们需要调用ds的析构,这样就死循环了,无法达到目的
	//所以利用静态成员放在静态区的特性,这样的话,程序结束的时候,静态区的ds也会结束生命周期
	//从而让ds自动调用析构,使得调用DelInstance()
	static DS ds;
};
//注意,也可以放在外面,这时候ds开不开静态无所谓。
//因为全局的对象也是放在静态区,所以开不开静态都一样,生命周期
//class DS {
//public:
//	~DS() {
//		Singleton::DelInstance();
//	}
//};
//DS ds;
Singleton* Singleton::_ps = nullptr;
Singleton::DS Singleton::ds;

int main()
{
	Singleton::GetInstance();
	return 0;
}

注意

这个不是完整的懒汉,完整的懒汉还需要设计到线程安全问题,需要加锁,具体的我后面会统一补充文章。目前涉及到线程安全问题的:shared_ptr,懒汉模式。

对于饿汉模式的缺陷懒汉完美解决了。

懒汉不再是一开始就创建对象。

而是在进入main()函数后,根据需求调用单例的GetInstance函数,只有第一次调用该单例类的GetInstance函数才会开辟空间并赋予该单例对象。类对象本身在未调用GetInstance函数前,单例对象指针保持空指针的状态。这样就算main()前有好多个单例类,但单例对象指针都是空指针,不需要多少时间。

而对于初始化顺序的要求,我们手动控制不同单例类的GetInstance函数调用顺序即可。

释放问题:

一般来说懒汉模式下,单例对象也不需要释放,因为我们需要的就是整个程序进程中一直可以访问这个单例对象。

但特殊情况:某个需求,要提前释放单例对象;要求释放的时候把数据写入文件中等等。

上面就是一种释放的写法,但是不一定是类,也可以弄个全局的智能指针来管理。

下面是更简洁的懒汉

cpp 复制代码
class Singleton
{
public:
	//调用对象
	static Singleton* GetInstance() {
		//局部的静态对象只有第一次调用函数的时候才会构造对象。
		//因此可以做到控制多个单例对象初始化顺序。
		//且又因为局部的静态对象,生命周期也是全局的,所以也可以做到
		//不显示调用析构,也可以随着生命周期结束自动调用析构。
		static Singleton _ps;
		return &_ps;
		//这个写法只能在c++11及之后写。因为c++11之前无法保证这里的线程安全。
	}
	

	void OutPut()
	{
		cout << a1 << endl;
		cout << a2 << endl;
		cout << str << endl;
	}
	void Adds(const string& s)
	{
		str += s;
	}
	Singleton(const Singleton& s) = delete;
	Singleton& operator=(const Singleton& s) = delete;

private:

	Singleton(int a11=0,int a22=0,const string&s="wdawdad")
		:a1(a11)
		,a2(a22)
		,str(s)
	{}
	~Singleton() {
		cout << "~";
		//可以做很多事情,比如释放的时候把数据写入某个文件等
	}
	int a1;
	int a2;
	string str;

};




int main()
{
	Singleton::GetInstance();
	return 0;
}
相关推荐
阳洞洞8 分钟前
c++中如何打印未知类型对象的类型
开发语言·c++·算法
froginwe1110 分钟前
CSS3 圆角:实现与优化指南
开发语言
L73S3730 分钟前
C++入门(2)
c++·程序人生·考研·蓝桥杯
JuicyActiveGilbert35 分钟前
第8天:面向对象编程入门 - 类与对象
开发语言·c++
Darkwanderor36 分钟前
类和对象——const修饰的类的对象和函数
c++·const
Darkwanderor36 分钟前
类和对象——拷贝对象时的一些编译器优化
c++
狂团商城小师妹1 小时前
JAVA多商户家政同城上门服务预约服务抢单派单+自营商城系统支持小程序+APP+公众号+h5
java·大数据·开发语言·微信小程序·小程序·uni-app·微信公众平台
码叔义2 小时前
Jsonpath 使用说明
android·开发语言·javascript
行十万里人生2 小时前
Qt 对象树详解:从原理到运用
开发语言·数据库·qt·华为od·华为·华为云·harmonyos
原来是猿2 小时前
蓝桥备赛(四)- 数组(下)
开发语言·数据结构·c++·算法