【C++】特殊类

2023-10-17 08:44:45 浏览数 (3)

前言

面试中,考官有时候会问一些特殊类的设计,今天我们来介绍一下常见的特殊类的设计方式。

一、设计一个类,不能被拷贝

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

C 98方法

将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可。

代码语言:javascript复制
//无法拷贝的类
class CopyBan
{
public:
	CopyBan()
	{}

private:
	CopyBan(const CopyBan&);

	CopyBan& operator=(const CopyBan&);
};
 
int main()
{
	CopyBan cb1;
	CopyBan cb2(cb1);

	return 0;
}

原因:

1. 设置成私有:如果只声明没有设置成 private ,用户自己如果在类外定义了,就不能禁止拷贝了。

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

C 11方法

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

代码语言:javascript复制
//无法拷贝的类
class CopyBan
{
public:
	CopyBan()
	{}

private:
	CopyBan(const CopyBan&) = delete;

	CopyBan& operator=(const CopyBan&) = delete;
};

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

方法一

实现方式:

1. 将类的构造函数私有,拷贝构造声明成私有。防止别人调用拷贝在栈上生成对象。 2. 提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建。 (静态作用在于不需要对象也能直接调用该函数)

代码语言:javascript复制
//只能在堆区构造的类
class HeapOnly
{
public:
	static HeapOnly* Create()
	{
		return new HeapOnly;
	}

private:
	HeapOnly(){}
	HeapOnly(const HeapOnly&) = delete;
};

int main()
{
    HeapOnly* hp = HeapOnly::Create();
}

需要的使用的时候,我们用指针来接收返回的对象。

关于delete释放问题:

因为是我们用new开辟出来的,一般我们是要delete来释放空间的,但是一般情况下用完进程就会回收资源了。只要不是一直跑的程序开辟了空间不回收就行。

方法二

实现方法:

1.将析构函数私有化,因为如果不能调用析构,那么就无法创建对象,编译器会报错。 2.提供一个成员函数,类内调用析构函数销毁对象。

代码语言:javascript复制
class HeapOnly
{
public:
	HeapOnly()
	{}

	void Destroy()
	{
		this->~HeapOnly();
	}
		 
private:
	~HeapOnly(){}

};

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

方法一

禁用opeartor new 和operator delete。

我们通常用new和delete来创建销毁对象都是其在类内会调用opeartor new 和operator delete,我们如果将其在类内禁用了,那么就无法使用new和delete来创建对象了。

代码语言:javascript复制
class StackOnly
{
public:
	void* operator new(size_t size) = delete;
	void operator delete(void* p) = delete;
};

方法二

将构造函数私有,然后提供一个静态函数Create,在函数中调用构造函数。

代码语言:javascript复制
class StackOnly
{
public:
	static StackOnly Creat()
	{
		return StackOnly();
	}
private:
	StackOnly(){}
};

分析:

因为我们需要用到拷贝构造,所以不能封掉拷贝构造。 但是我们却可以通过以下的方式创建静态变量。

所以这里设计的只能在栈区创建的对象是有缺陷的。


四、设计一个类,不能被继承

C 98方式

构造函数私有化,派生类中调不到基类的构造函数来完成初始化,则无法继承。

代码语言:javascript复制
class StackOnly
{
public:
	static StackOnly Create()
	{
		return StackOnly();
	}
private:
	StackOnly(){}

	int _a;
};

class NonInherit : public StackOnly
{
public:
	NonInherit()
	{}
private:
	int _b;
};

C 11方法

fifinal关键字,fifinal修饰类,表示该类不能被继承。

代码语言:javascript复制
class NonInherit final
{
	//......
};

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

设计模式

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

设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。

单例模式:

一个类只能创建一个对象,即单例模式 ,该模式可以保证系统中该类只有一个实例,并提供一个 访问它的全局访问点,该实例被所有程序模块共享 。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。

单例模式分为两种:

  • 饿汉模式

不管你将来用不用,程序启动时就创建一个唯一的实例对象。

代码语言:javascript复制
class Singleton
{
public:
	static Singleton& GetInstance()
	{
		return m_instance;
	}

	void fun()
	{
		cout << "Singleton::func()" << endl;
	}
private:
	Singleton() {}

	Singleton(const Singleton&) = delete;

	Singleton& operator= (const Singleton&) = delete;

private:
	//静态变量
	static Singleton m_instance;

};
Singleton Singleton::m_instance;  //类外初始化

1.我们在Singleton类中添加一个Singleton类的静态变量,并且在在类外初始化,这样整个类就这一个静态的对象。

2.需要获取的时候我们利用静态函数GetInstance返回,获取到类内的静态对象。

3.我们需要对类内成员进行操作的时候,只需要在类内创建对应的函数即可。

接收我们可以用引用接收:

优缺点:

优点:简单

