C++—特殊类设计&设计模式

发布于:2025-05-14 ⋅ 阅读:(16) ⋅ 点赞:(0)

C++—特殊类设计&设计模式

1.设计模式

设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结

为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打仗时都是人拼人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后来孙子就总结出了《孙子兵法》。孙子兵法也是类似

之前已经接触过一些设计模式了

比如:

  1. 迭代器模式
  2. 适配器模式

还有一些设计模式——工厂模式,装饰器模式,观察者模式,单例模式、

2.特殊类设计

下面是一些常见的特殊类设计

2.1设计一个无法被拷贝的类

拷贝只会放生在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可

这个前面在异常的时候其实就说过了,两种方式,分别是c++98和c++11提供的

  • c++11的方法:

C++11扩展delete的用法,delete除了释放new申请的资源外,如果在默认成员函数后跟上=delete,表示让编译器删除掉该默认成员函数。

class NotCopy
{
	// 其实就是将拷贝构造和赋值运算符重载给禁掉就行了,之前有讲过
	// 在c++98就是将之声明不定义,然后将两个成员函数弄成私有的,这样外面就无法使用者两个函数了
	// 在c++11可以使用delete关键字
public:
	NotCopy(const NotCopy& n) = delete;
	NotCopy& operator=(const NotCopy& n) = delete;
private:
	int _a;
};
  • c++98的方法:
class NotCopy
{
	// 在c++98就是将之声明不定义,然后将两个成员函数弄成私有的,这样外面就无法使用者两个函数了
	// 在c++11可以使用delete关键字
public:

private:
    NotCopy(const NotCopy& n);
	NotCopy& operator=(const NotCopy& n);
    
	int _a;
};
  • 原因:
  1. 设置成私有:如果只声明没有设置成private,用户自己如果在类外定义了,就可以不

能禁止拷贝了

  1. 只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写反而还简单,而且如果定义了就不会防止成员函数内部拷贝了。

2.2设计一个只能在堆上创建对象的类

实现方式:

  1. 将类的构造函数私有,拷贝构造声明成私有。防止别人调用拷贝在栈上生成对象。

  2. 提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建

//2.设计一个只能在堆上创建对象的类
class OnlyHead
{
public:
	static OnlyHead* GetObj()
	{
		return new OnlyHead;
		// new OnlyHead和new OnlyHead()的区别
		// 就是第一个是隐式调用默认构造函数
		// 第二个是显式调用默认构造函数
	}

	// 除了构造函数不能给外面直接调用,拷贝构造和赋值运算符重载也要禁掉
	OnlyHead(const OnlyHead& o) = delete;
	OnlyHead& operator=(const OnlyHead& o) = delete;

private:
	// 在栈上定义的对象一定会调用默认构造函数,所以直接弄成私有的
	OnlyHead()
	{}

};


int main()
{
	//OnlyHead hp;
	//OnlyHead* p = new OnlyHead;
	OnlyHead* p = OnlyHead::GetObj(); 
	// 如果不释放p就会内存泄漏
	shared_ptr<OnlyHead> sp(OnlyHead::GetObj()); //交给智能指针
	//OnlyHead copy(*sp); // 拷贝构造要禁掉

	return 0;
}

2.3设计一个只能在栈上创建对象的类

可以用2.2的思路,先将构造私有,然后直接给一个类内静态方法

也可以将new直接重载,直接禁掉

// 3.设计一个只能在栈上创建对象的类
class OnlyStack
{
public:
	// 第一种思路: 直接不给new,重载new【这个思路无法阻止静态区的对象的创建】
	void* operator new(size_t size) = delete;

	// 第二种思路:将构造函数设置为私有的,然后提供一个static方法给外部调用
	// 这个思路不能禁掉拷贝构造,因为getobj返回值这里会有一次拷贝构造
	//【这个思路无法阻止静态区的对象的创建】
	static OnlyStack getobj()
	{
		return OnlyStack(); //类里面可以调用私有的构造函数
	}
private:
	OnlyStack()
	{}
};

