当前位置: 首页 > news >正文

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...:剩下的变长参数包

行为流程:

  1. 打印当前的 value

  2. 如果还有参数(sizeof...(args) > 0)就递归调用 xprintf(...)

  3. 否则,调用 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()(真正租出)→ 负责租后维修

房东只做了“租出去”这一件事,其他琐事都由中介代理处理,体现了代理模式“控制访问 + 功能增强”的特性。

相关文章:

  • 闩锁效应(latch up)
  • bat脚本转换为EXE应用程序文件
  • systemctl管理指令
  • opencv 给图片和视频添加水印
  • MySQL运维三部曲初级篇:从零开始打造稳定高效的数据库环境
  • Dify快速入门之chatflow
  • Linux网络编程——基于ET模式下的Reactor
  • 【正则表达式】正则表达式使用总结
  • 如何在3090显卡上使用老版本torch
  • python 库 下载 ,整合在一个小程序 UIUIUI
  • LeetCode 239 滑动窗口最大值
  • 【机器学习】从炼丹到落地!模型部署与监控全流程实战指南 (MLOps 核心)
  • 【sylar-webserver】8 HOOK模块
  • Linux系统:进程终止的概念与相关接口函数(_exit,exit,atexit)
  • BT1120 BT656驱动相关代码示例
  • 计算机网络八股——HTTP协议与HTTPS协议
  • Linux疑难杂惑 | 云服务器重装系统后vscode无法远程连接的问题
  • 针对MCP认证考试中的常见技术难题进行实战分析与解决方案分享
  • Windows桌面图标变白的解决方案
  • springboot--web开发请求参数接收注解
  • 一周观展|上海,一系列特展大展渐次呈现
  • 央媒关注微短剧如何探索精品化之路:从“悬浮”落回“现实”
  • 白宫慌了!将设工作组紧急处理对中国加征关税危机
  • 夜读丨“看看世界”本身就是一种意义
  • 遇见古籍里的先贤,山西博物院“晋国垂棘”明日开展
  • 远洋渔船上的谋生