C++之特殊类的设计(单例模式)

特殊类的设计(单例模式)

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

方法一

正常情况下我们既可能在栈上有可能在堆上创建对象,现在要求只能在堆上创建对象,我们可以将析构函数设置成私有,当在栈上创建对象后,最后释放对象时会报错,因为析构函数是私有的,那么有人说堆上的对象也要调用析构函数呀,我们的解决办法是加一个公有的成员函数专门来delete,成员函数是可以访问私有的,故我们在外面释放堆上的对象时可以调用这个公有的接口

实现如下:

#include<iostream>
using namespace std;

class OnlyHeap
{
public:
    void DestoryObj(OnlyHeap* ptr)
    {
        delete ptr;//类里面可以访问私有
    }
    void DestoryObj()
    {
        delete this;//类里面可以访问私有
    }
private:
    ~OnlyHeap()
    {
        cout<<"~OnlyHeap()"<<endl;
    }
    int _a;  
};
int main()
{
    OnlyHeap oh;//报错,在析构时没有不能访问私有
    OnlyHeap *ptr = new OnlyHeap;
    ptr->DestoryObj();
    return 0;
}

方法二

也可以将构造函数私有:

  • 构造函数私有化
  • 提供一个公有函数创建对象,对象创建都在堆上
  • 公有函数设置成静态可以突破类域解决问题
#include<iostream>
using namespace std;

class OnlyHeap
{
public:
	//2. 提供一个公有函数创建对象,对象创建都在堆上
    OnlyHeap* CreateObj()
    {
        return new OnlyHeap;//类里面可以访问私有
    }
    //设置成静态可以解决问题:
    static OnlyHeap* CreateObj()
    {
        return new OnlyHeap;//类里面可以访问私有
    }
private:
    //1. 构造函数私有化
    OnlyHeap()
    {
        cout<<"OnlyHeap()"<<endl;
    }
    int _a;  
};
int main()
{
    OnlyHeap oh;//报错,在构造时没有不能访问私有
    OnlyHeap *ptr = OnlyHeap::CreateObj();//调用非静态成员函数需要实例化对象,突破类域就可以,成员函数设置成静态的,不然会报错
    ptr->DestoryObj();
    return 0;
}

但是上面的写法还是有缺陷:

OnlyHeap copy(*ptr);//拷贝构造

我们可以通过拷贝构造创建栈对象,所以我们可以将拷贝构造设置成私有:

  • C++98中将拷贝构造设置成私有称为防拷贝:只声明,不实现,声明成私有

  • C++11,可以将拷贝构造声明如下:

    ```cpp
    OnlyHeap(const OnlyHeap& oh) = delete;
    ```
    
//可以将拷贝构造设置成私有
#include<iostream>
using namespace std;

class OnlyHeap
{
public:
	//2. 提供一个公有函数创建对象,对象创建都在堆上
    OnlyHeap* CreateObj()
    {
        return new OnlyHeap;//类里面可以访问私有
    }
    //设置成静态可以解决问题:
    static OnlyHeap* CreateObj()
    {
        return new OnlyHeap;//类里面可以访问私有
    }
private:
    //1. 构造函数私有化
    OnlyHeap()
    {
        cout<<"OnlyHeap()"<<endl;
    }
    //C++98 防拷贝 -- 只声明,不实现,声明成私有
    OnlyHeap(const OnlyHeap& oh);//没必要去实现拷贝构造
    //这样做就不会生成默认的拷贝构造函数
    //or
    //C++11
    OnlyHeap(const OnlyHeap& oh) = delete;
    int _a;  
};
int main()
{
    OnlyHeap oh;//报错,在构造时没有不能访问私有
    OnlyHeap *ptr = OnlyHeap::CreateObj();//调用非静态成员函数需要实例化对象,突破类域就可以,成员函数设置成静态的,不然会报错
    ptr->DestoryObj();
    return 0;
}

如果是上面实现的话,就不用考虑防止拷贝构造了,因为拷贝构造出来最后也需要调用析构函数,因为析构是私有,所以也会报错,我们主要需要理解第二种方式,因为设计思路非常通用。

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

