C语言宏定义详解
宏定义引入
源程序在编译之前,会先进行预处理。
预处理并不是C语言编译器的组成部分,不能直接对它们进行编译。经过预处理后,程序就不再包括预处理命令了,最后再由编译程序对预处理之后的源程序进行编译处理,再经过链接得到可供执行的目标代码。
C 语言提供的预处理功能有三种,分别为宏定义、文件包含和条件编译。
宏定义在 C语言源程序中允许用一个标识符来表示一个字符串,称为“宏/宏体” ,被定义为“宏”的标识符称为“宏名”。
在预处理时,对程序中所有出现的宏名,都用宏定义中的字符串去完全替换,这称为“宏替换”或“宏展开”。
宏定义是由源程序中的宏定义命令完成的,宏代换是由预处理程序自动完成的。
在 C 语言中,宏分为有参数和无参数两种。无参宏的宏名后不带参数,其定义的一般形式为:
#define 标识符 字符串
#表示这是一条预处理命令(在C语言中凡是以#开头的均为预处理命令)
define:宏定义命令
标识符:所定义的宏名
字符串:可以是常数、表达式、格式串等无参宏和有参宏示例:
// 不带参数的宏定义 #define MAX 10 /*带参宏定义*/ #define M(y) (((y)*(y))+(3*(y))) /*宏调用*/ k=M(MAX);
注意宏不是语句,结尾不需要加“;”,否则会被替换进程序中,如:
#define N 10; // 宏定义 int c[N]; // 会被替换为: int c[10;]; //error:… main.c:133:11: Expected ']'
如果要写宏不止一行,则在结尾加反斜线符号使得多行能连接上,如:
#define HELLO "hello the world"
而且注意第二行要对齐,不要出现预期之外的空格,否则,如:
#define HELLO "hello the wo rld" printf("HELLO is %sn", HELLO); //输出结果为: HELLO is hello the wo rld
也就是行与行之间的空格也会被作为替换文本的一部分
而且由这个例子也可以看出:宏名如果出现在源程序中的“”内,则不会被当做宏来进行宏代换。宏定义的优点:
方便程序的修改
使用简单宏定义可用宏代替一个在程序中经常使用的常量,这样在将该常量改变时,不用对整个程序进行修改,只修改宏定义的字符串即可,而且当常量比较长时, 我们可以用较短的有意义的标识符来写程序,这样更方便一些。
相对于全局变量,两者的区别如下:
1、宏定义在编译前即会使用并替换,而全局变量要到运行时才可以。
2、宏定义的只是一段字符,在编译的时候被原封不动地替换到引用的位置。在运行中是没有宏定义的概念的。而变量在运行时要为其分配内存。
3、宏定义不可以被赋值,即其值一旦定义不可修改,而变量在运行过程中可以被修改。
4、宏定义只有在定义的所在文件中使用。 而全局变量可以在工程所有文件中使用,只要再使用前加一个声明就可以了。换句话说,宏定义不需要extern。提高程序的运行效率
使用带参数的宏定义可完成函数调用的功能,又能减少系统开销,提高运行效率。正如C语言中所讲,函数的使用可以使程序更加模块化,便于组织,而且可重复利用,但在发生函数调用时,需要保留调用函数的现场,以便子函数执行结束后能返回继续执行,同样在子函数执行完后要恢复调用函数的现场,这都需要一定的时间,如果子函数执行的操作比较多,这种转换时间开销可以忽略,但如果子函数完成的功能比较少,甚至于只完成一点操作,如一个乘法语句的操作,则这部分转换开销就相对较大了,但使用带参数的宏定义就不会出现这个问题,因为它是在预处理阶段即进行了宏展开,在执行时不需要转换,即在当地执行。宏定义可完成简单的操作,但复杂的操作还是要由函数调用来完成,而且宏定义所占用的目标代码空间相对较大。所以在使用时要依据具体情况来决定是否使用宏定义。
宏定义的缺点:
1、由于是直接嵌入的,所以代码可能相对多一点;
2、嵌套定义过多可能会影响程序的可读性,而且很容易出错,不容易调试。
3、对带参的宏而言,由于是直接替换,并不会检查参数是否合法,存在安全隐患。
注意:如果宏体为空,就相当于用空字符来替换。但是,尽可能不要这么做,而且也没必要这么做。很多时候都会报错的。
什么时候用宏定义
宏函数,函数比较
从时间上来看
- 宏只占编译时间,函数调用则占用运行时间(分配单元,保存现场,值传递,返回),每次执行都要载入,所以执行相对宏会较慢。
- 使用宏次数多时,宏展开后源程序很长,因为每展开一次都使程序增长,但是执行起来比较快一点(这也不是绝对的,当有很多宏展开,目标文件很大,执行的时候运行时系统换页频繁,效率就会低下)。而函数调用不使源程序变长。
从安全上来看
- 函数调用时,先求出实参表达式的值,然后带入形参。而使用带参的宏只是进行简单的字符替换。
- 函数调用是在程序运行时处理的,分配临时的内存单元;而宏展开则是在编译时进行的,在展开时并不分配内存单元,不进行值的传递处理,也没有“返回值”的概念。
- 对函数中的实参和形参都要定义类型,二者的类型要求一致,如不一致,应进行类型转换;而宏不存在类型问题,宏名无类型,它的参数也无类型,只是一个符号代表,展开时带入指定的字符即可。宏定义时,字符串可以是任何类型的数据。
- 宏的定义很容易产生二义性,如:定义#define S(a) (a)*(a),代码S(a++),宏展开变成(a++)*(a++)这个大家都知道,在不同编译环境下会有不同结果。
- 调用函数只可得到一个返回值,且有返回类型,而宏没有返回值和返回类型,但是用宏可以设法得到几个结果。
- 函数体内有Bug,可以在函数体内打断点调试。如果宏体内有Bug,那么在执行的时候是不能对宏调试的,即不能深入到宏内部。
- 附:C++中宏不能访问对象的私有成员,但是成员函数就可以。
宏函数的适用范围
- 一般来说,用宏来代表简短的表达式比较合适。
- 在考虑效率的时候,可以考虑使用宏,或者内联函数。
- 还有一些任务根本无法用函数实现,但是用宏定义却很好实现。比如参数类型没法作为参数传递给函数,但是可以把参数类型传递给带参的宏。
什么是内联函数?
在C99中引入了内联函数(inline),联函数和宏的区别在于,宏是由预处理器对宏进行替代 ,而内联函数是通过编译器控制来实现的。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销。可以象调用函数一样来调用内联函数,而不必担心会产生于处理宏的一些问题。内联函数也有一定的局限性。就是函数中的执行代码不能太多了,如果,内联函数的函数体过大,一般的编译器会放弃内联方式,而采用普通的方式调用函数。这样,内联函数就和普通函数执行效率一样了。
所以几乎可以这样认为:内联函数就是带了参数静态类型检查的宏。
宏使用注意事项
1、宏可以嵌套,但不参与运算:
#define M 5 // 宏定义 #define MM M * M // 宏的嵌套 printf("MM = %dn", MM); // MM 被替换为: MM = M * M, 然后又变成 MM = 5 * 5
宏代换的过程在上句已经结束,实际的 5 * 5 相乘过程则在编译阶段完成,而不是在预处理器工作阶段完成,所以宏不进行运算,它只是按照指令进行文字的替换操作。再强调下,宏进行简单的文本替换,无论替换文本中是常数、表达式或者字符串等,预处理程序都不做任何检查,如果出现错误,只能是被宏代换之后的程序在编译阶段发现。
2、宏定义必须写在函数之外,其作用域是 #define 开始,到源程序结束。如果要提前结束它的作用域则用 #undef命令,如:
#define M 5 // 宏定义 printf("M = %dn", M); // 输出结果为: M = 5 #undef M // 取消宏定义 printf("M = %dn", M); // error:… main.c:138:24: Use of undeclared identifier 'M'
一般情况下宏定义命令#define出现在函数的外部,宏名的有效范围是:从定义命令之后,到本文件结束。通常,宏定义命令放在文件开头处。但是是可以放在程序的函数内部的,只不过此时就违反了宏定义的初衷,而且只能在该函数内部使用。反正,就当做不能用在函数里就行,有的地方用也没错。
3、可以用宏定义表示数据类型,可以使代码简便(但不推荐):
#define STU struct Student // 宏定义STU struct Student{ // 定义结构体Student char *name; int sNo; }; STU stu = {"Jack", 20}; // 被替换为:struct Student stu = {"Jack", 20}; printf("name: %s, sNo: %dn", stu.name, stu.sNo);
4、如果重复定义宏(应当避免),则不同的编译器采用不同的重定义策略。有的编译器认为这是错误的,有的则只是提示警告。Xcode中采用第二种方式。如:
#define M 5 //宏定义 #define M 100 //重定义,warning:… main.c:26:9: 'M' macro redefined
5、建议给宏体和引用的每个参数加括号
不仅宏体是纯文本替换,宏参数也是纯文本替换。有以下一段简单的宏,实现乘法:
#define MULTIPLY(x, y) x * y
MULTIPLY(1, 2) 没问题,会正常展开成1 * 2 。有问题的是这种表达式MULTIPLY(1+2, 3) ,展开后成了==1+2 * 3 ,显然优先级错了。
对宏体和给引用的每个参数加括号,就能避免这问题。#define MULTIPLY(x, y) ((x) * (y))
6、注意分号吞噬问题
有如下宏定义:
#define foo(x) bar(x); baz(x)
假设你这样调用:
if (!feral) foo(wolf);
这将被宏扩展为:
if (!feral) bar(wolf); baz(wolf);
baz(wolf);,不在判断条件中,显而易见,这是错误。
如果用大括号将其包起来依然会有问题,例如#define foo(x) { bar(x); baz(x); } if (!feral) foo(wolf); else bin(wolf);
判断语言被扩展成:
if (!feral) { bar(wolf); baz(wolf); }; else bin(wolf);
else将不会被执行,因为if语句后有个分号;表示判断语句就结束了,后面的else是无效代码。
通过do{…}while(0) 能够解决上述问题
#define foo(x) do{ bar(x); baz(x); }while(0) if (!feral) foo(wolf); else bin(wolf);
被扩展成:
#define foo(x) do{ bar(x); baz(x); }while(0) if (!feral) do{ bar(x); baz(x); }while(0); else bin(wolf);
使用do{…}while(0)构造后的宏定义不会受到大括号、分号等的影响,总是会按你期望的方式调用运行。这是一种形式上的解决方案,代码其实也只会执行一次,只是巧妙借用do…while的特性来将其包装成一整条语句。
7、对自身的递归引用
有如下宏定义:
#define foo (4 + foo)
按前面的理解,==(4 + foo)==会展开成==(4 + (4 + foo))==,然后一直展开下去,直至内存耗尽。但是,预处理器采取的策略是只展开一次。也就是说,foo只会展开成==4 + foo==,而展开之后foo的含义就要根据上下文来确定了。
对于以下的交叉引用,宏体也只会展开一次。
#define x (4 + y) #define y (2 * x)
x展开成(4 + y) -> (4 + (2 * x)),y展开成(2 * x) -> (2 * (4 + y))。
注意,这是极不推荐的写法,程序可读性极差。
条件编译
假如现在要开发一个C语言程序,让它输出红色的文字,并且要求跨平台,在 Windows 和 Linux 下都能运行,怎么办呢?
这个程序的难点在于,不同平台下控制文字颜色的代码不一样,我们必须要能够识别出不同的平台。
Windows 有专有的宏_WIN32,Linux 有专有的宏__linux__,以现有的知识,我们很容易就想到了 if else,请看下面的代码:
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int
main(){
if
(_WIN32){
system
(
"color 0c"
);
printf
(
"http://www.baidu.comn"
);
}
else
if
(__linux__){
printf
(
"33[22;31mhttp://www.baidu.comn33[22;30m"
);
}
else
{
printf
(
"http://www.baidu.comn"
);
}
return
0;
}
但这段代码是错误的,在 Windows 下提示 __linux__ 是未定义的标识符,在 Linux 下提示 _Win32 是未定义的标识符。对上面的代码进行改进:
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int
main(){
#if _WIN32
system
(
"color 0c"
);
printf
(
"http://www.baidu.comn"
);
#elif __linux__
printf
(
"33[22;31mhttp://www.baidu.comn33[22;30m"
);
#else
printf
(
"http://www.baidu.comn"
);
#endif
return
0;
}
#if、#elif、#else 和 #endif 都是预处理命令,整段代码的意思是:如果宏 _WIN32 的值为真,就保留第 4、5 行代码,删除第 7、9 行代码;如果宏 __linux__ 的值为真,就保留第 7 行代码;如果所有的宏都为假,就保留第 9 行代码。
这些操作都是在预处理阶段完成的,多余的代码以及所有的宏都不会参与编译,不仅保证了代码的正确性,还减小了编译后文件的体积。
这种能够根据不同情况编译不同代码、产生不同目标文件的机制,称为条件编译。条件编译是预处理程序的功能,不是编译器的功能。
条件编译有多种形式,下面一一讲解。
#if 命令
#if 命令的完整格式为:
它的意思是:如常量“表达式1”的值为真(非0),就对“程序段1”进行编译,否则就计算“表达式2”,结果为真的话就对“程序段2”进行编译,为假的话就继续往下匹配,直到遇到值为真的表达式,或者遇到 #else。这一点和 if else 非常类似。
需要注意的是,#if 命令要求判断条件为“整型常量表达式”,也就是说,表达式中不能包含变量,而且结果必须是整数;而 if 后面的表达式没有限制,只要符合语法就行。这是 #if 和 if 的一个重要区别。
#elif 和 #else 也可以省略,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
int
main(){
#if _WIN32
printf
(
"This is Windows!n"
);
#else
printf
(
"Unknown platform!n"
);
#endif
#if __linux__
printf
(
"This is Linux!n"
);
#endif
return
0;
}
#ifdef 命令
这个命令通常是结合宏定义来使用。
#ifdef 命令的格式为:
它的意思是,如果当前的宏已被定义过,则对“程序段1”进行编译,否则对“程序段2”进行编译。
也可以省略 #else:
注意,因为一个宏要么定义了要么没定义,只有两种状态,所以最多只有一个分支。
举例:
VS/VC 有两种编译模式,Debug 和 Release。
- 在学习过程中,我们通常使用 Debug 模式,这样便于程序的调试;
- 而最终发布的程序,要使用 Release 模式,这样编译器会进行很多优化,提高程序运行效率,删除冗余信息。
为了能够清楚地看到当前程序的编译模式,我们不妨在程序中增加提示,请看下面的代码:
1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <stdlib.h>
int
main(){
#ifdef _DEBUG
printf
(
"正在使用 Debug 模式编译程序...n"
);
#else
printf
(
"正在使用 Release 模式编译程序...n"
);
#endif
system
(
"pause"
);
return
0;
}
当以 Debug 模式编译程序时,宏 _DEBUG 会被定义,预处器会保留第 5 行代码,删除第 7 行代码。反之会删除第 5 行,保留第 7 行。
#ifndef 命令
#ifndef 命令的格式为:
与 #ifdef 相比,仅仅是将 #ifdef 改为了 #ifndef。它的意思是,如果当前的宏未被定义,则对“程序段1”进行编译,否则对“程序段2”进行编译,这与 #ifdef 的功能正好相反。
区别
最后需要注意的是,
- #if 后面跟的是“整型常量表达式”,
- 而 #ifdef 和 #ifndef 后面跟的只能是一个宏名,不能是其他的。
例如,下面的形式只能用于 #if:
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#define NUM 10
int
main(){
#if NUM == 10 || NUM == 20
printf
(
"NUM: %dn"
, NUM);
#else
printf
(
"NUM Errorn"
);
#endif
return
0;
}
运行结果:NUM: 10
再如,两个宏都存在时编译代码A,否则编译代码B:
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#define NUM1 10
#define NUM2 20
int
main(){
#if (defined NUM1 && defined NUM2)
//代码A
printf
(
"NUM1: %d, NUM2: %dn"
, NUM1, NUM2);
#else
//代码B
printf
(
"Errorn"
);
#endif
return
0;
}
运行结果:NUM1: 10, NUM2: 20
#ifdef 可以认为是 #if defined 的缩写。
#ifndef和#if !defined的区别
//---------------------------------------------------------------------------------------------------------------------
//第一种方式:使用ifndef
#ifndef __HEADFILE_H__
#define __HEADFILE_H__
……头文件内容
#endif
//---------------------------------------------------------------------------------------------------------------------
//第二种方式:使用if !defined
#if !defined(__HEADFILE_H__)
#define __HEADFILE_H__
……头文件内容
#endif
//---------------------------------------------------------------------------------------------------------------------
两种方式的效果是一样的,以前常用的是第一种方式,但是在使用类似如下逻辑表达的时候,使用第二种方式将更加方便。#if !defined(HEADFILE1) && !defined(HEADFILE2)
#ifdef或者#ifndef都只能后接一个宏定义,如果有多个宏定义的逻辑表达式,就可以使用#if加上defined或者!defined的形式。
补充
1、预定义宏:
__FUNTION__ 获取当前函数名 __LINE__ 获取当前代码行号 __FILE__ 获取当前文件名 __DATE__ 获取当前日期 __TIME__ 获取当前时间 __STDC_VERSION__
2、宏参数创建字符串:”#运算符”
在宏体中,如果宏参数前加个#,那么在宏体扩展的时候,宏参数会被扩展成字符串的形式。如:
#include <stdio.h> #define PSQR(x) printf("the square of "#x" is %d.n",((x)*(x))) #define PSQR2(x) printf("the square of %s is %d.n",#x,((x)*(x))) int main() { int R=5; PSQR(R); //the square of R is 25. PSQR2(R); // the square of R is 25. return 0; }
3、预处理器的粘合剂:”##运算符”
和#运算符一样,##运算符可以用于类函数宏的替换部分。另外,##还可以用于类对象宏的替换部分。这个运算符把两个语言符号组合成单个语言符号。例如#define XNAME(n) x ## n int x1=10; XNAME(1)+=1; //x1 11
4、可变宏:… 和_VA_ARGS
有些函数(如==prinft() #06906d==)可以接受可变数量的参数。
int __cdecl printf(const char * __restrict__ _Format,...);
实现思想就是在宏定义中参数列表的最后一个参数作为省略号(三个句号)。这样,预定义宏_VA_ARGS就可以被用在替换部分中,以表明省略号代表什么。
例如
#define PR(...) printf(__VA_ARGS_) PR("Howdy"); PR("weight=%d,shipping=$%.2f.n",wt,sp)
宏定义中的空格问题
宏定义分为三个部分,宏定义指令,宏名,宏体
这三个部分是通过空格来识别以分隔的,三者之间至少需要1个空格才能被区分为三个部分,第二个部分,即宏名部分的有效部分是从第一个非空格字符开始,到遇到第一个空格字符结束;同理,宏体部分也是如此,以第一个非空格字符开始,直到遇到第一个空格字符作为结束。
也就是说,不管三者之间隔了多少个空格,都会被忽略,只认第一个非空格字符作为起始。
#define A 123
比如以上宏定义,宏名就是A,宏体就是123,里面没有任何空格。
正因为如此,所以任意部分之间都不能存在空格,一旦存在空格,就等于不止三个部分了,此时,宏定义就会报错,比如:
#define A 12 3
这是就会报错了。所以,三部分都是一个不能以空格作为分隔的整体。
对比下typedef中的空格问题:
typedef在定义形式上和宏定义类似,所以,想看看这里的空格是什么样的情况。
首先能确定的是,正常情况下,typedef的三部分和宏定义是一样的:
typedef int int32;
这三部分不以空格作为分隔的整体,并且,三部分之间至少要有一个空格,多的空格被忽略。
但是有时候,typedef不止三个部分,比如:
这种情况仍然可行,看语句中的颜色,可以猜想到,对于类型来说,是作为一个整体来看待的,不管中间有多少空格,都会给缩到一个空格,按照unsigned short int类型来作为一个整体。
debug宏
可以用宏定义来实现条件编译
程序有DEBUG版本和RELEASE版本,区别就是编译时有无定义DEBUG宏。程序调试的debug宏
程序调试的常见方案:单步调试、裸机LED调试、打印信息、log文件
1、利用调试器进行单步调试(譬如IDE中,Jlink)适用于新手,最大的好处就是直观,能够帮助找到问题。缺点是限制性大、速度慢。2、裸机使用LED、蜂鸣器等硬件调试,适合单片机裸机程序。
3、printf函数打印调试,比较常用,好处是具有普遍性,几乎在所有的情况下都能用。
4、log文件(日志文件)是系统运行过程中在特定时候会打印一些调试信息,日志文件记录下来这些调试信息以供后续查找追查问题。适合于系统级或者大型程序的调试。打印信息不能太多也不能太少,调试信息太少会不够信息找到问题所在。调试信息太多会有大量的无用的信息淹没有用信息,导致有用信息无法看见,等于没有。
调试(DEBUG)版本和发行(RELEASE)版本的区别
DEBUG版本就是包含了调试信息输出的版本,在程序测试过程中会发布debug版本,这种版本的程序运行时会打印出来调试信息/log文件,这些信息可以辅助测试人员判断程序的问题所在。DEBUG版本的坏处是输出调试信息占用了系统资源,拖慢了系统运行速度。因此DEBUG版本的性能低于RELEASE版本。
RELEASE版本就是最终的发布版本,相较于DEBUG版本的功能代码是一样的,但是去掉了所有的调试信息。适合最终测试通过要发布的程序,因为去掉了调试信息所以程序的运行效率要更高。
DEBUG和RELASE版本其实是一套源代码。源代码中是有很多的打印调试信息的语句的,如何来控制生成DEBUG和RELEEASE版本?靠条件编译,靠一个宏。DEBUG宏大概的原理是:
#ifdef DEBUG #define dbg() printf() #else #define dbg() #endif
工作方式是:如果我们要输出DEBUG版本则在条件编译语句前加上#define DEBUG即可,这样程序中的调试语句dbg()就会被替换成printf从而输出;如果我们要输出RELEASE版本则去掉#define DEBUG,则dbg()就会被替换成空,则程序中所有的dbg()语句直接蒸发了,这样的程序编译时就会生成没有任何调试信息的代码。
分析几个DEBUG宏
1、应用程序中DEBUG宏#ifdef DEBUG #define DBG(...) fprintf(stderr, " DBG(%s, %s(), %d): ", __FILE__, __FUNCTION__, __LINE__); fprintf(stderr, __VA_ARGS__) #else #define DBG(...) #endif
注:__FILE__等是C语言中的预定义宏,就是说这个东西是个宏定义,但是是C语言自己定义的。这些宏具有特殊的含义,譬如__FILE__表示当前正在编译的c文件的文件名。
2、内核中的DEBUG宏
#ifdef DEBUG_S3C_MEM #define DEBUG(fmt, args...) printk(fmt, ##args) #else #define DEBUG(fmt, args...) do {} while (0) #endif