Skip to content

LV060-printf扩展

本文主要是 C 语言基础——printf 扩展相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正 😃。

前边我们已经了解到了 printf 函数是可以打印出带颜色的提示信息的,接下来我们来使用宏定义重新定义一下 printf,便于我们调试。

一、颜色值

字体颜色和背景颜色值如下:

颜色名前景色(字)码背景色码
3040
3141
绿3242
3343
3444
品红3545
3646
3747
默认3949
重置00

注意: 重置 色重置所有颜色、字体效果,默认 色只重置颜色。

二、宏定义

1.调试类型

我们平时打印的信息大概会有这几种:错误、警告和信息

c
#define ERROR   "ERROR"           /* 错误 */
#define WARN    "WARN "           /* 警告 */
#define INFO    "INFO "           /* 信息 */

2.颜色定义

2.1 字体

c
/* 颜色定义——字体颜色 */
#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 背景

c
/* 颜色定义——背景颜色 */
#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 带背景的字体

c
/* 颜色定义——背景的字体 */
#define BLACK_RED      "\033[30;41m" /* 红底黑字 */
#define BLACK_GREEN    "\033[30;42m" /* 绿底黑字 */
#define BLACK_YELLOW   "\033[30;43m" /* 黄底黑字 */

3.调试信息的开关

c
#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.宏定义

c
#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 一致。

三、时间戳

有的时候,我们想要打印出的数据带上时间戳,类似这样:

shell
[12-4 12:00:05]

这样其实也是可以做到的,这样打印出时间戳有时候对我们也是很有用的。

1. 时间函数

在 linux 中有很多的时间函数可以获取到当前的系统时间,由于我们的代码大部分是跑在 linux 系统上,所以这里就使用 linux 内核为我们提供的时间函数来获取时间戳。

1.1 clock_gettime()

在 linux 下可以使用 man 命令查看该函数的帮助手册。

c
/* 需包含的头文件 */
#include <time.h>

/* 函数声明 */
int clock_gettime(clockid_t clk_id, struct timespec *tp);

函数说明】 该函数用于获取时间,它的计量单位为十亿分之一,就是纳秒( ns )。

函数参数

  • clk_id : clockid_t 类型,表示要获取的时间的类型。
c
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 文件中
c
struct timespec {
    time_t   tv_sec;        /* seconds */
    long     tv_nsec;       /* nanoseconds */
};

返回值】 int 类型,成功返回 0,失败返回 -1。

使用格式】 一般情况下基本使用格式如下:

c
/* 需要包含的头文件 */
#include <time.h>

/* 至少应该有的语句 */
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);

注意】 none

1.2 localtime() 函数

在 linux 下可以使用 man 3 localtime 命令查看该函数的帮助手册。

c
/* 需包含的头文件 */
#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 手册的时候,下边会有这个结构体成员的说明:

c
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 。

使用格式】 一般情况下基本使用格式如下:

c
/* 需要包含的头文件 */
#include <time.h>

/* 至少应该有的语句 */
struct tm nowTime;
time_t tm;
tm = time(NULL);
localtime_r(&tm, &nowTime);

注意】 none

2. 获取时间字符串

我们通过上边的两个函数就可以获取到一个时间,然后我们将其封装成一个函数,让其返回一个时间字符串,让 printf 打印的时候打印出来就可以啦。

c
#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;
}

通过这个函数我们就可以得到当前打印的时候的系统时间,例如:

c
printf("%s\n", timeString());

这样我们就会得到这样的一个时间:

shell
12-04 08:23:15

我们用于 printf 的时候就会打印出这个时间字符串啦。

四、文件名

其实我们这个例子中只有一个文件,所以 C 语言的预定义宏 FILE 是可以只打印出文件名,其实这个是会包含路径的,当我们的工程目录较深的时候,又想知道是哪个文件中的打印数据,这而路径太长又会影响,我们只想要文件名怎么办呢?其实也是可以做到的,我们可以使用相关函数去掉路径。

1. strrchr()

在 linux 下可以使用 man 命令查看该函数的帮助手册。

c
/* 需包含的头文件 */
#include <string.h>

/* 函数声明 */
char *strrchr(const char *s, int c);

函数说明】 该函数用于在参数 s 所指向的字符串中搜索最后一次出现字符 c(一个无符号字符)的位置,并返回这个位置,注意这里的返回值也是指针类型,这也就意味着它返回的是一个地址,我们可以使用这个地址访问这个字符 c 位置开始的数据。这样我们就可以使用这个函数来找到路径中的 / 字符,然后只要它后边的文件名即可。

函数参数

  • s : char *类型,表示要查找的字符串。

  • c : int 类型,表示要查找的字符。

返回值】 char *类型,该函数返回 str 中最后一次出现字符 c 的位置。如果未找到,则函数返回一个空指针。

使用格式】 none

注意】 none

2. 获取文件名

我们就可以定义一个宏,来获取文件名,如下所示:

c
// windows
#define filename(x) (strrchr(x,'\\')?(strrchr(x,'\\') + 1):x)
// linux
#define filename(x) (strrchr(x,'/')?(strrchr(x,'/')+1):x)

这样,我们就可以这样使用定义的宏啦:

c
printf("%s\n", filename("/a/b/c/d/file.c"));

然后我们就会得到一个不包含路径的文件名啦、

五、使用实例

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;
}

显示效果如下:

image-20221108223600467

六、自用 printf

经过上边的实例以及相关内容的了解,我们可以定义一个自己的 printf 函数,它可以包含时间戳,文件名,函数名以及函数的行号,这样在我们查看打印信息的时候会很方便。

1. 常用调试宏定义

1.1 pub_print.h

c
#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)

#endif

1.2 main.c

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

c
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 *.o

1.4 最终效果

这里就懒得放图片了,大概打印的内容如下:

shell
[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! sumu

2. 错误打印屏蔽

2.1 出现的问题

我们在写代码的时候会判断一个函数的返回值,若是执行出现错误,最好就是打印出错误信息,可是有些程序这里是一个循环,每次都会进,每次都会出现问题,那么打印就会一直刷,打印信息也是会占用资源的,狂刷的话,会影响我们其他功能,那怎么屏蔽呢?接下来来看一个实例吧。

2.2 打印屏蔽实例

c
// 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;
}

最终效果就是:

image-20260206184708979

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