方法一

通过上面的学习,相信这个问题就变得简单了,解决方法为:将构造函数私有化,给一个公有的静态成员函数在栈上创建对象

class StackOnly
{
public:
    static StackOnly CreateObj()
    {
        return StackOnly();
    }
private:
    StackOnly()
        :_a(0)
    {
        cout<<"StackOnly()"<<endl;
    }
    int _a;
}
int main()
{
    StackOnly so;//不能创建
    StackOnly* ptr = new StackOnly;//不能创建
    StackOnly::CreateObj();//突破类域去调用静态成员函数在栈上创建对象 
    return 0;
}

方法二

还有一种方法是重载一个类专属的operator new,将它设置成私有,因为new创建对象时会调用operator new申请空间。

class StackOnly
{
public:
    StackOnly()
        :_a(0)
    {
        cout<<"StackOnly()"<<endl;
    }
private:
    //重载一个类专属的operator new
    //只声明
    //C++98 防调用 -- 只声明不实现,声明为私有
    void* operator new(size_t size);
    void* operator delete(void* ptr);
    
    //C++11
    void* operator new(size_t size) = delete;
    void* operator delete(void* ptr) = delete;
    
    int _a;
}
int main()
{
    StackOnly so;
    StackOnly* ptr = new StackOnly;
    return 0;
}

我们这里将operator new限制住,这种方式存在一些漏洞,因为无法禁止在静态区创建对象:

static StackOnly sso;

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

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

C++98

C++98的实现方式:

class CopyBan
{
// ...
private:
    CopyBan(const CopyBan&);
    CopyBan& operator=(const CopyBan&);
//...
};

为什么设置成私有:如果只声明没有设置成private,用户自己如果在类外定义了,就可以不能禁止拷贝

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

C++11

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

class CopyBan
{
// ...
    CopyBan(const CopyBan&)=delete;
    CopyBan& operator=(const CopyBan&)=delete;
//...
};

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

C++98中我们将父类构造函数设置成私有,父类可以使用静态成员函数创建对象,这样可以解决这个问题,但是这种方式不够直接,没有直接禁止继承,这里其实是可以继承的,但是Derive不能创建对象,因为Derive的构造函数必须要调用父类NonInherit的构造函数,但是NonInherit构造函数是私有,在子类中不可见,那么这里继承不会报错,继承的子类创建对象就会报错

class NonInherit
{
public:
    //父类可以使用静态成员函数创建对象
    static NonInherit GetInstance()
    {
        return NonInherit();
    }
private:
    //构造函数私有
    NonInherit()
    {}
};
//C++98 这种方式不够直接,没有直接禁止继承
//这里是可以继承的,但是Derive不能创建对象,因为Derive的构造函数必须要调用父类NonInherit的构造函数,但是NonInherit构造函数是私有,在子类中不可见,那么这里继承不会报错,继承的子类创建对象就会报错
class Derive : NonInherit
{};
int main()
{
    
    return 0;
}

C++11的实现方法:我们可以在父类后面加一个关键字final,这样直接就禁止了继承

class NonInherit final
{};
class Derive : NonInherit
{};
int main()
{
    
    return 0;
}

C++11的不能被继承的方式,简单明了直接

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

设计模式

设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打仗时都是人拼人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后来孙子就总结出了《孙子兵法》。孙子兵法也是类似。
使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。

单例模式

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

单例模式的本质是去管理全局对象,定义一个全局对象,大家都能用也能保证单例,但是这种方式存在很大的缺陷,你要让大家都能用,这个对象你只能定义在一个.h,如果这个.h在包含在多个.cpp,链接会报错

Singleton.h

#include<vector>
//全局定义一个,大家都用它
std::vector<int> v;
void f1();

Singleton.cpp

//.h
#include"Singleton.h"
void f1()
{
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);
    
    for(auto e :v)
    {
        cout<< e <<" ";
    }
    cout<<endl;
}

test.cpp