int main()
{
	OnlyStack* p = new OnlyStack;
	OnlyStack* p = new OnlyStack();
	
	OnlyStack o = OnlyStack::getobj(); //创建栈上的对象,注意这个思路不能禁掉拷贝构造
	// 这里实际上调用了拷贝构造

	static OnlyStack os = OnlyStack::getobj();


	return 0;
}

2.4设计一个类,无法被继承

这个简单,就不仔细说了

// 4.设计一个类,无法被继承 
class NoInherit final
{
	// 两个思路,c++98:直接让构造函数私有化,因为子类的构造需要调用父类的构造
	// c++11:final关键字
};

2.5设计一个类。这个类只能创建一个对象【单例模式】

2.5.1懒汉模式实现

单例模式:一个类在全局(进程)中只能有一个实例对象

// 5.设计一个类。这个类只能创建一个对象【单例模式】
class Singleton
{
public:
	static Singleton* GetInstance()
	{
		if (_p == nullptr)
		{
			_p = new Singleton;
		}

		return _p;
	}

	// 在这个情况下,拷贝构造也要禁掉
	Singleton(const Singleton& s) = delete;

private:
	Singleton()
	{}

	// 需要再创建一个变量,来控制对象只能存在一个
	// 其实就是弄一个所有对象共用的变量,只要该变量标记了,就说明已经有一个对象了,那就不能再调用构造函数了
	static Singleton* _p; 
};

Singleton* Singleton::_p = nullptr; //初始化

int main()
{
	Singleton* s1 = Singleton::GetInstance();
	Singleton* s2 = Singleton::GetInstance();
	Singleton* s3 = Singleton::GetInstance();
	// 这里三个获取的都是同一个对象
	cout << s1 << endl;
	cout << s2 << endl;
	cout << s3 << endl;
	
	//Singleton s4(Singleton::GetInstance());//拷贝构造要禁掉

	return 0;
}

image-20250506085655120

但是上面这个代码是有问题的,这个单例模式会有线程安全问题,因为当多线程操作的时候,这里有一个公共资源,即临界资源——_p,因此就会出现线程安全问题。可能会创建出多个对象。这样单例模式就被破坏了

因此对临界资源_p加锁即可

#include<mutex>
class Singleton
{
public:
	static Singleton* GetInstance()
	{
		_mtx.lock();
		if (_p == nullptr)
		{
			_p = new Singleton;
		}
		_mtx.unlock();

		return _p;
	}

	// 在这个情况下,拷贝构造也要禁掉
	Singleton(const Singleton& s) = delete;

private:
	Singleton()
	{}

	// 需要再创建一个变量,来控制对象只能存在一个
	// 其实就是弄一个所有对象共用的变量,只要该变量标记了,就说明已经有一个对象了,那就不能再调用构造函数了
	static Singleton* _p; 
	// _p是一个临界资源,要保护它
	static mutex _mtx; //为了线程安全,加一个锁
};

Singleton* Singleton::_p = nullptr; //初始化
mutex Singleton::_mtx; //定义, 类里面的只是声明

这个代码就不会出现线程安全问题。

但是这个代码还是有点问题,因为虽然对临界区加锁了,但是临界区内有可能发生异常,new可能会抛异常,一旦抛异常就会造成死锁,因为这里应该用RAII思想来解决

#include<mutex>
class Singleton
{
public:
	static Singleton* GetInstance()
	{
		//_mtx.lock();
		{ //这个花括号是为了控制unique_lock的生命周期,不影响临界区后面的代码
			unique_lock<mutex> lock(_mtx);
			if (_p == nullptr)
			{
				_p = new Singleton;
			}
		}
		//_mtx.unlock();

		return _p;
	}

	// 在这个情况下,拷贝构造也要禁掉
	Singleton(const Singleton& s) = delete;

private:
	Singleton()
	{}

	// 需要再创建一个变量,来控制对象只能存在一个
	// 其实就是弄一个所有对象共用的变量,只要该变量标记了,就说明已经有一个对象了,那就不能再调用构造函数了
	static Singleton* _p; 
	// _p是一个临界资源,要保护它
	static mutex _mtx; //为了线程安全,加一个锁
};