缺点:占用更多的资源,可能会导致进程启动慢,且如果有多个单例类对象实例启动顺序不确定。

  • 懒汉模式

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

代码语言:javascript复制
class Singleton
{
public:
	static Singleton& GetInstance()
	{
		if (m_instance == nullptr)
		{
			m_instance = new Singleton;
		}

		return *m_instance;
	}

private:
	Singleton(){}

	Singleton(const Singleton&) {}
	Singleton& operator= (const Singleton&) {}

	static Singleton* m_instance;
};
Singleton* Singleton::m_instance = nullptr;

懒汉模式的线程安全问题

在多线程模式下,我们执行if判断的时候,如果刚好进了if语句但是此时时间片时间到了,线程被切走,那么另外一个线程开辟了空间分配给了m_instance,结束后切回最初的进程重复进行开辟空间分配的操作,那么此时就会造成内存泄漏的问题。

为了解决这个问题,我们需要对存在线程安全的代码进行加锁。

代码语言:javascript复制
class Singleton
{
public:
	static Singleton& GetInstance()
	{
		_mtx.lock();
		if (m_instance == nullptr)
		{
			m_instance = new Singleton;
		}
		_mtx.unlock();

		return *m_instance;
	}

private:
	Singleton(){}

	Singleton(const Singleton&) {}
	Singleton& operator= (const Singleton&) {}

	static Singleton* m_instance;
	static mutex _mtx;
};
Singleton* Singleton::m_instance = nullptr;
mutex Singleton::_mtx;

因为只有第一次进if语句的时候才用得上锁,其他的时候也加锁是会浪费性能的。对此我们可以加上一个双检查判断来优化一下。

代码语言:javascript复制
static Singleton& GetInstance()
{
	if (m_instance == nullptr)
	{
		_mtx.lock();
		if (m_instance == nullptr)
		{
			m_instance = new Singleton;
		}
		_mtx.unlock();
	}

	return *m_instance;
}

我们上面的代码中其实还是有一个问题:点那个new失败抛异常之后,锁的unlock操作就不会执行了。为了解决这个问题,我们可以用try-catch的方法来解决,但我们这里还可以用智能指针的办法来自动释放。

利用RAII机制自动回收锁

我们可以创建一个智能锁指针,构造函数中加锁,析构函数中解锁。从而达到了锁的自动加锁和解锁。

代码语言:javascript复制
class MutexGard
{
public:
	MutexGard(mutex& mtx)
		:_mtx(mtx)
	{
		_mtx.lock();
	}

	~MutexGard()
	{
		_mtx.unlock();
	}
private:
	mutex& _mtx; //所不允许拷贝,这里只能用引用
};

class Singleton
{
public:
	static Singleton& GetInstance()
	{
		if (m_instance == nullptr)
		{
			/*_mtx.lock();
			if (m_instance == nullptr)
			{
				m_instance = new Singleton;
			}
			_mtx.unlock();*/

			MutexGard mg(_mtx);
			if (m_instance == nullptr)
			{
				m_instance = new Singleton;
			}
		}
		
		return *m_instance;
	}

private:
	Singleton(){}

	Singleton(const Singleton&) {}
	Singleton& operator= (const Singleton&) {}

	static Singleton* m_instance;
	static mutex _mtx;
};
Singleton* Singleton::m_instance = nullptr;
mutex Singleton::_mtx;

资源的回收

一般情况下,资源是不需要我们自动回收的,但有时候,可能要将数据写进文件中进行保存。我们就需要手动在函数内保存了。

代码语言:javascript复制
void DelInstance()
{
	//保存数据的操作
	// ......

	if (m_instance != nullptr)
	{
		delete m_instance;
		m_instance = nullptr;
	}
}

为了省事,我们也可以封装成一个自动保存资源的类,然后在单例类中加入了一个资源回收类的对象,这样在最后调用析构的时候,就能够自动回收资源了。

代码语言:javascript复制
class GC
	{
	public:
		~GC()
		{
			//保存数据的操作
			// ......

			if (m_instance != nullptr)
			{
				delete m_instance;
				m_instance = nullptr;
			}
		}
	};

 懒汉模式的简易方法

代码语言:javascript复制
class Singleton {
public:
	static Singleton& GetInstance()
	{
		static Singleton m_instance;
		return m_instance;
	}

	Singleton(const Singleton& sin) = delete;
	Singleton& operator=(const Singleton& sin) = delete;

private:
	Singleton() {}
};

这种方法实现的非常简单巧妙:

1.因为是静态的变量,就算反复调用该函数,都只会在静态区开辟一个变量,并不会反复开辟。

2.并不需要在堆区开辟空间创建单例对象。

3.不需要考虑线程安全问题并加锁以及new抛异常问题

上述方法虽然巧妙,但是值得一提的是,只有在C 11之后的版本中才能保证局部创建的静态变量是线程安全的。

0 人点赞