C 和 C++ 宏 详解

From:https://www.cnblogs.com/njczy2010/p/5773061.html

C中的预编译宏详解:http://www.cppblog.com/bellgrade/archive/2010/03/18/110030.html

C语言的宏总结:http://blog.csdn.net/pirlck/article/details/51254590

C 语言中的 宏定义:http://www.360doc.com/content/13/0125/13/10906019_262310086.shtml

gcc 宏 :https://gcc.gnu.org/onlinedocs/cpp/Macros.html

C/C++ 中宏与预处理使用方法大全 (VC):http://www.oschina.net/question/234345_54797#

------------------------------------------------------------------------------------------------------------------------

 

 

 

C 和 C++ 宏 详解

 

 

宏 替换 发生的时机

 

    为了能够真正理解#define的作用,需要了解下C语言源程序的处理过程。当在一个集成的开发环境如Turbo C中将编写好的源程序进行编译时,实际经过了预处理、编译、汇编连接几个过程。其中预处理器产生编译器的输出,它实现以下的功能:

  1. 文件包含。
    可以把源程序中的#include 扩展为文件正文,即把包含的.h文件找到并展开到#include 所在处。
  2. 条件编译。
    预处理器根据#if和#ifdef等编译命令及其后的条件,将源程序中的某部分包含进来或排除在外,通常把排除在外的语句转换成空行。
  3. 宏展开。
    预处理器将源程序文件中出现的对宏的引用展开成相应的宏 定义,即本文所说的#define的功能,由预处理器来完成。经过预处理器处理的源程序与之前的源程序有所有不同,在这个阶段所进行的工作只是纯粹的替换与展开,没有任何计算功能,所以在学习#define命令时只要能真正理解这一点,这样才不会对此命令引起误解并误用。

 

 

1. #define的基本用法

 

  #define 是 C语言中提供的宏定义命令,其主要目的是为程序员在编程时提供一定的方便,并能在一定程度上提高程序的运行效率,但学生在学习时往往不能 理解该命令的本质,总是在此处产生一些困惑,在编程时误用该命令,使得程序的运行与预期的目的不一致,或者在读别人写的程序时,把运行结果理解错误,这对 C语言的学习很不利。

 

1.1 #define命令剖析

 

        #define命令是C语言中的一个宏定义命令,它用来将一个标识符定义为一个字符串,该标识符被称为宏名,被定义的字符串称为替换文本。该命令有两种格式:一种是简单的宏定义,另一种是带参数的宏定义

    一个标识符被宏定义后,该标识符便是一个宏名。这时,在程序中出现的是宏名,在该程序被编译前,先将宏名用被定义的字符串替换,这称为宏替换,替换后才进行编译,宏替换只是简单的替换,即 简单的纯文本替换,C预处理器不对宏体做任何语法检查,像缺个括号、少个分号什么的预处理器是不管的

 

  • 宏体换行需要在行末加反斜杠 \
  • 宏名之后带括号的宏 被认为是 宏函数。用法和普通函数一样,只不过在预处理阶段,宏函数会被展开。优点是没有普通函数保存寄存器和参数传递的开销,展开后的代码有利于CPU cache的利用和指令预测,速度快。缺点是可执行代码体积大。
    #define min(X, Y) ((X) < (Y) ? (X) : (Y))
    y = min(1, 2);会被扩展成y = ((1) < (2) ? (1) : (2));
  • 分号吞噬 问题:
    #define MAX(x,y) \
    { \return (x) > (y) ? (x):(y); \
    }if(1)MAX(20, 10); //这个分号导致这一部分代码块结束,致使else找不到对应的if分支而报错
    else;;上面 宏 展开后 if else 代码如下if(1){ return (20) > (10) ? (20):(10); };//后面多了一个分号,导致 if 代码块结束,致使else找不到对应的if分支而报错
    else;;

示例代码(test.c):

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>#define MAX(x,y) \  
{ \  return (x) > (y) ? (x):(y); \  
}  void main()
{ if(1)  MAX(20, 10); //这个分号导致这一部分代码块结束,致使else找不到对应的if分支而报错  else  ;;     
}

gcc -E test.c -o test.e  会生成 test.e 的预处理文件

gcc -E test.c 会直接把预处理后内容输出到屏幕上。直接输出到屏幕上截图:

 

可以看到后面多了一个分号。现在执行编译

可以看到 else 分支缺少 对应的 if 。

 

预处理并不分析整个源代码文件, 它只是将源代码分割成一些标记(token), 识别语句中哪些是C语句, 哪些是预处理语句. 预处理器能够识别C标记, 文件名, 空白符, 文件结尾标志.

预处理语句格式:    #command name(...) token(s)

1, command预处理命令的名称, 它之前以#开头, #之后紧随预处理命令, 标准C允许#两边可以有空白符, 但比较老的编译器可能不允许这样. 若某行中只包含#(以及空白符), 那么在标准C中该行被理解为空白. 整个预处理语句之后只能有空白符或者注释, 不能有其它内容.
2, name代表宏名称, 它可带参数. 参数可以是可变参数列表(C99).
3, 语句中可以利用"\"来换行.

e.g.
# define ONE 1 /* ONE == 1 */
等价于: #define ONE 1

#define err(flag, msg) if(flag) \
   printf(msg)
等价于: #define err(flag, msg) if(flag) printf(msg)

 

简单的宏定义

#define <宏名> <字符串>
例: #define PI      3.1415926#define FALSE   0

 

带参数的宏定义

#define <宏名>(<形式参数表>) <宏体>
例: #define A(x) x#define MAX(a,b) ( (a) > (b) ) ? (a) : (b)

 

取消宏定义:#undef 宏名

 

可变参数 的 宏

C/C++宏定义的可变参数详细解析_C 语言:https://yq.aliyun.com/ziliao/134584

#define LOG( format, ... ) printf( format, __VA_ARGS__ )
LOG( "%s %d", str, count );

__VA_ARGS__是系统预定义宏,被自动替换为参数列表。

#define debug(format, args...) fprintf (stderr, format, args)
#define debug(format, ...) fprintf (stderr, format, __VA_ARGS__)
或者

#define debug(format, ...) fprintf (stderr, format, ## __VA_ARGS__)
前两者存在多余逗号问题,第三个宏使用##去掉可能多余的逗号。

即可变参数被忽略或为空,’##’操作将使预处理器(preprocessor)去除掉它前面的那个逗号

 

当一个宏自己调用自己时,会发生什么?

例如:#define TEST( x ) ( x + TEST( x ) )
TEST( 1 ); 会发生什么?为了防止无限制递归展开,语法规定,当一个宏遇到自己时,就停止展开,也就是说,当对TEST( 1 )进行展开时,展开过程中又发现了一个TEST,那么就将这个TEST当作一般的符号。TEST(1) 最终被展开为:1 + TEST( 1) 。

 

宏参数的prescan(预扫描)

当一个宏参数被放进宏体时,这个宏参数会首先被全部展开(有例外,见下文)。当展开后的宏参数被放进宏体时,预处理器对新展开的宏体进行第二次扫描,并继续展开。例如:

#define PARAM( x ) x
#define ADDPARAM( x ) INT_##x
PARAM( ADDPARAM( 1 ) );

因为ADDPARAM( 1 ) 是作为PARAM的宏参数,所以先将ADDPARAM( 1 )展开为INT_1,然后再将INT_1放进PARAM。
例外情况:如果PARAM宏里对宏参数使用了#或##,那么宏参数不会被展开:

