文章目录
C++ 中比较主要的模式设计:适配器、迭代器、单例(工厂模式、观察者模式…)本篇重点为单例模式。
零、设计模式 六大原则
六大原则:
-
单一职责原则(Single Responsibility Principle)
-
类的职责应该单一,一个方法只做一件事。职责划分清晰了,每次改动到最小单位的方法或类。
网络聊天: 网络通信 & 聊天,应该分割成为网络通信类 & 聊天类。
-
-
开闭原则(Open Closed Principle)
-
对扩展开放,对修改封闭,
超市卖货: 降价商品不是修改商品的原来价格,而是新增促销价格。
-
-
里氏替换原则(Liskov Substitution Principle)
-
通俗点讲,就是只要父类能出现的地⽅,子类就可以出现,而且替换为子类也不会产生任何错误或异常。
-
使用建议:子类必须完全实现父类的方法,孩子类可以有自己的拓展。覆盖或实现父类的方法时,输入参数可以被放大,输出可以缩小
跑步运动员类 --> 会跑步 子类长跑运动员 --> 会跑步且擅长长跑 子类短跑运动员 --> 会跑步且擅长短跑
-
-
依赖倒置原则(Dependence Inversion Principle)
-
高层模块不应该依赖低层模块,两者都应该依赖其抽象。不可分割的原子逻辑就是低层模式,原子逻辑组装成的就是高层模块。
-
模块间依赖通过抽象(接口)发生,具体类之间不直接依赖。
-
使用建议:每个类都尽量有抽象类,任何类都不应该从具体类派生。尽量不要重写基类的方法。结合里氏替换原则使用。
奔驰车司机类 --> 只能开奔驰 司机类 --> 给什么车,就开什么车 开车的人:司机 --> 依赖于抽象
-
-
迪米特法则(Law of Demeter),又叫“最少知道法则”
- 尽量减少对象之间的交互,从而减小类之间的耦合。一个对象应该对其他对象有最少的了解。对类的低耦合提出了明确的要求:
-
只和直接的朋友交流,朋友之间也是有距离的。自己的就是自己的(如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,那就放置在本类中)。
老师让班长点名: 老师给班长一个名单,班长完成点名勾选,返回结果,而不是班长点名,老师勾选。
-
- 尽量减少对象之间的交互,从而减小类之间的耦合。一个对象应该对其他对象有最少的了解。对类的低耦合提出了明确的要求:
-
接口隔离原则(Interface Segregation Principle)
-
客户端不应该依赖它不需要的接口,类间的依赖关系应该建立在最小的接口上
修改密码: 不应该提供修改用户信息接口,更不要暴露数据库操作 就是单一的最小修改密码接口
-
-
总结一下:
- 单一职责原则 告诉我们实现类要职责单一
- 里氏替换原则 告诉我们不要破坏继承体系
- 依赖倒置原则 告诉我们要面向接口编程
- 接口隔离原则 告诉我们在设计接口的时候要精简单一
- 迪米特法则告 诉我们要降低耦合
- 开闭原则 是总纲,告诉我们要对扩展开放,对修改关闭
一、创建一个类,不能被拷贝
拷贝只会发生在两个场景中:拷贝构造函数 以及 赋值运算符重载,因此想要让一个类禁止拷贝,只需让该类不能调用拷贝构造函数以及赋值运算符重载即可。
C++98:
将拷贝构造函数与赋值运算符重载只声明不定义,并且将其访问权限设置为私有即可。
原因:
1. 设置成私有:如果只声明没有设置成 private,用户自己如果在类外定义了,
就不能禁止拷贝了
2. 只声明不定义:不定义是因为该函数根本不会调用,定义了其实也没有什么意义,
不写反而还简单,而且如果定义了就不会防止成员函数内部拷贝了。
C++11:
C++11 扩展 delete 的用法,delete 除了释放 new 申请的资源外,如果在默认成员函数后跟上= delete
,表示让编译器删除掉该默认成员函数。
二、创建一个类,这个类只能在堆上创建对象(只能 new)
思路1:析构函数 设成私有,普通对象就不给创建了,同时建立一个公共函数包装析构函数,需要析构的时候显示调用该函数。
class HeapOnly1
{
public:
void Destroy()
{
delete this;
}
private:
~HeapOnly1()
{
cout << "~HeapOnly()" << endl;
}
int _x;
};
int main1()
{
//HeapOnly1 ho1; // err..私有的析构调不动了,所以不给这样写
//static HeapOnly1 ho2; // err..
HeapOnly1* pho3 = new HeapOnly1;
pho3->Destroy();
return 0;
}
思路2:构造函数 设成私有,并提供一个在堆上创建对象的接口,还需要 delete 掉拷贝和赋值。(不推荐)
class HeapOnly
{
public:
static HeapOnly* CreateObj(int x = 0) // 没有static也不行,创建对象需要这个函数,调这个函数首先又要有个对象,设成静态就好了
{
HeapOnly* p = new HeapOnly(x);
return p;
}
private:
HeapOnly(int x = 0)
:_x(x)
{}
HeapOnly(const HeapOnly& hp) = delete; // 排除拷贝的风险(因为拷贝构造出来的对象还是在栈上)
HeapOnly& operator=(const HeapOnly& hp) = delete; // 赋值也顺便封蛤喽
int _x;
};
int main2()
{
//HeapOnly ho1; // err..
//static HeapOnly ho2; // err..
//HeapOnly* pho3 = new HeapOnly; // err..
HeapOnly* p1 = HeapOnly::CreateObj(1);
//HeapOnly p2(*p1); // err..
return 0;
}
三、创建一个类,这个类只能在栈上创建对象
思路:构造函数 设为私有,并提供一个在栈上创建对象的接口
class StackOnly
{
public:
static StackOnly CreateObj(int x = 0)
{
return StackOnly(x);
}
StackOnly(StackOnly&& st)
:_x(st._x)
{}
private:
StackOnly(int x = 0)
:_x(x)
{}
StackOnly(const StackOnly& st) = delete; // 拷贝封了,正常不能传值返回了,我们显式写一个移动构造,但其实这样也不能防止static对象去调move()
int _x;
};
int main3()
{
/*StackOnly st1;
static StackOnly st2;
StackOnly* st3 = new StackOnly;*/ // err..
StackOnly st1 = StackOnly::CreateObj(1);
//static StackOnly st2 = st1; // err..
//static StackOnly st2 = move(st1); // 防止不了,貌似没有很好的方法
return 0;
}
四、创建一个类,不能被继承
C++98:
构造函数 设成私有,派生类调不到基类的构造函数,无法继承。
C++11:
final
关键字声明,声明的类不能被继承。
五、🔺单例模式(创建一个类,只能创建一个对象)
保证一些数据(一个进程中)全局只有唯一一份,并且方便访问
- 把这些数据放进一个类里,把这个类设计成单例类
- 构造和拷贝构造都封死,提供一个static公有获取单例对象的函数
- 如何创造单例对象,饿汉 or 懒汉
1. 饿汉模式(先创建着,用的时候直接用)
利用 静态成员变量,一开始(在 main 函数之前) 就创建对象,提供一个静态接口,返回创建好的对象的 指针。
实际上是以空间换时间的优化
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();
}
private:
// 限制类外面随意创建对象:构造函数私有化
Singleton()
{}
// 防拷贝
Singleton(const Singleton& s) = delete;
Singleton& operator=(const Singleton& s) = delete;
private:
mutex _mtx;
vector<string> _v;
static Singleton* _ins; // 类内的静态成员,其实只是声明,故必须在类外初始化
};
Singleton* Singleton::_ins = new Singleton;
// 可以调用私有是因为这是类中声明类外定义的问题,本质还是一个类
// new对象不会有线程安全问题,因为是在main函数以前不会出现线程安全问题
int main4()
{
/*Singleton s1;
static Singleton s1;*/
Singleton::GetInstance()->Add("hello Kevin");
Singleton::GetInstance()->Add("hello Stella");
Singleton::GetInstance()->Add("hello Kim");
Singleton::GetInstance()->Print();
return 0;
}
2. 懒汉模式(用的时候再创建)
懒汉,懒加载,是延迟加载的思想。
提供一个静态接口,接口里面对对象进行实例化, 即使用的时候再进行实例化
对象在 第一次访问 实例对象 时创建
🎯version 1: 在 GetInstance 里面判断,第一次调用时创建单例对象。
- 使用 new,加双重锁控制
- 一般全局都要使用单例对象,所以单例对象一般不需要显示释放,如果需要显示释放见代码处(还可以利用内部类的销毁进行释放)。
class Singleton
{
public:
static Singleton* GetInstance() // 每次获取的时候都要加锁解锁
{
// 双检查加锁
if (_ins == nullptr) // 这是为了提高效率,不需要每次获取单例都加锁解锁
{
_imtx.lock();
if (_ins == nullptr) // 保证线程安全和只new一次,这里必须还要检查一次
{
_ins = new Singleton;
}
_imtx.unlock();
}
return _ins;
}
// 一般全局都要使用单例对象,所以单例对象一般不需要显示释放
// 如果有些特殊场景,想显示释放一下,如下:
static void DelInstance()
{
_imtx.lock();
if (_ins)
{
delete _ins;
_ins = nullptr;
}
_imtx.unlock();
}
// 有如果忘记调用 DelInstance 了?
// 定义一个内部类
// 可以保证单例对象回收:
class GC
{
public:
~GC()
{
DelInstance();
}
};
static GC _gc;
void Add(const string& str)
{
_vmtx.lock();
_v.push_back(str);
_vmtx.unlock();
}
void Print()
{
_vmtx.lock();
for (auto& e : _v)
{
cout << e << endl;
}
cout << endl;
_vmtx.unlock();
}
~Singleton()
{
// 持久化
// 比如要求程序结束时,将数据写到文件,单例对象析构时持久化就比较好
}
private:
// 限制类外面随意创建对象
Singleton()
{}
// 防拷贝
Singleton(const Singleton& s) = delete;
Singleton& operator=(const Singleton& s) = delete;
private:
mutex _vmtx; // 成员锁
vector<string> _v;
static Singleton* _ins;
static mutex _imtx; // 单例锁
};
Singleton* Singleton::_ins = nullptr;
mutex Singleton::_imtx; // 锁的初始化不需要显示的给值
Singleton::GC Singleton::_gc;
int main5()
{
// srand(time(0)); // 写在这里的随机数种子,线程里是用不到的,要在线程中各自定义才行
int n = 30;
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();
Singleton::GetInstance();
//Singleton s(*Singleton::GetInstance()); // 此时拷贝不动只是因为有锁,没锁还是可以拷贝的,怎么防止咧?delete拷贝和赋值
return 0;
}
🎯version 2: 直接在 GetInstance 中,用 static 进行初始化
- 在 C++11 之后才能使用,这时 static 才可以保证初始化静态对象的线程安全问题
// 这样写也是可以的,C++11之后才可以,C++11出来之前没法保证这里的线程安全
class Singleton
{
public:
static Singleton* GetInstance()
{
// C++11之前,这里不能保证初始化静态对象的线程安全问题
// C++11之后,这里可以保证初始化静态对象的线程安全问题
static Singleton inst;
return &inst;
}
void Add(const string& str)
{
_vmtx.lock();
_v.push_back(str);
_vmtx.unlock();
}
void Print()
{
_vmtx.lock();
for (auto& e : _v)
{
cout << e << endl;
}
cout << endl;
_vmtx.unlock();
}
~Singleton()
{
// 持久化
// 比如要求程序结束时,将数据写到文件,单例对象析构时持久化就比较好
}
private:
// 限制类外面随意创建对象
Singleton()
{
cout << "Singleton()" << endl;
}
// 防拷贝
Singleton(const Singleton& s) = delete;
Singleton& operator=(const Singleton& s) = delete;
private:
mutex _vmtx; // 成员锁
vector<string> _v;
};
int main()
{
Singleton::GetInstance();
Singleton::GetInstance();
return 0;
}
3. 分析:懒汉和饿汉的优缺点
-
饿汉的缺点:
- 1、如果单例对象初始化很慢(如初始化动作多,还会伴随一些IO行为,如读取配置文件等),main函数之前就要申请会有两个缺点,第一,暂时不需要使用却很占用资源,第二,程序启动会受影响。
- 2、如果两个单例都是饿汉,并且有依赖关系,要求单例1再创建,单例2再创建,饿汉无法控制顺序,懒汉才可以。 饿汉的优点:
- 简单(相对懒汉而言)
懒汉完美的解决了上面饿汉的问题,只是相对复杂一点点
六、工厂模式
厂模式是一种创建型设计模式,它提供了一种 创建对象的最佳方式 。在工厂模式中,我们创建对象时不会对上层暴露创建逻辑,而是通过使用一个共同结构来指向新创建的对象, 以此实现创建、使用的分离。
厂模式可以分为:
1. 简单工厂模式
- 简单工厂模式实现由一个工厂对象通过类型决定创建出来指定产品类的实例。
这个模式的结构和管理产品对象的方式十分简单,但是它的 扩展性非常差,当我们需要新增产品的时候,就需要去修改工厂类新增一个类型的产品创建逻辑,违背了 开闭原则(对拓展开放,对修改关闭)。
#include <iostream>
#include <memory>
using namespace std;
class Fruit
{
public:
virtual void name() = 0;
};
class Apple : public Fruit
{
public:
void name() override
{
cout << "i am an apple" << endl;
}
};
class Banana : public Fruit
{
public:
void name() override
{
cout << "i am a Banana" << endl;
}
};
class FruitFactory
{
public:
static shared_ptr<Fruit> create(const string &name)// 设计类的时候注意和抽象类产生关系而不是具体类
{
if (name == "苹果")
{
return make_shared<Apple>();
}
else if (name == "香蕉")
{
return make_shared<Banana>();
}
else
{
return nullptr;
}
}
};
int main()
{
shared_ptr<Fruit> fruit1 = FruitFactory::create("苹果");
fruit1->name();
shared_ptr<Fruit> fruit2 = FruitFactory::create("香蕉");
fruit2->name();
return 0;
}
2. 工厂方法模式
- 工厂方法模式在简单工厂模式下新增多个厂,多个产品,每个产品对应一个工厂。
优点:
- 减轻了工厂类的负担,将某类产品的生产交给指定的工厂来进行
- 开闭原则遵循较好,添加新产品只需要新增产品的工厂即可,不需要修改原先的工厂类
缺点:
- 工厂方法模式每次增加一个产品时,都需要增加一个具体产品类和工厂类,这会使得系统中类的个数成倍增加,在一定程度上增加了系统的耦合度。
- 对于某种可以形成一组产品族的情况处理较为复杂,需要创建大量的,大量的工厂类
#include <iostream>
#include <memory>
using namespace std;
class Fruit
{
public:
virtual void name() = 0;
};
class Apple : public Fruit
{
public:
void name() override
{
cout << "i am an apple" << endl;
}
};
class Banana : public Fruit
{
public:
void name() override
{
cout << "i am a Banana" << endl;
}
};
class FruitFactory
{
public:
virtual shared_ptr<Fruit> create() = 0;
};
class AppleFactory : public FruitFactory
{
public:
shared_ptr<Fruit> create()
{
return make_shared<Apple>();
}
};
class BananaFactory : public FruitFactory
{
public:
shared_ptr<Fruit> create()
{
return make_shared<Banana>();
}
};
int main()
{
shared_ptr<FruitFactory> ff(new AppleFactory());
shared_ptr<Fruit> fruit = ff->create();
fruit->name();
ff.reset(new BananaFactory());
fruit = ff->create();
fruit->name();
return 0;
}
3. 抽象工厂模式
工厂方法模式,通过引入工厂等级结构,解决了简单工厂模式中工厂类职责太重的问题,但由于工厂方法模式中的每个工厂只生产一类产品,可能会导致系统中存在大量的工厂类,势必会增加系统的开销。
若我们 将一些相关的产品组成一个产品族(位于不同产品等级结构中功能相关联的产品组成的家族),由同一个厂来统一生产,这就是抽象工厂模式的基本思想。
虽然整合了同类,但实际上也违背了开闭原则,三种工厂模式,需要结合实际情况去使用。
#include <iostream>
#include <memory>
using namespace std;
class Fruit
{
public:
virtual void name() = 0;
};
class Apple : public Fruit
{
public:
void name() override
{
cout << "i am an apple" << endl;
}
};
class Banana : public Fruit
{
public:
void name() override
{
cout << "i am a Banana" << endl;
}
};
class Animal
{
public:
virtual void name() = 0;
};
class Lamp : public Animal
{
public:
void name() override
{
cout << "我是一只山羊" << endl;
}
};
class Dog : public Animal
{
public:
void name() override
{
cout << "我是一只修勾" << endl;
}
};
class Factory
{
public:
virtual shared_ptr<Fruit> getFruit(const string &name) = 0;
virtual shared_ptr<Animal> getAnimal(const string &name) = 0;
};
class FruitFactory : public Factory
{
public:
shared_ptr<Fruit> getFruit(const string &name)
{
if (name == "苹果")
{
return make_shared<Apple>();
}
else if (name == "香蕉")
{
return make_shared<Banana>();
}
else
{
return nullptr;
}
}
shared_ptr<Animal> getAnimal(const string &name)
{
return shared_ptr<Animal>();
}
};
class AnimalFactory : public Factory
{
public:
shared_ptr<Animal> getAnimal(const string &name)
{
if (name == "山羊")
{
return make_shared<Lamp>();
}
else if (name == "小狗")
{
return make_shared<Dog>();
}
else
{
return nullptr;
}
}
shared_ptr<Fruit> getFruit(const string &name)
{
return shared_ptr<Fruit>();
}
};
class FactoryProducer
{
public:
static shared_ptr<Factory> create(const string &name) // static 后就可以不用实例化对象了
{
if (name == "水果")
{
return make_shared<FruitFactory>();
}
else if (name == "动物")
{
return make_shared<AnimalFactory>();
}
else
{
return nullptr;
}
}
};
int main()
{
shared_ptr<Factory> ff = FactoryProducer::create("水果"); // ff 是水果工厂
shared_ptr<Fruit> fruit = ff->getFruit("苹果");
fruit->name();
fruit = ff->getFruit("香蕉");
fruit->name();
shared_ptr<Factory> af = FactoryProducer::create("动物"); // af 是动物工厂
shared_ptr<Animal> animal = af->getAnimal("山羊");
animal->name();
animal = af->getAnimal("小狗");
animal->name();
return 0;
}
七、建造者模式
- 适用于复杂对象的建造
当构造一个对象时,还需要构造很多别的对象作为参数才能成立,对使用者来说编写成本很大。此时就可以用建造者模式,降低使用者的构造成本。
#include <iostream>
#include <string>
#include <memory>
using namespace std;
// 电脑类
class Computer
{
public:
Computer() {}
void setBoard(const string &board)
{
_board = board;
}
void setDisplay(const string &display)
{
_display = display;
}
virtual void setOs() = 0; // 不需要实现的纯虚函数,记得 = 0
void showParamaters()
{
string param = "Computer Paramaters:\n";
param += "\tBoard" + _board + "\n";
param += "\tDisplay" + _display + "\n";
param += "\tOS" + _os + "\n";
cout << param << endl;
}
protected:
string _board;
string _display;
string _os;
};
// mac电脑类
class MacBook : public Computer // 抽象产品类
{
public:
void setOs() override
{
_os = "Mac OS x12";
}
};
// 建造者类
class Builder // 建造者类就是创建好零件,最后给出对象
{
public:
virtual void buildBoard(const string &board) = 0;
virtual void buildDisplay(const string &display) = 0;
virtual void buildOs() = 0;
virtual shared_ptr<Computer> build() = 0;
};
// 造具体产品的建造者
class MacBookBuilder : public Builder
{
public:
MacBookBuilder() : _computer(new MacBook())
{}
void buildBoard(const string &board)
{
_computer->setBoard(board);
}
void buildDisplay(const string &dispaly)
{
_computer->setDisplay(dispaly);
}
void buildOs()
{
_computer->setOs();
}
shared_ptr<Computer> build()
{
return _computer;
}
private:
shared_ptr<Computer> _computer;
};
// 应对各个零部件可能有建造顺序,需要一个指挥者
class Director
{
public:
Director(Builder *builder)
:_builder(builder)
{}
void construct(const string &board, const string &display)
{
_builder->buildBoard(board); // 顺序安装
_builder->buildDisplay(display);
_builder->buildOs();
}
private:
shared_ptr<Builder> _builder;
};
int main()
{
Builder *Builder = new MacBookBuilder();
unique_ptr<Director> director(new Director(Builder));
director->construct("华硕主板","三星显示器");
shared_ptr<Computer> computer = Builder->build();
computer->showParamaters();
return 0;
}
八、代理模式
代理模式指代理对象控制对原对象的引用。 在某些情况下,一个对象不适合或者不能直接被引用访问,而代理对象可以在客户端和目标对象之间起到中介的作用。
代理模式的结构包括一个是真正的你要访问的对象(目标类)、一个是代理对象。目标对象与代理对象实现同一个接口,先访问代理类再通过代理类访问目标对象。代理模式分为静态代理、动态代理:
- 静态代理指的是, 在编译时就已经确定好了代理类和被代理类的关系。也就是说,在编译时就已经确定了代理类要代理的是哪个被代理类。
- 动态代理指的是,在运行时才动态生成代理类,并将其与被代理类绑定。这意味着,在运行时才能确定代理类要代理的是哪个被代理类。
以租房为例,租客租房,中间经过房屋中介向房东租房。通过代理模式实现:
#include <iostream>
using namespace std;
class RentHouse
{
public:
virtual void rentHouse() = 0;
};
class LandLord : public RentHouse
{
public:
void rentHouse()
{
cout << "将房子租出去" << endl;
}
};
class Intermediary : public RentHouse
{
public:
void rentHouse()
{
cout << "发布招租启示" << endl;
cout << "带人看房" << endl;
_landLord.rentHouse();
cout << "负责租后维修" << endl;
}
private:
LandLord _landLord;
};
// 代理模式
int main()
{
Intermediary intermediary;
intermediary.rentHouse();
return 0;
}