【同步/异步 日志系统】 --- 前置技术
Welcome to 9ilk's Code World
(๑•́ ₃ •̀๑) 个人主页: 9ilk
(๑•́ ₃ •̀๑) 文章专栏: 项目
本篇博客主要对项目中涉及到的一些前置技术进行介绍,主要是不定参函数的多种实现,以及几种设计模式,单例/工厂/建造者/代理模式的简单讲解。
不定参函数
我们日志系统是支持不定格式进行日志输出的,因此我们需要进行不定参的学习。
不定参宏函数
在初学C语言的时候,我们都用过printf
函数进行打印。其中printf
函数就是一个不定参函数,在函数内部可以根据格式化字符串中格式化字符分别获取不同的参数进行数据的格式化。如果我们想使用printf
打印的时候明确是哪一文件哪一行,我们可以使用预定义符号__FILE__
以__LINE__
, 但是每次都需要指定格式化字符串[%s-%d]
,此时我们可以使用不定参宏函数来简化我们操作:
#define LOG(format,...) printf("[%s-%d]"format,__FILE__,__LINE__,__VA_ARGS__);
...
表示变长参数部分。__VA_ARGS__
用于引用这些变长的参数 。
上面的宏函数封装存在的一个问题是:当变长宏的参数为空时,使用__VA_ARGS__
可能会导致语法错误,比如打印 LOG("hello world"); 会出现下面的结果:
可以看到,由于我们没有格式化字符串中没有格式化字符,此时__VA_ARGS__被替换为空,但是多了个逗号,这样是会报错的。
为了解决这个问题,我们可以给__VA_ARGS__
前面加上##
,##
可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符 , 在这里可以用于在变长参数为空时移除逗号。这样我们就完整的实现了一个不定参的宏函数。
#define LOG(fmt,...) printf("[%s-%d] " fmt "\n",__FILE__,__LINE__,##__VA_ARGS__)
C语言不定参函数
C语言提供了如下几个用于支持可变参数函数的标准宏:
#include <stdarg.h>void va_start(va_list ap, last);
type va_arg(va_list ap, type);
void va_end(va_list ap);
void va_copy(va_list dest, va_list src);
们写一个demo
来进行不定参数访问整形数据并了解这几个宏的用法:
#include<stdarg.h>void printfNum(int count,...) //count表示可变参数个数{va_list arg;va_start(arg,count);for(int i = 0 ; i < count ; i++ ){int curr = va_arg(arg,int);printf("param[%d]:%d\n",i,curr);} va_end(ap);}int main(){ printfNum(5,111,222,333,444,555);return 0;}
va_list arg
: 定义可以访问可变参数部分的变量,其实是一个char*
类型va_satrt(arg,count):
使arg
指向可变参数部分,即指向count之后。va_arg(arg,int):
根据类型获取可变参数列表中的第一个数据,根据类型决定获取多少字节。va_end():
arg
使用完毕,本质是将arg
指向NULL。
打印结果如下:
上面的demo中我们的可变参数列表是一个个int类型,如果要取出不同类型的数据,我们可以使用vasprintf
这个函数 , 这个函数可以根据指定的格式化字符串(告诉编译器后面分别是什么参数类型,取几个字节数据)取出一个个参数进行数据组织然后放到申请的空间里,注意这块空间需要记得释放,否则会造成内存泄漏。
#include <stdio.h>
int vasprintf(char **strp, const char *fmt, va_list ap);RETURN VALUEWhen successful, these functions return the number of bytes printed, just like sprintf(3). If memory allocation wasn't possible, or some other error oc‐curs, these functions will return -1, and the contents of strp are undefined.
//失败返回-1,成功返回字符串的长度
了解了这个函数之后,我们可以根据这个函数进行封装模拟实现一个myprintf:
void myprintf(const char* fmt,...)
{va_list arg;va_start(arg,fmt);char* res;int ret = vasprintf(&res,fmt,arg);if(ret != -1){printf(res);printf("\n");free(res); //记得释放!!!}va_end(arg);
}
C++不定参函数
我们先了解几个语法:
template<typename T,typename ...Args>
void xprintf(Args&&... args)
{}
- 模板参数
Args
前面有省略号,代表它是一个可变模板参数,我们把带省略号的参数称为参数包,参数包里面可以包含0到N(N>=0)个模板参数,args
则是一个函数形参参数包。
sizeof ...(args)
- 我们可以使用
sizeof ...()
来获取参数包中参数个数 - 但是我们无法直接获取参数包中的每个参数,只能通过展开参数包的方式来获取 , 这是使用可变参数模板的一个主要特点 , 语法并不支持使用
args[i]
的方式来获取参数包中的参数。
因此要获取参数包中的各个参数,只能通过展开参数包的方式来获取,一般我们会通过递归或逗号表达式来展开参数包,这里我们使用递归展开参数包的方式,递归展开参数包的方式如下:
- 给函数模板增加一个模板参数,这样就可以从接收到的参数包中分离出一个参数。
- 在函数模板中递归调用该函数模板,调用时传入剩下的参数包。
- 如此递归下去,每次分离出参数包中的每一个参数,直到参数包中的所有参数都被取出来。
- 递归到最后传入参数包中参数个数为0时,我们需要设置一个无参的递归终止函数来结束递归
void xprintf()
{cout << "调用xprintf()"<< endl;
}template<typename T ,typename ...Args>
void xprintf(const T& val,Args &&...args)
{cout << val << endl;if(sizeof ...(args) > 0)xprintf(std::forward<Args>(args)...);else cout << endl;
}int main()
{xprintf(1,2,3);return 0;
}
Q:能不能不写这个无参的递归终止函数呢?最后参数包个数为0的时候,不是为进行else分支打印换行吗?比如按下面代码的更改是不行的。
#include<iostream>
using namespace std;template<typename T ,typename ...Args>
void xprintf(const T& val,Args &&...args)
{cout << val << endl;if(sizeof ...(args) > 0)xprintf(std::forward<Args>(args)...);else cout << endl; //或写成xprintf()
}int main()
{xprintf(1,2,3);return 0;
}
主要原因如下:
- 函数模板并不能被调用,函数模板需要在编译时根据传入的参数类型进行推演,生成对应的函数,这个生成的函数才能够被调用。
- 这个推演过程是在编译时进行的,当推演到参数包中args中参数个数为0时,还需要将当前函数推演完,这时就会继续推演传入0个参数时的
xpintf
函数,此时就会报错,因为xprintf()
函数在上述代码中至少要求传入一个参数。 - 而
if
判断是在运行时(即编译完成后)才走的逻辑,也就是运行时逻辑,而函数模板是一个编译时逻辑。
设计模式
设计模式好似孙子兵法 , 是前辈们对代码开发经验的总结 , 是解决特定问题的一系列套路. 它不是语法规定 , 而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案。设计模式主要遵循下面的六大原则:
- 单一职责原则:类的职责应该单一,一个方法只做一件事。职责划分清晰了,每次改动到最小单位的方法或类。
- 开闭原则:对扩展开放,对修改封闭。
- 里氏替换原则:通俗点讲,就是只要父类能出现的地方,子类就可以出现,而且替换为子类也不会产生任何错误或异常,即不要破坏继承体系。
- 依赖倒置原则:高层模块不应该依赖低层模块,两者都应该依赖其抽象.不可分割的原子逻辑就是低层模式,原子逻辑组装成的就是高层模块。即面向接口编程。
- 迪米特法则(最少知道法则):尽量减少对象之间的交互,从而减小类之间的耦合。一个对象应该该对其他对象有最少的了解。即要降低耦合。
- 接口隔离原则:客户端不应该依赖它不需要的接口,类间的依赖关系应该建立在最小的接口上。即设计接口的时候要精简单一。
单例模式
一个类只能创建一个对象,即单例对象。它能保证系统中该类中只有一个实例 , 并提供一个访问它的全局访问点 , 该实例被所有程序模块共享。比如在服务器程序中,常将服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务器进程中的其他对象再通过这个单例对象获取这些配置信息,这种做法有效简化了在复杂环境下的配置管理。
单例模式有两种模式,分别是饿汉模式和懒汉模式。
1. 饿汉模式:做法是在程序启动时就创建一个唯一的实例对象,这种做法在类加载的时候就立即创建实例,省去了运行时判断和同步的开销,是一种以空间换时间的方式。这种模式由于单例对象已经确定,因此比较适用于多线程环境,多线程获取单例对象时不需要加锁,可以有效避免资源竞争,提高性能。缺点是会增加程序初始化的时间。
为了保证类在类外实例化对象且存在唯一实例对象,我们需要禁用它的拷贝构造和私有化构造函数,同时在类内私有的静态对象,不要忘记在类外进行定义。公有接口只提供GetInstance
供外界访问唯一实例。
class Singleton
{
private:static Singleton _eton;//类内静态成员Singleton():_data(99){cout << "获取单例..." << endl;}~Singleton(){};Singleton(const Singleton& sig) = delete;
private:int _data;
public:static Singleton& GetInstance(){return _eton;} int GetData(){return _data;}
};Singleton Singleton::_eton; //类外真正定义类内静态成员int main()
{// cout << Singleton::GetInstance().GetData() << endl;return 0;
}
2. 懒汉模式
饿汉模式在程序初始化的时候就实例化出对象,但这个唯一实例对象可能在整个过程中没有使用到,会白白浪费内存空间。而懒汉模式采用懒加载的思想解决这个问题,即定义一个静态资源指针,用的时候再去进行new一个实例化对象。
但是这种做法是存在线程安全的,有可能多个线程同时发现指针为null,同时去new多个对象,不满足单例的要求,此时我们需要进行加锁来保证多线程情况下只调用一次new。
//效率较低
static T* GetInstance(){lock.lock(); // 使⽤互斥锁, 保证多线程情况下也只调⽤⼀次 new.if (inst == NULL) {inst = new T();} lock.unlock();return inst;}
这样难免会造成锁冲突效率较低,即使已经有线程new出对象了,后面都要进行加锁解锁访问同步代码,,因此进一步要采用double check
的方式来降低锁冲突的概率以提高性能,即两层判断静态资源指针是否为空
,在第一次new出对象之后,往后获取就直接返回指针了。
static T* GetInstance(){if (inst == NULL) { // 双重判定空指针, 降低锁冲突的概率, 提⾼性能.lock.lock(); // 使⽤互斥锁, 保证多线程情况下也只调⽤⼀次 new.if (inst == NULL) {inst = new T();} lock.unlock();} return inst;}
还存在的一个问题是由于单例对象是被多个线程共享的 , 如果编译器对其进行优化使用CPU缓存 , 此时用的可能是过时的了,此时我们可以使用设置volatile关键字让每次都去内存找,拿最新的:
template <typename T>
class Singleton
{volatile static T* inst; // 需要设置 volatile 关键字, 否则可能被编译器优化static std::mutex lock;
public:static T* GetInstance(){if (inst == NULL) { // 双重判定空指针, 降低锁冲突的概率, 提⾼性能.lock.lock(); // 使⽤互斥锁, 保证多线程情况下也只调⽤⼀次 new.if (inst == NULL) {inst = new T();} lock.unlock();} return inst;}};
C++11静态局部变量实现单例: Static local variables
特性以确保C++11起,静态变量将能够在满足thread-safe(线程安全)的前提下唯一地被构造和析构,从 C++11 开始,函数体内的静态局部变量的初始化由编译器自动加锁,多线程同时到达时也只会初始化一次。。局部静态对象在程序执行期间会一直存在 , 它们在第一次调用时初始化,并在程序结束时销毁 , 局部静态对象在作用域外仍然存在,直到程序终止 , 因此第一个线程调用时就初始化它 , 后面多个其他线程就不再初始化了 , 同时这个过程是线程安全的。
//懒汉模式C++11静态对象版
class Singleton
{
private:Singleton():_data(99){cout << "获取单例..." << endl;}~Singleton(){};Singleton(const Singleton& sig) = delete;
private:int _data;
public: static Singleton& GetInstance(){static Singleton _eton;return _eton;} int GetData(){return _data;}};
现代C++线程安全懒汉单例实现:
template <typename T>
class Singleton {
protected:Singleton() = default;Singleton(const Singleton<T>&) = delete;Singleton& operator=(const Singleton<T>& st) = delete;static std::shared_ptr<T> _instance;
public:static std::shared_ptr<T> GetInstance() {static std::once_flag s_flag;std::call_once(s_flag, [&]() {_instance = std::shared_ptr<T>(new T);});return _instance;}void PrintAddress() {std::cout << _instance.get() << std::endl;}~Singleton() {std::cout << "this is singleton destruct" << std::endl;}
};template <typename T>
std::shared_ptr<T> Singleton<T>::_instance = nullptr;
-
从 C++11 开始,函数体内的静态局部变量(这里是
s_flag
)的初始化由编译器自动加锁,多线程同时到达时也只会初始化一次。 -
once_flag
相当于是std::call_once
的“已执行标记”,让多线程同时调用时只初始化一次单例。 -
第一次调用call_once之前s_flag是false,在内部置为true;后续s_flag由于是静态变量,因此后续调用该函数都是true,因此不会再调用call_once。只有一个线程能拿到“第一次”机会去执行 lambda;其余线程在 lock 处阻塞,直到 lambda 完成并被标记为已执行;之后这些线程再进来时,flag.done == true,直接跳过,不再进入 lambda。
工厂模式
工厂模式是一种创建型设计模式,它提供了一种创建对象的最佳方式。在工厂模式中,我们创建对象时不会对上层暴露创建逻辑 , 而是通过使用一个共同结构来指向新创建的对象 , 以此实现创建-使用的分离。
工厂模式可以分为简单工厂模式、工厂方法模式、抽象工厂模式。
简单工厂模式
简单工厂模式实现由一个工厂对象通过类型决定创建出来指定产品类的实例。假设有个工厂能生产出水果,当客户需要产品的时候明确告知工厂生产哪类水果,工厂需要接收用户提供的类别信息,当新增产品的时候,工厂内部去添加新产品的生产方式。
假设当前有如下几种水果类:Fruit是基类,派生出苹果和香蕉类。
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;}
};
我们可以生产这些水果的时候统一通过FruitFactory
这个工厂类进行生产,用户只需指明要生产的产品类别给工厂即可,工厂内部通过类别生成然后统一返回基类Fruit指针:
class FruitFactory
{
public:static std::shared_ptr<Fruit> CreateFruit(const string& product){if(product == "苹果")return make_shared<Apple>();elsereturn make_shared<Banana>(); } };int main()
{shared_ptr<Fruit> f = FruitFactory::CreateFruit("苹果");f->name();f = FruitFactory::CreateFruit("香蕉");f->name();return 0;
}
总结一下
简单工厂模式:在一个统一的大工厂可以通过参数控制生产任何产品。
- 优点:简单粗暴,直观易懂。使用一个工厂生产同一等级结构下的任意产品。
- 缺点:a. 所有东西生产在一起,产品太多会导致代码量庞大。 b. 开闭原则(开放拓展,关闭修改)不遵循,如果需要新增产品就必须修改工厂方法(比如上面代码中的
CreateFruit()
)
工厂方法模式
在简单工厂模式下新增多个工厂,多个产品,每个产品对应一个工厂。假设现在有A、B两种产品,则开两个工厂,工厂A负责生产产品A,工厂B负责生产产品B,用户只知道产品的工厂名,而不知道具体的产品信息,工厂不需要再接收客户的产品类型,而只负责生产产品,用户需要什么产品只需要声明产品对应的工厂即可,由不同工厂生产出不同产品。
/* 工厂方法模式 */
class FruitFactory
{
public:virtual std::shared_ptr<Fruit> create() = 0;
};class AppleFactory : public FruitFactory
{
public:std::shared_ptr<Fruit> create() override{return make_shared<Apple>();}
};class BananaFactory : public FruitFactory
{
public: std::shared_ptr<Fruit> create() override{return make_shared<Banana>();}
};int main()
{shared_ptr<FruitFactory> ff(new AppleFactory());shared_ptr<Fruit> f = ff->create();f->name();ff.reset(new BananaFactory()); //reset方法提供了重新绑定指针的方式,同时释放原有指针指向的资源f = ff->create();f->name();return 0;
}
总结一下
工厂方法定义了一个创建对象的接口 , 但是由子类来决定创建哪种对象,使用多个工厂分别生产指定的固定产品。
-
优点:a. 减轻了工厂类的负担,将某类产品的生产交给指定的工厂来进行。b. 开闭原则遵循较好,添加新产品只需增加新产品的工厂即可,不需要修改原先的工厂类。
-
缺点:对于某种可以形成一组产品族的情况处理较为复杂,需要创建大量的工厂类。工厂方法模式每次增加一个产品时,都需要增加一个产品类和工厂类,这会使得系统中类的个数成倍增加,在一定程度上增加系统耦合度。
抽象工厂模式
工厂方法模式通过引入工厂等级结构,解决了简单工厂模式中工厂类职责太重(所有产品都由这一个工厂生产)的问题,但由于工厂方法模式中的每个工厂只生产一类产品,可能会导致系统中存在大量的工厂类,势必会增加系统开销。此时,我们可以考虑将一些相关的产品组成一个产品族(位于不同产品等级结构中功能相关联的产品组成的家族),由同一个工厂来统一生产,这就是抽象工厂模式的基本思想。
假设现在有动物类别和水果类别:
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 Dog : public Animal
{
public:void name() override{cout << "I am a dog" << endl;}
};class Cat : public Animal
{
public:void name() override{cout << "I am a cat" << endl;}
};
对于水果类,我们使用水果工厂生产对应产品;对于动物,我们统一使用动物工厂:基类Factory提供了不同产品组工厂生产的接口,然后不同派生类重写对应的接口即可,如下的getFruit和getAnimal。
class Factory
{
public:virtual std::shared_ptr<Fruit> getFruit(const string& fruit) = 0;virtual std::shared_ptr<Animal> getAnimal(const string& animal) = 0;
};class FruitFactory : public Factory
{
public:virtual std::shared_ptr<Fruit> getFruit(const string& fruit){if(fruit == "苹果") return make_shared<Apple>();else return make_shared<Banana>();}virtual std::shared_ptr<Animal> getAnimal(const string& animal){return shared_ptr<Animal();>}
};class AniamlFactory : public Factory
{
public:virtual std::shared_ptr<Fruit> getFruit(const string& fruit){return shared_ptr<Fruit>();}virtual std::shared_ptr<Animal> getAnimal(const string& animal){if(animal == "小狗") return make_shared<Dog>();else return make_shared<Cat>();}
};int main()
{//生产水果:shared_ptr<Factory> ff(new FruitFactory());shared_ptr<Fruit> fruit = ff->getFruit("苹果");fruit->name();//生产动物shared_ptr<Factory> af(new AniamlFactory());shared_ptr<Animal> animal = af->getAnimal("小狗");animal->name();return 0;
}
总结一下
抽象工厂:围绕一个超级工厂创建其他工厂,每个生成的工厂按照工厂模式提供对象。其实就是将工厂抽象为两层,抽象工厂&具体工厂子类,在工厂子类中生产不同类型的子产品。
缺点:抽象工厂模式适用于生产多个工厂系列产品衍生的设计模式,增加新的产品等级结构复杂,需要对原有系统进行较大的修改,甚至需要修改抽象层代码,违背了"开闭原则"。
建造者模式
建造者模式是一种创建型设计模式,使用多个简单的对象一步一步构建成一个复杂对象,能将一个复杂对象的构建与它的表示分离,提供一种创建对象的最佳方式。它主要用于解决对象的构建过于复杂的问题。
建造者模式主要基于四个核心类实现:(主要是产品-建造者-指挥者三大模块)
- 抽象产品类
- 具体产品类:一个具体的产品对象类
- 抽象Builder类:创建一个产品所需要的各个部件的抽象接口
- 具体产品的Builder类:实现抽象接口,构建各个部分。
- 指挥者Director类:统一组建过程,提供给调用者使用,通过指挥者来构造产品。
假设现在有抽象的电脑类,我们可以选择生产不同操作系统的电脑,比如Mac,电脑有不同的部件。由建造者来生产电脑所需部件,最后指挥者指挥建造者的制造零件顺序。层级关系 : 指挥者->建造者->产品
class Computer
{
public:void SetBoard(const string& board){_board = board;} void SetDisplay(const string& display){_display = display;}virtual void setOs() = 0; //不同电脑的操作系统不同void ShowParams(){cout << "computer params: " << endl;cout << "Board: " << _board << endl;cout << "Display: " << _display << endl;cout << "OS:" << _os << endl;}protected:string _board; //主板string _display; //显示屏string _os; //操作系统
};class Mac : public Computer
{
public:void setOs(){_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 std::shared_ptr<Computer> build() = 0;
};class MacBuilder : public Builder
{
public:MacBuilder():_computer(new Mac()){}void BuildBoard(const string& board) override{_computer->SetBoard(board);}void BuildDisplay(const string& display) override{_computer->SetDisplay(display);} void BuildOS() override{_computer->setOs();}std::shared_ptr<Computer> build() override{return _computer;}
private:std::shared_ptr<Computer> _computer;
};class Director
{
public: Director(Builder* builder):_builder(builder){}void Construct(const string& board,const string& display){_builder->BuildDisplay(display);_builder->BuildBoard(board);_builder->BuildOS();}private:shared_ptr<Builder> _builder;
};int main()
{Builder* builder = new MacBuilder();unique_ptr<Director> director(new Director(builder));director->Construct("华硕主板","三星显示器");shared_ptr<Computer> mac = builder->build();mac->ShowParams();return 0;
}
代理模式
代理模式指代理控制对其他对象的访问,也就是代理对象控制对原对象的引用。在某些情况下,一个对象不适合或者不能直接被引用访问,而代理对象可以在客户端和目标对象之间起到中介的作用。
代理模式的结构包括一个是真正的你要访问的对象(目标类)、一个是代理对象。目标对象与代理对象实现同一个接口,先访问代理类再通过代理类访问目标对象。
代理模式分为静态代理、动态代理:
- 静态代理指的是 , 在编译时就已经确定好了代理类和被代理类的关系。也就是说,在编译时就已经确定了代理类要代理的是哪个是被代理类。
- 动态代理指的是 , 在运行时才动态生成代理类,并将其与被代理类绑定。这意味着,在运行时才能确定代理类要代理的是哪个被代理类。
下面以租房为例 ,租客租房,中间经过房屋中介向房东租房。通过代理模式实现 :
#include<iostream>
using namespace std;/* 抽象接口类 */
class RentHouse
{
public:virtual void rentHouse() = 0;
};/* 租房类(目标对象) */
class Landlord : public RentHouse
{
public:void rentHouse() override{cout << "将房子租出去" << endl;}
};/* 代理类 */
class Intermediary : public RentHouse
{
public:void rentHouse() override{cout << "发布招租启示" << endl;cout << "看房.." << endl;_landlord.rentHouse();cout << "售后.." << endl;}private: Landlord _landlord; //代理控制对目标类的访问
};int main()
{Intermediary intermediary;intermediary.rentHouse();return 0;
}
我们可以看到代理完成之后 , 实现了原对象基础功能之外的额外功能。