1. 设计一个类, 不能被拷贝
💕 C++98方式:
在C++11之前,想要一个一个类不被拷贝,只有将拷贝构造函数
定义为私有,这样在类外就不能调用拷贝构造函数来构造对象了。但是在类内还是可以调用拷贝构造函数来构造对象。
所以正确的做法是 将拷贝构造函数定义为私有,同时拷贝构造函数只声明,不实现。这样即使在类中掉哦那个了拷贝构造函数,编译器也会将错误检查出来。
class CopyBan
{
public:
CopyBan()
{
_ptr = new char[10]{ 0 };
}
~CopyBan()
{
delete[] _ptr;
}
void func()
{
CopyBan tmp(*this);
}
private:
// 重写深拷贝构造函数
CopyBan(const CopyBan& cb);
char* _ptr;
};
💕 C++11方式:
C++11扩展delete
的用法,delete除了释放new申请的资源外,如果在默认成员函数后跟上=delete,表示让编译器删除掉该默认成员函数。 同时,这种方法也不再需要将拷贝构造函数定义为私有。
class CopyBan
{
public:
CopyBan()
{
_ptr = new char[10]{ 0 };
}
~CopyBan()
{
delete[] _ptr;
}
CopyBan(const CopyBan& cb) = delete;
private:
char* _ptr;
};
2. 设计一个类, 不能被继承
💕 C++98方式:
C++98中构造函数私有化,派生类中调不到基类的构造函数,则无法调用父类的构造函数完成父类成员的初始化工作,从而达到父类不能被继承的效果。
class NonInherit
{
public:
static NonInherit GetInstance()
{
return NonInherit();
}
private:
NonInherit()
{}
};
class subClass : public NonInherit
{
public:
subClass()
{}
private:
int _a = 0;
};
💕 C++11方式:
使用final
关键字来修饰该类,表示该类不能被继承
class NonInherit final
{
public:
static NonInherit GetInstance()
{
return NonInherit();
}
private:
NonInherit()
{}
};
3. 设计一个类, 只能在堆上创建对象
一般的类可以在三个不同的存储位置创建对象:
- 在栈上创建对象,对象出了局部作用域自动销毁
- 通过
new
关键字在堆上创建对象,对象出了局部作用域不会自动销毁,需要我们手动销毁。否则则会发生内存泄漏 - 通过
static
关键字在静态区创建对象,对象的作用域为定义时所在的局部域,而对象的生命周期伴随着整个进程,这个对象在mian调用结束后由操作系统自动回收
💕 方法一:构造函数私有化
将构造函数声明为私有,同时删除拷贝构造函数,然后提供一个静态成员函数,在该静态成员函数中完成堆对象的创建。
class HeapOnly
{
public:
static HeapOnly* CreateObj()
{
return new HeapOnly;
}
HeapOnly(const HeapOnly& ho) = delete;
private:
HeapOnly()
{}
};
静态成员没有this指针,所以可以通过类名+域作用限定符
来进行调用而不需要通过对象调用。同时我们还需要删除拷贝构造函数,防止在类外通过下面这种取巧的方式来创建栈区或者堆区对象:
HeapOnly* pho1 = HeapOnly::CreateObj();
HeapOnly pho2(*pho1); // 通过拷贝构造函数在栈上创建对象
static HeapOnly ho3(*pho1); // 通过拷贝构造函数在静态区上创建对象
💕 方法二:析构函数私有化
将析构函数私有化,同时提供一个专门的成员函数,在该成员函数中完成堆对象的析构
class HeapOnly
{
public:
HeapOnly()
{}
void Destroy()
{
this->~HeapOnly();
}
private:
~HeapOnly()
{}
};
对于在堆上创建的对象,编译器并不会主动调用析构函数来回收资源,而是由用户手动进行delete
或者进程退出后由操作系统回收
,所以编译器并不会报错。但对于自定义类型的对象,delete会首先调用其析构函数
完成兑现资源的清理,然后再调用operator delete 释放对象的空间,所以这里我们不能使用delete关键字来手动释放new出来的对象,因为调用析构函数会失败。
所以我们需要一个Destroy成员函数,通过它来调用析构函数完成资源的清理。这个Destroy函数不需要声明为静态类型,因为只有类的对象才需要调用它。最后,我们也不需要再删除拷贝构造函数了,因为拷贝出来的栈对象或者静态对象压根儿无法创建出来。
3. 设计一个类, 只能在栈上创建对象
💕 方法一:在类中禁用operator new 和 operator delete函数
new和delete是C++中的关键字,其底层通过调用operator new 和operator delete函数来开辟和释放空间。
operator new 和 operator delete 函数是普通的全局函数
,而并非运算符重载,它们的函数名就长这样罢了。因此,我们可以在类中重载operator new和 operator delete 函数,然后将他们声明为删除函数,这样就不能通过new和delete再堆上创建对象了,但是我们仍然可以在静态区创建对象,与类的要求不符。
class StackOnly
{
public:
StackOnly(int x = 0)
:_x(x)
{}
~StackOnly()
{}
void* operator new(size_t size) = delete;
void operator delete(void* p) = delete;
private:
int _x;
};
💕 方法二:构造函数私有化
,提供一个在栈上创建对象的静态成员函数
这种方式和设计一个 只能在堆上创建对象的类的思路是一样的。但是不能删除拷贝构造函数,否则就不能通过下面这种方式构造栈对象了。
StackOnly st = StackOnly::CreateObj();
但是不禁用拷贝构造函数又会导致可以通过拷贝构造函数创建出静态区上的对象,所以我们设计出的只能在栈上创建对象的类是有缺陷的。
class StackOnly
{
public:
static StackOnly CreateObj(int x)
{
return StackOnly(x);
}
private:
StackOnly(int x = 0)
:_x(x)
{}
int _x;
};
int main()
{
StackOnly st1 = StackOnly::CreateObj(1);
return 0;
}
4. 创建一个类, 只能创建一个对象(单例模式)
设计模式(Design Pattern)
是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打仗时都是人拼人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后来孙子就总结出了《孙子兵法》。孙子兵法也是类似。
使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。
单例模式:
一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。
单例模式有两种实现模式,饿汉模式和懒汉模式。
饿汉模式
饿汉模式是将构造函数私有,然后删除拷贝构造函数和赋值运算符重载函数,由于单例模式全局只允许有一个唯一对象,所以我们可以定义一个静态类对象作为类的承运,然后提供一个GetInstance
接口来获取这个静态类对象。静态类对象需要在类内声明,类外定义,定义时需要指定类域。同时,GetInstance接口也必须是静态函数。
饿汉模式的特点是在类加载的时候就创建单例对象,因此其实梨花在程序运行之前(main函数调用之前就已经完成)实现如下:
class Singleton
{
public:
static Singleton* GetInstance()
{
return _ins;
}
Singleton(const Singleton& s) = delete;
Singleton& operator=(const Singleton& s) = delete;
private:
// 限制在类外面随意创建对象
Singleton()
{}
static Singleton* _ins;
};
Singleton* Singleton::_ins = new Singleton;
因为饿汉模式在main函数前就被创建,所以它不存在线程安全问题,但是它也存在一些缺点
:
- 有的单例对象构造十分耗时或者需要占用很多资源,比如加载插件、 初始化网络连接、读取文件等等,会导致程序启动时加载速度慢。
- 饿汉模式在程序启动时就创建了单例对象,所以即使在程序运行期间并没有用到该对象,它也会一直存在于内存中,浪费了一定的系统资源。
- 多个单例类存在初始化依赖关系时,饿汉模式无法控制。比如A、B两个单例类存在于不同的文件中,我们要求先初始化A,再初始化B,但是A、B谁先启动初始化是由OS自动进行调度控制的,我们无法进行控制。
多线程模式下的饿汉模式:
class Singleton
{
public:
static Singleton* GetInstance()
{
return _ins;
}
void Add(const string& str)
{
_mtx.lock();
_v.push_back(str);
_mtx.unlock();
}
void Print()
{
_mtx.lock();
for (auto& e : _v)
cout << e << endl;
cout << endl;
_mtx.unlock();
}
Singleton(const Singleton& s) = delete;
Singleton& operator=(const Singleton& s) = delete;
private:
// 限制在类外面随意创建对象
Singleton()
{}
vector<string> _v;
static Singleton* _ins;
mutex _mtx;
};
Singleton* Singleton::_ins = new Singleton;
int main()
{
srand(time(0));
int n = 100;
thread t1([n]() {
for (size_t i = 0; i < n; ++i)
{
Singleton::GetInstance()->Add("t1线程:" + to_string(rand()));
}
});
thread t2([n]() {
for (size_t i = 0; i < n; ++i)
{
Singleton::GetInstance()->Add("t2线程:" + to_string(rand()));
}
});
t1.join();
t2.join();
Singleton::GetInstance()->Print();
return 0;
}
懒汉模式
如果单例对象构造十分耗时或者占用很多资源,比如加载插件啊, 初始化网络连接啊,读取文件啊等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。 所以这种情况使用 懒汉模式(延迟加载)更好。
懒汉模式
是另一种实现单例模式的方式,与饿汉模式不同的是:懒汉模式是延迟示例化的,即在第一次访问时才创建唯一的实例。
懒汉模式的实现思路是将构造函数私有化,然后提供一个静态私有指针成员来保存唯一实例的地址,并通过一个公共的静态方法来获取该实例。
class Singleton
{
public:
static Singleton& GetInstance()
{
// 双检查加锁
if (_ins == nullptr)
{
_smtx.lock();
if(_ins == nullptr)
_ins = new Singleton;
_smtx.unlock();
}
return *_ins;
}
static void DelInstance()
{
_smtx.lock();
if (_ins) {
delete _ins;
_ins = nullptr;
}
_smtx.unlock();
}
Singleton(const Singleton& s) = delete;
Singleton& operator=(const Singleton& s) = delete;
~Singleton()
{
// 持久化
// 比如要求程序结束时,将数据写到文件,单例对象析构时持久化就比较好
}
private:
// 限制在类外面随意创建对象
Singleton()
{}
static Singleton* _ins;
static mutex _smtx;
};
Singleton* Singleton::_ins = nullptr;
mutex Singleton::_smtx;
懒汉模式下的线程安全问题以及双检查加锁
懒汉模式也引入了新的问题:单例对象的创建线程是不安全的。对于懒汉模式来说,由于其单例对象是在第一次使用时才创建的,那么在多线程模式下,就有可能会存在多个线程并行/并发的去执行 _psins = new Singleton
语句,从而导致前面创建出来单例对象指针被后面的覆盖,最终发生内存泄露。
单例对象的资源释放与保存问题
一般来说单例对象都是不需要考虑释放的,因为不管是饿汉模式还是懒汉模式,单例对象都是全局的,全局资源在程序结束后会被自动回收 (进程退出后OS会解除进程地址空间与物理内存的映射)。但是我们也可以手动对其进行回收。需要注意的是,有时我们需要在回收资源之前将资源的相关数据保存到文件中,这种情况下我们就必须手动回收了。
- 在类中定义一个静态的 DelInstance接口,来回收与保存资源。
static void DelInstance()
{
_smtx.lock();
if (_ins) {
delete _ins;
_ins = nullptr;
}
_smtx.unlock();
}
- 定义一个内部的GC类,通过Singleton类中的一个静态GC类对象,使得程序在结束回收GC对象时自动调用GC类的析构从而完成资源回收与数据保存工作。避免我们忘记调用Dellnstance接口而丢失数据。
例如:
class Singleton
{
public:
static Singleton& GetInstance()
{
if (_ins == nullptr)
{
// 双检查加锁
_smtx.lock();
if(_ins == nullptr)
_ins = new Singleton;
_smtx.unlock();
}
return *_ins;
}
void Add(const string& str)
{
_mtx.lock();
_v.push_back(str);
_mtx.unlock();
}
void Print()
{
_mtx.lock();
for (auto& e : _v)
cout << e << endl;
cout << endl;
_mtx.unlock();
}
static void DelInstance()
{
_smtx.lock();
if (_ins) {
delete _ins;
_ins = nullptr;
}
_smtx.unlock();
}
// 单例对象回收
class GC
{
public:
~GC()
{
DelInstance();
}
};
static GC _gc;
Singleton(const Singleton& s) = delete;
Singleton& operator=(const Singleton& s) = delete;
~Singleton()
{
// 持久化
// 比如要求程序结束时,将数据写到文件,单例对象析构时持久化就比较好
}
private:
// 限制在类外面随意创建对象
Singleton()
{}
vector<string> _v;
static Singleton* _ins;
mutex _mtx;
static mutex _smtx;
};
Singleton* Singleton::_ins = nullptr;
mutex Singleton::_smtx;
Singleton::GC Singleton::_gc;
懒汉模式的一种简单实现方式
class Singleton
{
public:
static Singleton& GetInstance()
{
static Singleton sins;
return sins;
}
Singleton(const Singleton& sin) = delete;
Singleton& operator=(const Singleton& sin) = delete;
private:
Singleton()
{}
};
上面这种实现方式的缺点就是不稳定,因为只有在 C++11 及其之后的标准中局部静态对象的初始化才是线程安全的,而在 C++11 之前的版本中并不能保证。