C++:特殊类设计

目录

一.前几个特殊类设计

1.请设计一个类,不能被拷贝

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

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

4. 请设计一个类,不能被继承

C++98方式

C++11方法

二.单例模式:请设计一个类,只能创建一个对象(单例模式)

1.饿汉模式        

(1)介绍

(2)实例:维护一份地址和秘钥

(3)new的不需要delete释放吗?

2.懒汉模式

(1)和饿汉的区别:

(2)数据库管理和缓存类,都设计成单例,要求先初始化数据库对象,再初始化缓存对象        

假设文件销毁时需要把信息写到文件持久化,但是指针又不自动调析构函数


一.前几个特殊类设计

1.请设计一个类,不能被拷贝

这种防拷贝需求例如:

unique_ ptr
thread(线程)
mutex (锁)
istream(IO流对象)
ostream

拷贝只会放生在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝,
只需让该类不能调用拷贝构造函数以及赋值运算符重载即可
C++98 将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可。
缺陷:类里面还是支持拷贝
class CopyBan
{
    // ...
    
private:
    CopyBan(const CopyBan&);
    CopyBan& operator=(const CopyBan&);
    //...
};
原因:
1. 设置成私有:如果只声明没有设置成private,用户自己如果在类外定义了,就可以不
能禁止拷贝了(设私有是防止老六在类外定义)
2. 只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写
反而还简单,而且如果定义了就不会防止成员函数内部拷贝了。
C++11
C++11扩展delete的用法,delete除了释放new申请的资源外,如果在默认成员函数后跟上
=delete,表示让编译器删除掉该默认成员函数,成为已删除函数。
class CopyBan
{
    // ...
    CopyBan(const CopyBan&)=delete;
    CopyBan& operator=(const CopyBan&)=delete;
    //...
};

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

我们通常创建对象时,对象调用构造函数后创建在这三个区域:

class HeapOnly
{}

    HeapOnly h1;                    栈
    static HeapOnly h2;             静态区
    HeapOnly* ph3 = new HeapOnly;   堆区
实现方式:
1. 将类的构造函数私有,先把3个通道封住,再开放一条通道;拷贝构造声明成私有,防止别人调用拷贝在栈上生成对象。
2. 提供一个静态的成员函数:提供返回构造函数的公有函数时,发现没对象调不了,对象还得通过调用这个函数去构造生成——先有鸡还是先有蛋的循环,所以给这个成员函数加上static,静态的成员函数没有this指针,可以直接通过类域调用该函数,在该静态成员函数中完成堆对象的创建

3.拷贝构造需要防止:Heap0nly copy( *ph4) ; 指针ph4对应的对象虽然在堆上,但是拷贝后copy这个对象在栈上,所以要用delete删除拷贝构造函数。

4.赋值重载可以不禁:拷贝构造是把已构造对象拷贝给一个未构造的对象,赋值是把两个已经构造的函数互相赋值,这里构造只能再堆上,互相赋值仍是在堆上。

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

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

这里的拷贝构造依然可以去掉,因为CreateObj()中的 return StackOnly(); 先构造一个匿名对象,再拷贝构造一个临时变量,临时变量再拷贝构造给h1,被编译器优化为一次构造,直接构造h1


class StackOnly
{
public:
	static StackOnly CreateObj()
	{
		return StackOnly();
		//StackOnly st;
		//return st;
	}

	StackOnly(const StackOnly&) = delete;

	//void Print()
	//{
	//	cout << "Stack Only" << endl;
	//}
private:
	// 构造函数私有
	StackOnly()
	{}
};

int main()
{
	StackOnly h1 = StackOnly::CreateObj();
	//StackOnly::CreateObj().Print();

	//static StackOnly h2;
	//StackOnly* ph3 = new StackOnly;

	return 0;
}

4. 请设计一个类,不能被继承

C++98方式

// C++98中构造函数私有化,派生类中调不到基类的构造函数。则无法继承
class NonInherit
{
public:
	static NonInherit GetInstance()
	{
		return NonInherit();
	}
private:
	NonInherit()
	{}
};

C++11方法

fifinal关键字,fifinal修饰类,表示该类不能被继承。
class A  final
{
    ....
};

二.单例模式:请设计一个类,只能创建一个对象(单例模式)

