目录
C++—特殊类设计&设计模式
1.设计模式
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。
为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打仗时都是人拼人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后来孙子就总结出了《孙子兵法》。孙子兵法也是类似
之前已经接触过一些设计模式了
比如:
- 迭代器模式
- 适配器模式
还有一些设计模式——工厂模式,装饰器模式,观察者模式,单例模式、
2.特殊类设计
下面是一些常见的特殊类设计
2.1设计一个无法被拷贝的类
拷贝只会放生在两个场景中:拷贝构造函数以及赋值运算符重载,因此想要让一个类禁止拷贝,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。
这个前面在异常的时候其实就说过了,两种方式,分别是c++98和c++11提供的
- c++11的方法:
C++11扩展delete的用法,delete除了释放new申请的资源外,如果在默认成员函数后跟上=delete,表示让编译器删除掉该默认成员函数。
class NotCopy
{
// 其实就是将拷贝构造和赋值运算符重载给禁掉就行了,之前有讲过
// 在c++98就是将之声明不定义,然后将两个成员函数弄成私有的,这样外面就无法使用者两个函数了
// 在c++11可以使用delete关键字
public:
NotCopy(const NotCopy& n) = delete;
NotCopy& operator=(const NotCopy& n) = delete;
private:
int _a;
};
- c++98的方法:
class NotCopy
{
// 在c++98就是将之声明不定义,然后将两个成员函数弄成私有的,这样外面就无法使用者两个函数了
// 在c++11可以使用delete关键字
public:
private:
NotCopy(const NotCopy& n);
NotCopy& operator=(const NotCopy& n);
int _a;
};
- 原因:
- 设置成私有:如果只声明没有设置成private,用户自己如果在类外定义了,就可以不
能禁止拷贝了
- 只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,不写反而还简单,而且如果定义了就不会防止成员函数内部拷贝了。
2.2设计一个只能在堆上创建对象的类
实现方式:
将类的构造函数私有,拷贝构造声明成私有。防止别人调用拷贝在栈上生成对象。
提供一个静态的成员函数,在该静态成员函数中完成堆对象的创建
//2.设计一个只能在堆上创建对象的类
class OnlyHead
{
public:
static OnlyHead* GetObj()
{
return new OnlyHead;
// new OnlyHead和new OnlyHead()的区别
// 就是第一个是隐式调用默认构造函数
// 第二个是显式调用默认构造函数
}
// 除了构造函数不能给外面直接调用,拷贝构造和赋值运算符重载也要禁掉
OnlyHead(const OnlyHead& o) = delete;
OnlyHead& operator=(const OnlyHead& o) = delete;
private:
// 在栈上定义的对象一定会调用默认构造函数,所以直接弄成私有的
OnlyHead()
{}
};
int main()
{
//OnlyHead hp;
//OnlyHead* p = new OnlyHead;
OnlyHead* p = OnlyHead::GetObj();
// 如果不释放p就会内存泄漏
shared_ptr<OnlyHead> sp(OnlyHead::GetObj()); //交给智能指针
//OnlyHead copy(*sp); // 拷贝构造要禁掉
return 0;
}
2.3设计一个只能在栈上创建对象的类
可以用2.2的思路,先将构造私有,然后直接给一个类内静态方法
也可以将new直接重载,直接禁掉
// 3.设计一个只能在栈上创建对象的类
class OnlyStack
{
public:
// 第一种思路: 直接不给new,重载new【这个思路无法阻止静态区的对象的创建】
void* operator new(size_t size) = delete;
// 第二种思路:将构造函数设置为私有的,然后提供一个static方法给外部调用
// 这个思路不能禁掉拷贝构造,因为getobj返回值这里会有一次拷贝构造
//【这个思路无法阻止静态区的对象的创建】
static OnlyStack getobj()
{
return OnlyStack(); //类里面可以调用私有的构造函数
}
private:
OnlyStack()
{}
};
int main()
{
OnlyStack* p = new OnlyStack;
OnlyStack* p = new OnlyStack();
OnlyStack o = OnlyStack::getobj(); //创建栈上的对象,注意这个思路不能禁掉拷贝构造
// 这里实际上调用了拷贝构造
static OnlyStack os = OnlyStack::getobj();
return 0;
}
2.4设计一个类,无法被继承
这个简单,就不仔细说了
// 4.设计一个类,无法被继承
class NoInherit final
{
// 两个思路,c++98:直接让构造函数私有化,因为子类的构造需要调用父类的构造
// c++11:final关键字
};
2.5设计一个类。这个类只能创建一个对象【单例模式】
2.5.1懒汉模式实现
单例模式:一个类在全局(进程)中只能有一个实例对象
// 5.设计一个类。这个类只能创建一个对象【单例模式】
class Singleton
{
public:
static Singleton* GetInstance()
{
if (_p == nullptr)
{
_p = new Singleton;
}
return _p;
}
// 在这个情况下,拷贝构造也要禁掉
Singleton(const Singleton& s) = delete;
private:
Singleton()
{}
// 需要再创建一个变量,来控制对象只能存在一个
// 其实就是弄一个所有对象共用的变量,只要该变量标记了,就说明已经有一个对象了,那就不能再调用构造函数了
static Singleton* _p;
};
Singleton* Singleton::_p = nullptr; //初始化
int main()
{
Singleton* s1 = Singleton::GetInstance();
Singleton* s2 = Singleton::GetInstance();
Singleton* s3 = Singleton::GetInstance();
// 这里三个获取的都是同一个对象
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
//Singleton s4(Singleton::GetInstance());//拷贝构造要禁掉
return 0;
}
但是上面这个代码是有问题的,这个单例模式会有线程安全问题,因为当多线程操作的时候,这里有一个公共资源,即临界资源——_p,因此就会出现线程安全问题。可能会创建出多个对象。这样单例模式就被破坏了
因此对临界资源_p加锁即可
#include<mutex>
class Singleton
{
public:
static Singleton* GetInstance()
{
_mtx.lock();
if (_p == nullptr)
{
_p = new Singleton;
}
_mtx.unlock();
return _p;
}
// 在这个情况下,拷贝构造也要禁掉
Singleton(const Singleton& s) = delete;
private:
Singleton()
{}
// 需要再创建一个变量,来控制对象只能存在一个
// 其实就是弄一个所有对象共用的变量,只要该变量标记了,就说明已经有一个对象了,那就不能再调用构造函数了
static Singleton* _p;
// _p是一个临界资源,要保护它
static mutex _mtx; //为了线程安全,加一个锁
};
Singleton* Singleton::_p = nullptr; //初始化
mutex Singleton::_mtx; //定义, 类里面的只是声明
这个代码就不会出现线程安全问题。
但是这个代码还是有点问题,因为虽然对临界区加锁了,但是临界区内有可能发生异常,new可能会抛异常,一旦抛异常就会造成死锁,因为这里应该用RAII思想来解决
#include<mutex>
class Singleton
{
public:
static Singleton* GetInstance()
{
//_mtx.lock();
{ //这个花括号是为了控制unique_lock的生命周期,不影响临界区后面的代码
unique_lock<mutex> lock(_mtx);
if (_p == nullptr)
{
_p = new Singleton;
}
}
//_mtx.unlock();
return _p;
}
// 在这个情况下,拷贝构造也要禁掉
Singleton(const Singleton& s) = delete;
private:
Singleton()
{}
// 需要再创建一个变量,来控制对象只能存在一个
// 其实就是弄一个所有对象共用的变量,只要该变量标记了,就说明已经有一个对象了,那就不能再调用构造函数了
static Singleton* _p;
// _p是一个临界资源,要保护它
static mutex _mtx; //为了线程安全,加一个锁
};
Singleton* Singleton::_p = nullptr; //初始化
mutex Singleton::_mtx; //定义, 类里面的只是声明
上述代码仍然有一些缺陷,可以继续优化。【即,我们只需要对第一次访问临界资源_p进行加锁保护即可,因为后面不会再有修改临界资源的情况出现】
这里做一个经典的双检查
static Singleton* GetInstance()
{
//_mtx.lock();
if(_p == nullptr) //这里是一个双检查
{
unique_lock<mutex> lock(_mtx);
if (_p == nullptr)
{
_p = new Singleton;
}
} //并且出了这里,unique_lock<mutex>的生命周期就结束了
//_mtx.unlock();
return _p;
}
下面是完整的代码:【这里是一个懒汉模式的单例模式】
#include<mutex>
class Singleton
{
public:
static Singleton* GetInstance()
{
//_mtx.lock();
if(_p == nullptr) //这里是一个双检查
{
unique_lock<mutex> lock(_mtx);
if (_p == nullptr)
{
_p = new Singleton;
}
} //并且出了这里,unique_lock<mutex>的生命周期就结束了
//_mtx.unlock();
return _p;
}
// 在这个情况下,拷贝构造也要禁掉
Singleton(const Singleton& s) = delete;
// 释放资源
static void DelInstance()
{
unique_lock<mutex> lock(_mtx);
delete _p;
_p = nullptr;
}
private:
Singleton()
{}
// 需要再创建一个变量,来控制对象只能存在一个
// 其实就是弄一个所有对象共用的变量,只要该变量标记了,就说明已经有一个对象了,那就不能再调用构造函数了
static Singleton* _p;
// _p是一个临界资源,要保护它
static mutex _mtx; //为了线程安全,加一个锁
};
Singleton* Singleton::_p = nullptr; //初始化
mutex Singleton::_mtx; //定义, 类里面的只是声明
如果想程序在结束之后,自动释放单例对象,可以引入尝试下面这个代码的思路
// 释放资源
static void DelInstance()
{
// 这种思路下,不需要加锁了。因为_mtx锁可能已经释放了
// 并且main函数都结束了,主线程结束了,其他线程肯定都结束了,不存在多线程的场景了
delete _p;
_p = nullptr;
}
// 如果手动释放单例对象,可以手动调用DelInstance()
// 如果想在程序结束之后自动释放单例对象,可以引入一个小机制,利用生命周期
class GC
{
public:
~GC()
{
Singleton::DelInstance();
}
};
// 这里定义一个gc的静态变量,在当前程序结束之后,就会调用DelInstance()来释放单例对象
static GC gc;
2.5.2饿汉模式实现
class Singleton
{
public:
static Singleton* GetInstance()
{
return &_inst;
}
Singleton(const Singleton& s) = delete;
private:
Singleton()
{}
static Singleton _inst;
};
Singleton Singleton::_inst; //在main函数之前就在静态区把唯一的对象创建好了, 不存在线程安全的问题了
2.5.3懒汉的饿汉的区别
懒汉模式需要考虑线程安全和内存泄漏的问题,实现相对更复杂。饿汉就不需要考虑这些问题,实现起来也相对简单、
懒汉模式是在需要的时候才创建对象并初始化,相对来说不会影响程序的进行,而饿汉模式是一开始就创建对象并初始化,如果代码量大,就会导致程序运行缓慢,影响用户体验
饿汉模式无法保证对象的创建顺序,比如有多个单例类,并且有依赖关系(B依赖A), 要求先创建A,在创建B,那就不能用饿汉模式,要用懒汉
如果在构造函数中有动态库链接,和创建线程(要链接线程库),那么就不能用饿汉模式。因为饿汉在main函数之前就初始化了,这个时候不知道库是否链接了,如果没有链接就会崩