一、将调试信息输出到屏幕中
1.1 一般写法
我们平常在写代码时,肯定会有一些调试信息的输出:
#include <stdio.h>
#include <stdlib.h>
int main()
{
char szFileName[] = "test.txt";
FILE *fp = fopen(szFileName, "r");
if (fp == NULL)
{
// 文件打开失败,提示错误并退出
printf("open file(%s) error.\n", szFileName);
exit(0);
}
else
{
// 文件打开成功,进行相应的文件读/写操作
}
return 0;
}
假设当前目录下没有 test.txt 文件。当程序执行到第 7 行时,必然返回 NULL,这时候通过第 11 行的调试信息,我们可以帮助我们精确排查到程序退出的原因:原来是文件打开失败了。
那如果当前目录下存在 test.txt 文件,只是不可读呢?
- 同样输出了 open file(test.txt) error
在这种情况下如何快速定位文件打开失败的原因呢?我们可以考虑使用 errno。
1.2 使用 errno
errno 是记录系统的最后一次错误代码。错误代码是一个 int 型的值,在 errno.h 中定义。
#include <errno.h> // errno 头文件
#include <string.h> // strerror 头文件
// 文件打开失败,提示错误并退出
printf("open file(%s) error, errno[%d](%s).\n", szFileName, errno, strerror(errno));
修改后再次运行 main.exe:
如果代码中包含很多的调试信息呢?我们并不能一下子知道这条信息到底是在哪里打印出来的,于是,我们又想,能不能把当前调试信息所在的文件名和源码行位置也打印出来呢,这样不就一目了然了吗。基于此,便有了 1.3 的内容。
1.3 编译器内置宏
ANSI C 标准中有几个标准预定义宏:
__LINE__
:在源代码中插入当前源代码行号__FILE__
:在源文件中插入当前源文件名__FUNCTION_
:在源文件中插入当前函数名__DATE__
:在源文件中插入当前的编译日期__TIME__
:在源文件中插入当前编译时间__STDC__
:当要求程序严格遵循ANSI C标准时该标识被赋值为 1__cplusplus
:当编写C++程序时该标识符被定义
于是我们这么修改输出语句:
// 文件打开失败,提示错误并退出
printf("[%s][%s:%d] open file(%s) error, errno[%d](%s).\n",
__FILE__,
__FUNCTION__,
__LINE__,
szFileName,
errno, strerror(errno));
- 从日志信息中,我们可以精确的获取到:main.c 文件中的 main 函数的第 16 行报错了,错误原因是 Permission denied
相比于之前,确实是能帮助我们精准的定位问题,但是,总不能每次都要写这么长的 printf 吧,有没有偷懒的办法呢?
1.4 使用可变宏输出调试信息
1.4.1 可变宏介绍
用可变参数宏(variadic macros)传递可变参数表,你可能很熟悉在函数中使用可变参数表,如:
在 1999 年版本的 ISO C 标准中,宏可以像函数一样,定义时可以带有可变参数。宏的语法和函数的语法类似,如下所示:
#define DEBUG(...) printf(__VA_ARGS__)
int main()
{
int x = 10;
DEBUG("x = %d\n", x); // 等价于 printf("x = %d\n", x);
return 0;
}
- 缺省号(
...
)指可变参数 __VA_ARGS__
宏用来接收不定数量的参数
这类宏在被调用时,它(这里指缺省号...
)被表示成零个或多个符号(包括里面的逗号),一直到右括弧结束为止。当被调用时,在宏体( macro body )中,这些符号序列集合将代替里面的 _VA_ARGS_ 标识符。当宏的调用展开时,实际的参数就传递给 printf
了。
相比于 ISO C 标准,GCC 始终支持复杂的宏,它使用一种不同的语法从而可以使你可以给可变参数一个名字,如同其它参数一样。例如下面的例子:
#define DEBUG(format, args...) printf(format, args)
int main()
{
int x = 10;
DEBUG("x = %d\n", x); // 等价于 printf("x = %d\n", x);
return 0;
}
- 这和上面举的「ISO C」定义的宏例子是完全一样的,但是这么写可读性更强并且更容易进行描述
在标准 C 里,你不能省略可变参数,但是你却可以给它传递一个空的参数。例如,下面的宏调用在「ISO C」里是非法的,因为字符串后面没有逗号:
#define DEBUG(...) printf(__VA_ARGS__)
int main()
{
DEBUG("hello world.\n"); // 非法调用
}
GCC 在这种情况下可以让你完全的忽略可变参数。在上面的例子中,编译是仍然会有问题,因为宏展开后,里面的字符串后面会有个多余的逗号。为了解决这个问题, GCC 使用了一个特殊的##
操作。书写格式为:
#define DEBUG(format, args...) printf(format, ##args)
-
这里,如果可变参数被忽略或为空,
##
操作将使预处理器去除掉它前面的那个逗号 -
如果你在宏调用时,确实提供了一些可变参数,该宏定义也会工作正常,它会把这些可变参数放到逗号的后面
1.4.2 使用可变宏输出调试信息
有了 1.4.1 的基础知识,我们可以这么修改代码:
#define DEBUG(format, args...) \
printf("[%s][%s:%d] "format"\n", \
__FILE__, \
__FUNCTION__, \
__LINE__, \
##args)
// 文件打开失败,提示错误并退出
DEBUG("open file(%s) error, errno[%d](%s).", szFileName, errno, strerror(errno));
- 通过可变宏,完美解决了调试信息书写过长的问题
书写过长的问题解决后,又来新问题了,如果我想知道某一调试信息是何时被打印的呢?
下面让我们学习一下 Linux 中与时间相关的内容。
二、Linux 中与时间相关的函数
2.1 表示时间的结构体
通过查看头文件「/usr/include/time.h」和「/usr/include/bits/time.h」,我们可以找到下列四种表示「时间」的结构体