设计模式:
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的
总结。为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打
仗时都是人拼人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后
来孙子就总结出了《孙子兵法》。孙子兵法也是类似。
使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模
式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。
我们常见的设计模式:适配器模式/单例模式/迭代器模式
单例模式:
一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个
访问它的全局访问点 GetInstance(),该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。
单例模式有两种实现模式:

1.饿汉模式        

(1)介绍

就是说不管你将来用不用,程序启动时就创建一个唯一的实例对象(main函数之前创建并初始化静态变量)。
①这里我们创建静态变量,为了只有一个实例对象,需要把构造函数私有,这个对象为了能访问私有的构造函数,我们需要把它放在类中声明,让它成为类的成员,可以在类外定义。
②需要在类中 声明一个此类类型的静态成员对象 static Singleton* _spInst; ,这就是单例模式唯一的一个对象, 为什么不在类外定义呢?:如果在类外定义就无法访问私有的构造函数,则无法创建。但是如果在类中声明,这个对象就属于这个类,就可以访问私有的构造函数,有两种定义的方式:定义

 

③注意:        Singleton* Singleton::_spInst = new Singleton; 类外定义可以访问类中的私有(构造),因为我们指定了_spInst属于Singleton这个类。

④注意:Singleton copy(*Singleton::GetInstance()); 为了防止这种拷贝出第二个实例对象,我们需要禁用拷贝构造。拷贝赋值operator=也可以禁用一下,但是不禁用,影响也不大,因为都是自己一个对象。
 
例子:

// 适配器模式/单例模式/迭代器模式    扩展学习:观察者模式/工厂模式

// 饿汉 -- 一开始(main函数之前)就创建
class Singleton
{
public:
//GetInstance()不能通过对象方式访问,只能通过类的方式访问,所以要加static
	static Singleton* GetInstance()
	{
		return _spInst;
	}

	void Print();
private:
	Singleton()
	{}

	Singleton(const Singleton&) = delete;


	//static Singleton _sInst; // 声明
	static Singleton* _spInst; // 声明

	int _a = 0;
};

//Singleton Singleton::_sInst; // 定义
Singleton* Singleton::_spInst = new Singleton; // 定义


void Singleton::Print()
{
	cout << _a << endl;
}

int main()
{
	        // GetInstance()可以获取到这个Singleton类的单例对象
	Singleton::GetInstance()->Print();
	//Singleton st1;                                 无法创建对象
	//Singleton* st2 = new Singleton;                无法创建对象
	//Singleton copy(*Singleton::GetInstance());     无法创建对象

	return 0;
}

(2)实例:维护一份地址和秘钥

想维护一份地址和秘钥,仅仅只有一份,假如一个线程创建了地址和秘钥,其他线程读到的地址和秘钥都是这一份。(如果能定义多个对象,就无法保证信息唯一)

想让进程中某个信息只有一份,就可以设为单例。

InfoMgr -- 单例
class InfoMgr
{
public:
	static InfoMgr* GetInstance()
	{
		return _spInst;
	}

	void SetAddress(const string& s)    //修改“_address”这个信息的函数
	{
		_address = s;
	}

	string& GetAddress()
	{
		return _address;
	}

private:
	InfoMgr()
	{}

	InfoMgr(const InfoMgr&) = delete;

	string _address;
	int _secretKey;

	static InfoMgr* _spInst; // 声明
};

InfoMgr* InfoMgr::_spInst = new InfoMgr; // 定义

int main()
{
	//InfoMgr info1;
	//static InfoMgr info2;
	//InfoMgr info3;

	// 全局只有一个InfoMgr对象
	InfoMgr::GetInstance()->SetAddress("陕西省西安市雁塔区");//创建一份信息

	cout << InfoMgr::GetInstance()->GetAddress() << endl;//访问都是这一份信息

	return 0;
}

如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避
免资源竞争,提高响应速度更好。

(3)new的不需要delete释放吗?

答:这里不需要,static变量在进程中是一直要用到的,当进程退出时,new的static变量也会归还操作系统。

2.懒汉模式

(1)和饿汉的区别:

①懒汉上来不初始化对象,把指针给成nullptr;饿汉会初始化对象,会直接new对象。

②懒汉第一次初始化时 if (_spInst == nullptr) 要加锁,因为如果是多线程同时执行,就会生成多个对象;饿汉不需要加锁,因为他在main函数之前就把静态的单例对象初始化出来了,在main函数之前只有主线程,没有新线程。