#include<Singleton.h>
//.h
void f2()
{
    v.push_back(11);
    v.push_back(22);
    v.push_back(33);
    
    for(auto e :v)
    {
        cout<< e <<" ";
    }
    cout<<endl;
}
int main()
{
    f1();
    f2();
    return 0;
}

这样编译会报错,为什么呢?全局链接时会发现有两个命名v,会产生二义性,所以会报错,为了不报错,我们这样解决:

#include<vector>
void f1();
static std::vector<int> v;

将v定义成全局静态,每个文件独自拥有v,但是不再是同一个对象,每个.cpp中各自是一个对象,但是这样会有多个vector对象了,那么怎么让不同的CPP访问到同一个vector呢?

将声明和定义分离,在.h里面声明,在.cpp里面定义就可以解决

注意不能在.h里面定义,这样的话所有包了头文件的地方都会有定义,这就重复定义了

.h文件对他声明:

#include<vector>
#nclude<iostream>
using namespace std;
//全局定义一个,大家都用它
std::vector<int> v;
void f1();
//static vector<int> v;
extern vector<int> v;//声明

extern关键字可以置于变量或函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其它模块寻找其定义

.cpp文件进行定义:

#include<Singleton.h>
//.h
void f1()
{
    v.push_back(11);
    v.push_back(22);
    v.push_back(33);
    
    for(auto e :v)
    {
        cout<< e <<" ";
    }
    cout<<endl;
}
//定义
vector<int> v;

这样此时只会有一个vector的对象了。

单例模式的实现

全局只有唯一的实例对象,那么他里面的成员也就是单例的。static修饰成员变量保证了全局只有一个唯一一个。

饿汉模式

饿汉模式:在进入main函数之前就创建对象

  1. 构造函数私有化,保证不能随意创建对象
  2. 类里面声明一个static Singleton对象(static修饰的成员全局就一个),在.cpp文件定义这个对象,声明和定义分离
  3. 提供一个获取单例对象的static成员函数

定义需要放在.cpp文件中:

//定义
Singleton Singleton::_sinst;
//获取单例对象的static成员函数的实现
Singleton& Singleton::GetInstance()
{
    return _sinst;
}

.h文件

class Singleton
{
public:
    //3、提供一个获取单例对象的static成员函数
    static Singleton& GetInstance();
    //获取实例对象
    //如果vector对象是私有,想访问,只能再封装一层
    void PushBack(int x)
    {
        _v.push_back(x);
    }
private:
    //1、构造函数私有化,不能随意创建对象
    Singleton()
    {}
private:
    vector<int> _v;
    //2、类里面声明一个static Singleton对象,在cpp定义这个对象、
    //保证了全局只有一个唯一对象
    static Singleton _sinst;
};
int main()
{
    Singleton::GetInstance();//这样就获取到了这个对象
    return 0;
}

但是我们上面设置的类还有缺陷,当前类可以拷贝构造,所以将拷贝构造也设置成私有:

//防拷贝
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;

饿汉模式缺陷:单例对象是main函数之前创建初始化的

  1. 如果单例对象的构造函数中要做很多工作,可能会导致程序启动慢

  2. 如果多个单例类,并且它们之间有依赖关系,那么饿汉模式无法保证

假设有三个单例类,要求三个之间有依赖关系,需要Singleton1先生成才能有Singleton2,需要有Singleton2才能有Singleton3:

image-20220313114916321

那么饿汉模式无法保证,全局的静态变量在初始化时不能保证谁先初始化谁后初始化,为了更好的控制这些问题,有人就给出懒汉模式:

懒汉模式

第一次调用GetInstance时,才会创建初始化单例对象,相对于饿汉,不存在可能会导致启动慢的问题,也可以控制顺序依赖的问题了

.cpp文件里定义:

