C++特殊类的设计
文章目录
- 一、只能在堆上创建对象
- 二、只能在栈上创建对象
- 三、不能被拷贝
- 四、禁止继承
- 五、只能创建一个对象(单例模式)
- 1. 饿汉模式
- 2. 懒汉模式
- 3. 饿汉模式和懒汉模式对比
一、只能在堆上创建对象
- 实现方式:
- 将类的构造函数私有,拷贝构造声明成私有。防止别人调用拷贝在栈上生成对象。
- 提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建
class HeapOnly
{
public:
static HeapOnly *CreateObj()
{
return new HeapOnly;
}
private:
// 构造函数设置为私有
HeapOnly(){};
// C++98 将拷贝构造函数设置为私有,并且只声明不实现
HeapOnly(const HeapOnly &);
// C++11
HeapOnly(const HeapOnly &) = delete;
};
二、只能在栈上创建对象
方法一:同上将构造函数私有化,然后设计静态方法创建对象返回即可。
class StackOnly
{
public:
// 提供一个获取对象的接口,并且该接口必须设置为静态成员函数
static StackOnly CreateObj()
{
return StackOnly();
}
private:
// 将构造函数设置为私有
StackOnly() {}
};
三、不能被拷贝
拷贝只会放生在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。
-
C++98
将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可。 -
原因:
- 设置成私有:如果只声明没有设置成private,用户自己如果在类外定义了,就可以不能禁止拷贝了
- 只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写反而还简单,而且如果定义了就不会防止成员函数内部拷贝了。
-
C++11
- C++11扩展delete的用法,delete除了释放new申请的资源外,如果在默认成员函数后跟上=delete,表示让编译器删除掉该默认成员函数。
class CopyBan
{
public:
CopyBan() {}
private:
//C++98
CopyBan(const CopyBan &);
CopyBan &operator=(const CopyBan &);
//C++11
CopyBan(const CopyBan &) = delete;
CopyBan &operator=(const CopyBan &) = delete;
};
四、禁止继承
- C++98方式
将该类的构造函数设置为私有即可,因为子类的构造函数被调用时,必须调用父类的构造函数初始化父类的那一部分成员,但父类的私有成员在子类当中是不可见的,所以在创建子类对象时子类无法调用父类的构造函数对父类的成员进行初始化,因此该类被继承后子类无法创建出对象。
代码如下:
class NonInherit
{
public:
static NonInherit CreateObj()
{
return NonInherit();
}
private:
//将构造函数设置为私有
NonInherit()
{}
};
- C++11方法
final关键字,final修饰类,表示该类不能被继承。
class NonInherit final
{
};
五、只能创建一个对象(单例模式)
设计模式:
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打仗时都是人拼人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后来孙子就总结出了《孙子兵法》。孙子兵法也是类似。使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。
设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。
单例模式:
一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。
单例模式有两种实现模式:
1. 饿汉模式
就是说不管你将来用不用,程序启动时就创建一个唯一的实例对象。
创建方法:
- 将构造函数设置为私有,并将拷贝构造函数和赋值运算符重载函数设置为私有或删除,防止外部创建或拷贝对象。
- 提供一个指向单例对象的static指针,并在程序入口之前完成单例对象的初始化。
- 提供一个全局访问点获取单例对象。
class Singleton{
public:
//提供一个全局访问点获取单例对象
static Singleton* GetInstance(){
return _ins;
}
private:
Singleton(){}
//禁止拷贝构造和赋值重载生成,防止拷贝
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&)=delete;
private:
//提供一个指向单例对象的static指针
static Singleton* _ins;
};
Singleton* Singleton::_ins=new Singleton;
线程安全相关问题:
- 饿汉模式在程序运行主函数之前就完成了单例对象的创建,由于main函数之前是不存在多线程的,因此饿汉模式下单例对象的创建过程是线程安全的。
- 后续所有多线程要访问这个单例对象,都需要通过调用GetInstance函数来获取,这个获取过程是不需要加锁的,因为这是一个读操作。
- 当然,如果线程通过GetInstance获取到单例对象后,要用这个单例对象进行一些线程不安全的操作,那么这时就需要加锁了。
- 如果这个单例对象在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避免资源竞争,提高响应速度更好。
2. 懒汉模式
如果单例对象构造十分耗时或者占用很多资源,比如加载插件啊, 初始化网络连接啊,读取文件啊等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载)更好。
创建方法:
- 将构造函数设置为私有,并将拷贝构造函数和赋值运算符重载函数设置为私有或删除,防止外部创建或拷贝对象。
- 提供一个指向单例对象的static指针,并在程序入口之前先将其初始化为空。
- 提供一个全局访问点获取单例对象。
class Singleton
{
public:
// 3、提供一个全局访问点获取单例对象
static Singleton *GetInstance()
{
// 双检查
if (_inst == nullptr)
{
_mtx.lock();
if (_inst == nullptr)
{
_inst = new Singleton;
}
_mtx.unlock();
}
return _inst;
}
private:
// 1、将构造函数设置为私有,并防拷贝
Singleton()
{
}
Singleton(const Singleton &) = delete;
Singleton &operator=(const Singleton &) = delete;
// 2、提供一个指向单例对象的static指针
private:
static Singleton *_inst;
static mutex _mtx; // 互斥锁
};
// 在程序入口之前先将static指针初始化为空
Singleton *Singleton::_inst = nullptr;
mutex Singleton::_mtx; // 初始化互斥锁
线程安全问题:
-
在懒汉模式中,
GetInstance
函数负责创建和返回单例对象。首次调用该函数时,会检查一个静态指针(通常指向单例实例)是否为空。如果为空,则表明单例对象尚未创建,此时需要创建它。然而,由于多个线程可能同时调用GetInstance
,如果不采取保护措施,就可能导致多个线程同时创建单例对象,违背了单例模式的初衷。 -
为了解决这个问题,最直接的方法是在
GetInstance
函数中加入互斥锁,以确保在任何时刻只有一个线程能够执行创建单例对象的代码。然而,这种方法虽然简单有效,但在单例对象已经创建之后,每次调用GetInstance
仍然需要进行加锁和解锁操作,这无疑增加了不必要的开销,降低了程序的运行效率。 -
为了提高效率,可以采用双检查加锁机制。这种机制在
GetInstance
函数内部首先进行一次无锁的判空检查(第一次检查)。如果静态指针不为空,则直接返回单例对象,无需进一步操作。如果静态指针为空,则进入加锁区域。在加锁区域内,再次进行判空检查(第二次检查),这是为了确保在第一个线程进入加锁区域后,如果有其他线程也尝试进入,它们会在第一次检查失败后被阻塞,直到第一个线程完成单例对象的创建并解锁。第二次检查是为了防止在第一个线程持有锁期间,其他线程已经创建了单例对象(尽管这种情况在正确实现的双检查加锁中不会发生,但这一步是出于严谨性考虑)。 -
通过双检查加锁机制,可以确保只有在首次创建单例对象时才需要进行加锁操作,从而避免了后续调用中的不必要开销。这种方式既保证了线程安全,又提高了程序的运行效率。
3. 饿汉模式和懒汉模式对比
- 饿汉模式的优点就是简单,但是它的缺点也比较明显。饿汉模式在程序运行主函数之前就会创建单例对象,如果单例类的构造函数中所做的工作比较多,就会导致程序迟迟无法进入主函数,在外部看来就好像是程序卡住了。
- 此外,如果有多个单例类需要创建单例对象,并且它们之间的初始化存在某种依赖关系,比如单例对象A的创建必须在单例对象B之后,此时饿汉模式也会存在问题,因为我们无法保证这多个单例对象中的哪个对象先创建。
- 而懒汉模式就能很好的解决上述饿汉模式的缺点,因为懒汉模式并不是一开始就完成单例对象的创建,因此不会导致程序迟迟无法进入主函数,并且懒汉模式中各个单例对象创建的顺序是由各个单例类中的GetInstance函数第一次被调用的顺序决定,因此是可控制的。
- 懒汉模式的缺点就是,在编码上比饿汉模式复杂,在创建单例对象时需要考虑线程安全的问题。