特殊类设计
简单的特殊类设计
设计一个不能拷贝的类
这个还是挺简单的,在C++98和C++11中有两种不同的设计方法,在C++98中可以将构造函数封装成私有成员,在C++11中可以在构造函数后面加 =delete,让编译器删除该默认成员函数
代码也很简单:
// C++98
class CopyForbid
{
private:
CopyForbid(const CopyForbid& cb);
CopyForbid& operator= (const CopyForbid& cb);
};
// C++11
class CopyForbid11
{
private:
CopyForbid(const CopyForbid& cb)=delete;
CopyForbid& operator= (const CopyForbid& cb)=delete;
};
设计一个只能在堆上创建对象的类
设计一个只能在堆上创建对象的类,首先要知道,这个类的构造函数不能再放在public中,所以首先将构造函数私有,在public中通过调用函数从而创建对象,但是这里又出现的一个问题是:没有对象如何调用对象的成员函数呢?所以这里就要将这个函数用static修饰为静态函数。于此同时还要把拷贝构造函数封掉,否则的话还是可以通过拷贝构造函数构造一个栈上的对象
class HeapOnly
{
public:
static HeapOnly* CreateObj()
{
return new HeapOnly;
}
private:
HeapOnly()
{}
HeapOnly(const HeapOnly&) = delete;
};
另外还有一种方法是将析构函数写进private,但是回收对象需要调用一个public成员函数进行对对象的回收
class HeapONly
{
public:
HeapONly()
{}
void Destory()
{
this->~HeapONly();
}
private:
~HeapONly()
{}
};
设计一个只能在栈上创建对象的类
一样的思维,既然对创建这个类有限制,那么首先要做的就是将这个类的构造函数设置为private,其次写一个公有静态成员函数函数进行对象的创建,当然也要记得讲拷贝构造函数封掉,否则的话可能会导致赋值给一个静态区的变量。代码如下:
class StackOnly
{
public:
static StackOnly CreateObj()
{
return StackOnly();
}
private:
StackOnly()
{}
StackOnly(const StackOnly&) = delete;
};
设计一个不能被继承的类
在C++98中,如果想要设计一个不能被子类继承的类,只能将父类的构造函数设置为私有,子类找不到父类的构造函数,就无法继承。而在C++11中可以使用final关键字。
// 设计一个类,不能被继承
class NoInherit
{
public:
static NoInherit CreateProject()
{
return NoInherit();
}
private:
NoInherit()
{}
}; // C++98
class A final
{
// ...
}; // C++11
单例模式
饿汉模式
单例模式是应用最广的设计模式之一,也是程序员应该很熟悉的一个设计模式,使用单例模式必须保证类只能创建一个对象。
那为什么会有单例模式呢?
在开发过程中,很多时候一个类我们希望它只创建一个对象,比如:线程池、缓存、网络请求等。当这类对象有多个实例时,程序就可能会出现异常,比如:程序出现异常行为、得到的结果不一致等。
那么如何实现一个单例模式?
// 设计一个类,只能创建一个对象(单例模式)
class InfoSingleton
{
public:
static InfoSingleton& GetInstance()
{
return _sins;
}
void Insert(string name, int salary)
{
_info[name] = salary;
}
void Print()
{
for (auto kv : _info)
{
cout << kv.first << " : " << kv.second << endl;
}
}
private:
InfoSingleton()
{}
InfoSingleton(const InfoSingleton& info) = delete;
InfoSingleton& operator=(const InfoSingleton& info) = delete;
map<string, int> _info;
private:
static InfoSingleton _sins;
};
如果想要控制类创建对象,首先就是将本来公有的构造函数限制起来,在之前的简单特殊类设计中,我们还是使用构造函数构造对象,因为之前的限制并没有限制构造对象的数量,但是这里的单例模式限制了构造对象的数量,只能有一个,所以这里的构造函数几乎可以说没什么用处,关键的在于一个私有的静态成员_sins,静态成员在类外初始化,并且可以调用的函数的返回值也是返回这个静态成员变量,那么就让这个类只能在main函数之前实例化出一个对象。所以这就是为什么这个模式也叫饿汉模式。
那么这个类如何使用呢?
这里举的例子中,这个类中有两个成员变量_info和 _sins , _info是一个关联式容器
但是饿汉模式也存在一些问题:
- 单例对象初始化是数据过多,导致程序启动慢
- 多个单例类如果有初始化依赖关系,饿汉模式无法控制
懒汉模式
懒汉模式大的变化就是将静态成员变量变为了对象指针,与此同时GetInstance函数也做出了一些改变:
// 懒汉模式
class InfoSingLenton
{
public:
static InfoSingLenton& GetInstance()
{
if (_psins == nullptr)
{
_psins = new(InfoSingLenton);
}
return *_psins;
}
void Insert(string name, int salary)
{
_info[name] = salary;
}
void Print()
{
for (auto kv : _info)
{
cout << kv.first << " : " << kv.second << endl;
}
}
private:
InfoSingLenton()
{}
InfoSingLenton(const InfoSingLenton& info) = delete;
InfoSingLenton& operator=(const InfoSingLenton& info) = delete;
map<string, int> _info;
private:
static InfoSingLenton* _psins;
};
将静态成员变量设置为指针并初始化为nullptr,在GetInstance函数中进行判断,如果为空就创建对象,依然和饿汉模式一样对拷贝构造和赋值重载进行封锁,很好地保证了在第一次获取单例对象的时候再创建对象,避免了饿汉模式的缺点,但是也带来了一些线程安全的问题:
如果多个线程在调用GetInstance函数的时候,此时_psins还是nullptr,那么就会有几个线程一起进入if判断内部new新的空间对已近new好的空间进行覆盖,而被覆盖的旧空间也会有内存泄露的问题。所以这里需要加锁。
先来看第一中加锁方式:
// 懒汉模式
class InfoSingLenton
{
public:
static InfoSingLenton& GetInstance()
{
_smtx.lock();
if (_psins == nullptr)
{
_psins = new(InfoSingLenton);
}
_smtx.unlock();
return *_psins;
}
void Insert(string name, int salary)
{
_info[name] = salary;
}
void Print()
{
for (auto kv : _info)
{
cout << kv.first << " : " << kv.second << endl;
}
}
private:
InfoSingLenton()
{}
InfoSingLenton(const InfoSingLenton& info) = delete;
InfoSingLenton& operator=(const InfoSingLenton& info) = delete;
map<string, int> _info;
private:
static InfoSingLenton* _psins;
static mutex _smtx;
};
上面的代码就解决了多线程的问题,但是会有另外的一个值得优化的问题存在:如果空间开辟成功,那么但是调用InfoSingLenton函数时每次都要加锁解锁,第二个问题就是这里的new如果抛异常,那么就会导致没有解锁的问题。
对于每次调用函数都会加锁解锁的问题,这里可以使用双重判断的方法,在这一层判断外再加一层if判断来判断_psins是否为空;而抛异常的问题就只需要对其进行捕获解锁后重新抛出。
static InfoSingLenton& GetInstance()
{
if (_psins == nullptr)
{
_smtx.lock();
try
{
if (_psins == nullptr)
{
_psins = new(InfoSingLenton);
}
}
catch (...)
{
_smtx.unlock();
throw;
}
_smtx.unlock();
}
return *_psins;
}
仔细观察这里,其实我们可以将锁封装成一个对象,RAII的思想这不就用起来了嘛:
// RAII锁管理
template<class Lock>
class LockGuard
{
private:
Lock& _lk;
public:
LockGuard(Lock& lk)
:_lk(lk)
{
_lk.lock();
}
~LockGuard()
{
_lk.unlock();
}
};
在构造函数部分进行加锁,在析构函数部分解锁。这个类的构造函数和成员变量有个细节:因为锁是不可以拷贝的,所以这里的成员变量是一个锁的引用,那么在初始化列表处直接进行赋值即可。
// 懒汉模式
class InfoSingLenton
{
public:
static InfoSingLenton& GetInstance()
{
if (_psins == nullptr)
{
LockGuard<mutex> lock(_smtx);
if (_psins == nullptr)
{
_psins = new(InfoSingLenton);
}
}
return *_psins;
}
void Insert(string name, int salary)
{
_info[name] = salary;
}
void Print()
{
for (auto kv : _info)
{
cout << kv.first << " : " << kv.second << endl;
}
}
private:
InfoSingLenton()
{}
InfoSingLenton(const InfoSingLenton& info) = delete;
InfoSingLenton& operator=(const InfoSingLenton& info) = delete;
map<string, int> _info;
private:
static InfoSingLenton* _psins;
static mutex _smtx;
};
InfoSingLenton* InfoSingLenton::_psins = nullptr;
mutex InfoSingLenton::_smtx;
对象如何回收?
单例模式只可以创建一个对象,通过上面的学习我们可以知道,饿汉模式下这个对象创建在静态区中,懒汉模式中这个对象创建在堆上;其实这个对象回不回收都影响不大,程序在正常退出后,无论是在堆上创建的对象还是在静态区中创建的对象,操作系统都可以帮我们回收,但是在实际生产应用中有一些资源需要保存,这就要求我们必须手动处理。这里以懒汉模式举例:
这里思路就是创建一个static函数,函数中实现了对数据的保存和对堆空间上对象的清理,但是要注意这个函数也是存在线程安全问题的,需要加锁。
static void DelInstance()
{
// 保存数据到文件
// ...
LockGuard<mutex> lock(_smtx);
if (_psins)
{
delete _psins;
_psins = nullptr;
}
}
那如果我忘记调用这个函数怎么办呢?可不可以自动调用这个函数?
这里使用内部类解决了这个问题:内部设计一个类GC,GC的析构函数调用DelInstance函数,增加一个静态成员变量为GC类型并在类外初始化,那么程序结束时调用这个对象的析构函数就自动调用了DelInstance函数,在GC的析构函数也可以加一个if判断,如果已经手动调用回收函数,就避免了重复调用。
// 懒汉模式
class InfoSingLenton
{
public:
static void DelInstance()
{
// 保存数据到文件
// ...
LockGuard<mutex> lock(_smtx);
if (_psins)
{
delete _psins;
_psins = nullptr;
}
}
class GC
{
public:
~GC()
{
if (_psins)
{
DelInstance();
}
}
};
static InfoSingLenton& GetInstance()
{
if (_psins == nullptr)
{
LockGuard<mutex> lock(_smtx);
if (_psins == nullptr)
{
_psins = new(InfoSingLenton);
}
}
return *_psins;
}
void Insert(string name, int salary)
{
_info[name] = salary;
}
void Print()
{
for (auto kv : _info)
{
cout << kv.first << " : " << kv.second << endl;
}
}
private:
InfoSingLenton()
{}
InfoSingLenton(const InfoSingLenton& info) = delete;
InfoSingLenton& operator=(const InfoSingLenton& info) = delete;
map<string, int> _info;
private:
static InfoSingLenton* _psins;
static mutex _smtx;
static GC _gc;
};
InfoSingLenton* InfoSingLenton::_psins = nullptr;
mutex InfoSingLenton::_smtx;
InfoSingLenton::GC InfoSingLenton::_gc;
总结
通过对上面这些特殊类的设计,可以发现如果想要对对象的创建进行限制,首先要做的就是将构造函数进行限制,其次就要注意拷贝构造和赋值重载的控制。其次单例模式也是需要掌握的,代码中有很多的细节。
希望大家学习愉快,早日拿到心仪的offer!
foSingLenton& operator=(const InfoSingLenton& info) = delete;
map<string, int> _info;
private:
static InfoSingLenton* _psins;
static mutex _smtx;
static GC _gc;
};
InfoSingLenton* InfoSingLenton::_psins = nullptr;
mutex InfoSingLenton::_smtx;
InfoSingLenton::GC InfoSingLenton::_gc;
**总结**
通过对上面这些特殊类的设计,可以发现如果想要对对象的创建进行限制,首先要做的就是将构造函数进行限制,其次就要注意拷贝构造和赋值重载的控制。其次单例模式也是需要掌握的,代码中有很多的细节。
> 希望大家学习愉快,早日拿到心仪的offer!