#define PARAM( x ) #x
#define ADDPARAM( x ) INT_##x
PARAM( ADDPARAM( 1 ) );        //将被展开为"ADDPARAM( 1 )"。

使用这么一个规则,可以创建一个很有趣的技术:打印出一个宏被展开后的样子,这样可以方便你分析代码:

#define TO_STRING( x ) TO_STRING1( x )
#define TO_STRING1( x ) #x

TO_STRING首先会将x全部展开(如果x也是一个宏的话),然后再传给TO_STRING1转换为字符串。

现在你可以这样:

        const char *str = TO_STRING( PARAM( ADDPARAM( 1 ) ) );

去一探PARAM展开后的样子。

 

一个很重要的补充:

    如果一个像函数的宏在使用时没有出现括号,那么预处理器只是将这个宏作为一般的符号处理(即 就是不处理)。

    函数宏对参数类型是不敏感的, 你不必考虑将何种数据类型传递给宏. 那么, 如何构建对参数类型敏感的宏呢? 参考关于"##"的介绍.

 

对象宏
不带参数的宏被称为"对象宏(objectlike macro)"
#define 经常用来定义常量, 此时的宏名称一般为大写的字符串. 这样利于修改这些常量.
e.g.
#define MAX 100
int a[MAX];

#ifndef __FILE_H__
#define __FILE_H__
#include "file.h"
#endif
#define __FILE_H__ 中的宏就不带任何参数, 也不扩展为任何标记. 这经常用于包含头文件.

要调用该宏, 只需在代码中指定宏名称, 该宏将被替代为它被定义的内容.

 

函数宏

带参数的宏也被称为"函数宏". 利用宏可以提高代码的运行效率: 子程序的调用需要压栈出栈, 这一过程如果过于频繁会耗费掉大量的CPU运算资源. 所以一些代码量小但运行频繁的代码如果采用带参数宏来实现会提高代码的运行效率.

函数宏的参数是固定的情况

函数宏的定义采用这样的方式: #define name( args ) tokens
其中的args和tokens都是可选的. 它和对象宏定义上的区别在于宏名称之后不带括号.

