详解C语⾔的宏定义
宏定义介绍
假设我们有⼀个 C 源⽂件 main.c,那么只需要通过 gcc main.c - 即可编译成可执⾏⽂件(如果只写 gcc main.c,那么 Windows 上会默认⽣成 a.exe、Linux 上会默认⽣成 a.out ),但是这⼀步可以拆解成如下步骤:
预处理:gcc -E main.c -o main.i,根据 C 源⽂件得到预处理之后的⽂件,这⼀步只是对 main.c 进⾏了预处理:⽐如宏定义展开、头⽂件展开、条件编译等等,同时将代码中的注释删除,注意:这⾥并不会检查语法;
编译:gcc -S main.i -o main.s,将预处理后的⽂件进⾏编译、⽣成汇编⽂件,这⼀步会进⾏语法检测、变量的内存分配等等;
汇编:gcc -c main.s -o main.o,根据汇编⽂件⽣成⽬标⽂件,当然我们也可以通过 gcc -c main.c -o main.o 直接通过 C 源⽂件得到⽬标⽂件;
链接:gcc main.o -,程序是需要依赖各种库的,可以是静态库也可以是动态库,因此需要将⽬标⽂件和其引⽤的库链接在⼀起,最终才能构成可执⾏的⼆进制⽂件。
⽽这⾥我们主要来介绍⼀下预处理中的宏定义,相信很多⼈都觉得宏定义⾮常简单,但其实宏定义有很多⾼级⽤法。我们先来看看简单的宏定义:
#include <stdio.h>
// 宏定义的⽅式为:#define 标识符常量
// 然后会将所有的 PI 替换成 3.14
#define PI 3.14
int main() {
printf("%f\n", PI);
}
我们⽣成预处理之后的⽂件:gcc -E main.c -o main.i
我们看到 PI 被替换成了 3.14,当然除了浮点型之外,也可以是其它的类型:
#include <stdio.h>
#define NAME "satori"
#define AGE 17
#define GENDER 'f'
int main() {
printf("%s %d %c\n", NAME, AGE, GENDER); // satori 17 f
}
我们再来查看⽣成的预处理⽂件:
我们看到确实只是简单替换,除此之外,没有做任何的处理。注意,我们不能这么写:
#define NAME satori
如果是上⾯这种写法的话,那么 NAME 会被换成 satori,但 satori 的周围没有引号,因此它是⼀个变量。如果是这种情况的话,那么我们必须定义好名为 satori 的变量,举个栗⼦:
#define NAME1 "satori"
#define NAME2 satori
int main() {
int satori = 123;
printf("%s %d\n", NAME1, NAME2); // satori 123
}
相信此时都能理解,但是为了更直观,我们还是看⼀下预处理之后的⽂件:
如果我们不定义⼀个名为 satori 的变量,那么执⾏的时候是会报错的,因此我们可以看到宏定义只是在预处理的时候进⾏⽂本间的替换,不涉及任何的语法检查。
#include <stdio.h>
#define end );
int main() {
printf("%s\n", "hello world"end // hello world
}
虽然很奇怪,但确实是合法的 C 代码,只不过在⼯作中不要这么⼲。
宏名的命名规则和变量是⼀样的,但为了和普通的变量区分,宏的名字我们约定全部使⽤⼤写字母组成。既然是约定,那么很明显你也可以不⼤写,因为约定不是约束。
下⾯我们来介绍宏定义的更⾼级的⽤法,但是不管怎么⾼级,有⼀点是不变的,那就是宏定义只做替换,预处理这⼀步不进⾏语法检查。
宏定义的作⽤域与嵌套
宏⼀旦定义好,那么它的作⽤域就是全局的,如果我们想中⽌某个宏定义该怎么做呢?
#include <stdio.h>
#define PI 3.14
int main() {
// 程序会报错,因为预处理之后这⼀⾏会变成 float 3.14 = 3.1415926,⽽ = 的左边不能是常量
double PI = 3.1415926;
printf("%lf\n", PI);
}
所以我们可以中⽌宏定义:
#include <stdio.h>
#define PI 3.14
int main() {
#undef PI // 从这⼀⾏之后,PI 这个宏将失去作⽤
double PI = 3.1415926;
printf("%lf\n", PI); // 3.141593
}
宏定义还可以进⾏嵌套,举个栗⼦:
#define PI 3.14
#define R 4
// 在⼀个宏⾥⾯使⽤了别的宏,我们称之为宏的嵌套
// 此外我们也可以看到宏定义中也可以涉及到表达式,因为 AREA 最终就等价于 3.14 * 4 * 4
#define AREA PI * R * R
int main() {
printf("%lf\n", AREA); // 50.240000
}
带参数的宏定义
如果只是简单的替换,那么未免太⽆趣了,没错,宏定义也可以带参数。c语言中的sprintf用法
#include <stdio.h>
// MAX(1, 2) 会被换成 1 > 2 ? 1 : 2 等于是求出 x 和 y 之间⼤的那⼀个
// 这就意味着我们传递的时候,x 和 y 都应该接收数值类型的值
#define MAX(x, y) x > y ? x : y
// sum(1, 2) 会被换成 1 + 2
#define SUM(x, y) x + y
// 注意:宏名和括号之前不能有空格,也就是我们必须写成 MAX(x, y) 和 SUM(x, y),不可以写成 MAX (x, y) 或 SUM (x, y)
int main() {
printf("%d\n", MAX(22, 33)); // 33
printf("%d\n", SUM(22, 33)); // 55
}
还是⽐较简单的,但是需要注意的是⼩括号⾥⾯的参数可以有空格,但是宏名和⼩括号之间不能有空格。否则的话,就不叫带参数的宏定义了,⽽是⼀个普通的宏,但显然这个普通的宏在被替换之后是会造成语法错误的。
所以我们看到这个宏就类似于⼀个函数⼀样,但是它不是函数,函数的参数是需要指定类型的,⽽宏的参数则不需要,因为预处理之后它会被替换掉。当然我们也可以把宏也可以写成多⾏,只不过每⼀⾏的结尾需要使⽤反斜杠进⾏标识,表⽰该⾏尚未结束。举个栗⼦:
#include <stdio.h>
#define SUM(start, end) \
long result = 0; \
int i; \
for (i = start; i <= end; i++){ \
result += i; \
}
int main() {
SUM(1, 100);
printf("%ld\n", result); // 5050
}
在替换之后就等价于如下:
int main() {
long result = 0; int i; for (i = 0; i <= 100; i++){ result += i; }
printf("%ld\n", result); // 5050
}
当然对于带参数的宏定义,还有⼀个陷阱,这个陷阱是什么呢?我们举个栗⼦:
#include <stdio.h>
#define SQUARE(x) x * x
int main() {
printf("%d\n", SQUARE(1 + 2)); // 5
}
按照我们的猜测,它应该打印 9 才对,但是为什么打印 5 呢?我们看⼀下预处理之后的⽂件:
根据 C 源⽂件的编译过程我们之后,预处理这⼀步是最先发⽣的,所以它不会将 1 + 2 计算好之后再进⾏替换,⽽是直接对1 + 2 这个整体进⾏⽂本上的替换,所以此时得到的结果就不符合我们的预期了。那么怎么办呢?没错,应该将每⼀步都使⽤括号括起来:
#include <stdio.h>
#define SQUARE(x) ((x) * (x))
int main() {
printf("%d\n", SQUARE(1 + 2)); // 9
}
同理我们之前的#define MAX(x, y) x > y ? x : y也是不规范的,后⾯的部分应该写成:(((x) > (y)) ? (x) : (y))
内联函数
通过带参数的宏我们可以实现类似函数⼀样的效果,但是带参数的宏也容易出现很多问题,⽐如:
#include <stdio.h>
#define SQUARE(x) (x) * (x)
int main() {
int i = 2;
printf("%d\n", SQUARE(++i)); // 16
}
i ⾃增 1 之后变成 3,理论上结果应该是 9 才对,为什么是 16 呢?原因就是SQUARE(++i)会被替换成(++i) * (++i)。此时 i 连续⾃增两次,因为变成了 4,所以平⽅得 16。
因此宏定义虽然可以实现类似于函数的效果,也没有函数调⽤带来的开销,但是它代替不了函数,因为简单的⽂本上的替换肯定是有局限性的。当我们想省去函数调⽤的开销时,应该使⽤内联函数,⽐如我们需要在⼀个多重嵌套的循环中调⽤⼀个函数、或者该函数会被频繁调⽤,那么就可以将其声明为内联函数。
int inline square(int x) {
return x * x;
}
int main() {
int i = 2;
printf("%d\n", square(++i)); // 9
}
只需要在函数名的前⾯加上⼀个 inline 关键字即可,它所做的事情本质上和宏定义是类似的,也是将内联函数嵌⼊到调⽤者的代码中,只不过它避免了宏定义的缺点。我们使⽤内联函数,完全可以像使⽤常规的函数⼀样,但是它没有函数调⽤所带来的额外开销,它和宏定义类似只是进⾏展开⽽已。
内联函数实际上是⼀种优化⼿段,只有在进⾏优化编译时才会执⾏代码的嵌⼊处理。如果在编译过程中没有指定优化选项 -O,那么内联函数就不会像宏定义展开⼀样,⽽是会被当成普通的函数调⽤来处理。所以为了使内联函数⽣效,我们编译的时候应该指定 -O 选项:gcc main.c -O -。但对于当前 Windows 上的 8.1.0 版本的gcc 来说,如果定义了内联函数,那么编译的时候必须指定 -O 选项,否则报错。
既然内联函数这么好,那么我们是不是应该把所有的函数都声明为内联函数呢?
其实不然,内联函数虽然节省了函数调⽤的时间消耗,但由于每⼀个函数出现的地⽅都要进⾏替换,因此增加了代码编译的时间。另外,并不是所有的函数都可以变成内联函数。
当然现在的编译器都很聪明,即使你不写 inline,它也会⾃动将⼀些函数优化成内联函数。因为编译器⽐你更了解哪些函数应该内联、哪些函数应该不内联。
# 和 ##
# 和 ## 是两个预处理运算符,那么它们的作⽤是什么呢?在带参数的宏定义中,# 运算符后⾯需要跟⼀个参数,编译器会⾃动把这个参数转成字符串。
#include <stdio.h>
#define NAME1 "satori"
#define NAME2(s) #s
int main() {
printf("%s %s\n", NAME1, NAME2(satori)); // satori satori
}
我们看到 satori 并没有被定义,但是编译器会⾃动转成字符串,但如果我们写成#define NAME2(s) s就会报错了,会提⽰satori 没有定义。同理我们也不可以写成#define NAME2 #s,因为必须在带参数的宏定义中,# 才会有效。
此外,如果存在多个空格,那么等同于⼀个空格,什么意思呢?我们举个栗⼦:
#include <stdio.h>
#define STR(s) #s
int main() {
printf(STR(name = %s age = %d \n), STR(satori), 16); // name = satori age = 16
}
## 运算符被称为记号连接运算符,⽐如我们可以使⽤ ## 来连接两个参数:
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论