特殊类设计
目录
前言
设计一个类,不能被拷贝
设计一个类,只能在堆上创建对象
设计一个类,只能在栈上创建对象
设计一个类,不能被继承
设计一个类,只能创建一个对象(单例模式)
单例模式
(1)饿汉模式
(2)懒汉模式
总结
前言
小伙伴们大家好,本文主要介绍一些常见特殊类的设计方式,希望能给大家带来帮助。
设计一个类,不能被拷贝
拷贝只会放生在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝,
只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。
C++98:
实现方法:将拷贝构造函数和赋值运算符都设为私有即可满足要求。这里只需声明无需定义,因为该函数根本不会调用,定义了也没有意义。
class CopyBan
{
// ...
private:
CopyBan(const CopyBan& x);
CopyBan& operator=(const CopyBan& x);
//...
};
c++11:
实现方法:c++11新增加了delete的用法,除了用来释放new申请的空间外,还可以用来禁用类中的函数,在后面加上=delete即可。
class CopyBan
{
// ...
CopyBan(const CopyBan& x)=delete;
CopyBan& operator=(const CopyBan& x)=delete;
//...
};
设计一个类,只能在堆上创建对象
- 将类的构造函数私有,为了防止居心叵测者用拷贝构造搞事,我们要把拷贝构造也声明的私有。
- 创建一个静态成员函数,在静态成员函数里面去创建对象,而且指定在堆中创建。这里一定要用静态成员函数,不然没有对象调不出来,就变成了先有鸡还是先有蛋的问题。
class HeapOnly
{
public:
static HeapOnly* create()
{
return new HeapOnly();
}
private:
HeapOnly()
{
//
}
HeapOnly(const HeapOnly& x);
};
设计一个类,只能在栈上创建对象
c++ 98:
实现方法:在类中重载new和delete,并设为私有化,编译器会优先调用类中的new和delete。
class StackOnly
{
public:
StackOnly() {}
private:
void* operator new(size_t size);
void operator delete(void* p);
};
c++11:
实现方法:直接使用delete禁掉重载后的new和delete
class StackOnly
{
public:
StackOnly() {}
void* operator new(size_t size) = delete;
void operator delete(void* p) = delete;
};
设计一个类,不能被继承
c++98:
实现方法:将类的构造函数私有化,派生类调不到基类的构造函数,无法创建对象。这种操作其实并不彻底,派生类在定义类时依旧可以进行继承操作,只有在创建对象时才会报错。
class NonInherit
{
public:
static NonInherit GetInstance()
{
return NonInherit();
}
private:
NonInherit()
{}
};
c++11:
实现方法: final关键字,final修饰类,表示该类不能被继承。该操作会让类彻底不能继承。
class A final
{
// ....
};
//class C : A 会报错
//{};
设计一个类,只能创建一个对象(单例模式)
设计模式是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打仗时都是人拼人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后来孙子就总结出了《孙子兵法》。孙子兵法也是类似。使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。
单例模式
一个类只能创建一个对象,这种模式叫做单例模式。该模式保证了系统中该类只有一个实例,并提供一个全局接口,该实例被所有程序模块共享。单例模式有两种实现模式。
(1)饿汉模式
饿汉模式,顾名思义,这个模式处于一个非常饥饿的状态。表现形式就是不管将来用的到用不到,程序启动时就要创建一个唯一的实例对象。
优点和缺点:
- 优点:非常简单
- 缺点:在类的构造函数中需要完成大量任务时会导致程序启动很慢,比如登陆qq时需要等好久,一直卡在登陆页面,让人分分钟有砸键盘的冲动。而且多个单例类一般处在不同的文件,实例化顺序不确定。
下面我们来学习饿汉模式是怎样实现的。单例模式最重要的就是要保证一个类只能创建一个对象,所以首先我们要把他的构造函数,拷贝,赋值等操作都封起来都私有化。然后设置一个静态变量指针,利用静态变量在类外初始化并且所有对象共享静态变量这一特性,我们可以利用静态变量new一个对象出来,并保存这个对象的地址。由于静态变量是私有成员,除初始化外不能在类外被访问,所以不会被更改,保证了对象的唯一性。具体代码如下:
class Singleton
{
public:
static Singleton* get_instance()
{
return _inst;
}
private:
Singleton()
{}
Singleton(const Singleton& x);
Singleton& operator=(Singleton& x);
static Singleton* _inst;
};
Singleton* Singleton::_inst = new Singleton();//在函数入口前就完成初始化
int main()
{
cout << Singleton::get_instance()<<endl;
cout << Singleton::get_instance() << endl;
cout << Singleton::get_instance() << endl;
const int a = 10;
}
(2)懒汉模式
如果单例对象构造十分耗时或者占用很多资源,比如加载插件啊, 初始化网络连接啊,读取
文件啊等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,
就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载)更好。
优点和缺点:
- 优点:实例对象时进程无负载,可以快速启动,而且可以自由控制单例实例的启动顺序。
- 缺点:复杂,需要加锁来保证安全
懒汉模式和饿汉模式的最大区别就是需要的时候才会创建单例对象,所以我们把创建单例对象的步骤写在接口内部。要注意的是,饿汉模式是在函数入口内部就已经初始化好了单例对象,并不会受到多线程的影响,而懒汉模式在函数入口进行创建对象,在创建对象过程中必须加锁保护。
在加锁时采用的是双检查模式,这种模式是非常巧妙的,如果取消最外层的检查,只保留一层检查,看上去似乎也能完成任务,但实际会大大影响效率。因为单例对象只会被创建一次,也就是说这把锁只需要被用到一次以后就没有任何意义了,在只有一层检查的情况下,_inst指针不为空后进程跑到这里依旧会去申请锁,没有申请到锁的进程就需要挂起等待,严重影响了效率。所以需要在外面再加一层检查,如果指针已经不为空了进程根本不会再去申请锁。
class Singleton
{
public:
static Singleton* GetInstance()
{
// 保护第一次需要加锁,后面都不需要加锁的场景,可以使用双检查加锁
// 特点:第一次加锁,后面不加锁,保护线程安全,同时提高了效率
if (_inst == nullptr)
{
_mtx.lock();
if (_inst == nullptr)
{
_inst = new Singleton;
}
_mtx.unlock();
}
return _inst;
}
private:
Singleton()
{
// 假设单例类构造函数中,要做很多配置初始化
}
~Singleton()
{
// 程序结束时,需要处理一下,持久化保存一些数据
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 实现一个内嵌垃圾回收类
class CGarbo {
public:
~CGarbo()
{
if (_inst)
{
delete _inst;
_inst = nullptr;
}
}
};
static Singleton* _inst;
static std::mutex _mtx;
static CGarbo _gc; //定义一个静态变量,程序结束后会自动调用它的析构函数从而释放单例对象
};
总结
今天的内容就到这里了,本文主要简单介绍了一些特殊类的设计方法,希望能给大家带来帮助。江湖路远,来日方长,我们下次见。