注意, name之后的左括号(必须紧跟name, 之间不能有空格, 否则这就定义了一个对象宏, 它将被替换为 以(开始的字符串. 但在调用函数宏时, name与(之间可以有空格.

e.g.
#define mul(x,y) ((x)*(y))

注意, 函数宏之后的参数要用括号括起来, 看看这个例子:
e.g.
#define mul(x,y) x*y
"mul(1, 2+2);" 将被扩展为: 1*2 + 2
同样, 整个标记串也应该用括号引用起来:
e.g.
#define mul(x,y) (x)*(y)
sizeof mul(1,2.0) 将被扩展为 sizeof 1 * 2.0

调用函数宏时候, 传递给它的参数可以是函数的返回值, 也可以是任何有意义的语句:
e.g.
mul (f(a,b), g(c,d));

e.g.
#define insert(stmt) stmt
insert ( a=1; b=2;) 相当于在代码中加入 a=1; b=2 .
insert ( a=1, b=2;) 就有问题了: 预处理器会提示出错: 函数宏的参数个数不匹配. 预处理器把","视为参数间的分隔符. 
insert ((a=1, b=2;)) 可解决上述问题.

在定义和调用函数宏时候, 要注意一些问题:
1, 我们经常用{}来引用函数宏被定义的内容, 这就要注意调用这个函数宏时的";"问题.
example_3.7:
#define swap(x,y) { unsigned long _temp=x; x=y; y=_tmp}
如果这样调用它: "swap(1,2);" 将被扩展为: { unsigned long _temp=1; 1=2; 2=_tmp}; 
明显后面的;是多余的, 我们应该这样调用: swap(1,2)
虽然这样的调用是正确的, 但它和C语法相悖, 可采用下面的方法来处理被{}括起来的内容:

#define swap(x,y) \
   do { unsigned long _temp=x; x=y; y=_tmp} while (0)
swap(1,2); 将被替换为:
do { unsigned long _temp=1; 1=2; 2=_tmp} while (0);
在Linux内核源代码中对这种do-while(0)语句有这广泛的应用.

2, 有的函数宏是无法用do-while(0)来实现的, 所以在调用时不能带上";", 最好在调用后添加注释说明.
eg_3.8:
#define incr(v, low, high) \
   for ((v) = (low),; (v) <= (high); (v)++)
只能以这样的形式被调用: incr(a, 1, 10) /* increase a form 1 to 10 */

函数宏中的参数包括可变参数列表的情况
C99标准中新增了可变参数列表的内容. 不光是函数, 函数宏中也可以使用可变参数列表.

#define name(args, ...) tokens
#define name(...) tokens
"..."代表可变参数列表, 如果它不是仅有的参数, 那么它只能出现在参数列表的最后. 调用这样的函数宏时, 传递给它的参数个数要不少于参数列表中参数的个数(多余的参数被丢弃). 
通过__VA_ARGS__来替换函数宏中的可变参数列表. 注意__VA_ARGS__只能用于函数宏中参数中包含有"..."的情况.

e.g.
#ifdef DEBUG
#define my_printf(...) fprintf(stderr, __VA_ARGS__)
#else
#define my_printf(...) printf(__VA_ARGS__)
#endif

tokens中的__VA_ARGS__被替换为函数宏定义中的"..."可变参数列表. 

注意在使用#define时候的一些常见错误:
#define MAX = 100
#define MAX 100;
=, ; 的使用要值得注意. 再就是调用函数宏是要注意, 不要多给出";".

 

 

1. 关于定义宏的另外一些问题

 

(1)宏可以被多次定义, 前提是这些定义必须是相同的。

这里的"相同"要求先后定义中空白符出现的位置相同, 但具体的空白符类型或数量可不同, 比如原先的空格可替换为多个其他类型的空白符: 可为tab, 注释...
e.g.
#define NULL 0
#define NULL /* null pointer */     0
上面的重定义是相同的, 但下面的重定义不同:
#define fun(x) x+1
#define fun(x) x + 1 或: #define fun(y) y+1
如果多次定义时, 再次定义的宏内容是不同的, gcc会给出"NAME redefined"警告信息.

应该避免重新定义函数宏, 不管是在预处理命令中还是C语句中, 最好对某个对象只有单一的定义. 在gcc中, 若宏出现了重定义, gcc会给出警告.

 

(2) 在gcc中, 可在命令行中指定对象宏的定义:
e.g.
gcc -Wall -DMAX=100 -o tmp tmp.c
相当于在tmp.c中添加" #define MAX 100".

那么, 如果原先tmp.c中含有MAX宏的定义, 那么再在gcc调用命令中使用-DMAX, 会出现什么情况呢?
---若-DMAX=1, 则正确编译.
---若-DMAX的值被指定为不为1的值, 那么gcc会给出MAX宏被重定义的警告, MAX的值仍为1.

注意: 若在调用gcc的命令行中不显示地给出对象宏的值, 那么gcc赋予该宏默认值(1), 如: -DVAL == -DVAL=1

 

(3) #define所定义的宏的作用域
宏在定义之后才生效, 若宏定义被#undef取消, 则#undef之后该宏无效. 并且字符串中的宏不会被识别
e.g.
#define ONE 1
sum = ONE + TWO    /* sum = 1 + TWO */
#define TWO 2
sum = ONE + TWO    /* sum = 1 + 2    */ 
#undef ONE
sum = ONE + TWO    /* sum = ONE + 2 */
char c[] = "TWO"   /* c[] = "TWO", NOT "2"! */

 

(4) 宏的替换可以是递归的, 所以可以嵌套定义宏.
e.g.
# define ONE NUMBER_1
# define NUMBER_1 1
int a = ONE /* a = 1 */

 

2. #undef
#undef用来取消宏定义, 它与#define对立:
#undef name
如够被取消的宏实际上没有被#define所定义, 针对它的#undef并不会产生错误.
当一个宏定义被取消后, 可以再度定义它. 

 

3. #if, #elif, #else, #endif
#if, #elif, #else, #endif用于条件编译:
#if 常量表达式1
   语句...
#elif 常量表达式2
   语句...
#elif 常量表达式3
   语句...
...
#else
   语句...
#endif

#if和#else分别相当于C语句中的if, else. 它们根据常量表达式的值来判别是否执行后面的语句. #elif相当于C中的else-if. 使用这些条件编译命令可以方便地实现对源代码内容的控制.
else之后不带常量表达式, 但若包含了常量表达式, gcc只是给出警告信息.

使用它们可以提升代码的可移植性---针对不同的平台使用执行不同的语句. 也经常用于大段代码注释.
e.g.
#if 0
{
   一大段代码;
}
#endif

 

常量表达式可以是包含宏, 算术运算, 逻辑运算等等的合法C常量表达式, 如果常量表达式为一个未定义的宏, 那么它的值被视为0.
#if MACRO_NON_DEFINED == #if 0
在判断某个宏是否被定义时, 应当避免使用#if, 因为该宏的值可能就是被定义为0. 而应当使用下面介绍的#ifdef或#ifndef.

注意: #if, #elif, #else之后的宏只能是对象宏. 如果name为名的宏未定义, 或者该宏是函数宏. 那么在gcc中使用"-Wundef"选项会显示宏未定义的警告信息.

 

4. #ifdef, #ifndef, defined.
#ifdef, #ifndef, defined用来测试某个宏是否被定义
#ifdef name 或 #ifndef name

它们经常用于避免头文件的重复引用:
#ifndef __FILE_H__
#define __FILE_H__
#include "file.h"
#endif

defined(name): 若宏被定义,则返回1, 否则返回0.
它与#if, #elif, #else结合使用来判断宏是否被定义, 乍一看好像它显得多余, 因为已经有了#ifdef和#ifndef. defined用于在一条判断语句中声明多个判别条件:

#if defined(VAX) && defined(UNIX) && !defined(DEBUG) 

和#if, #elif, #else不同, #indef, #ifndef, defined测试的宏可以是对象宏, 也可以是函数宏. 在gcc中使用"-Wundef"选项不会显示宏未定义的警告信息.

 

5. #include , #include_next
#include用于文件包含. 在#include 命令所在的行不能含有除注释和空白符之外的其他任何内容.
#include "headfile"
#include <headfile>
#include 预处理标记
前面两种形式大家都很熟悉, "#include 预处理标记"中, 预处理标记会被预处理器进行替换, 替换的结果必须符合前两种形式中的某一种.

实际上, 真正被添加的头文件并不一定就是#include中所指定的文件. #include"headfile"包含的头文件当然是同一个文件, 但#include <headfile>包包含的"系统头文件"可能是另外的文件. 但这不值得被注意. 感兴趣的话可以查看宏扩展后到底引入了哪些系统头文件.

关于#include "headfile"和#include <headfile>的区别以及如何在gcc中包含头文件的详细信息, 参考本blog的GCC笔记.

相对于#include, 我们对#include_next不太熟悉. #include_next仅用于特殊的场合. 它被用于头文件中(#include既可用于头文件中, 又可用于.c文件中)来包含其他的头文件. 而且包含头文件的路径比较特殊: 从当前头文件所在目录之后的目录来搜索头文件.
比如: 头文件的搜索路径一次为A,B,C,D,E. #include_next所在的当前头文件位于B目录, 那么#include_next使得预处理器从C,D,E目录来搜索#include_next所指定的头文件.

 

可参考cpp手册进一步了解#include_next

 

6. 预定义 的 宏
标准C中定义了一些对象宏, 这些宏的名称以"__"开头和结尾, 并且都是大写字符. 这些预定义宏可以被#undef, 也可以被重定义.

下面列出一些标准C中常见的预定义对象宏(其中也包含gcc自己定义的一些预定义宏:

__LINE__           当前语句所在的行号, 以10进制整数标注.
__FILE__           当前源文件的文件名, 以字符串常量标注.
__DATE__           程序被编译的日期, 以"Mmm dd yyyy"格式的字符串标注.
__TIME__           程序被编译的时间, 以"hh:mm:ss"格式的字符串标注, 该时间由asctime返回.__STDC__           如果当前编译器符合ISO标准, 那么该宏的值为1
__STDC_VERSION__   如果当前编译器符合C89, 那么它被定义为199409L, 如果符合C99, 那么被定义为199901L. 我用gcc, 如果不指定-std=c99, 其他情况都给出__STDC_VERSION__未定义的错误信息, 咋回事呢?
__STDC_HOSTED__    如果当前系统是"本地系统(hosted)", 那么它被定义为1. 本地系统表示当前系统拥有完整的标准C库.

ANSI标准说明了五个预定义的宏名。它们是:

_LINE_             /* (两个下划线),对应%d*/
_FILE_             /* 对应%s */
__FUNCTION__       /* 对应%s */
_DATE_             /* 对应%s */
_TIME_             /* 对应%s */

 

gcc定义的预定义宏:

__OPTMIZE__         如果编译过程中使用了优化, 那么该宏被定义为1.
__OPTMIZE_SIZE__    同上, 但仅在优化是针对代码大小而非速度时才被定义为1.
__VERSION__         显示所用gcc的版本号.

可参考"GCC the complete reference".
要想看到gcc所定义的所有预定义宏, 可以运行: $ cpp -dM /dev/null

 

7. #line
#line用来修改__LINE__和__FILE__. 
e.g.
printf("line: %d, file: %s\n", __LINE__, __FILE__);
#line 100 "haha"
printf("line: %d, file: %s\n", __LINE__, __FILE__);
printf("line: %d, file: %s\n", __LINE__, __FILE__);

显示:
line: 34, file: 1.c
line: 100, file: haha
line: 101, file: haha 

 

8. #pragma 和 _Pragma
#pragma用编译器用来添加新的预处理功能或者显示一些编译信息. #pragma的格式是各编译器特定的, gcc的如下:
#pragma GCC name token(s)

#pragma之后有两个部分: GCC和特定的pragma name. 下面分别介绍gcc中常用的.

 

(1) #pragma GCC dependency
dependency测试当前文件(既该语句所在的程序代码)与指定文件(既#pragma语句最后列出的文件)的时间戳. 如果指定文件比当前文件新, 则给出警告信息. 
e.g.
在demo.c中给出这样一句:
#pragma GCC dependency "temp-file"
然后在demo.c所在的目录新建一个更新的文件: $ touch temp-file, 编译: $ gcc demo.c 会给出这样的警告信息: warning: current file is older than temp-file
如果当前文件比指定的文件新, 则不给出任何警告信息.

还可以在在#pragma中给添加自定义的警告信息.
e.g.
#pragma GCC dependency "temp-file" "demo.c needs to be updated!"
1.c:27:38: warning: extra tokens at end of #pragma directive

1.c:27:38: warning: current file is older than temp-file
注意: 后面新增的警告信息要用""引用起来, 否则gcc将给出警告信息.

 

(2) #pragma GCC poison token(s)
若源代码中出现了#pragma中给出的token(s), 则编译时显示警告信息. 它一般用于在调用你不想使用的函数时候给出出错信息.
e.g.
#pragma GCC poison scanf
scanf("%d", &a); 
warning: extra tokens at end of #pragma directive
error: attempt to use poisoned "scanf"
注意, 如果调用了poison中给出的标记, 那么编译器会给出的是出错信息. 关于第一条警告, 我还不知道怎么避免, 用""将token(s)引用起来也不行.

 

(3) #pragma GCC system_header
从#pragma GCC system_header直到文件结束之间的代码会被编译器视为系统头文件之中的代码. 系统头文件中的代码往往不能完全遵循C标准, 所以头文件之中的警告信息往往不显示. (除非用 #warning显式指明). 
(这条#pragma语句还没发现用什么大的用处)

 

由于#pragma不能用于宏扩展, 所以gcc还提供了_Pragma:
e.g.
#define PRAGMA_DEP #pragma GCC dependency "temp-file"
由于预处理之进行一次宏扩展, 采用上面的方法会在编译时引发错误, 要将#pragma语句定义成一个宏扩展, 应该使用下面的_Pragma语句:
#define PRAGMA_DEP _Pragma("GCC dependency \"temp-file\"")
注意, ()中包含的""引用之前引该加上\转义字符.

 

9. #warning, #error
#warning, #error分别用于在编译时显示警告和错误信息, 格式如下:
#warning tokens
#error tokens
e.g.
#warning "some warning"
注意, #error 和 #warning 后的 token 要用""引用起来!
(在gcc中, 如果给出了warning, 编译继续进行, 但若给出了error, 则编译停止. 若在命令行中指定了 -Werror, 即使只有警告信息, 也不编译.

 

10. 常用的预处理命令

预处理命令由#(hash字符)开头, 它独占一行, #之前只能是空白符. 以#开头的语句就是预处理命令,不以#开头的语句为C中的代码行。

常用的预处理命令如下:

#define              定义一个预处理宏
#undef               取消宏的定义#include             包含文件命令
#include_next        与#include相似, 但它有着特殊的用途#if                  编译预处理中的条件命令, 相当于C语法中的if语句
#ifdef               判断某个宏是否被定义, 若已定义, 执行随后的语句
#ifndef              与#ifdef相反, 判断某个宏是否未被定义
#elif                若#if, #ifdef, #ifndef或前面的#elif条件不满足, 则执行#elif之后的语句, 相当于C语法中的else-if
#else                与#if, #ifdef, #ifndef对应, 若这些条件不满足, 则执行#else之后的语句, 相当于C语法中的else
#endif               #if, #ifdef, #ifndef这些条件命令的结束标志.
defined              与#if, #elif配合使用, 判断某个宏是否被定义#line                标志该语句所在的行号
#                    将宏参数替代为以参数值为内容的字符窜常量
##                   将两个相邻的标记(token)连接为一个单独的标记
#pragma              说明编译器信息#warning             显示编译警告信息
#error               显示编译错误信息

 

 

 

2. #define使用中的常见问题解析

 

2.1 简单宏定义使用中出现的问题

 

 在简单宏定义的使用中,当替换文本所表示的字符串为一个表达式时,容易引起误解和误用。如下例:

#define N 2+2
void main()
{int a=N*N;printf(“%d”,a);
}

 

出现问题:

    在此程序中存在着宏定义命令,宏N代表的字符串是2+2,在程序中有对宏N的使用,一般同学在读该程序时,容易产生的问题是先求解N为2+2=4,然后在程序中计算a时使用乘法,即N*N=4*4=16,其实该题的结果为8,为什么结果有这么大的偏差?

 

问题解析:

宏展开是在预处理阶段完成的,这个阶段把替换文本只是看作一个字符串,并不会有任何的计算发生,在展开时是在宏N出现的地方 只是简单地使用串2+2来代替N,并不会增添任何的符号,所以对该程序展开后的结果是a=2+2*2+2,计算后=8,这就是宏替换的实质,如何写程序才能完成结果为16的运算呢?

 

解决办法:

/*将宏定义写成如下形式*/
#define N (2+2)
/*这样就可替换成(2+2)*(2+2)=16*/

总结:把 宏体 和 所有的宏变量 都用 括号括起来

 

 

2.2  带参数的宏定义出现的问题

 

    在带参数的宏定义的使用中,极易引起误解。例如我们需要做个宏替换能求任何数的平方,这就需要使用参数,以便在程序中用实际参数来替换宏定义中的参数。一般容易写成如下形式:

#define area(x) x*x
/*这在使用中是很容易出现问题的,看如下的程序*/
void main()
{int y = area(2+2);  printf(“%d”,y);
}

    按理说给的参数是2+2,所得的结果应该为4*4=16,但是错了,因为该程序的实际结果为8,仍然是没能遵循纯粹的简单替换的规则,又是先计算再替换 了,在这道程序里,2+2即为area宏中的参数,应该由它来替换宏定义中的x,即替换成2+2*2+2=8了。那如果遵循(1)中的解决办法,把2+2 括起来,即把宏体中的x括起来,是否可以呢?#define area(x) (x)*(x),对于area(2+2),替换为(2+2)*(2+2)=16,可以解决,但是对于area(2+2)/area(2+2)又会怎么样呢,有的学生一看到这道题马上给出结果,因为分子分母一样,又错了,还是忘了遵循先替换再计算的规则了,这道题替换后会变为 (2+2)*(2+2)/(2+2)*(2+2)即4*4/4*4按照乘除运算规则,结果为16/4*4=4*4=16,那应该怎么呢?解决方法是在整个宏体上再加一个括号,即#define   area(x) ((x)*(x)),不要觉得这没必要,没有它,是不行的。
    要想能够真正使用好宏定义,那么在读别人的程序时,一定要记住先将程序中对宏的使用全部替换成它所代表的字符串,不要自作主张地添加任何其他符号,完全展开后再进行相应的计算,就不会写错运行结果。

    如果是自己编程使用宏替换,则在使用简单宏定义时,当字符串中不只一个符号时,加上括号表现出优先级,如果是带参数的宏定义,则要给宏体中的每个参数加上括号,并在整个宏体上再加一个括号。看到这里,不禁要问,用宏定义这么麻烦,这么容易出错,可不可以摒弃它, 那让我们来看一下在C语言中用宏定义的好处吧。

如下代码:

#include <iostream.h>
#define product(x)    x*x
int main()
{int i=3;int j,k;j = product(i++);cout<<"j="<<j<<endl;cout<<"i="<<i<<endl;k = product(++i);cout<<"k="<<k<<endl;cout<<"i="<<i<<endl;return 0;
}

依次输出结果:

j=9;i=5;k=49;i=7

 

 

 

3. 宏定义的优点

 

(1)   方便程序的修改

    使用简单宏定义可用宏代替一个在程序中经常使用的常量,这样在将该常量改变时,不用对整个程序进行修改,只修改宏定义的字符串即可,而且当常量比较长时, 我们可以用较短的有意义的标识符来写程序,这样更方便一些。我们所说的常量改变不是在程序运行期间改变,而是在编程期间的修改,举一个大家比较熟悉的例子,圆周率π是在数学上常用的一个值,有时我们会用3.14来表示,有时也会用3.1415926等,这要看计算所需要的精度,如果我们编制的一个程序中 要多次使用它,那么需要确定一个数值,在本次运行中不改变,但也许后来发现程序所表现的精度有变化,需要改变它的值, 这就需要修改程序中所有的相关数值,这会给我们带来一定的不便,但如果使用宏定义,使用一个标识符来代替,则在修改时只修改宏定义即可,还可以减少输入 3.1415926这样长的数值多次的情况,我们可以如此定义 #define   pi   3.1415926,既减少了输入又便于修改,何乐而不为呢?

 

(2) 提高程序的运行效率

    使用带参数的宏定义可完成函数调用的功能,又能减少系统开销,提高运行效率。正如C语言中所讲,函数的使用可以使程序更加模块化,便于组织,而且可重复利用,但在发生函数调用时,需要保留调用函数的现场,以便子 函数执行结束后能返回继续执行,同样在子函数执行完后要恢复调用函数的现场,这都需要一定的时间,如果子函数执行的操作比较多,这种转换时间开销可以忽 略,但如果子函数完成的功能比较少,甚至于只完成一点操作,如一个乘法语句的操作,则这部分转换开销就相对较大了,但使用带参数的宏定义就不会出现这个问 题,因为它是在预处理阶段即进行了宏展开,在执行时不需要转换,即在当地执行。宏定义可完成简单的操作,但复杂的操作还是要由函数调用来完成,而且宏定义所占用的目标代码空间相对较大。所以在使用时要依据具体情况来决定是否使用宏定义。

 

 

 

2. define中的三个特殊符号:#,##,#@ 和 do while

 

  1. #define Conn(x,y) x##y
  2. #define ToChar(x) #@x
  3. #define ToString(x) #x

 

(1) x##y 表示什么?表示x连接y。

    ##符号会连接两个符号,从而产生新的符号(词法层次),即 “##”是一种分隔连接方式,它的作用是先分隔,然后进行强制连接。 例如:
    #define SIGN( x ) INT_##x
    int SIGN( 1 ); 宏被展开后将成为:int INT_1;

    举例说:

        int n = Conn(123,456);               /*  结果就是n=123456;        */
        char* str = Conn("asdf", "adf");   /* 结果就是 str = "asdfadf"; */
        #define TYPE1(type,name) type name_##type##_type
        #define TYPE2(type,name) type name##_##type##_type
        TYPE1(int, c); 转换为:int  name_int_type ; (因为##号将后面分为 name_ 、type 、 _type三组,替换后强制连接)
        TYPE2(int, d);转换为: int  d_int_type ; (因为##号将后面分为 name、_、type 、_type四组,替换后强制连接)

 

(2)再来看#@x,其实就是给x加上单引号,结果返回是一个const char。举例说:

        char a = ToChar(1);结果就是a='1';
        做个越界试验char a = ToChar(123);结果就错了;
        但是如果你的参数超过四个字符,编译器就给给你报错了!

        error C2015: too many characters in constant   :P

 

(3)最后看看#x,估计你也明白了,他是给x加双引号。即 #符号把一个符号直接转换为字符串。
        也就是
 #是“字符串化”的意思,出现在宏定义中的#是把跟在后面的参数转换成一个字符串

        char* str = ToString(123132);就成了str="123132";

        #define ERROR_LOG(module) fprintf(stderr,"error: "#module"\n")

        ERROR_LOG("add"); 转换为 fprintf(stderr,"error: "add"\n");
        ERROR_LOG(devied =0); 转换为 fprintf(stderr,"error: devied=0\n");

 

(4) 宏定义用 do{ }while(0)

复杂宏定义及do{}while(0)的使用

#define foo() do{}while(0)

采用这种方式是为了防范在使用宏过程中出现错误,主要有如下几点:

  (1)空的宏定义避免warning:
  #define foo() do{}while(0)
  (2)存在一个独立的block,可以用来进行变量定义,进行比较复杂的实现。
  (3)如果出现在判断语句过后的宏,这样可以保证作为一个整体来是实现:
      #define foo(x) \
        action1(); \
        action2();
    在以下情况下:
    if(NULL == pPointer)
         foo();
    就会出现action1和action2不会同时被执行的情况,而这显然不是程序设计的目的。
  (4)以上的第3种情况用单独的{}也可以实现,但是为什么一定要一个do{}while(0)呢,看以下代码:
      #define switch(x,y) {int tmp; tmp="x";x=y;y=tmp;}
      if(x>y)
        switch(x,y);
      else       //error, parse error before else
      otheraction();

在把宏引入代码中,会多出一个分号,从而会报错。使用do{….}while(0) 把它包裹起来,成为一个独立的语法单元,从而不会与上下文发生混淆。同时因为绝大多数的编译器都能够识别do{…}while(0)这种无用的循环并进行优化,所以使用这种方法也不会导致程序的性能降低。

为了看起来更清晰,这里用一个简单点的宏来演示:

#define SAFE_DELETE(p) do{ delete p; p = NULL} while(0)

假设这里去掉do...while(0),

#define SAFE_DELETE(p) delete p; p = NULL;

那么以下代码:

if(NULL != p) SAFE_DELETE(p)
else ...do sth...

就有两个问题:

  • 1) 因为if分支后有两个语句,else分支没有对应的if,编译失败
  • 2) 假设没有else, SAFE_DELETE中的第二个语句无论if测试是否通过,会永远执行。

你可能发现,为了避免这两个问题,我不一定要用这个令人费解的do...while, 我直接用{}括起来就可以了

#define SAFE_DELETE(p) { delete p; p = NULL;}

的确,这样的话上面的问题是不存在了,但是我想对于C++程序员来讲,在每个语句后面加分号是一种约定俗成的习惯,这样的话,以下代码:

if(NULL != p) SAFE_DELETE(p);
else ...do sth...

其else分支就无法通过编译了(原因同上),所以采用do...while(0)是做好的选择了。也许你会说,我们代码的习惯是在每个判断后面加上{}, 就不会有这种问题了,也就不需要do...while了,如:
if(...) 
{
}
else
{
}

现有一个例子:#define PROJECT_LOG(level,arg) \ dosomething();\ if (level <= PROJECT_LOG_get_level()) \ PROJECT_LOG_wrapper_##level(arg);

现在假设有以下应用,现有一个例子:

#define PROJECT_LOG(level,arg) \dosomething();\if (level <= PROJECT_LOG_get_level()) \PROJECT_LOG_wrapper_##level(arg);现在假设有以下应用:if(L==1)PROJECT_LOG(L,"AAA");宏转开为:if(L==1)dosomething();if (1 <= PROJECT_LOG_get_level())PROJECT_LOG_wrapper_1("AAA"); ;显然if(L==1)只管到dosomething();而后面的if (1 <= PROJECT_LOG_get_level())PROJECT_LOG_wrapper_1("AAA"); ;则成了独立的语句。假如使用do{}while(0)语句块,进行宏定义:#define PROJECT_LOG(level,arg)do{ \dosomething();\if (level <= PROJECT_LOG_get_level()) \PROJECT_LOG_wrapper_##level(arg); \
}while(0)上述应用转开后为:
if(L==1)
do{dosomething();if (1<= PROJECT_LOG_get_level())PROJECT_LOG_wrapper_1("AAA");
}while(0);这样避免了意外的麻烦。OK现在明白了很多C程序中奇怪的do{}while(0)宏定义了吧

使用示例代码:

#include <stdio.h>#define PRINT1(a,b)        \{                  \printf("print a\n"); \printf("print b\n"); \}#define      PRINT2(a, b)      \do{               \printf("print a\n"); \printf("print b\n"); \}while(0)  #define PRINT(a) \do{\printf("%s: %d\n",#a,a);\printf("%d: %d\n",a,a);\}while(0)#define TYPE1(type,name)   type name_##type##_type
#define TYPE2(type,name)   type name##_##type##_type#define ERROR_LOG(module)   fprintf(stderr,"error: "#module"\n")int main()
{int a = 20;int b = 19;TYPE1(int, c);ERROR_LOG("add");name_int_type = a;TYPE2(int, d);d_int_type = a;PRINT(a);if(a > b){PRINT1(a, b);}else{PRINT2(a, b);}return 0;
}

 

 

 

3. 常用的一些宏定义

 

1 防止一个头文件被重复包含

#ifndef BODYDEF_H 
#define BODYDEF_H //头文件内容 
#endif

2 得到指定地址上的一个字节或字

#define MEM_B( x ) ( *( (byte *) (x) ) ) 
#define MEM_W( x ) ( *( (word *) (x) ) )

用法如下:

#include <iostream>
#include <windows.h>
#define MEM_B(x) (*((byte*)(x)))
#define MEM_W(x) (*((WORD*)(x)))
int main()
{int bTest = 0x123456;byte m = MEM_B((&bTest));/*m=0x56*/int n = MEM_W((&bTest));/*n=0x3456*/return 0;
}

3 得到一个field在结构体(struct)中的偏移量

#define OFFSETOF( type, field ) ( (size_t) &(( type *) 0)-> field )

  请参考文章:详解写宏定义:得到一个field在结构体(struct type)中的偏移量

 

4 得到一个结构体中field所占用的字节数

#define FSIZ( type, field ) sizeof( ((type *) 0)->field )

5 得到一个变量的地址(word宽度)

#define B_PTR( var ) ( (byte *) (void *) &(var) ) 
#define W_PTR( var ) ( (word *) (void *) &(var) )

6 将一个字母转换为大写

#define UPCASE( c ) ( ((c) >= ''a'' && (c) <= ''z'') ? ((c) - 0x20) : (c) )

7 判断字符是不是10进值的数字

#define DECCHK( c ) ((c) >= ''0'' && (c) <= ''9'')

8 判断字符是不是16进值的数字

#define HEXCHK( c ) ( ((c) >= ''0'' && (c) <= ''9'') ||((c) >= ''A'' && (c) <= ''F'') ||((c) >= ''a'' && (c) <= ''f'') )

9 防止溢出的一个方法

#define INC_SAT( val ) (val = ((val)+1 > (val)) ? (val)+1 : (val))

10 返回数组元素的个数

#define ARR_SIZE( a ) ( sizeof( (a) ) / sizeof( (a[0]) ) )

 

 

 

4. 宏的使用场景

 

1. 打印错误信息

如果程序的执行必须要求某个宏被定义,在检查到宏没有被定义是可以使用#error,#warning打印错误(警告)信息,如:

#ifndef __unix__
#error "This section will only work on UNIX systems"
#endif

只有__unix__宏被定义,程序才能被正常编译。

 

2. 方便调试

__FILE, __LINE, __FUNCTION是由编译器预定义的宏,其分别代表当前代码所在的文件名,行号,以及函数名。可以在代码中加入如下语句来跟踪代码的执行情况:

if(err) 
{printf("%s(%d)-%s\n",__FILE__,__LINE__,__FUNCTION__);
}

 

3. C/C++的混合编程

        函数int foo(int a, int b);
        在C语言的该函数在编译器编译后在库中的名字为_foo,而C++中该函数被编译后在库中的名字为_foo_int_int(为实现函数重载所做的改变)。如果C++中需要使用C编译后的库函数,则会提示找不到函数,因为符号名不匹配。C++中使用extern “C”解决该问题,说明要引用的函数是由C编译的,应该按照C的命名方式去查找符号。
        如果foo是C编译的库,如果要在C++中使用foo,需要加如下声明,其中__cplusplus是c++编译器预定义的宏,说明该文件是被C++编译器编译,此时引用C的库函数,就需要加extern “C”。

#ifdef __cplusplus
extern “C” {
#endifextern int foo(int a, int b);#ifdef __cplusplus
}
#endif

4. 使用宏打印 Log 使用示例

#include <stdio.h>typedef enum
{ERROR_ONE,    // 0ERROR_TWO,ERROR_THREE,ERROR_END
}E_ERROR_CODE;unsigned long g_error_statistics[ERROR_END] = {0};/* LOG 打印, # 直接常亮字符串替换 */
#define LOG_PRINT(ERROR_CODE)                                             \
do {                                                                      \g_error_statistics[ERROR_CODE]++;                                     \printf("[%s : %d], error is %s\n", __FILE__, __LINE__, #ERROR_CODE);  \
} while (0)/* ERROR 公共前缀,传参时省略的写法, ## 直接展开拼接 */
#define LOG_PRINT_2(CODE)                                                        \
do {                                                                             \g_error_statistics[ERROR_ ## CODE]++;                                        \printf("[%s : %d], error is %s\n", __FILE__, __LINE__, "ERROR_" #CODE);      \
} while (0)int main()
{LOG_PRINT(ERROR_TWO);LOG_PRINT_2(ONE);for (unsigned int i = 0; i < ERROR_END; ++i) {printf("error %u statistics is %lu \n", i, g_error_statistics[i]);}return 0;
}

写文件记录log

#include <stdio.h>  
#include <stdarg.h>  
#include <time.h>  int write_log (FILE* pFile, const char *format, ...) {  va_list arg;  int done;  va_start (arg, format);  //done = vfprintf (stdout, format, arg);  time_t time_log = time(NULL);  struct tm* tm_log = localtime(&time_log);  fprintf(pFile, "%04d-%02d-%02d %02d:%02d:%02d ", tm_log->tm_year + 1900, tm_log->tm_mon + 1, tm_log->tm_mday, tm_log->tm_hour, tm_log->tm_min, tm_log->tm_sec);  done = vfprintf (pFile, format, arg);  va_end (arg);  fflush(pFile);  return done;  
}  int main() {  FILE* pFile = fopen("123.txt", "a");  write_log(pFile, "%s %d %f\n", "is running", 10, 55.55);  fclose(pFile);  return 0;  
}  /*
编译运行:
gcc log.c -o log
./log返回结果:cat 123.txt
2016-12-13 13:10:02 is running 10 55.550000
2016-12-13 13:10:04 is running 10 55.550000
2016-12-13 13:10:04 is running 10 55.550000
*/

使用示例代码:

#include<stdio.h>
#include <time.h>
#include <windows.h>
#include <string.h>
#include <stdarg.h>#define DBG_WRITE(fmt,args)  DBG_Write_Log(strrchr(__FILE__, '\\')+1, __LINE__, fmt,##args);void DBG_Write_Log(char* filename, int line, char* fmt, ...)
{   FILE* fp;va_list argp;char* para;char logbuf[512];char timeStr[20];time_t tt;struct tm *local;tt = time(NULL);local = localtime(&tt);strftime(timeStr, 20, "%Y-%m-%d %H:%M:%S", local);sprintf(logbuf, "[%s] %s[%d]", timeStr, filename, line);va_start(argp, fmt);vsprintf(logbuf+strlen(logbuf), fmt, argp);va_end(argp);fprintf(fp, logbuf);fclose(fp);printf(logbuf);   
}void main()
{DBG_WRITE("test log [%d]system[%s][%d]\n", 1234,"add by test", 5);DBG_Write_Log(strrchr(__FILE__,'\\')+1,  __LINE__, "%s %d\n",  "add by test", 5);
}

几种 log 打印 printf 函数 的 宏定义 示例代码

#include <stdio.h>#define lU_DEBUG_PREFIX "##########"#define LU_DEBUG_CMD 0x01
#define LU_DEBUG_DATA 0x02
#define LU_DEBUG_ERROR 0x04#define LU_PRINTF_cmd(msg...) do{if(g_lu_debugs_level & LU_DEBUG_CMD)printf(lU_DEBUG_PREFIX msg);}while(0)
#define LU_PRINTF_data(msg...) do{if(g_lu_debugs_level & LU_DEBUG_DATA)printf(lU_DEBUG_PREFIX msg);}while(0)
#define LU_PRINTF_error(msg...) do{if(g_lu_debugs_level & LU_DEBUG_ERROR)printf(lU_DEBUG_PREFIX msg);}while(0)#define lu_printf(level, msg...) LU_PRINTF_##level(msg)
#define lu_printf2(...) printf(__VA_ARGS__)
#define lu_printf3(...) lu_printf(__VA_ARGS__)
static int lu_printf4_format(int prio, const char *fmt, ...);
#define lu_printf4(prio, fmt...) lu_printf4_format(prio, fmt)int g_lu_debugs_level; //控制打印等级的全局开关
//lu_printf 类似内核的分等级打印宏,根据g_lu_debugs_level和输入的第一个标号名来决定该句打印是否输出。
//lu_printf3 等同于 lu_printf
//lu_printf2 等同于 printf
//lu_printf4 等同于 lu_printf4_format,作用是把输入的第一个整型参数用<val>的格式打印出来
int main(int argc, char *argv[])
{g_lu_debugs_level |= LU_DEBUG_CMD | LU_DEBUG_DATA | LU_DEBUG_ERROR;printf("g_lu_debugs_level = %p\n", g_lu_debugs_level);lu_printf(cmd,"this is cmd\n");lu_printf(data,"this is data\n");lu_printf(error,"this is error\n");g_lu_debugs_level &= ~(LU_DEBUG_CMD | LU_DEBUG_DATA);printf("g_lu_debugs_level = %p\n", g_lu_debugs_level);lu_printf(cmd,"this is cmd\n");lu_printf(data,"this is data\n");lu_printf(error,"this is error\n");lu_printf2("aa%d,%s,%dbbbbb\n", 20, "eeeeeee", 100);g_lu_debugs_level |= LU_DEBUG_CMD | LU_DEBUG_DATA | LU_DEBUG_ERROR;printf("g_lu_debugs_level = %p\n", g_lu_debugs_level);lu_printf3(cmd,"this is cmd \n");lu_printf3(data,"this is data\n");lu_printf3(error,"this is error\n");lu_printf4(0,"luther %s ,%d ,%d\n", "gliethttp", 1, 2);return 0;
}#include <stdarg.h>
static int lu_printf4_format(int prio, const char *fmt, ...)
{
#define LOG_BUF_SIZE (4096)va_list ap;char buf[LOG_BUF_SIZE];va_start(ap, fmt);vsnprintf(buf, LOG_BUF_SIZE, fmt, ap);va_end(ap);printf("<%d>: %s", prio, buf);printf("------------------------\n");printf(buf);
}#define ENTER() LOGD("enter into %s", __FUNCTION__)#define LOGD(...) ((void)LOG(LOG_DEBUG, LOG_TAG, __VA_ARGS__))#define LOG(priority, tag, ...) \LOG_PRI(ANDROID_##priority, tag, __VA_ARGS__)#define LOG_PRI(priority, tag, ...) \android_printLog(priority, tag, __VA_ARGS__)#define android_printLog(prio, tag, fmt...) \__android_log_print(prio, tag, fmt)

使用示例代码:

#include <stdio.h>/* Define Log print macro */
#define MyLog(DebugLevel, format, ...)   \do{  \switch (DebugLevel)  \{  \case 1:  \printf(format, ##__VA_ARGS__);  \break;  \case 2: \printf("Function: "__FUNCTION__", Line: %d, ---> "format"", __LINE__, ##__VA_ARGS__); \break;  \case 3:  \printf("File: "__FILE__", Function: "__FUNCTION__", Line: %d, ---> "format"", __LINE__, ##__VA_ARGS__); \break; \default:   \break;  \}   \}while(0)int main(void)
{MyLog(1, "Simple Log print!\r\n");MyLog(2, "Satndard Log display!\r\n");MyLog(3, "Detail Log view!\r\n");MyLog(1, "If debug level is not equal 1,2 or 3 that log is invisible, such as next line :\r\n");MyLog(6, "I am invisible log!\r\n");MyLog(1, "Now, I think you have understood how to use MyLog macro.\r\n");return 0;
}

使用示例代码:

#include <stdio.h>  #define LOG_DEBUG "DEBUG"  
#define LOG_TRACE "TRACE"  
#define LOG_ERROR "ERROR"  
#define LOG_INFO  "INFOR"  
#define LOG_CRIT  "CRTCL"  #define LOG(level, format, ...) \  do { \  fprintf(stderr, "[%s|%s@%s,%d] " format "\n", \  level, __func__, __FILE__, __LINE__, ##__VA_ARGS__ ); \  } while (0)  int main()  
{  LOG(LOG_DEBUG, "a=%d", 10);  return 0;  
}  // 或者 
/*
#define DBG(format, ...) fprintf(stderr, "[%s|%s@%s,%d] " format "\n", APP_NAME, __FUNCTION__, __FILE__, __LINE__, ##__VA_ARGS__ );  
*/

使用示例代码:

#define LOG(level, format, ...) \
do { \
fprintf(stderr, "[%s|%s@%s,%d] " format "/n", \level, __func__, __FILE__, __LINE__, ##__VA_ARGS__ ); \
} while (0)

使用示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>#define LOG(fmt, ...) do \
{                        \printf("[%s][%s][%s:%d] %s:"fmt"\n", __DATE__, __TIME__, __FILE__, __LINE__, __FUNCTION__, ##__VA_ARGS__); \
}while(0)int main(void)
{char* str = "this is test string";int num = 100;LOG("test string : %s . test num: %d", str, num);return 0;
}

 

有关宏定义的经验与技巧-简化代码-增强Log:http://blog.csdn.net/zh_2608/article/details/46646385

C语言日志处理:https://www.cnblogs.com/274914765qq/p/4589929.html

 

------------------------------------------------------------------------------------------------------------------------

 

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/496259.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Spring Data JPA 从入门到精通~查询方法的创建

查询方法的创建 内部基础架构中有个根据方法名的查询生成器机制&#xff0c;对于在存储库的实体上构建约束查询很有用&#xff0c;该机制方法的前缀 find…By、read…By、query…By、count…By 和 get…By 从所述方法和开始分析它的其余部分&#xff08;实体里面的字段&#x…

人工智能在能源行业的5个应用

作者&#xff1a;CB Insights . 来源&#xff1a;CometLabs摘要&#xff1a;自2012年以来&#xff0c;把人工智能和能源产业放在一起进行报道的新闻开始增多。本文简要描述了人工智能在能源行业的5个应用方向&#xff0c;及对应的案例。能源行业会产生大量的数据。为了将这些数…

VMware 安装 win7、win10、MAC 和网络模式VMnet0、VMnet1、VMnet8解释

VMware虚拟机安装ghost win7系统方法&#xff1a;http://www.xitongcheng.com/jiaocheng/xtazjc_article_15314.html VMWare14 安装Mac OS系统&#xff08;图解&#xff09;&#xff1a;http://blog.csdn.net/u011415782/article/details/78505422 虚拟机&#xff08;VMware …

Spring Data JPA 从入门到精通~关键字列表

注意除了 find 的前缀之外&#xff0c;我们查看 PartTree 的源码&#xff0c;还有如下几种前缀&#xff1a; private static final String QUERY_PATTERN "find|read|get|query|stream"; private static final String COUNT_PATTERN "count"; private s…

当科学遇上众包:9个值得关注的前沿科技算力众包平台

来源&#xff1a; 资本实验室 . 作者&#xff1a;李鑫找到癌症治疗的方法&#xff0c;预测气候的变化&#xff0c;追踪可能与地球相撞的小行星……甚至预测地震&#xff0c;我们每天都面临着各种世界性难题。如果你想参与解决这些难题&#xff0c;公民科学应用将让你发挥作用…

htop 命令详解

htop 官网&#xff1a;http://htop.sourceforge.net/ Linux top 命令的用法详细详解&#xff1a;https://www.cnblogs.com/zhoug2020/p/6336453.html htop 使用详解&#xff1a;https://www.cnblogs.com/programmer-tlh/p/11726016.html 使用 yum 无法直接安装 htop&#xff…

linux主机服务器日志采集,Linux通过Rsyslog搭建集中日志服务器

(一)Rsyslog简介ryslog 是一个快速处理收集系统日志的程序&#xff0c;提供了高性能、安全功能和模块化设计。rsyslog 是syslog 的升级版&#xff0c;它将多种来源输入输出转换结果到目的地。rsyslog是一个开源工具&#xff0c;被广泛用于Linux系统以通过TCP/UDP协议转发或接收…

IDC预测2022年全球智能家居连接设备市场规模将达10亿台!

来源&#xff1a; IDC官网、智慧生活&#xff1b; 物联网资本论编译摘要&#xff1a;2017年&#xff0c;全球智能家居连接设备市场规模达到43310万台&#xff0c;比上一年增长27.6&#xff05;。2022年市场达到9.397亿台&#xff0c;IDC预计复合年增长率&#xff08;CAGR&#…

effective C++ 读书笔记

本篇文章都是摘自 《Effective C》 中文版 第三版 和 第二版。 再好的记性也有忘记的一天&#xff0c;记录下以备随时查看。。。 电子书下载地址&#xff1a;https://download.csdn.net/download/freeking101/10278088 《Effective C》第二版在线教程&#xff1a;http://www.…

Spring Data JPA 从入门到精通~思维导图

#原图 System.out.println("https://www.processon.com/view/61c7227c0e3e7474fb9b4b76?fromnew1");

高通5G版图现身!你的网络生活将迎来巨变?

来源&#xff1a;36Kr 作者&#xff1a;桐由于骁龙845移动平台和骁龙636移动平台的首发&#xff0c;3月的手机市场对于持币代购的消费者而言注定是充满期待的&#xff0c;在三星S9和红米Note5刷屏之时&#xff0c;曾经隐身手机幕后的高通也再一次引发用户热议&#xff0c;高通…

linux添加nginx,linux下安装Nginx1.16.0的教程详解

因为最近在倒腾linux&#xff0c;想安装新版本的nginx&#xff0c;找了一圈教程没有找到对应的教程&#xff0c;在稍微倒腾了一会之后终于成功的安装了最新版。服务器环境为centos&#xff0c;接下来是详细步骤&#xff1a;安装必要依赖插件?创建文件夹并切换过去?下载安装包…

深度|2030年8亿人会失业!图解机器人如何取代你的工作

来源&#xff1a;财看见-腾讯财经&#xff08;ID&#xff1a;qqckj2017&#xff09;未来智能实验室是人工智能学家与科学院相关机构联合成立的人工智能&#xff0c;互联网和脑科学交叉研究机构。未来智能实验室的主要工作包括&#xff1a;建立AI智能系统智商评测体系&#xff0…

Java使用Itext5.5.10进行pdf签章

来源&#xff1a;Java使用Itext5.5.10进行pdf签章_liumengya007007的博客-CSDN博客_itext 签章 啰嗦 说到PDF数字签名签章&#xff0c;这个其实也是数字证书信息安全的应用范畴&#xff0c;关于数字证书和数字签名&#xff0c;网上有很多解释说明&#xff0c;但讲解都多不够详…

Python的bool类型

写习惯了C#的代码&#xff0c;在想要将一个字符串False转换为bool型的时候&#xff0c;很自然的写了如下的Python代码&#xff1a; 看到上面的结果了没&#xff1f;是True。突然记起Python中除了、""、0、()、[]、{}、None为False之外&#xff0c;其他的都是True。也…

工业机器人发展趋势分析 未来营收规模达到百亿级水平

来源&#xff1a;前瞻产业研究院摘要&#xff1a;工业机器人是打造自动化工厂的重要组成,可有效提高效率生产、降低成本和控制质量。工业机器人是面向工业领域的多关节机械手或多自由度的机器装置,它能自动执行工作,是靠自身动力和控制能力来实现各种功能的一种机器。工业机器人…

【itext学习之路】--5.对pdf进行盖章/签章/数字签名

来源&#xff1a;【itext学习之路】-------&#xff08;第五篇&#xff09;对pdf进行盖章/签章/数字签名_tomatocc的博客-CSDN博客_itext 数字签名 在上一篇文章中&#xff0c;我们学习了使用itext对pdf增加图片水印和文本水印&#xff0c;那么这篇文章我们将要学习更高级一点…

python selenium 用法 和 Chrome headless

From: http://cuiqingcai.com/2599.html Selenium教程&#xff1a;https://www.yiibai.com/selenium selenium 官方参考文档&#xff1a;https://selenium-python.readthedocs.io/index.html Selenium Documentation&#xff1a;https://www.seleniumhq.org/docs Selenium 与 …

【itext学习之路】--1.创建一个简单的pdf文档

来源&#xff1a;https://blog.csdn.net/tomatocc/article/details/80666011 iText是著名的开放源码的站点sourceforge一个项目&#xff0c;是用于生成PDF文档的一个java类库。通过iText不仅可以生成PDF或rtf的文档&#xff0c;而且可以将XML、Html文件转化为PDF文件 本教程中…

并发服务器设计思路,参考apache学习UDP和QoS,研究成果

研究了快1个月的服务器架构&#xff0c;把研究成果记录一下。参考的有&#xff1a;Apache vlc ACE ftp我主要需要其中的并发处理&#xff0c;内存管理&#xff0c;TCP/UDP.QoS&#xff0c;速度限制等方面的内容&#xff0c;所以着重说这几方面。首先看一下Apache的基本框图&…