C++零基础到工程实战(3.6):逻辑实战示例—日志模块

目录

一、本节学习内容概要图

二、前言

三、案例需求与整体思路

[3.1 日志级别设计](#3.1 日志级别设计)

[3.2 用户需求说明](#3.2 用户需求说明)

[3.3 本案例的核心思想](#3.3 本案例的核心思想)

四、完整代码

[4.1 示例代码](#4.1 示例代码)

[4.2 Powershell 运行结果](#4.2 Powershell 运行结果)

五、代码实现与逻辑分析

[5.1 先获取用户输入的日志级别](#5.1 先获取用户输入的日志级别)

[5.2 再准备要输出的测试日志](#5.2 再准备要输出的测试日志)

[5.3 日志过滤的核心判断](#5.3 日志过滤的核心判断)

[5.4 为什么这里可以直接比较大小](#5.4 为什么这里可以直接比较大小)

[5.5 switch 在这里起什么作用](#5.5 switch 在这里起什么作用)

[(1)switch 不是"判断和外面传入的 logLevel 比较"](#(1)switch 不是“判断和外面传入的 logLevel 比较”)

[(2)switch 不会影响"后续输出内容"](#(2)switch 不会影响“后续输出内容”)

(3)代码真实逻辑:

1)先判断能不能输出

2)如果能输出,再决定输出前缀是什么

3)最后输出

[六、当前 if 不成立,为什么后续日志还能继续输出](#六、当前 if 不成立,为什么后续日志还能继续输出)

七、小结

[7.1 本节涉及的核心知识](#7.1 本节涉及的核心知识)

[7.2 这个案例的实战意义](#7.2 这个案例的实战意义)

[7.3 日志流程](#7.3 日志流程)


一、本节学习内容概要图


二、前言

前面我们已经学习了**if / elseswitchenum classmain 函数参数传递**等内容。

如果只是单独看这些语法,很多人会觉得"会写了,但不知道有什么用"。

所以这一节,不再只讲单个语法点,而是通过一个完整的小案例,把这些内容串起来:实现一个最基础的日志模块逻辑

在实际工程中,日志模块几乎无处不在。程序运行时,经常会输出调试信息、普通信息、错误信息、严重错误信息

并不是所有日志都要一直显示出来,通常都会让用户指定一个"最低显示级别",然后程序根据这个级别决定哪些日志该输出,哪些日志不输出

本节案例代码核心思路:
通过命令行参数控制日志级别,再结合枚举、条件判断和 switch,实现一个最基础的日志过滤功能。


三、案例需求与整体思路

案例的目标:让用户控制程序显示哪一级及以上的日志信息

3.1 日志级别设计

程序中定义了四种日志级别:

cpp 复制代码
enum class LogLevel
{
DEBUG,
INFO,
ERROR,
FATAL
};

它们默认按顺序对应为:

  • DEBUG = 0
  • INFO = 1
  • ERROR = 2
  • FATAL = 3

也就是说,日志级别是从低到高逐渐变严重的。

这也是后面能够直接用大小比较来判断日志是否输出的基础。

3.2 用户需求说明

用户运行程序时,可以在命令行(Powershell)中输入一个参数,例如:

bash 复制代码
.\test_main_log.exe debug
.\test_main_log.exe info
.\test_main_log.exe error
.\test_main_log.exe fatal

这个参数表示:用户希望看到的最低日志级别

例如:

(1)输入 debug
表示显示所有日志。

(2)输入 info
表示只显示 INFO 及以上日志。

(3)输入 error
表示只显示 ERRORFATAL 日志。

(4)输入 fatal
表示只显示最严重的 FATAL 日志。

3.3 本案例的核心思想

这个案例本质上就是一句话:

当前日志级别如果大于等于用户设置的最低级别,就输出;否则就不输出。

也就是代码中的核心判断:

cpp 复制代码
if (level >= logLevel)

四、完整代码

4.1 示例代码

cpp 复制代码
#include <iostream>
using namespace std;
enum class LogLevel
{
	DEBUG,
	INFO,
	ERROR,
	FATAL
};
int main(int argc,char *argv[])
{
	//用户传递日志的最低显示级别
	//debug < info < error < fatal
	// test_main_log info
	auto logLevel = LogLevel::DEBUG;
	if (argc > 1)
	{
		string levelstr = argv[1];
		if ("info" == levelstr)
			logLevel = LogLevel::INFO;
		else if("error" == levelstr)
			logLevel = LogLevel::ERROR;
		else if ("fatal" == levelstr)
			logLevel = LogLevel::FATAL;
	}


	///测试日志1 debug
	{
		auto level = LogLevel::DEBUG;
		string context = "test log 1";
		if (level >= logLevel)
		{
			string levelstr = "debug";
			switch (level)
			{
			case LogLevel::INFO:
				levelstr = "info";break;
			case LogLevel::ERROR:
				levelstr = "error";break;
			case LogLevel::FATAL:
				levelstr = "fatal";break;
			}

			cout << levelstr << ":" << context << endl;
		}
	}

	///测试日志2 INFO
	{
		auto level = LogLevel::INFO;
		string context = "test log 2";
		if (level >= logLevel)
		{
			string levelstr = "debug";
			switch (level)
			{
			case LogLevel::INFO:
				levelstr = "info";break;
			case LogLevel::ERROR:
				levelstr = "error";break;
			case LogLevel::FATAL:
				levelstr = "fatal";break;
			}

			cout << levelstr << ":" << context << endl;
		}
	}

	///测试日志3 ERROR
	{
		auto level = LogLevel::ERROR;
		string context = "test log 3";
		if (level >= logLevel)
		{
			string levelstr = "debug";
			switch (level)
			{
			case LogLevel::INFO:
				levelstr = "info";break;
			case LogLevel::ERROR:
				levelstr = "error";break;
			case LogLevel::FATAL:
				levelstr = "fatal";break;
			}

			cout << levelstr << ":" << context << endl;
		}
	}

	///测试日志4 FATAL
	{
		auto level = LogLevel::FATAL;
		string context = "test log 4";
		if (level >= logLevel)
		{
			string levelstr = "debug";
			switch (level)
			{
			case LogLevel::INFO:
				levelstr = "info";break;
			case LogLevel::ERROR:
				levelstr = "error";break;
			case LogLevel::FATAL:
				levelstr = "fatal";break;
			}

			cout << levelstr << ":" << context << endl;
		}
	}

}

4.2 Powershell 运行结果


五、代码实现与逻辑分析

5.1 先获取用户输入的日志级别

主函数写法如下:

cpp 复制代码
int main(int argc, char *argv[])

这里:

(1)argc 表示命令行参数个数
(2)argv 表示命令行参数数组

程序一开始先给出默认值:

cpp 复制代码
auto logLevel = LogLevel::DEBUG;

这表示如果用户没有传参数,那么默认显示所有日志。

接着程序判断用户是否输入了额外参数:

cpp 复制代码
if (argc > 1)
{
string levelstr = argv[1];
if ("info" == levelstr)
logLevel = LogLevel::INFO;
else if ("error" == levelstr)
logLevel = LogLevel::ERROR;
else if ("fatal" == levelstr)
logLevel = LogLevel::FATAL;
}

这里的含义是:

  • 如果输入的是 info,最低显示级别改成 INFO
  • 如果输入的是 error,最低显示级别改成 ERROR
  • 如果输入的是 fatal,最低显示级别改成 FATAL

至于 debug,代码里没有单独写判断,是因为默认值本来就是 DEBUG,所以即使不写也成立。


5.2 再准备要输出的测试日志

程序里一共模拟了四条日志,分别是:

cpp 复制代码
DEBUG : test log 1
INFO : test log 2
ERROR : test log 3
FATAL : test log 4

例如第一条日志的写法是:

cpp 复制代码
{
auto level = LogLevel::DEBUG;
string context = "test log 1";
...
}

这里:

  • level 表示这条日志本身的级别
  • context 表示日志内容

后面的三条日志也完全类似,只是级别和内容不同。

这样设计的好处是很直观,便于我们观察不同级别在不同输入条件下的输出效果。


5.3 日志过滤的核心判断

每一条日志在输出之前,都会先判断:

cpp 复制代码
if (level >= logLevel)

这里一定要分清两个量:

(1)level
表示当前这一条日志自己的级别。

(2)logLevel
表示用户要求的最低显示级别。

例如用户输入:

cpp 复制代码
.\test_main_log.exe error

那么:

cpp 复制代码
logLevel = LogLevel::ERROR;

这时程序依次判断:

  • DEBUG >= ERROR 不成立,不输出
  • INFO >= ERROR 不成立,不输出
  • ERROR >= ERROR 成立,输出
  • FATAL >= ERROR 成立,输出

所以最后只能看到:

bash 复制代码
error:test log 3
fatal:test log 4

这正好符合日志模块的设计目标。


5.4 为什么这里可以直接比较大小

很多初学者看到这里会有一个疑问:
枚举类型为什么能直接写 >= 比较?

原因就在于这里的枚举值本身是有顺序的

虽然代码里没有手动写具体数字,但默认就是从 0 开始递增

cpp 复制代码
DEBUG = 0
INFO = 1
ERROR = 2
FATAL = 3

所以:

cpp 复制代码
LogLevel::ERROR >= LogLevel::INFO

本质上就相当于:

cpp 复制代码
2 >= 1

结果当然为真。

也就是说,这个案例并不是随便定义一个枚举就能这样比较,而是因为 日志级别本身就天然具备"从低到高"的顺序关系,所以非常适合用枚举来表达。


5.5 switch 在这里起什么作用

(1)switch 不是"判断和外面传入的 logLevel 比较"

switch(level) 判断的对象是当前日志自己的级别 level,不是外面的 logLevel

也就是说:

cpp 复制代码
switch (level)

这里是在看:

当前这条日志到底是 DEBUG、INFO、ERROR 还是 FATAL。

它的作用不是决定"能不能输出",而是决定:

输出时前面的字符串写成什么。


(2)switch 不会影响"后续输出内容"

如果 level 和某个 case 匹配,就把当前这一小段代码里的 levelstr 改成对应字符串,供这一条日志输出使用。

它只是改了当前这一条日志输出前缀,不会影响后面的其他日志块。

比如这一段里:

cpp 复制代码
string levelstr = "debug";

先默认写成 "debug"

然后:

cpp 复制代码
switch (level)
{
case LogLevel::INFO:
levelstr = "info";break;
case LogLevel::ERROR:
levelstr = "error";break;
case LogLevel::FATAL:
levelstr = "fatal";break;
}

如果 level 是:

  • INFO,就把 levelstr 改成 "info"
  • ERROR,就改成 "error"
  • FATAL,就改成 "fatal"

但如果 levelDEBUG,由于这里没有写 case LogLevel::DEBUG,就不会进入任何 case,于是:

cpp 复制代码
levelstr

仍然保持最开始的 "debug"

(3)代码真实逻辑:

1)先判断能不能输出
cpp 复制代码
if (level >= logLevel)
2)如果能输出,再决定输出前缀是什么
cpp 复制代码
switch (level)
3)最后输出
cpp 复制代码
cout << levelstr << ":" << context << endl;

六、当前 if 不成立,为什么后续日志还能继续输出

这里有一个非常容易混淆的地方,需要特别说明。

当程序执行到某一条日志时,会先判断:

cpp 复制代码
if (level >= logLevel)

这个判断 只负责控制当前这一条日志是否输出,并不会让整个程序后面的代码都停止执行。

例如,第一条日志是:

cpp 复制代码
auto level = LogLevel::DEBUG;

如果用户输入的是:

cpp 复制代码
.\test_main_log.exe info

那么此时用户设置的最低显示级别就是:

cpp 复制代码
logLevel = LogLevel::INFO;

因此第一条日志对应的判断就是:

cpp 复制代码
if (LogLevel::DEBUG >= LogLevel::INFO)

这个条件不成立,于是结果只是:

(1)当前这一条 DEBUG 日志不输出;
(2)当前这个 if 大括号内部的 switchcout 都不会执行;
(3)程序继续往下运行,去判断下一条日志。

也就是说,程序并不会因为第一条日志没有通过判断,就把后面所有代码都跳过。

它只是跳过了"当前这一条日志对应的输出代码块",然后继续执行下面的 INFOERRORFATAL 日志判断。

可以把它理解成下面这样:

  • 第一条日志判断一次
  • 第二条日志再判断一次
  • 第三条日志再判断一次
  • 第四条日志再判断一次

它们彼此是独立的。

所以当用户输入 info 时,程序的执行效果实际上是:

bash 复制代码
DEBUG >= INFO // 不成立,第一条不输出
INFO >= INFO // 成立,第二条输出
ERROR >= INFO // 成立,第三条输出
FATAL >= INFO // 成立,第四条输出

最终结果就是:

bash 复制代码
info:test log 2
error:test log 3
fatal:test log 4

因此,这里一定要明确:

if 语句只决定"当前这一条日志要不要输出",不会阻止程序继续判断后面的日志。


七、小结

这个日志模块虽然不大,但已经把前面学过的多个知识点串联了起来,包括:

7.1 本节涉及的核心知识

(1)使用 enum class 表示一组固定状态;
(2)使用 argcargv 获取用户输入;
(3)使用 if / else if 解析命令行参数;
(4)使用关系运算完成日志级别过滤;
(5)使用 switch 把枚举值转换成可输出字符串。

7.2 这个案例的实战意义

在真正的工程开发中,日志模块是非常常见的功能。

虽然这里写的还是一个简化版,但它已经具备了日志系统最核心的思想:不是所有日志都无条件输出,而是根据设定的级别进行筛选。

7.3 日志流程

  1. 程序启动后,先将默认最低日志级别设为 LogLevel::DEBUG,这意味如果用户没有额外传参,程序会显示全部日志。
  2. 随后,程序通过 argcargv 读取命令行参数;如果用户输入了 infoerrorfatal,就把内部的 logLevel 修改为对应级别。
  3. 接着,程序并不是一次性统一处理所有日志,而是按顺序分别处理四条测试日志:DEBUGINFOERRORFATAL
  4. 每处理一条日志,都会先取出这条日志自己的级别 level,再与用户设置的最低显示级别 logLevel 进行比较,即判断 if (level >= logLevel) 是否成立。
  5. 如果成立,说明这条日志应该显示,于是进入 if 内部,再通过 switch(level) 把枚举级别转换成对应的字符串,例如 "debug""info""error""fatal",最后与日志内容 context 一起输出;
  6. 如果条件不成立,则说明这条日志级别不够,不会输出,但程序不会停止,而是继续往下执行,判断下一条日志。
  7. 正因为每条日志都是独立判断的,所以当用户输入 info 时,第一条 DEBUG 日志会被过滤掉,但后面的 INFOERRORFATAL 仍然会继续判断并正常输出。

整个模块的核心思想就是:用命令行参数确定最低显示级别,用条件判断决定当前日志是否输出,用 switch 负责把日志级别转换成最终显示给用户的文本。

相关推荐
熬夜敲代码的猫4 小时前
C/C++:内存管理
c语言·c++·动态内存管理
故事和你914 小时前
洛谷-数据结构1-2-二叉树1
开发语言·数据结构·c++·算法·leetcode·动态规划·图论
大橘4 小时前
【qml-5.1】qml与c++交互(QML_ELEMENT/QML_SINGLETON)
开发语言·c++·qt·交互·qml
aq55356004 小时前
PHP vs C++ vs 易语言:编程语言终极对比
开发语言·c++·php
tankeven4 小时前
HJ181 相差不超过k的最多数
数据结构·c++·算法
AlbertS5 小时前
distcc + ccache 编译递归问题排查总结
c++·cmake·gcc·g++·distcc·ccache
小苗卷不动5 小时前
ps axj | grep 和 which命令
c++
云泽8085 小时前
第十五届蓝桥杯大赛软件赛省赛C/C++大学B组
c语言·c++·算法·蓝桥杯
Wadli5 小时前
集群C++聊天服务器
服务器·开发语言·c++
洛水水6 小时前
# 线程池详解:从原理到实现
c++·线程池