③饿汉中 如果单例对象构造十分耗时或者占用很多资源,比如加载插件啊, 初始化网络连接啊,读取文件啊等等,而有可能该对象程序运行时不会用到,因为单例对象在main函数之前, 那么程序要在main函数之前 就进行初始化,就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载)更好。         

懒汉—— 一开始不创建对象,只是在获取单例对象时需要调用函数GetInstance时再创建对象

和饿汉相比,饿汉会初始化对象:Singleton* Singleton::_spInst = new Singleton;
懒汉不会初始化对象,而是会给成空指针:Singleton* Singleton::_spInst = nullptr;
并且因为静态对象都是main函数之前初始化的,所以懒汉只能给空指针。直接定义对象都是饿汉Singleton Singleton::_sInst; ,因为main函数之前对象就已经创建初始化了。

// 懒汉 -- 一开始不创建对象,第一调用GetInstance再创建对象

class InfoMgr
{
public:
//GetInstance()不能通过对象方式访问,只能通过类的方式访问,所以要加static
	static InfoMgr* GetInstance()
	{
		// 还需要加锁,这个后面讲  -- 双检查加锁
		if (_spInst == nullptr) //只在第一次初始化
		{
			_spInst = new InfoMgr;
		}

		return _spInst;
	}

	void SetAddress(const string& s)
	{
		_address = s;
	}

	string& GetAddress()
	{
		return _address;
	}

// 实现一个内嵌垃圾回收类
//为什么不能直接把析构函数设为公有,在析构函数内设置delete?
//——因为你的static对象(static InfoMgr* _spInst)是指针,指针不会自动调用析构,
	class CGarbo {
	public:
		~CGarbo() {
			if (_spInst)
				delete _spInst;
		}
	};

	// 定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象
	static CGarbo Garbo;

private:
	InfoMgr()
	{}

	~InfoMgr()
	{
		// 假设析构时需要信息写到文件持久化
	}

	InfoMgr(const InfoMgr&) = delete;

	string _address;     ————————————成员数据
	int _secretKey;



	static InfoMgr* _spInst; // 声明
};

InfoMgr* InfoMgr::_spInst = nullptr; // 定义
InfoMgr::CGarbo Garbo;

int main()
{
	// 全局只有一个InfoMgr对象
	InfoMgr::GetInstance()->SetAddress("陕西省西安市雁塔区");

	cout << InfoMgr::GetInstance()->GetAddress() << endl;

	return 0;
}

(2)数据库管理和缓存类,都设计成单例,要求先初始化数据库对象,再初始化缓存对象        

单例:SQLMgr
单例:CacheMgr
饿汉控制不住,可能会控制不住顺序而报错——饿汉对象的初始化顺序不确定
懒汉可以控制
SQLMgr::GetInstance()
CacheMgr:GetInstance()

总结一下:
饿汉特点:简单一点、初始化顺序不确定,如果有依赖关系就会有问题。饿汉对象初始化慢且多个饿汉单例对象会影响程序启动
懒汉特点:复杂一点。第一次调用时初始化,可以控制初始化顺序,延迟加载初始化,不影响程序启动

假设文件销毁时需要把信息写到文件持久化,但是指针又不自动调析构函数

这个写到文件持久化 的功能在 ~InfoMgr()中实现,但是 你的static对象是指针,指针又不自动调析构函数,就需要实现一个内嵌垃圾回收类,定义这个类的对象,这个内部类的对象结束时调用内部类的析构函数,内部类的析构函数会delete _spInst,delete _spInst就会去调用 ~InfoMgr() ,完成写入文件

class InfoMgr
{
public:
    ~InfoMgr()
	{
		// 假设析构时需要信息写到文件持久化
	}

    	// 实现一个内嵌垃圾回收类
	class CGarbo {
	public:
		~CGarbo() {
			if (_spInst)
				delete _spInst;
		}
	};

	// 定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象
	static CGarbo Garbo;
}
InfoMgr::CGarbo Garbo;

为什么不能直接把析构函数设为公有,在析构函数内设置delete?
你的static对象是指针,指针又不会调用析构,所以得借助一个其他类对象析构,反过来调。

猜你喜欢

转载自blog.csdn.net/zhang_si_hang/article/details/127331158
今日推荐