extern和宏函数简介
extern简介
extern
是C语言的一个关键字,用于修饰全局变量
和函数
,它告诉编译器,当前编译单元(即.c
文件)里引用的某个变量,并没有在本单元内部定义,而是在其他编译单元里定义,所以找不到变量定义的时候别报错,就当变量存在。至于生成可执行文件的时候咋办,这是链接器(linker)要操心的事,编译器不管。
举个例子
timer.c
定义了一个全局变量g_sys_tick
和中断处理程序Timer_ISR
,用于记录系统当前已运行时间,每当Timer中断触发,g_sys_tick
就会加一。
//timer.c
unsigned int g_sys_tick;
void Timer_ISR (void)
{
g_sys_tick ++;
}
uart.c
想在打印日志时添加时间戳,就必须用extern
声明该变量,然后在printf
里引用。
//uart.h
extern unsigned int g_sys_tick;
//uart.c
#include "uart.h"
void operation() {
printf("[%u] system panic!", g_sys_tick);
}
extern
一般放在头文件里,不过放在.c
文件里也行。
这种做法有个问题,一般日志都不会直接用printf
,而是用宏函数
实现简单的包装(例如添加时间戳),或者用专门的第三方库。
宏函数简介
宏函数
也是一种C语言的宏
,只不过它像函数一样支持用逗号
隔开的参数列表
#define MAX(a, b) (a > b ? a : b)
printf("max num = %d \n", MAX(3, 5));
宏函数还有一种形式,用于将多条语句封装成一条语句:
//func.h
#define FUNC() \
do { \
func1(); \
func2(); \
} while (0)
//test.c
#include "func.h"
void operarion(){
FUNC();
}
在上面的代码中,FUNC函数调用会被C语言的预处理器展开成do{} while(0)
循环,每行末尾的反斜杠用于转义换行符(因为宏定义必须在一行内完成)。
这种奇怪的形式主要用于包装多条语句,因为相比下面这种,上面的可读性更好,同时相比正常while语句,不用担心会执行多次。
#define FUNC() func1();func2();
宏函数实现日志打印功能
很多不想引入太多第三方库的系统,特别是单片机
系统,通常会这样实现日志打印函数
#define DEBUG(fmt, args...) \
do { \
printf("[%u] ",g_sys_tick); \
printf(fmt, ##args); \
} while (0)
DEBUG("spi query failed, second_counter: %d\r\n", second_counter);
第一条语句打印日志对应的事件发生时间,第二条语句打印具体的事件内容。这个宏函数编译会出错,报错信息是"找不到g_sys_tick的定义"之类。
为什么会这样?因为宏函数在某个c文件的某个函数内展开后,第一条printf访问的g_sys_tick并没有在对应的头文件中用extern声明。
怎么办?如果每个c文件开头都加一行
extern unsigned int g_sys_tick;
重复太多,不利于后续修改维护。
如果将其放到一个公共头文件里让所有c文件都包含,又污染了名称空间。
这时,我们可以求助于局部extern
机制
局部extern来帮忙
局部extern简介
还是第一节那个例子,如果uart.c
的operation
函数想要访问g_sys_tick
,但又不想让c文件内的其他函数访问,这时就可以将extern
从函数外部 挪到 函数内部
//uart.c
#include "uart.h"
void operation() {
extern unsigned int g_sys_tick; // 局部extern
printf("[%u] system panic!", g_sys_tick);
}
上述代码能够编译通过并正常工作,亲测有效!
跟宏函数组合起来
于是我们可以将局部extern跟宏函数结合起来,造出宏日志函数
#define DEBUG(fmt, args...) \
do { \
extern unsigned int g_sys_tick; \
printf("[%u] ",g_sys_tick); \
printf(fmt, ##args); \
} while (0)
上面这个宏函数,仅在调用它的函数内展开,也就只有那些函数能看到g_sys_tick
,便于后续修改、维护,而且运行时开销为零。
调用也很简单
DEBUG("spi query failed, second_counter: %d\r\n", second_counter);
运行效果
[714] spi query failed, second_counter: 2