//定义
Singleton* Singleton::_spinst = nullptr;
Singleton& Singleton::GetInstance()
{
    if(_spinst == nullptr)
    {
        _spinst = new Singleton;
    }
    return *_spinst;
}
//获取实例对象
class Singleton
{
public:
    //3、提供一个获取单例对象的static成员函数
    static Singleton& GetInstance();
    //获取实例对象
    //如果vector对象是私有,想访问,只能再封装一层
    void PushBack(int x)
    {
        _v.push_back(x);
    }
private:
    //1、构造函数私有化,不能随意创建对象
    Singleton()
    {}
private:
    vector<int> _v;
    //2、类里面声明一个static Singleton对象,在cpp定义这个对象、
    //保证了全局只有一个唯一对象
    static Singleton* _spinst;
    
};

我们现在这样写还是有问题的,有线程安全问题。假设有t1,t2两个线程,当_spinst不为nullptr,t1线程进去还没创建对象,而t2线程来了,此时t2也判断_spinst不为nullptr,也去创建对象了,此时就保证不了单例

所以我们需要再给一个成员mutex进行加锁:

class Singleton
{
public:
    //3、提供一个获取单例对象的static成员函数
    static Singleton& GetInstance();
    //获取实例对象
    //如果vector对象是私有,想访问,只能再封装一层
    void PushBack(int x)
    {
        _v.push_back(x);
    }
private:
    //1、构造函数私有化,不能随意创建对象
    Singleton()
    {}
private:
    vector<int> _v;
    //2、类里面声明一个static Singleton对象,在cpp定义这个对象、
    //保证了全局只有一个唯一对象
    static Singleton* _spinst;
    static mutex _mtx;//线程操作
};

需要注意的是_mtx在.cpp文件中定义,不然链接时会出错:

//定义
Singleton* Singleton::_spinst = nullptr;
mutex Singleton:: _mtx;
//加锁保证在多线程环境下面,一定只有一个线程区new对象,只创建出一个单例对象
Singleton& Singleton::GetInstance()
{
    _mtx.lock();
    if(_spinst == nullptr)
    {
        _spinst = new Singleton;
    }
    _mtx.unlock();
    return *_spinst;
}

但是这样加锁后还会有问题,我们访问这段代码有两种情况:

1、同时有多个线程进来

2、不同时

但是当_spinst不为nullptr了以后,其他线程来了还是要加锁解锁,效率低,那么能这样加锁吗?

//定义
Singleton* Singleton::_spinst = nullptr;
mutex Singleton:: _mtx;
//加锁保证在多线程环境下面,一定只有一个线程区new对象,只创建出一个单例对象
Singleton& Singleton::GetInstance()
{
    if(_spinst == nullptr)
    {
    	_mtx.lock();
        _spinst = new Singleton;
    	_mtx.unlock();   
    }
    return *_spinst;
}

不能,这样会存在线程安全问题,当第一个线程来了后,_spinst不为空,进去加锁创建对象了,在第一个线程还没有创建对象时,第二个线程来了,也判断_spinst不为空,但是此时有锁他进不去,当第一个线程创建完线程后解锁后,第二个线程也来了,但是此时_spinst已经不为空,再创建就有问题了

所以我们进行双检查提高效率,保证了除了第一个线程需要获取锁之外,其他线程都不需要获取锁,直接返回对象,既能保证线程安全又能提高效率

//定义
Singleton* Singleton::_spinst = nullptr;
mutex Singleton:: _mtx;
//加锁保证在多线程环境下面,一定只有一个线程区new对象,只创建出一个单例对象
Singleton& Singleton::GetInstance()
{
    //双检查
    if(_spinst == nullptr)
    {
        _mtx.lock();
        if(_spinst == nullptr)
        {
            _spinst = new Singleton;
        }
        _mtx.unlock();
    }
    reurn *_spinst;
}

懒汉模式完整代码:

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指针
	static Singleton* _inst;
	static mutex _mtx; //互斥锁
};

//在程序入口之前先将static指针初始化为空
Singleton* Singleton::_inst = nullptr;
mutex Singleton::_mtx; //初始化互斥锁

其次,懒汉是可以主动去释放的:

static void DelInstance();
static void DelInstance()
{
    if(_spinst != nullptr)
    {
        _mtx.lock();
        if(_spinst!=nullptr)
        {
            delete _spinst;
            _spinst = nullptr;
        }
        _mtx.unlock();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小赵小赵福星高照~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值