C++ 基于多设计模式下的同步异步⽇志系统-1准备工作
一.项目介绍
项⽬介绍
本项⽬主要实现⼀个⽇志系统, 其主要⽀持以下功能:
• ⽀持多级别⽇志消息
• ⽀持同步⽇志和异步⽇志
• ⽀持可靠写⼊⽇志到控制台、⽂件以及滚动⽂件中
• ⽀持多线程程序并发写⽇志
• ⽀持扩展不同的⽇志落地⽬标地
二.日志系统的三种实现方式
实现方式 原理简述 优点 缺点 适用场景 1. 控制台输出 ( printf
/std::cout
)直接在控制台输出日志信息,不进行落地文件记录 简单、直观、便于开发调试 无法记录历史日志、对线上调试不适用 本地开发调试、单线程程序 2. 同步写日志 在当前业务线程中执行日志格式化 + 写入文件操作,每条日志调用都同步 write() 实现简单、数据可靠 每条日志都阻塞主流程,尤其在高并发下 write IO 成为性能瓶颈 简单后端系统、低并发写日志场景 3. 异步写日志 主线程仅负责将日志写入缓冲区,由专门线程写日志到文件 高性能、非阻塞、不影响业务流程,适合高并发 实现复杂、涉及线程、锁、双缓冲,落地时间略有延迟 高性能服务、后台系统、分布式
三.相关技术知识补充
1 不定参宏函数
#include <iostream>
#include <cstdarg>
#define LOG(fmt, ...) printf("[%s:%d] " fmt "\n", __FILE__, __LINE__,##__VA_ARGS__)
int main()
{LOG("%s-%s", "hello", "wws);return 0;
}
之前rpc项目介绍过。
解释 ##__VA_ARGS__
__VA_ARGS__ 是 C 语言宏的可变参数,它允许宏接受不定数量的参数。
## 用于处理 "参数为空" 的情况,它的作用是:
如果 __VA_ARGS__ 为空,就去掉前面的 ' , ',防止格式错误。
如果 __VA_ARGS__ 有内容,它会正常展开。
2.C⻛格不定参函数
#include <iostream>
#include <cstdarg>
void printNum(int n, ...)
{va_list al;va_start(al, n); // 让al指向n参数之后的第⼀个可变参数for (int i = 0; i < n; i++){int num = va_arg(al, int); // 从可变参数中取出⼀个整形参数std::cout << num << std::endl;}va_end(al); // 清空可变参数列表--其实是将al置空
}
int main()
{printNum(3, 11, 22, 33);printNum(5, 44, 55, 66, 77, 88);return 0;
}
printNum(int n, ...)的作用就是打印n个整型
1. va_list al;
定义一个变参处理变量 va_list 它是 C 语言提供的一个宏(实际上是一个结构体指针类型),专门用来处理 ... 这些变长参数。你可以把它理解为:一个“变参读取器”指针。
2.va_start(al, n);
初始化变参指针(定位起点)它告诉 al“变长参数”是从 n之后开始的,且后面参数的个数为n。(C 语言没有反射或参数数量的机制,编译器也不会告诉你 ... 有几个参数。必须通过最后一个确定参数的地址来推断后面变参的起始地址,这就是 va_start 的原理)
3.va_arg(al, int);
使用 va_arg() 逐个读取参数(大小为第二个类型的大小)。从 al 指向的地方读取一个 int 类型的值,并且把 al 自动向后移动。
4. va_end(ap);
清理资源(让 ap 无效)清空指针
#include <iostream>
#include <cstdarg>
void myprintf(const char *fmt, ...)
{// int vasprintf(char **strp, const char *fmt, va_list ap);char *res;va_list al;va_start(al, fmt);int len = vasprintf(&res, fmt, al);va_end(al);std::cout << res << std::endl;free(res);
}
int main()
{myprintf("%s-%d", "⼩明", 18);return 0;
}
你写了一个叫 myprintf 的函数,能像 printf 一样,接收格式字符串和多个参数,把结果 格式化成字符串,并通过 std::cout 打印出来。
fmt 是格式字符串:"%s-%d"
va_start(al, fmt); 告诉 al:从参数 fmt 后面的地方开始读取变参("小明", 18)。
vasprintf(&res, fmt, al);
这个函数做了三件事:
1.根据 fmt 和 al,拼出格式化后的字符串;
2.自动调用 malloc 分配内存,存放结果字符串;
3.把 res 设为这个字符串的地址。
free(res);
因为 vasprintf 分配了堆内存,你必须用 free 手动释放,否则会内存泄漏。
3.C++⻛格不定参函数
#include <iostream>
void xprintf()
{std::cout << std::endl;
}
template <typename T, typename... Args>
void xprintf(const T &value, Args &&...args)
{std::cout << value << " ";if ((sizeof...(args)) > 0){xprintf(std::forward<Args>(args)...);}else{xprintf();}
}
int main()
{xprintf("wws");xprintf("wws", 666);xprintf("wws", "0721", 666);return 0;
}
参数说明:
T
:当前要处理的第一个参数
Args...
:剩下的变长参数包行为流程:
打印当前的
value
如果还有参数(
sizeof...(args) > 0
)就递归调用xprintf(...)
否则,调用
xprintf()
(终点),输出一个换行
...
在前面:定义一个 参数包。
...
在后面:展开一个 参数包。(std::forward<Args>(args)...) 完美转发剩余的参数,右值传递完还是右值,左值还是左值。
为什么要写xprintf()参数为空的特化函数?
模板会一直展开直到参数为空
模板的递归展开并不会因为你进入
else
分支而立即停止递归。递归停止是通过“没有更多参数”来控制的。关键点是 你在调用xprintf()
时,会不断把剩余的参数传递给下一个递归调用,直到参数包为空。所以执行else后,并不会结束,还会继续递归直到参数包为空,所以必须写参数包为空的特化函数。
四.设计模式
1.六大设计原则
1.单一职责原则(SRP)
定义:一个类只负责一项职责。
应用:
Logger
只负责组织和发起日志输出。
Formatter
专注于格式化日志内容。
Sink
专注于日志“落地”(文件/控制台等输出方式)。
LogMsg
专注于日志数据结构封装。
🔓 2. 开闭原则(OCP)
定义:对扩展开放,对修改关闭。
应用:
增加新的日志输出格式、日志落地方式(如新增 TCP 日志输出)→ 新增类即可,无需改动原逻辑。
格式化模块通过解析
%d %m %t
等 pattern 字符串,支持灵活扩展。
🔁 3. 里氏替换原则(LSP)
定义:子类对象可以替代父类对象使用。
应用:
所有日志输出类继承自抽象类
LogSink
,只要实现log()
方法,就能无缝替换。
SyncLogger
/AsyncLogger
都继承自Logger
,任何需要 Logger 的地方都可以使用这两个实现。
🔌 4. 依赖倒置原则(DIP)
定义:高层模块不应该依赖底层模块,二者都应该依赖抽象。
应用:
所有 Sink 都通过
LogSink::ptr
操作,具体使用的是哪个子类并不关心。日志器的创建通过 Builder 构建,调用方不直接依赖 Logger 实现类。
🧼 5. 接口隔离原则(ISP)
定义:类不应依赖它不使用的方法。
应用:
Formatter::format()
只依赖LogMsg
数据,不暴露额外无关的操作。
Logger
类的debug/info/warn/...
分开封装,调用者按需使用。
🧍 6. 迪米特法则(LoD)
定义:只与直接朋友通信,降低耦合。
应用:
日志器通过
Logger::Builder
封装所有配置细节,调用者无需了解Sink
/Formatter
等底层实现。管理器
loggerManager
提供统一接口getLogger()
,外部无需知道 Logger 的创建细节。
总结一句话:
“用抽象构建框架,用实现扩展细节”,整个日志系统正是依据这一原则,通过设计模式把每个模块解耦,提升了系统的灵活性与可扩展性。
2.单例模式
单例模式是一种常见的设计模式,它保证一个类只有一个实例,并提供一个全局访问点。单例模式有两种实现方式 饿汉模式和懒汉模式
饿汉模式
程序启动时就会创建⼀个唯⼀的实例对象。 因为单例对象已经确定, 所以⽐较适⽤于多
线程环境中, 多线程获取单例对象不需要加锁, 可以有效的避免资源竞争, 提⾼性能。
//1.饿汉模式
class Singleton
{
private:static Singleton _eton;//在类内进行声明Singleton()// 私有构造函数:_data(66){std::cout<<"单例对象构造"<<std::endl;}~Singleton() {} // 私有析构函数Singleton (const Singleton&)=delete;//禁止拷贝Singleton&operator=(const Singleton&)=delete;//禁止赋值
private:int _data;
public:static Singleton& getInstance(){return _eton;}
};
Singleton Singleton:: _eton;//类外定义
1.构造析构私有 拷贝赋值函数禁止delete且私有
2.类内声明 静态成员变量 类外定义(程序运行时自动实例化)
3.类中提供静态函数(不需要类对象就能调用),用来获取单例对象。
优点:
线程安全:由于单例对象在程序启动时就已经创建,多个线程在调用
getInstance()
时无需加锁,可以避免资源竞争,因此性能较高。简单:代码结构简单,容易理解和实现。
缺点:
提前创建:单例对象会在程序启动时就创建,即使在程序运行过程中并不需要这个实例,也会被创建,这可能导致不必要的资源浪费。
不可延迟加载:如果创建单例对象的过程非常复杂或资源消耗很大,程序启动时就会受到影响。
适用场景:
适合在程序启动时就需要加载的资源,例如配置管理、日志系统等。
适用于实例的创建比较轻量,或者实例的创建和销毁不会占用太多资源的场景。
懒汉模式
第⼀次使⽤要使⽤单例对象的时候创建实例对象。如果单例对象构造特别耗时或者耗费济
源(加载插件、加载⽹络资源等), 可以选择懒汉模式, 在第⼀次使⽤的时候才创建对象。
//2.懒汉模式
class Singleton
{
private:Singleton()// 私有构造函数:_data(66){std::cout<<"单例对象构造"<<std::endl;}~Singleton() {} // 私有析构函数Singleton (const Singleton&)=delete;//禁止拷贝Singleton&operator=(const Singleton&)=delete;//禁止赋值
private:int _data;
public:static Singleton& getInstance(){static Singleton _eton;//只有第一次调用时创建实例(C++11 此时线程安全不需要加锁)return _eton;}
};
1.构造析构私有 拷贝赋值函数禁止delete且私有
2.在获取单例对象时getInstance()内部创建单例对象(static对象只会初始化一次)
优点:
延迟创建:单例对象只有在真正需要时才会被创建,避免了不必要的资源浪费,适用于实例化过程耗时或消耗资源的情况。
线程安全:使用 C++11 的
static
关键字保证静态局部变量在多线程环境下的安全初始化。缺点:
延迟加载开销:虽然避免了程序启动时的资源消耗,但在首次调用
getInstance()
时,会有一定的延迟开销。复杂度较高:相比饿汉模式,懒汉模式的实现稍微复杂一些,尤其是早期版本的 C++,静态局部变量的线程安全性没有保证,需要额外的锁机制。
适用场景:
适合实例化开销较大、资源消耗较多的单例对象,或者对象的创建是延迟的、条件不固定的情况。
特性 | 饿汉模式 | 懒汉模式 |
---|---|---|
实例化时机 | 程序启动时即创建实例 | 第一次调用 getInstance() 时创建实例 |
线程安全 | 默认线程安全 | 静态局部变量保证线程安全(C++11后) |
内存消耗 | 启动时即创建,可能浪费资源 | 只有在首次访问时才创建,节省内存资源 |
性能 | 更高性能,无锁定和延迟 | 初次调用有延迟,可能有少许性能开销 |
实现复杂度 | 简单易实现 | 稍复杂,涉及线程安全和延迟加载 |
适用场景 | 启动时必须加载的对象,资源轻量 | 对象创建耗时或资源消耗较大的情况 |
3.工厂模式
1.简单工厂模式
通过一个统一的工厂类,根据传入的参数判断创建哪种产品(对象)。
所有产品类的创建逻辑都集中在一个工厂类中。
客户端↓
SimpleFactory::create("苹果") or "香蕉"↓
返回具体产品(Apple / Banana)
只有一个工厂,根据传入类型的不同,来生产不同的对象。
class Fruit {
public:virtual void show() = 0;
};class Apple : public Fruit {
public:void show() override { std::cout << "我是苹果\n"; }
};class Banana : public Fruit {
public:void show() override { std::cout << "我是香蕉\n"; }
};class FruitFactory {
public:static std::shared_ptr<Fruit> createFruit(const std::string &type) {if (type == "apple") return std::make_shared<Apple>();if (type == "banana") return std::make_shared<Banana>();return nullptr;}
};
✅ 优点:
简单易懂、实现成本低。
客户端不需要知道具体产品类名,只需要告诉工厂“我要什么”。
❌ 缺点:
违反开闭原则:添加新产品必须修改工厂代码。
工厂类过于臃肿,职责过重,易造成维护困难。
✅ 适用场景:
产品种类较少,变动不频繁的小项目或初期开发阶段。
方法二:模板函数
template<typename T, typename... Args> static std::shared_ptr<T> create(Args&&... args) {return std::make_shared<T>(std::forward<Args>(args)...); }
符合开闭原则
2.工厂方法模式
每个产品类对应一个具体工厂类。
抽象出一个工厂接口,具体工厂负责创建对应的产品。
客户端只需使用对应的工厂,不再传入类型参数。
客户端↓
AppleFactory::create() BananaFactory::create()↓ ↓
返回 Apple 返回 Banana
有多个工厂,一个子类就对应一个工厂。
class Fruit {
public:virtual void show() = 0;
};class Apple : public Fruit {
public:void show() override { std::cout << "我是苹果\n"; }
};class Banana : public Fruit {
public:void show() override { std::cout << "我是香蕉\n"; }
};class FruitFactory {
public:virtual std::shared_ptr<Fruit> createFruit() = 0;
};class AppleFactory : public FruitFactory {
public:std::shared_ptr<Fruit> createFruit() override {return std::make_shared<Apple>();}
};class BananaFactory : public FruitFactory {
public:std::shared_ptr<Fruit> createFruit() override {return std::make_shared<Banana>();}
};
✅ 优点:
遵循开闭原则:新增产品只需新增产品类和工厂类,无需修改现有代码。
更加符合“职责单一”的设计原则。
❌ 缺点:
每新增一个产品都要新增一个工厂类,类数量增多。
不适合产品种类太多的场景,维护成本较高。
✅ 适用场景:
产品变化频繁,且对扩展性有要求的中大型项目。
3.抽象工厂模式
不再是创建“单一”产品,而是创建产品族(多个功能相关的产品对象)。
定义一组工厂接口,每个工厂可以创建多个类型的产品。
有多种物品,水果 动物... 里面还可以细分苹果 香蕉,狗 羊
每一种物品对应一个工厂,每个工厂中有具体对象生成函数
所有工厂都继承于一个抽象工厂。
抽象工厂(AbstractFactory)↓------------------------↓ ↓
水果工厂(FruitFactory) 动物工厂(AnimalFactory)↓ ↓
createApple() createDog()
createBanana() createSheep()
#include <iostream>
#include <memory>
#include <string>// ==== 抽象产品 ====
class Fruit {
public:virtual void show() = 0;virtual ~Fruit() = default;
};class Animal {
public:virtual void voice() = 0;virtual ~Animal() = default;
};// ==== 具体产品 ====
class Apple : public Fruit {
public:void show() override {std::cout << "我是苹果🍎" << std::endl;}
};class Banana : public Fruit {
public:void show() override {std::cout << "我是香蕉🍌" << std::endl;}
};class Dog : public Animal {
public:void voice() override {std::cout << "汪汪汪🐶" << std::endl;}
};class Sheep : public Animal {
public:void voice() override {std::cout << "咩咩咩🐑" << std::endl;}
};// ==== 抽象工厂接口 ====
class AbstractFactory {
public:virtual ~AbstractFactory() = default;
};// ==== 水果工厂接口 ====
class FruitFactory : public AbstractFactory {
public:virtual std::shared_ptr<Fruit> createApple() = 0;virtual std::shared_ptr<Fruit> createBanana() = 0;
};// ==== 动物工厂接口 ====
class AnimalFactory : public AbstractFactory {
public:virtual std::shared_ptr<Animal> createDog() = 0;virtual std::shared_ptr<Animal> createSheep() = 0;
};// ==== 水果工厂实现 ====
class ConcreteFruitFactory : public FruitFactory {
public:std::shared_ptr<Fruit> createApple() override {return std::make_shared<Apple>();}std::shared_ptr<Fruit> createBanana() override {return std::make_shared<Banana>();}
};// ==== 动物工厂实现 ====
class ConcreteAnimalFactory : public AnimalFactory {
public:std::shared_ptr<Animal> createDog() override {return std::make_shared<Dog>();}std::shared_ptr<Animal> createSheep() override {return std::make_shared<Sheep>();}
};// ==== 工厂选择器 ====
class FactorySelector {
public:enum class Type { FRUIT, ANIMAL };static std::shared_ptr<AbstractFactory> getFactory(Type type) {if (type == Type::FRUIT) {return std::make_shared<ConcreteFruitFactory>();} else {return std::make_shared<ConcreteAnimalFactory>();}}
};// ==== 使用示例 ====
int main() {// 选择水果工厂auto fruitFactory = std::dynamic_pointer_cast<FruitFactory>(FactorySelector::getFactory(FactorySelector::Type::FRUIT));auto apple = fruitFactory->createApple();apple->show();auto banana = fruitFactory->createBanana();banana->show();// 选择动物工厂auto animalFactory = std::dynamic_pointer_cast<AnimalFactory>(FactorySelector::getFactory(FactorySelector::Type::ANIMAL));auto dog = animalFactory->createDog();dog->voice();auto sheep = animalFactory->createSheep();sheep->voice();return 0;
}
✅ 优点:
遵循开闭原则,支持产品族的统一创建。
便于对产品进行分组管理,提高模块间协作性。
❌ 缺点:
系统复杂度提高,类之间的依赖关系增多。
如果要添加新产品(而不是产品族),修改成本大(会破坏工厂接口)。
✅ 适用场景:
一个系统需要成组创建多个互相依赖的对象。
比如 GUI 库中,不同操作系统(Windows/Mac/Linux)下的按钮、菜单、文本框需要成套配合。
4.建造者模式
建造者模式是⼀种创建型设计模式, 使⽤多个简单的对象⼀步⼀步构建成⼀个复杂的对象,能够将⼀个复杂的对象的构建与它的表⽰分离,提供⼀种创建对象的最佳⽅式。主要⽤于解决对象的构建过于复杂的问题。
建造者模式主要基于四个核⼼类实现:
• 抽象产品类:定义复杂对象结构、属性和接口
• 具体产品类:⼀个具体的产品对象类
• 抽象Builder类:创建⼀个产品对象所需的各个部件的抽象接⼝
• 具体产品的Builder类:实现抽象接⼝,构建各个部件
• 指挥者Director类:统⼀组建过程,提供给调⽤者使⽤,通过指挥者按顺序来构造产品
抽象产品类:需要设置的属性
具体产品类:不同产品设置的属性不同
抽象Builder类:设置对应的属性的接口
具体产品的Builder类:具体怎么设置产品属性 (实现接口)
指挥者Director类:设置属性的先后顺序
// 1. 抽象产品类
class Computer {std::string _board, _display, _os;void setBoard(...); void setDisplay(...); virtual void setOs() = 0;
};// 2. 具体产品类
class MacBook : public Computer {void setOs() override { _os = "Mac OS X"; }
};// 3. 抽象建造者
class Builder {virtual void buildBoard(...) = 0;virtual void buildDisplay(...) = 0;virtual void buildOs() = 0;virtual Computer::ptr build() = 0;
};// 4. 具体建造者
class MacBookBuilder : public Builder {Computer::ptr _computer;void buildBoard(...) override { _computer->setBoard(...); }...
};// 5. 指挥者
class Director {Builder::ptr _builder;void construct(...) {_builder->buildBoard(...);_builder->buildDisplay(...);_builder->buildOs();}
};
int main()
{Builder* buidler = new MackBookBuilder();std::unique_ptr<Director> pd(new Director(buidler));pd->construct("英特尔主板", "VOC显⽰器");Computer::ptr computer = buidler->build();std::cout << computer->toString();return 0;
}
角色 | 类名 | 职责描述 |
---|---|---|
抽象产品类 | Computer | 定义电脑的组成部分(主板、显示器、系统)以及接口 |
具体产品类 | MacBook | 继承 Computer ,实现操作系统的设定 |
抽象建造者 | Builder | 定义构建各部分(主板、显示器、OS)和最终组装的接口 |
具体建造者 | MacBookBuilder | 实现构建过程,封装构建细节,返回构造结果 |
指挥者(Director) | Director | 控制建造流程,调用建造者接口完成构造 |
5.代理模式
代理模式指代理控制对其他对象的访问, 也就是代理对象控制对原对象的引⽤。在某些情况下,⼀个对象不适合或者不能直接被引⽤访问,⽽代理对象可以在客⼾端和⽬标对象之间起到中介的作⽤。代理模式的结构包括⼀个是真正的你要访问的对象(⽬标类)、⼀个是代理对象。⽬标对象与代理对象实现同⼀个接⼝,先访问代理类再通过代理类访问⽬标对象。代理模式分为静态代理、动态代理:
• 静态代理指的是,在编译时就已经确定好了代理类和被代理类的关系。也就是说,在编译时就已经确定了代理类要代理的是哪个被代理类。
• 动态代理指的是,在运⾏时才动态⽣成代理类,并将其与被代理类绑定。这意味着,在运⾏时才能确定代理类要代理的是哪个被代理类。
实现了一个“租房场景”的静态代理模式:
房东(Landlord)是被代理对象(目标对象)
中介(Intermediary)是代理对象
通过中介代理类来控制、增强对房东租房功能的访问
#include <iostream>
#include <string>// 抽象租房接口
class RentHouse {
public:virtual void rentHouse() = 0;virtual ~RentHouse() = default;
};// 房东类:目标对象(实际提供租房服务)
class Landlord : public RentHouse {
public:void rentHouse() override {std::cout << "房东:将房子租出去\n";}
};// 中介代理类:代理对象,封装对房东的访问并增强功能
class Intermediary : public RentHouse {
public:void rentHouse() override {std::cout << "中介:发布招租启示\n";std::cout << "中介:带人看房\n";_landlord.rentHouse(); // 委托给房东完成真正的租房std::cout << "中介:租后负责维修服务\n";}private:Landlord _landlord; // 中介内部持有真实房东对象
};// 客户端调用
int main() {Intermediary intermediary;intermediary.rentHouse(); // 客户通过代理租房return 0;
}
→ 中介的 rentHouse()→ 发布招租启事→ 带人看房→ 调用房东的 rentHouse()(真正租出)→ 负责租后维修
房东只做了“租出去”这一件事,其他琐事都由中介代理处理,体现了代理模式“控制访问 + 功能增强”的特性。