一、宏的初识
宏在代码中是为了确保整个头文件的内容在单个编译单元中只被包含和编译一次。即使在项目中的其他代码文件中没有直接使用 __WORK__
这个宏,它在头文件中定义和使用也是有意义的,因为它提供了包含保护。
如果你在项目中的代码文件里没有找到关于 __WORK__
的使用,这很正常,因为包含保护宏的使用通常是隐式的,不需要在源代码文件中显式引用。只要头文件被正确包含,这个宏就会起作用,防止头文件内容被重复包含。
#ifndef __WORK__
#define __WORK__
// 环长度
#define RING_BUFFER_SIZE (8192)
#if (RING_BUFFER_SIZE & (RING_BUFFER_SIZE - 1)) != 0
#error "RING_BUFFER_SIZE must be a power of two"
#endif
判断是否为2的幂的技巧:与操作& ,( num & num-1 ) != 0,true则是中断编译,输出错误提示"RING_BUFFER_SIZE must be a power of two" ,要求num为2的幂
二、作用域
-
文件作用域: 如果宏在文件的全局范围内定义,那么它在整个文件中都可见,直到遇到
#undef
指令取消宏定义。 -
条件编译作用域: 如果宏是在条件编译块中定义的,例如
#ifdef
、#ifndef
、#if
、#else
、#elif
、#endif
,宏的作用域将受限于这些条件编译块的逻辑。 -
命令行定义: 在编译时通过编译器命令行使用
-D
选项定义的宏,将在整个编译单元中有效。 -
包含保护: 使用包含保护的头文件中的宏定义,其作用域限于该头文件。
-
局部宏定义: 在函数或代码块内部定义的宏,其作用域限于该代码块或函数内部。
-
作用域嵌套: 宏定义可以嵌套,但内部定义的宏在其外部是不可见的。
-
作用域链: 如果在不同的作用域中多次定义同一个宏,预处理器将使用最内层的定义。
-
跨文件宏: 若宏在多个文件中定义,且没用
#undef
取消,那宏最新定义将覆盖之前同名定义 -
编译单元: 一个编译单元(一个源文件及其包含的头文件)的宏定义对整个编译单元有效。
三、包含保护与pragma once比较
#ifndef __WORK__
和#pragma once
都是用来防止头文件被多次包含的机制,但二者有差异:
传统 C/C++ 包含保护方法:
-
检查宏:
#ifndef __WORK__
检查是否未定义。若已定义,编译器将跳过到#endif
代码块。 -
定义宏:如果
__WORK__
未定义,#define __WORK__
将定义这个宏。 -
结束保护:
#endif
标记了包含保护的结束。 -
可移植性:几乎所有的 C/C++ 编译器都支持这种方法。
-
兼容性:这种方法与 C89 和 C++98 标准兼容。
-
宏名称:通常使用独特的标识符来避免与其他宏冲突。
#ifndef _WORK_ 如果没被定义
#define _WORK_ 宏定义
#undef _WORK_ 取消宏定义
#ifdef _WORK_
#if defined _WORK_ 如果已被定义,则。。。
#else
#elif defined _WORK_ 可与if配套使用,defined可换
#error “错误提示” 中断编译,给出错误提示信息
#pragma once:
-
一次性包含:
#pragma once
指示编译器只包含一次头文件,无论是否已定义了包含保护宏。 -
简洁性:
#pragma once
更简洁,不需要编写宏定义和结束标记。 -
标准性:
#pragma once
是 C++11 标准的一部分,但可能在一些旧的编译器中不被支持。 -
可移植性:虽然
#pragma once
在现代编译器中广泛支持,但在一些非常旧的编译器或非标准的编译环境中可能不可用。 -
性能:
#pragma once
可能比传统的包含保护更快,因为它避免了宏定义和检查的开销。
比较
- 使用场景:
#pragma once
是现代且简洁的防止头文件多次包含的方法;传统包含保护与旧编译器兼容性更好。 - 性能:
#pragma once
可能在某些编译器中提供更好的性能,因为它避免了宏处理的开销。 - 可读性:传统的包含保护提供了更好的可读性,因为它明确地显示了宏的名称和作用域。
四、预定义的宏
-
__LINE__
:获取当前代码实际行号。在调试和生成错误消息时非常有用。 -
__FILE__
:获取当前实际文件名。常用于记录日志和错误报告。 -
__DATE__
和__TIME__
:分别用于获取编译日期和时间。
格式通常为"Mmm DD YYYY"
和"HH:MM:SS"
。 -
__FUNCTION__
(C99及以上标准):获取当前函数或lambda表达式的名字。
在日志记录和断言检查中很有用。 -
__COUNTER__
(GCC和Clang扩展):生成一个唯一的静态计数器值,通常用于创建多个匿名变量或在模板元编程中。 -
__STDC__
:如果编译器遵循ANSI C标准,则定义此宏。其值通常为1
。 -
__STDC_VERSION__
:若编译器遵循特定C标准版本,此宏将被定义,并包含相应标准版本号
五、案例记录分析
用于在程序中生成带有文件名和日志级别的信息输出,方便调试和记录程序运行状态。
#define ___LOG___(fmt,level,path, ...) do{\
char cache[24];\ //存储时间信息
debug_get_time(cache, sizeof(cache));\ //将当前时间格式化为字符串,并存在cache中
debug_print_time("[%s] [%s] [%s:%d] [%s] " fmt "\n", cache, level, path, __LINE__, __func__, ## __VA_ARGS__);\ //输出格式化的日志信息
}while(0)
#define print_info(fmt, ...) ___LOG___(fmt, "INFO" ,__FILE__, ## __VA_ARGS__)
__LOG__分析:
fmt
、level
、path
是宏的前三个参数,分别用于格式化字符串、日志级别和文件路径__LINE__
是预定义的宏,在编译时会被替换为当前代码实际行号。__func__
是预定义的宏,在编译时会被替换为当前函数名称。
print_info
分析:
创建一个自定义的日志记录宏,fmt
是一个参数,用于指定要打印的格式化字符串。...
是C99标准引入的可变参数宏,允许宏接受任意数量的参数。
fmt
:格式化字符串。"INFO"
:通常是日志级别或类型,这里指定为 "INFO"。__FILE__
:预定义的宏,它在编译时会被替换为当前文件名称,记录日志信息的来源文件。## __VA_ARGS__
:宏变量参数的展开,将可变参数列表连接到格式化字符串中。##
是C99标准引入的预处理器操作符,将宏参数转变字符串字面量。__VA_ARGS__
是可变参数宏的一部分,将宏调用中提供的额外参数作为列表传递给__LOG___
宏。
...
和 __VA_ARGS__
一起工作,允许宏定义接受任意数量的参数。