Singleton* Singleton::_p = nullptr; //初始化
mutex Singleton::_mtx; //定义, 类里面的只是声明

上述代码仍然有一些缺陷,可以继续优化。【即,我们只需要对第一次访问临界资源_p进行加锁保护即可,因为后面不会再有修改临界资源的情况出现】

这里做一个经典的双检查

static Singleton* GetInstance()
{
	//_mtx.lock();
	if(_p == nullptr) //这里是一个双检查
	{ 
		unique_lock<mutex> lock(_mtx);
		if (_p == nullptr)
		{
			_p = new Singleton;
		}
	} //并且出了这里,unique_lock<mutex>的生命周期就结束了
	//_mtx.unlock();

	return _p;
}

下面是完整的代码:【这里是一个懒汉模式的单例模式】

#include<mutex>
class Singleton
{
public:
	static Singleton* GetInstance()
	{
		//_mtx.lock();
		if(_p == nullptr) //这里是一个双检查
		{ 
			unique_lock<mutex> lock(_mtx);
			if (_p == nullptr)
			{
				_p = new Singleton;
			}
		} //并且出了这里,unique_lock<mutex>的生命周期就结束了
		//_mtx.unlock();

		return _p;
	}

	// 在这个情况下,拷贝构造也要禁掉
	Singleton(const Singleton& s) = delete;

	// 释放资源
	static void DelInstance()
	{
		unique_lock<mutex> lock(_mtx);
		delete _p;
		_p = nullptr;
	}

private:
	Singleton()
	{}

	// 需要再创建一个变量,来控制对象只能存在一个
	// 其实就是弄一个所有对象共用的变量,只要该变量标记了,就说明已经有一个对象了,那就不能再调用构造函数了
	static Singleton* _p; 
	// _p是一个临界资源,要保护它
	static mutex _mtx; //为了线程安全,加一个锁
};

Singleton* Singleton::_p = nullptr; //初始化
mutex Singleton::_mtx; //定义, 类里面的只是声明

如果想程序在结束之后,自动释放单例对象,可以引入尝试下面这个代码的思路

// 释放资源
	static void DelInstance()
	{
		// 这种思路下,不需要加锁了。因为_mtx锁可能已经释放了
        // 并且main函数都结束了,主线程结束了,其他线程肯定都结束了,不存在多线程的场景了
		delete _p;
		_p = nullptr;
	}

// 如果手动释放单例对象,可以手动调用DelInstance()
// 如果想在程序结束之后自动释放单例对象,可以引入一个小机制,利用生命周期
class GC
{
public:
	~GC()
	{
		Singleton::DelInstance();
	}
};

// 这里定义一个gc的静态变量,在当前程序结束之后,就会调用DelInstance()来释放单例对象
static GC gc; 
2.5.2饿汉模式实现
	class Singleton
	{
	public:
		static Singleton* GetInstance()
		{
			return &_inst;
		}
		
		Singleton(const Singleton& s) = delete;

	private:
		Singleton()
		{}

		static Singleton _inst;
	};

	Singleton Singleton::_inst; //在main函数之前就在静态区把唯一的对象创建好了, 不存在线程安全的问题了

2.5.3懒汉的饿汉的区别
  • 懒汉模式需要考虑线程安全和内存泄漏的问题,实现相对更复杂。饿汉就不需要考虑这些问题,实现起来也相对简单、

  • 懒汉模式是在需要的时候才创建对象并初始化,相对来说不会影响程序的进行,而饿汉模式是一开始就创建对象并初始化,如果代码量大,就会导致程序运行缓慢,影响用户体验

  • 饿汉模式无法保证对象的创建顺序,比如有多个单例类,并且有依赖关系(B依赖A), 要求先创建A,在创建B,那就不能用饿汉模式,要用懒汉

  • 如果在构造函数中有动态库链接,和创建线程(要链接线程库),那么就不能用饿汉模式。因为饿汉在main函数之前就初始化了,这个时候不知道库是否链接了,如果没有链接就会崩


网站公告

今日签到

点亮在社区的每一天
去签到