LV060-printf扩展
本文主要是 C 语言基础——printf 扩展相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正 😃。
前边我们已经了解到了 printf 函数是可以打印出带颜色的提示信息的,接下来我们来使用宏定义重新定义一下 printf,便于我们调试。
一、颜色值
字体颜色和背景颜色值如下:
| 颜色名 | 前景色(字)码 | 背景色码 |
|---|---|---|
| 黑 | 30 | 40 |
| 红 | 31 | 41 |
| 绿 | 32 | 42 |
| 黄 | 33 | 43 |
| 蓝 | 34 | 44 |
| 品红 | 35 | 45 |
| 青 | 36 | 46 |
| 白 | 37 | 47 |
| 默认 | 39 | 49 |
| 重置 | 0 | 0 |
注意: 重置 色重置所有颜色、字体效果,默认 色只重置颜色。
二、宏定义
1.调试类型
我们平时打印的信息大概会有这几种:错误、警告和信息
#define ERROR "ERROR" /* 错误 */
#define WARN "WARN " /* 警告 */
#define INFO "INFO " /* 信息 */2.颜色定义
2.1 字体
/* 颜色定义——字体颜色 */
#define CLS_ALL "\033[0m" /* 清除所有颜色 */
#define F_BLACK "\033[30m" /* 黑色字体 */
#define F_RED "\033[31m" /* 红色字体 */
#define F_GREEN "\033[32m" /* 绿色字体 */
#define F_YELLOW "\033[33m" /* 黄色字体 */
#define F_BLUE "\033[34m" /* 蓝色字体 */
#define F_PURPLE "\033[35m" /* 紫色字体 */
#define F_CYAN "\033[36m" /* 青色字体 */
#define F_WHITE "\033[37m" /* 白色字体 */2.2 背景
/* 颜色定义——背景颜色 */
#define B_BLACK "\033[40m" /* 黑色背景 */
#define B_RED "\033[41m" /* 红色背景 */
#define B_GREEN "\033[42m" /* 绿色背景 */
#define B_YELLOW "\033[43m" /* 黄色背景 */
#define B_BLUE "\033[44m" /* 蓝色背景 */
#define B_PURPLE "\033[45m" /* 紫色背景 */
#define B_CYAN "\033[46m" /* 青色背景 */
#define B_WHITE "\033[47m" /* 白色背景 */2.3 带背景的字体
/* 颜色定义——背景的字体 */
#define BLACK_RED "\033[30;41m" /* 红底黑字 */
#define BLACK_GREEN "\033[30;42m" /* 绿底黑字 */
#define BLACK_YELLOW "\033[30;43m" /* 黄底黑字 */3.调试信息的开关
#define DEBUG_ENABLE 1 /* 是否开启调试信息 */
#define LOG_LEVEL DEBUG_ALL /* 调试信息显示级别 */
/* 调试等级枚举类型定义 */
enum DEBUG_LEVEL
{
DEBUG_OFF = 0,
DEBUG_ERROR = 1,
DEBUG_WARN = 2,
DEBUG_INFO = 3,
DEBUG_ALL = 4
};4.宏定义
#define MYDEBUG(TYPE, COLOR, FMT, ARGS...) \
do \
{ \
if(DEBUG_ENABLE && (DEBUG_##TYPE <= LOG_LEVEL)) \
{ \
printf(COLOR); \
printf("[%s][%s:%s:%d]"FMT, TYPE, __FILE__, __FUNCTION__, __LINE__, ##ARGS); \
printf(CLS_ALL"\n"); \
} \
} while(0)【参数说明】
- TYPE :调试类型,可选值有 ERROR、WARN 和 INFO。
- COLOR : 打印信息的颜色。
- FMT :格式化字符串,与 printf 一致。
- ARGS ... :可变参数,与 printf 一致。
三、时间戳
有的时候,我们想要打印出的数据带上时间戳,类似这样:
[12-4 12:00:05]这样其实也是可以做到的,这样打印出时间戳有时候对我们也是很有用的。
1. 时间函数
在 linux 中有很多的时间函数可以获取到当前的系统时间,由于我们的代码大部分是跑在 linux 系统上,所以这里就使用 linux 内核为我们提供的时间函数来获取时间戳。
1.1 clock_gettime()
在 linux 下可以使用 man 命令查看该函数的帮助手册。
/* 需包含的头文件 */
#include <time.h>
/* 函数声明 */
int clock_gettime(clockid_t clk_id, struct timespec *tp);【函数说明】 该函数用于获取时间,它的计量单位为十亿分之一,就是纳秒( ns )。
【函数参数】
- clk_id : clockid_t 类型,表示要获取的时间的类型。
CLOCK_REALTIME 从1970.1.1到目前的时间
CLOCK_MONOTONIC 系统启动到现在的时间
CLOCK_THREAD_CPUTIME_ID 线程所消耗的时间
CLOCK_PROCESS_CPUTIME_ID 进程所消耗的时间
CLOCK_MONOTONIC_RAW 基于硬件时间
CLOCK_REALTIME_COARSE 不精确的REALTIME
CLOCK_MONOTONIC_COARSE 不精确的启动时间在这里我们可以使用 CLOCK_REALTIME 来获取时间。
- tp :truct timespec 类型结构体指针变量,他有两个成员,分别代表秒和纳秒。结构体定义在 time.h 文件中
struct timespec {
time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};【返回值】 int 类型,成功返回 0,失败返回 -1。
【使用格式】 一般情况下基本使用格式如下:
/* 需要包含的头文件 */
#include <time.h>
/* 至少应该有的语句 */
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);【注意】 none
1.2 localtime() 函数
在 linux 下可以使用 man 3 localtime 命令查看该函数的帮助手册。
/* 需包含的头文件 */
#include <time.h>
/* 函数声明 */
struct tm *localtime(const time_t *timep);
struct tm *localtime_r(const time_t *timep, struct tm *result);【函数说明】 该函数可以把 time() 或 gettimeofday() 或者 clock_gettime()得到的秒数( time_t 时间或日历时间)变成一个 struct tm 结构体所表示的时间,该时间对应的是本地时间。 localtime_r() 为 localtime() 函数的可重入版本,更加推荐使用。
【函数参数】
timep : time_t 类型指针变量,该参数包含了一个日历时间,也就是上边获取时间函数获取的那个秒数。
result : struct tm 类型结构体指针变量,该参数为可重入版本的函数 localtime_r() 所有,转换后的本地时间将存放在该参数指向的结构体中。 struct tm 结构体成员如下:
在使用 man 手册的时候,下边会有这个结构体成员的说明:
struct tm
{
int tm_sec; /* 秒 Seconds (0-60) */
int tm_min; /* 分 Minutes (0-59)*/
int tm_hour; /* 时 Hours (0-23) */
int tm_mday; /* 日 Day of the month (1-31) */
int tm_mon; /* 月 Month (0-11) */
int tm_year; /* 年 Year - 1900 */
int tm_wday; /* 周 Day of the week (0-6, Sunday = 0) */
int tm_yday; /* 一年中的第几天 Day in the year (0-365, 1 Jan = 0) */
int tm_isdst; /* 夏令时 Daylight saving time */
};要想得到实际的时间,那么年份需要加上 1900 ,月份需要加上 1 。这是因为这个函数内部转换的时候,年份从 1900 开始算,月份从 0 开始算了。
【返回值】 struct tm 类型的结构体指针,成功则返回一个有效的 struct tm 结构体指针,而对于可重入版本 localtime_r() 来说,成功执行情况下,返回值将会等于参数 result ;失败则返回 NULL 。
【使用格式】 一般情况下基本使用格式如下:
/* 需要包含的头文件 */
#include <time.h>
/* 至少应该有的语句 */
struct tm nowTime;
time_t tm;
tm = time(NULL);
localtime_r(&tm, &nowTime);【注意】 none
2. 获取时间字符串
我们通过上边的两个函数就可以获取到一个时间,然后我们将其封装成一个函数,让其返回一个时间字符串,让 printf 打印的时候打印出来就可以啦。
#include <time.h>
// 获取时间函数
char *timeString(void)
{
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
struct tm *timeinfo = localtime(&ts.tv_sec);
static char timeString[30];
sprintf(timeString, "%.2d-%.2d %.2d:%.2d:%.2d",
timeinfo->tm_mon + 1,
timeinfo->tm_mday,
timeinfo->tm_hour,
timeinfo->tm_min,
timeinfo->tm_sec);
return timeString;
}通过这个函数我们就可以得到当前打印的时候的系统时间,例如:
printf("%s\n", timeString());这样我们就会得到这样的一个时间:
12-04 08:23:15我们用于 printf 的时候就会打印出这个时间字符串啦。
四、文件名
其实我们这个例子中只有一个文件,所以 C 语言的预定义宏 FILE 是可以只打印出文件名,其实这个是会包含路径的,当我们的工程目录较深的时候,又想知道是哪个文件中的打印数据,这而路径太长又会影响,我们只想要文件名怎么办呢?其实也是可以做到的,我们可以使用相关函数去掉路径。
1. strrchr()
在 linux 下可以使用 man 命令查看该函数的帮助手册。
/* 需包含的头文件 */
#include <string.h>
/* 函数声明 */
char *strrchr(const char *s, int c);【函数说明】 该函数用于在参数 s 所指向的字符串中搜索最后一次出现字符 c(一个无符号字符)的位置,并返回这个位置,注意这里的返回值也是指针类型,这也就意味着它返回的是一个地址,我们可以使用这个地址访问这个字符 c 位置开始的数据。这样我们就可以使用这个函数来找到路径中的 / 字符,然后只要它后边的文件名即可。
【函数参数】
s : char *类型,表示要查找的字符串。
c : int 类型,表示要查找的字符。
【返回值】 char *类型,该函数返回 str 中最后一次出现字符 c 的位置。如果未找到,则函数返回一个空指针。
【使用格式】 none
【注意】 none
2. 获取文件名
我们就可以定义一个宏,来获取文件名,如下所示:
// windows
#define filename(x) (strrchr(x,'\\')?(strrchr(x,'\\') + 1):x)
// linux
#define filename(x) (strrchr(x,'/')?(strrchr(x,'/')+1):x)这样,我们就可以这样使用定义的宏啦:
printf("%s\n", filename("/a/b/c/d/file.c"));然后我们就会得到一个不包含路径的文件名啦、
五、使用实例
#include <stdio.h>
#define DEBUG_ENABLE 1 /* 是否开启调试信息 */
#define LOG_LEVEL DEBUG_ALL /* 调试信息显示级别 */
#define ERROR "ERROR" /* 错误 */
#define WARN "WARN " /* 警告 */
#define INFO "INFO " /* 信息 */
/* 颜色定义——字体颜色 */
#define CLS_ALL "\033[0m" /* 清除所有颜色 */
#define F_BLACK "\033[30m" /* 黑色字体 */
#define F_RED "\033[31m" /* 红色字体 */
#define F_GREEN "\033[32m" /* 绿色字体 */
#define F_YELLOW "\033[33m" /* 黄色字体 */
#define F_BLUE "\033[34m" /* 蓝色字体 */
#define F_PURPLE "\033[35m" /* 紫色字体 */
#define F_CYAN "\033[36m" /* 青色字体 */
#define F_WHITE "\033[37m" /* 白色字体 */
/* 颜色定义——背景颜色 */
#define B_BLACK "\033[40m" /* 黑色背景 */
#define B_RED "\033[41m" /* 红色背景 */
#define B_GREEN "\033[42m" /* 绿色背景 */
#define B_YELLOW "\033[43m" /* 黄色背景 */
#define B_BLUE "\033[44m" /* 蓝色背景 */
#define B_PURPLE "\033[45m" /* 紫色背景 */
#define B_CYAN "\033[46m" /* 青色背景 */
#define B_WHITE "\033[47m" /* 白色背景 */
/* 颜色定义——背景的字体 */
#define BLACK_RED "\033[30;41m" /* 红底黑字 */
#define BLACK_GREEN "\033[30;42m" /* 绿底黑字 */
#define BLACK_YELLOW "\033[30;43m" /* 黄底黑字 */
/* 调试等级枚举类型定义 */
enum DEBUG_LEVEL
{
DEBUG_OFF = 0,
DEBUG_ERROR = 1,
DEBUG_WARN = 2,
DEBUG_INFO = 3,
DEBUG_ALL = 4
};
#define MYDEBUG(TYPE, COLOR, FMT, ARGS...) \
do \
{ \
if(DEBUG_ENABLE && (DEBUG_##TYPE <= LOG_LEVEL)) \
{ \
printf(COLOR); \
printf("[%s][%s:%s:%d]"FMT, TYPE, __FILE__, __FUNCTION__, __LINE__, ##ARGS); \
printf(CLS_ALL"\n"); \
} \
} while(0)
int main(int argc, char *argv[])
{
/* code */
MYDEBUG(ERROR, BLACK_RED, "%s", "hello World!");
MYDEBUG(INFO, BLACK_GREEN, "%s", "hello World!");
MYDEBUG(WARN, BLACK_YELLOW, "%s", "hello World!");
MYDEBUG(INFO, F_BLACK, "%s", "hello World!");
MYDEBUG(INFO, F_BLUE, "%s", "hello World!");
MYDEBUG(INFO, F_CYAN, "%s", "hello World!");
MYDEBUG(INFO, F_GREEN, "%s", "hello World!");
MYDEBUG(INFO, F_PURPLE, "%s", "hello World!");
MYDEBUG(INFO, F_RED, "%s", "hello World!");
MYDEBUG(INFO, F_WHITE, "%s", "hello World!");
MYDEBUG(INFO, F_YELLOW, "%s", "hello World!");
return 0;
}显示效果如下:

六、自用 printf
经过上边的实例以及相关内容的了解,我们可以定义一个自己的 printf 函数,它可以包含时间戳,文件名,函数名以及函数的行号,这样在我们查看打印信息的时候会很方便。
1. 常用调试宏定义
1.1 pub_print.h
#ifndef __PUB_PRINTF_H__
#define __PUB_PRINTF_H__
#include <stdio.h>
#define DEBUG_ENABLE 1 /* 是否开启调试信息 */
#define LOG_LEVEL DEBUG_ALL /* 调试信息显示级别 */
#define ERROR "ERROR" /* 错误 */
#define WARN "WARN " /* 警告 */
#define INFO "INFO " /* 信息 */
/* 颜色定义——字体颜色 */
#define CLS_ALL "\033[0m" /* 清除所有颜色 */
#define F_BLACK "\033[30m" /* 黑色字体 */
#define F_RED "\033[31m" /* 红色字体 */
#define F_GREEN "\033[32m" /* 绿色字体 */
#define F_YELLOW "\033[33m" /* 黄色字体 */
#define F_BLUE "\033[34m" /* 蓝色字体 */
#define F_PURPLE "\033[35m" /* 紫色字体 */
#define F_CYAN "\033[36m" /* 青色字体 */
#define F_WHITE "\033[37m" /* 白色字体 */
/* 颜色定义——背景颜色 */
#define B_BLACK "\033[40m" /* 黑色背景 */
#define B_RED "\033[41m" /* 红色背景 */
#define B_GREEN "\033[42m" /* 绿色背景 */
#define B_YELLOW "\033[43m" /* 黄色背景 */
#define B_BLUE "\033[44m" /* 蓝色背景 */
#define B_PURPLE "\033[45m" /* 紫色背景 */
#define B_CYAN "\033[46m" /* 青色背景 */
#define B_WHITE "\033[47m" /* 白色背景 */
/* 颜色定义——背景的字体 */
#define BLACK_RED "\033[30;41m" /* 红底黑字 */
#define BLACK_GREEN "\033[30;42m" /* 绿底黑字 */
#define BLACK_YELLOW "\033[30;43m" /* 黄底黑字 */
/* 调试等级枚举类型定义 */
enum DEBUG_LEVEL
{
DEBUG_OFF = 0,
DEBUG_ERROR = 1,
DEBUG_WARN = 2,
DEBUG_INFO = 3,
DEBUG_ALL = 4
};
/* 自定义类型和字体颜色 */
#define MYDEBUG(TYPE, COLOR, FMT, ARGS...) \
do \
{ \
if(DEBUG_ENABLE && (DEBUG_##TYPE <= LOG_LEVEL)) \
{ \
printf(COLOR); \
printf("[%s][%s:%s:%d]"FMT, TYPE, __FILE__, __FUNCTION__, __LINE__, ##ARGS); \
printf(CLS_ALL"\n"); \
} \
} while(0)
/* 平时调试使用 */
#include <string.h>
#include <time.h> // 时间函数用
#define filename(x) (strrchr(x,'/')?(strrchr(x,'/')+1):x)
#define NORMAL_DEBUG 1 // 注意使用下边两个宏需要自己添加换行,比较符合平时的习惯
// 获取时间函数
char *timeString(void)
{
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
struct tm *timeinfo = localtime(&ts.tv_sec);
static char timeString[30];
sprintf(timeString, "%.2d-%.2d %.2d:%.2d:%.2d",
timeinfo->tm_mon + 1,
timeinfo->tm_mday,
timeinfo->tm_hour,
timeinfo->tm_min,
timeinfo->tm_sec);
return timeString;
}
// 获取系统时间戳 单位 ms
int sys_pts_get_ms(void)
{
struct timespec tv;
long long lasttime = 0;
clock_gettime(CLOCK_MONOTONIC, &tv);
lasttime = tv.tv_sec * 1000 + tv.tv_nsec / (1000 * 1000);
return lasttime;
}
#define PRT(fmt...) \
do \
{ \
if(NORMAL_DEBUG) \
{ \
printf(F_YELLOW); \
printf("[%s][%s:%d][%s]", timeString(), filename(__FILE__), __LINE__, __FUNCTION__); \
printf(CLS_ALL); \
printf(fmt); \
} \
} while(0)
#define PRTE(fmt...) \
do \
{ \
if(NORMAL_DEBUG) \
{ \
printf(F_RED); \
printf("[%s][%s:%d][%s]", timeString(), filename(__FILE__), __LINE__, __FUNCTION__); \
printf(CLS_ALL); \
printf(fmt); \
} \
} while(0)
#endif1.2 main.c
#include <stdio.h>
#include "pub_print.h"
int main(int argc, char *argv[])
{
int *p = NULL;
int a = 100;
MYDEBUG(INFO, F_CYAN, "hello world!");
PRT("hello world!\n");
PRTE("hello world!\n");
PRT("hello world! %s\n", "sumu");
PRTE("hello world! %s\n", "sumu");
return 0;
}1.3 Makefile
TARGET := main
CC := $(CROSS_COMPILE)gcc
## 目录路径设置
INCDIRS := .
SRCDIRS := .
OBJDIRS := .
# 生成编译器的 -I dir_path 参数
INCLUDE := $(patsubst %, -I %, $(INCDIRS))
# 获取所有的 .C 文件名称(包含路径)
CFILES := $(foreach dir, $(SRCDIRS), $(wildcard $(dir)/*.c))
CFILENDIR := $(notdir $(CFILES))
COBJS := $(patsubst %, $(OBJDIRS)/%, $(CFILENDIR:.c=.o))
OBJS := $(SOBJS) $(COBJS)
# 设置源文件路径
VPATH := $(SRCDIRS)
# 生成目标文件
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) $^ -o $@
$(COBJS) : $(OBJDIRS)/%.o : %.c
$(CC) $(CFLAGS) $(INCLUDE) -c $< -o $@
## 清除文件
.PHONY: clean clean_o
# 删除所有编译链接生成的文件命令
clean: clean_o
@rm -vf $(TARGET)
# 删除所有的 .o 文件命令
clean_o:
@rm -vf *.o1.4 最终效果
这里就懒得放图片了,大概打印的内容如下:
[INFO ][main.c:main:20]hello world!
[12-04 08:43:47][HK_LOG][main.c:21][main]hello world!
[12-04 08:43:47][HK_ERR][main.c:22][main]hello world!
[12-04 08:43:47][HK_LOG][main.c:23][main]hello world! sumu
[12-04 08:43:47][HK_ERR][main.c:24][main]hello world! sumu2. 错误打印屏蔽
2.1 出现的问题
我们在写代码的时候会判断一个函数的返回值,若是执行出现错误,最好就是打印出错误信息,可是有些程序这里是一个循环,每次都会进,每次都会出现问题,那么打印就会一直刷,打印信息也是会占用资源的,狂刷的话,会影响我们其他功能,那怎么屏蔽呢?接下来来看一个实例吧。
2.2 打印屏蔽实例
// main.c 此实例不带颜色打印
#include <stdio.h>
#include <time.h>
#include <unistd.h>
typedef unsigned int (*PRT_FILTER_FUNC)(const char func[], unsigned int line, unsigned int filterTime);
extern unsigned int print_filter(const char func[], unsigned int line, unsigned int filterTime);
PRT_FILTER_FUNC errPrtFilter = print_filter;
#define PRT_MAX_PRT_LOC_CNT (32)
unsigned int gPrtLocSum[PRT_MAX_PRT_LOC_CNT] = {0};
unsigned long long gPrtLocTime[PRT_MAX_PRT_LOC_CNT] = {0};
unsigned int gPrtLogIdx = 0;
unsigned int errDebug = 1;
unsigned long long sys_pts_get_ms()
{
struct timespec tv;
unsigned long long lasttime = 0;
/*CLOCK_REALTIME:这种类型的时钟可以反映 wall clocktime,用的是绝对时间*/
/*当系统的时钟源被改变,或者系统管理员重置了系统时间之后,这种类型的时钟可以得到相应的调整*/
/*CLOCK_MONOTONIC:用的是相对时间,他的时间是通过 jiffies 值来计算的*/
/*该时钟不受系统时钟源的影响,只受 jiffies 值的影响。*/
clock_gettime(CLOCK_MONOTONIC, &tv);
lasttime = tv.tv_sec * 1000 + tv.tv_nsec/(1000 * 1000);
return lasttime;
}
unsigned int print_filter(const char func[], unsigned int line, unsigned int filterTime)
{
unsigned long long curTime = 0, maxTime = 0;
unsigned int sum = 0;
unsigned int i = 0;
// 获取当前时间
curTime = sys_pts_get_ms();
//提取特征
while(func[i])
{
sum += func[i];
i++;
}
sum = ((sum & 0x1FFF) << 19) + ((line * i) & 0x7FFFF);
// 匹配最近一次打印
for(i = 0; i < PRT_MAX_PRT_LOC_CNT; i++)
{
if(gPrtLocSum[i] == sum && maxTime < gPrtLocTime[i])
{
maxTime = gPrtLocTime[i];
}
}
if(curTime > maxTime && (curTime - maxTime) < filterTime)
{
return 0; // 短时间内连续打印,需要屏蔽
}
gPrtLocSum[gPrtLogIdx] = sum;
gPrtLocTime[gPrtLogIdx] = curTime;
gPrtLogIdx = (gPrtLogIdx + 1) % PRT_MAX_PRT_LOC_CNT;
return 1;
}
#define PRTE(fmt...) \
do {\
if (errDebug) \
{ \
if(errPrtFilter && errPrtFilter(__FUNCTION__, __LINE__, 10000)) \
{ \
printf("[%s][%d]...", __FUNCTION__, __LINE__);\
printf(fmt); \
} \
} \
}while(0)
int main(int argc, char *argv[])
{
int i = 0;
while(1)
{
PRTE("i = %d\n", i);
printf("i = %d\n", i++);
i %= 100;
sleep(1);
}
return 0;
}最终效果就是:

前面设置的是10s只打印一次。