日志系统的介绍及前置技术
目录
1、项目介绍
1.1 开发环境
1.2 核心技术
2、日志系统的介绍
2.1 日志系统的必要性
2.2 日志系统的技术实现
3、不定参数函数
3.1 C语言不定参宏函数
3.2 C语言不定参函数
3.3 C++不定参函数
4、设计模式
4.1 六大原则(SOLID 原则)
4.2 单例模式
4.2.1 饿汉模式
4.2.2 懒汉模式
4.2.3 适用场景的对比
4.3 工厂模式
4.3.1 简单工厂模式
4.3.2 工厂方法模式
4.3.3 抽象工厂模式
4.3.4 适用场景的对比
4.4 建造者模式
4.5 代理模式
完整代码:PreTech。
1、项目介绍
- 本项目主要实现一个日志系统,其主要支持以下功能:
- 支持多级别日志消息。
- 支持同步(自己做)日志和异步(给别人做)日志。
- 支持可靠写入日志到控制台、文件以及滚动文件(当日志文件达到预设条件,创建新文件写入,同时管理旧文件)中。
- 支持多线程并发写日志。
- 支持扩展不同的日志落地日标地。
1.1 开发环境
- Ubuntu 22.04
- vscode/vim
- g++/gdb
- Makefile
1.2 核心技术
- 类层次设计(继承和多态的应用)。
- C++11(多线程、智能指针、右值引用、auto等)。
- 双缓冲区。
- 生产者消费者模型。
- 设计模式(单例、工厂、代理、建造者等)。
2、日志系统的介绍
2.1 日志系统的必要性
- 出问题时,查日志找原因。
- 运行时,靠日志看状态。
- 要追责时,用日志留证据。
2.2 日志系统的技术实现
-
日志系统的技术实现,按输出方式和复杂度可分为几类:
-
基础控制台输出:直接用 printf、std::cout 等原生函数,将日志打印到控制台(适合调试或简单程序)。
-
文件 / 数据库落地:针对大型项目,为便于排查和分析,将日志写入文件或数据库,核心实现方式分两种:
- 同步写日志:业务线程直接完成日志写入,写完再继续执行。
- 异步写日志:业务线程仅提交日志到队列,由后台线程异步写入,不阻塞业务。
-
3、不定参数函数
- 在初学C语言的时候,我们都用过printf函数进行打印。其中printf函数就是一个不定参函数,不定参函数在实际的使用中比较常见,这里简单做一下介绍。
3.1 C语言不定参宏函数
#include <stdio.h>// 当可变参数为空时,##会消除前面的逗号,避免语法错误
#define Print_1(fmt, ...) printf("[%s:%d] " fmt "\n", __FILE__, __LINE__,
##__VA_ARGS__)int main()
{Print_1();Print_1("%s","Hello Log");return 0;
}
- 输出结果:

3.2 C语言不定参函数
- va_list ap; // 声明一个可变参数列表指针。
- void va_start(va_list ap, last)
- 功能:初始化 va_list 变量 ap,让它指向函数中第一个可变参数。
- 参数:
- ap:待初始化的 va_list 变量。
- last:函数中最后一个固定参数的名称(即可变参数 ... 前的最后一个参数)。
- type va_arg(va_list ap, type)
- 功能:从 ap 中获取下一个参数,并返回该参数的值(类型为 type),同时自动将 ap 移动到下一个参数。
- 参数:
- ap:已初始化的 va_list 变量。
- type:要获取的参数的类型(如 int、double 等,必须是完整类型)。
- void va_end(va_list ap)
- 功能:结束对可变参数列表的访问,清理 ap(释放可能的资源)。
- int vasprintf(char **strp, const char *format, va_list ap);
- 功能:格式化字符串并动态分配内存存储结果。
- 参数:
- strp:输出型参数,是一个指向 char* 的指针。函数成功执行后,会将动态分配的字符串的地址存入 *strp(即 *strp 指向格式化后的字符串)。
- format:格式字符串(与 printf 的格式字符串规则相同,如 %d、%s 等)。
- ap:已初始化的 va_list 变量;。
- 返回值:
- 成功:返回格式化后字符串的长度。
- 失败:返回 -1。
- 注意:在使用完毕后必须通过 free(*strp) 释放内存,否则会导致内存泄漏。
#include <iostream>
#include <stdarg.h>void PrintNum(int n, ...)
{va_list ap;va_start(ap, n);for(int i = 0; i < n; ++i){int num = va_arg(ap, int); // 拿到一个int类型的值std::cout << num << std::endl;}va_end(ap);
}void Print_2(const char* fmt, ...)
{va_list ap;va_start(ap, fmt);char* str;vasprintf(&str, fmt, ap);std::cout << str << std::endl;va_end(ap);free(str);
}int main()
{PrintNum(3, 1, 2, 3);Print_2("%s\n", "Hello Log");return 0;
}
- 输出结果:

3.3 C++不定参函数
#include <iostream>void Print_3()
{// 编译时递归的终止条件,参数包是0个时,直接匹配这个函数。std::cout << "end" << std::endl;
}template<typename T, typename ...Args>
void Print_3(const T& val, Args&& ...args)
{std::cout << val << std::endl;Print_3(std::forward<Args>(args)...); // 保留原始实参的 “左值 / 右值身份”
}int main()
{Print_3();Print_3(1, 2, 3, "Hello Log");return 0;
}
- 输出结果:

4、设计模式
- 设计模式是软件开发中总结的可复用解决方案,针对常见问题,提高代码的可维护性、可扩展性等。
- 下面我们介绍日志系统中用到的几个设计模式。
4.1 六大原则(SOLID 原则)
- 六大原则是 “道”(设计思想),设计模式是 “术”(具体解法)。
- 单一职责原则(Single Responsibility Principle, SRP)
- 核心:一个类 / 模块只负责一个明确的职责(只做一件事)。
- 目的:减少类的复杂度,降低修改风险(修改一个职责不会影响其他职责)。
- 例子:一个 “用户类” 不应同时负责 “用户信息管理” 和 “支付逻辑”,应拆分为 User 和 Payment 两个类。
- 开闭原则(Open-Closed Principle, OCP)
- 核心:对扩展开放(新增功能时可新增代码),对修改关闭(不改动已有代码)。
- 目的:避免修改旧代码导致的风险(如引入新 bug)。
- 例子:用 “抽象类 / 接口” 定义规范,新增功能时通过 “子类继承 / 实现接口” 扩展,而非修改抽象类本身(如用 Shape 接口定义图形,新增 Circle/Rectangle 只需实现接口,无需改 Shape)。
- 里氏替换原则(Liskov Substitution Principle, LSP)
- 核心:子类对象可以替换父类对象(在任何使用父类的地方,用子类替代后逻辑仍成立)。
- 目的:保证继承关系的合理性,避免子类破坏父类的预期行为。
- 反例:正方形继承长方形,重写 setWidth 和 setHeight 时强制宽高相等,导致 “长方形的宽高独立” 的父类逻辑被破坏,此时用正方形替换长方形会出错。
- 依赖倒置原则(Dependency Inversion Principle, DIP)
- 核心:客户端不应依赖它不需要的接口(接口要小而专,避免 “大而全”)。
- 目的:防止接口臃肿导致的实现负担(类只需实现自己需要的方法)。
- 反例:一个 “动物接口” 包含 fly()、swim()、run(),让 “狗” 实现时被迫空实现 fly() 和 swim(),应拆分为 Flyable、Swimmable、Runnable 多个小接口。
- 迪米特法则(Law of Demeter, LoD,又称 “最少知道原则”)
- 核心:一个对象应尽可能少地了解其他对象(只与直接朋友通信,避免间接依赖)。
- 目的:减少对象间的耦合,降低系统复杂度。
- 例子:学生(A)想知道班级平均分,应直接问班长(B),而非让班长去问班主任(C)后再转告 A(A 只需依赖 B,无需知道 C 的存在)。
4.2 单例模式
- 单例模式是一种创建型设计模式,核心目标是:保证一个类在程序中只有唯一实例,并提供一个全局访问点。
- 核心实现要点:
- 私有构造函数:禁止外部通过 new 或 直接实例化 创建对象;
- 静态成员变量:在类内部存储唯一的实例(全局仅一份);
- 静态访问方法:提供全局唯一的接口(如 GetInstance()),供外部获取这个实例。
- 单例模式有两种实现模式:饿汉模式和懒汉模式。
4.2.1 饿汉模式
- 饿汉模式:程序启动时就会创建一个唯一的实例对象。因为单例对象已经确定,所以没有 “多线程并发创建实例” 的安全问题。
-
#include <iostream>class Singleton { private:Singleton() {std::cout << "单例对象构造成功!" << std::endl;};~Singleton() {}; public:Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;static Singleton& GetInstance(){return _ston;} private:static Singleton _ston; }; Singleton Singleton::_ston;int main() {return 0; } - 输出结果:

4.2.2 懒汉模式
- 懒汉模式:第一次要使用单例对象的时候创建实例对象。存在 “多线程并发创建实例” 的安全问题。
- 这里介绍一下,Scott Meyers 在《Effective C++》中提出Meyers' Singleton(迈耶斯单例)。在全局访问函数 getInstance() 中定义一个局部静态变量,该变量会在函数第一次被调用时初始化,且 C++11 标准明确规定 “局部静态变量的初始化是线程安全的”。
-
class Singleton { private:Singleton(){std::cout << "单例对象构造成功!" << std::endl;}~Singleton(){} public:Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;static Singleton& GetInstance(){static Singleton _ston;return _ston;} };int main() {Singleton::GetInstance();return 0; } -
输出结果:
-

4.2.3 适用场景的对比
- 饿汉模式:适合实例初始化成本低、且程序必然会用到的场景(如全局配置管理器)。
- 懒汉模式:适合实例初始化成本高(如大内存对象)、或可能不被使用的场景(如可选功能的管理器)。
4.3 工厂模式
- 工厂模式是一种创建型设计模式,核心思想是将对象的创建过程封装在专门的 “工厂类” 中。目的是解耦 “对象的创建” 和 “对象的使用”。
- 工厂模式主要分为三种常见形式:简单工厂,工厂方法,抽象工厂。
4.3.1 简单工厂模式
- 最基础的工厂模式,通过一个单一工厂类,根据输入的 “类型参数” 创建不同的产品实例。
- 核心结构:
- 抽象产品(Product):定义所有产品的公共接口(通常是纯虚类)。
- 具体产品(ConcreteProduct):实现抽象产品接口的具体类(如 “圆形”“矩形”)。
- 具体工厂(Factory):提供静态或成员方法,根据参数创建并返回具体产品的实例。
-
// 简单工厂模式// 抽象产品:形状 class Shape { public:virtual void Draw() = 0; // 公共接口// 基类析构函数设为虚函数后,子类析构函数会自动参与多态,// 确保通过基类指针销毁子类对象时,析构函数的调用符合 “实际对象类型”。virtual ~Shape() = default; };// 具体产品1:圆形 class Circle : public Shape { public:void Draw() override{std::cout << "draw circle" << std::endl;} };// 具体产品2:矩阵 class Rectangle : public Shape { public:void Draw() override{std::cout << "draw rectangle" << std::endl;} };// 具体工厂 class ShapeFactory { public:static std::unique_ptr<Shape> CreateShape(const std::string& type){if(type == "circle")return std::make_unique<Circle>();else if(type == "rectangle")return std::make_unique<Rectangle>();elsereturn nullptr;} };int main() {std::unique_ptr<Shape> circle = ShapeFactory::CreateShape("circle");std::unique_ptr<Shape> rect = ShapeFactory::CreateShape("rectangle");if (circle) circle->Draw();if (rect) rect->Draw();return 0; } - 输出结果:

- 优点:简单直观,用户只需关注 “用什么”,无需关心 “怎么创建”。
- 缺点:新增产品时,需要修改工厂类的CreateShape方法(违反 “开闭原则”:对扩展开放,对修改关闭),适合产品类型固定、很少扩展的场景。
4.3.2 工厂方法模式
- 为解决简单工厂的 “开闭原则” 问题,将工厂也抽象化:抽象工厂定义创建产品的接口(通常是纯虚类),具体工厂负责创建对应产品。每个具体产品对应一个具体工厂。
- 核心结构:
- 抽象产品(Product):同上。
- 具体产品(ConcreteProduct):同上。
- 抽象工厂(AbstractFactory):定义创建产品的纯虚方法(返回抽象产品)。
- 具体工厂(ConcreteFactory):继承抽象工厂,实现创建方法,返回具体产品。
-
// 抽象产品:形状 class Shape { public:virtual void Draw() = 0; // 公共接口// 基类析构函数设为虚函数后,子类析构函数会自动参与多态,// 确保通过基类指针销毁子类对象时,析构函数的调用符合 “实际对象类型”。virtual ~Shape() = default; };// 具体产品1:圆形 class Circle : public Shape { public:void Draw() override{std::cout << "draw circle" << std::endl;} };// 具体产品2:矩阵 class Rectangle : public Shape { public:void Draw() override{std::cout << "draw rectangle" << std::endl;} };// 抽象工厂 class ShapeFactory { public:virtual std::unique_ptr<Shape> CreateShape() = 0;virtual ~ShapeFactory() = default; };// 具体工厂1:创建圆形 class CircleFactory : public ShapeFactory { public:std::unique_ptr<Shape> CreateShape() override{return std::make_unique<Circle>();} };// 具体工厂2:创建矩形 class RectangleFactory : public ShapeFactory { public:std::unique_ptr<Shape> CreateShape() override{return std::make_unique<Rectangle>();} };int main() {// 工厂本身也用智能指针,彻底避免手动释放std::unique_ptr<ShapeFactory> circle_factory = std::make_unique<CircleFactory>();std::unique_ptr<ShapeFactory> rect_factory = std::make_unique<RectangleFactory>();std::unique_ptr<Shape> circle = circle_factory->CreateShape();std::unique_ptr<Shape> rect = rect_factory->CreateShape();circle->Draw();rect->Draw();return 0; } - 输出结果:

- 优点:新增产品时,只需新增 “具体产品类” 和 “对应具体工厂类”,无需修改原有代码(符合开闭原则)。
- 缺点:每增加一个产品,需额外增加一个工厂类,可能导致类数量过多。
4.3.3 抽象工厂模式
- 用于创建一系列相互关联或依赖的产品族(如 “华为手机 + 华为平板”“苹果手机 + 苹果平板”)。抽象工厂定义创建整个产品族的接口,具体工厂负责创建一整个产品族。
- 核心结构:
- 抽象产品族:多个相关的抽象产品(如Phone、Tablet)。
- 具体产品族:每个品牌下的具体产品(如HuaweiPhone、HuaweiTablet)。
- 抽象工厂:定义创建整个产品族的接口(如createPhone()、createTablet())。
- 具体工厂:实现抽象工厂,创建对应品牌的所有产品。
-
// 抽象产品族:手机和平板 class Phone { public:virtual void Call() = 0;virtual ~Phone() = default; }; class Tablet { public:virtual void Watch() = 0;virtual ~Tablet() = default; };// 具体产品族1:华为系列 class HuaweiPhone : public Phone { public:void Call() override{std::cout << "Huawei Call" << std::endl;} }; class HuaweiTablet : public Tablet { public:void Watch() override{std::cout << "Huawei videos" << std::endl;} };// 具体产品族2:苹果系列 class IPhone : public Phone { public:void Call() override{std::cout << "IPhone Call" << std::endl;} }; class Ipad : public Tablet { public:void Watch() override{std::cout << "Ipad videos" << std::endl;} };// 抽象工厂:创建整个产品族的接口 class AbstractFactory { public:virtual std::unique_ptr<Phone> CreatePhone() = 0;virtual std::unique_ptr<Tablet> CreateTablet() = 0;virtual ~AbstractFactory() = default; };// 具体工厂1:华为工厂 class HuaweiFactory : public AbstractFactory { public:std::unique_ptr<Phone> CreatePhone() override{return std::make_unique<HuaweiPhone>();}std::unique_ptr<Tablet> CreateTablet() override{return std::make_unique<HuaweiTablet>();} };// 具体工厂2:苹果工厂 class AppleFactory : public AbstractFactory { public:std::unique_ptr<Phone> CreatePhone() override{return std::make_unique<IPhone>();}std::unique_ptr<Tablet> CreateTablet() override{return std::make_unique<Ipad>();} };int main() {std::unique_ptr<AbstractFactory> huawei_factory = std::make_unique<HuaweiFactory>();std::unique_ptr<AbstractFactory> apple_factory = std::make_unique<AppleFactory>();std::unique_ptr<Phone> huawei_phone = huawei_factory->CreatePhone();std::unique_ptr<Tablet> huawei_tablet = huawei_factory->CreateTablet();std::unique_ptr<Phone> iphone = apple_factory->CreatePhone();std::unique_ptr<Tablet> ipad = apple_factory->CreateTablet();huawei_phone->Call();huawei_tablet->Watch();iphone->Call();ipad->Watch();return 0; } - 输出结果:

- 优点:能保证同一产品族的产品相互匹配,新增具体产品族时只需新增具体工厂(符合开闭原则)。
- 缺点:若新增产品族需要的某个新产品(如新增 “手表”),需新增抽象产品+具体产品+修改所有工厂接口和实现,扩展性较差。
4.3.4 适用场景的对比
- 简单工厂:适合产品少、变化少的场景,直接通过参数创建对象。
- 工厂方法:适合产品可能扩展的场景, “一对一”(一个工厂造一种产品),遵循开闭原则。
- 抽象工厂:适合需要创建 “产品族” 的场景(如同一品牌的系列产品),“一对多”(一个工厂造一族产品)。
4.4 建造者模式
- 建造者模式(Builder Pattern) 是一种创建型设计模式,核心思想是:将复杂对象的构建过程与它的表示分离。
- 工厂模式是 “一键生成”,建造者模式是 “分步定制”。
- 核心结构:
- 产品(Product):需要构建的复杂对象(如 “电脑”“汽车”)。
- 抽象建造者(Builder):定义构建产品各部件的接口(如 “装 CPU”“装内存”),以及返回最终产品的方法。
- 具体建造者(ConcreteBuilder):实现抽象建造者的接口,负责具体部件的构建(如 “游戏本建造者” 用高性能 CPU,“办公本建造者” 用低功耗 CPU)。
- 指挥者(Director):负责调用建造者的方法,按固定步骤组织构建过程(如 “先装 CPU→再装内存→最后装硬盘”),不关心具体部件细节。
-
#include <iostream> #include <string> #include <memory>// 复杂产品:电脑(cpu+memory+disk) class Computer { public:using ptr = std::shared_ptr<Computer>;void SetCPU(const std::string& cpu){_cpu = cpu;}void SetMemory(const std::string& memory){_memory = memory;}void SetDisk(const std::string& disk){_disk = disk;}void Show(){std::cout << "电脑配置: "<< "\n\tCPU: " << _cpu<< "\n\tMemory: " << _memory<< "\n\tDisk: " << _disk<< std::endl;}private:std::string _cpu;std::string _memory;std::string _disk; };// 抽象建造者:定义构建电脑各组件的接口 class ComputerBuilder { public:using ptr = std::shared_ptr<ComputerBuilder>;virtual void BuildCPU() = 0;virtual void BuildMemory() = 0;virtual void BuildDisk() = 0;virtual Computer::ptr GetResult() = 0;virtual ~ComputerBuilder() = default; };// 具体建造者1:游戏本建造者 class GamingComputerBuilder : public ComputerBuilder { public:GamingComputerBuilder():_computer(std::make_shared<Computer>()){}void BuildCPU() override{_computer->SetCPU("Intel i9");}void BuildMemory() override{_computer->SetMemory("32GB DDR5");}void BuildDisk() override{_computer->SetDisk("1TB SSD");}Computer::ptr GetResult() override{return _computer;}private:Computer::ptr _computer; };// 具体建造者2:办公本建造者 class OfficeComputerBuilder : public ComputerBuilder { public:OfficeComputerBuilder():_computer(std::make_shared<Computer>()){}void BuildCPU() override{_computer->SetCPU("Intel i5");}void BuildMemory() override{_computer->SetMemory("16GB DDR4");}void BuildDisk() override{_computer->SetDisk("512GB SSD");}Computer::ptr GetResult() override{return _computer;}private:Computer::ptr _computer; };// 指挥者:控制构建步骤 class Director { public:Director(ComputerBuilder::ptr computer_builder):_computer_builder(computer_builder){}void Construct(){_computer_builder->BuildCPU();_computer_builder->BuildMemory();_computer_builder->BuildDisk();}private:ComputerBuilder::ptr _computer_builder; };int main() {std::shared_ptr<ComputerBuilder> gaming_computer_builder = std::make_shared<GamingComputerBuilder>();std::shared_ptr<ComputerBuilder> office_computer_builder = std::make_shared<OfficeComputerBuilder>();std::unique_ptr<Director> gaming_computer_director = std::make_unique<Director>(gaming_computer_builder);std::unique_ptr<Director> office_computer_director = std::make_unique<Director>(office_computer_builder);gaming_computer_director->Construct();office_computer_director->Construct();Computer::ptr gaming_computer = gaming_computer_builder->GetResult();Computer::ptr office_computer = office_computer_builder->GetResult();gaming_computer->Show();office_computer->Show();return 0; } - 输出结果:

- 已经存在的智能指针,如果要传递,就使用std::shared_ptr。
4.5 代理模式
- 代理模式(Proxy Pattern) 是一种结构型设计模式,核心思想是:为一个对象提供 “代理”(中间层),通过代理间接访问原对象,从而在不修改原对象的前提下,对访问过程增加控制逻辑(如权限校验、日志记录、延迟加载等)。
- 核心结构:
- 抽象主题(Subject):定义真实对象和代理的共同接口(纯虚类),确保代理可替代真实对象。因为要通过代理访问,代理应该要有真实对象的所有操作。
- 真实主题(RealSubject):实际执行业务逻辑的对象(被代理的对象)。
- 代理(Proxy):实现抽象主题接口,内部持有真实主题的引用;在调用真实主题的方法前后,可添加额外操作(如权限检查)。
- 代理模式分为静态代理(编译时确定)、动态代理(运行时确定),动态代理目前有点复杂,下面介绍静态代理。
-
#include <iostream>class RentHouse { public:virtual void rentHouse() = 0;virtual ~RentHouse() = default; };class LandLord : public RentHouse { public:void rentHouse() override{std::cout << "rent house" << std::endl;} };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; } -
输出结果:
-

