C语言-预处理(1)

C·语法 2019-04-20 8306 字 1396 浏览 点赞

预处理

C 程序编译流程:1. 预处理阶段 2. 编译阶段 3. 汇编阶段 4. 链接阶段 5. 运行可执行程序。

如何让编译停在预处理结束的时候呢?用参数 -E,完整的示例为 : gcc -E -o src/example.i src/example.c,即:编译 src 目录下的 example.c 文件,在预处理阶段结束后停止,将内容保存到 src 目录下的 example.i 文件中(你甚至可以不用 “.i” 结尾,但这已约定俗成)。

什么是预处理

在说什么是预处理之前,先讲讲预处理的好处吧。那就是:合理使用预处理编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。

预处理阶段由预处理程序负责,这个程序会在进行编译的第一遍扫描(词法扫描和语法分析)之前工作。也就是说,预处理不会对语法什么的做分析,总之 “错就错了,我这里都让过”。

所以预处理要搞啥呢?预处理的任务大致有这 4 点:1. 删除注释;2. 插入被 #include 指令包含的文件内容; 3. 定义和替换由 #define 指令定义的符号(也叫宏替换或宏扩展);4. 条件编译。

预处理需要处理的是预处理部分,而预处理部分由预处理指令构成。C 语言对预处理指令的格式要求为:

  • # 开头;
  • 占单独书写行(自然可以用连字符 “\” ,但你应该确保反斜线是换行符前的最后一个字符);
  • 语句尾不加分号。

接下来就针对性的说说 宏定义 、条件编译 和 文件包含。

宏定义

C 语言中允许用一个标识符表示一个文本(文本可以是字符串,数字,代码语句),这个标识符称为。在预处理时,对程序中所有出现的宏,都用宏定义中的文本替换,这个过程称为宏替换宏展开。如:

#define PI 3.1415926  // PI 称为宏   // 文本是 浮点数
#define MY_NAME "Jone"  // 文本为 字符串
#define GREET printf("hello")  // 文本为 代码语句

宏定义由源程序中的宏定义指令(#define)完成;宏替换由预处理程序自动完成。C 语言中,宏定义分为:无参宏定义带参宏定义

无参宏定义

语法结构:

#define 标识符(宏名) [文本(宏体)]

功能:定义可在程序中使用的宏,宏的内容由文本(宏体)替代。像是下面这段代码:

// example.c
#define WORD "hello, world"

int main()
{
    printf("%s", WORD);
}

预处理之后会变成这样:

// example.i
...

int main()
{
    printf("%s", "hello, world");
}

可见,预处理部分不见了,WORD 也被宏体取而代之。同时,定义过的宏还可以被取消。取消宏有两种方式,一种是 “缺省宏体”;另一种是用预处理指令 #undef

两种方式会得到各自不同的结果:

// example.c
#define WORD "hello, world"
#define WORD  // 缺省宏体

int main()
{
    printf("%s", WORD);
}
/***************************/
// example.i
...

int main()
{
    printf("%s", );
}
// example.c
#define WORD "hello, world"
#undef WORD  // 预处理命令取消

int main()
{
    printf("%s", WORD);
}
/***************************/
// example.i
...

int main()
{
    printf("%s", WORD);
}

前面说过宏体也可以是代码语句,但需要注意的是,宏定义不是 C 语句,所以末尾不必加分号(尽管你可以这么做,但并不这么建议)。

// example.c
#define GREET printf("hello, world\n")  // 没有分号

int main()
{
    GREET;  // 使用时需要加上分号
}

带参宏定义

带参宏也叫做类函数宏,语法结构:

#define 宏名(形参列表) 宏体

宏名(实参表);  // 调用

对于带参数的宏,在调用中会进行宏展开,然后用实参替换形参。就像下面这个获取最大值的例子:

// example.c
#define MAX(a, b) ( (a)>(b)? (a):(b) )

int main()
{
    int rlt = MAX(10, 1);
    printf("%d", rlt);
}
/************************************/
// example.i
...

int main()
{
    int rlt = ( (10)>(1)? (10):(1) );
    printf("%d", rlt);
}

通过带参宏定义可以实现 C 语言的泛型编程。众所周知,C 语言中变量需要声明类型,但宏定义不需要声明类型。上述代码中的 MAX() 既可以传入整数类型的 10 和 1,也可以是浮点类型的 5.12 和 10.24 。另外,关于带参宏定义有以下规则值得遵守与注意:

  • 宏名和形参列表之间不能有空格出现(如果出现空格,那么参数列表会被视作宏体);
  • 形参不分配内存单元,因此不必做类型定义;
  • 宏定义中的形参是标识符,宏调用中的实参可以是表达式
  • 宏体内的形参通常要用括号括起来以避免出错,如:

    #define POWER(x) ((x) * (x))
    // 此时 POWER(10+1) =》 ((10+1) * (10+1)) =》 结果:121
    
    #define POWER(x) x * x
    // 此时 POWER(10+1) =》 10 + 1 * 10 + 1 =》 结果:20
  • 为体现表达式的整体性,在表达式最外层加上括号也是值得的操作。

宏定义的注意事项

第一点:你无法通过宏展开的方式创建预处理命令。

// example.c
#define PRE_CMD #define PI 3.1415  // 替换文本是预处理命令

int main()
{
    PRE_CMD
    printf("%f", PI);
}
/********************************/
// example.i
int main()
{
    #define PI 3.1415  // 替换后
    printf("%f", PI);
}

可以看到,尽管 #define PI 3.1415 在形式上是正确的预处理命令,但预处理器不会执行它。

第二点:宏不可以递归地展开。此时分两种情况讨论:

  • 当宏名为 WORD 时,宏体中也存在 WORD ,预处理器无法展开宏体中 WORD 。

    //example.c
    #define WORD "hello" WORD  // 替换文本是 "hello" WORD
    
    int main()
    {
      printf("%s", WORD);
    }
    /***************************/
    // example.i
    ...
    int main()
    {
      printf("%s", "hello" WORD);  // 不能对 WORD 做第二次展开
    }
  • 当有两个宏,WORD1 和 WORD2。如果 WORD1 的宏体中有 WORD2 ,同时 WORD2 的宏体有 WORD1 ,也不能展开。

    // example.c
    #define WORD1 "hello" WORD2
    #define WORD2 "world" WORD1
    
    int main()
    {
      printf("%s", WORD1);
    }
    /**************************/
    // example.i
    ...
    int main()
    {
      printf("%s", "hello" "world" WORD1);  // 可见,对 WORD1 展开后,WORD2 也被展开了,
                                            // 但 WORD2 中的 WORD1 没被展开
                                            // 即,宏定义时允许使用其他的宏,只是不允许自己使用自己
    }

第三点:带参宏定义和带参函数很相似,但有本质上的不同。

~带参宏定义函数
处理时间编译时程序运行时
参数类型无参数类型需要声明实参和形参类型
处理过程不分配内存,只是简单的字符置换分配内存,先求实参值,再传递给形参
程序长度变长不变
运行速度不占运行时间调用和返回占用时间
支持递归不支持支持

第四点:取消带参宏定义不需要写参数列表。

#define MAX(a, b) ( (a)>(b)? (a):(b) )
#undef MAX  // 直接写宏名即可

条件编译

当我们希望对源代码中的一部分内容,在满足一定条件下才进行编译,就需要条件编译。也就是说,条件编译可以指定这段代码要不要被忽略。条件编译的好处是:有利于提升程序的可移植性,增强程序的灵活性。

条件编译的预处理命令有:#if#elif#else#endif#ifndef#ifdef

比如我们想灵活的控制调试信息,开发的时候需要日志调试,上线的时候取消调试信息。于是代码可以这样写:

#include <stdarg.h>
#define DEBUG 1  // DEBUG 状态

void debug(char* fmt, ...)  // 行调试功能的函数
{
    char msg[1024*4] = {};
    va_list valist;                                                                                 
    va_start(valist, fmt);
    vsprintf(msg, fmt, valist);
    va_end(valist);
    printf("debug: ");
    printf("%s\n", msg);
}

int main()
{
    printf("running ...\n");
#if DEBUG  // 如果是 DEBUG 模式
    debug("%s", "this is a debug info");
#endif  // 结束如果
    printf("exit ...\n");
}

编译之后运行结果可得:

Guan@Orchard:~/CC++/19/src$ ./a.out 
running ...
debug: this is a debug info
exit ...

当程序上线,只需要将 #define DEBUG 1 修改为 #define DEBUG 0 ,再编译程序就不会有 debug 信息了。


条件编译与寻常的 if ... else ... 用法一致,只是末尾需要 #endif 作为结束标志。

#if 常量表达式
    程序段1
#else
    程序段2
#endif

/*******************/

#if 常量表达式1
    程序段1
#elif 常量表达式2
    程序段2
#else
    程序段3
#endif

#ifdef 则是 #if defined 的缩写,表示后面接上的标识符是否被 #define 指令定义过。定义过返回真,否则返回假。相应的还有 #ifndef,是 #if !defined 的缩写,表示如果没有定义,那么……

判断是否定义过某个标识符的用法极为常见,如防卫式声明。拿标准库 stdio.h 文件来说,其内容就如下:

#ifndef _TR1_STDIO_H  // 如果没有定义过 _TR1_STDIO_H ,那就执行下面逻辑
#define _TR1_STDIO_H 1

#include <tr1/cstdio>

#endif

头文件应该总是有防卫式声明,避免重复导入。

文件包含

文件导入在实际使用中很平凡,比如 C 程序中一般都会有这样一句话:#include <stdio.h> 。这段代码翻译过来就是:“请把 stdio.h 中声明的代码给我用用”。

语法格式用两种:

#include <文件名>
// 或者
#include "文件名"

二者的不同在于搜索路径。用 #include <...> 时,预处理器会在特定的系统路径下搜索,在 Unix 中会是 /usr/local/include/usr/include 。用 #incude "..." 时,预处理器通常首先在当前目录下寻找,如果没找到,再去系统的 include 路径中找。甚至允许添加导入文件的所在路径,如 #include "./demo/a.h" (意为:去当前路径下的 demo 目录中,找 a.h 文件),尽管这种方式不推荐。最好是在编译时添加搜索路径,需要参数 I(大写 i),完整命令:gcc a.c -Idemo


#include 命令支持嵌套使用,举个例子:

// a.c 源码
#include <stdio.h>
#include "a.h"

int main()
{
    printf("%f\n", PI);
}
// a.h 头文件
#include "b.h"
// b.h 头文件
#include "c.h"
// c.h 头文件
#define PI 3.14

此时编译 a.c 文件不会报错,运行程序也可以正常输出 PI 代表的值。这正是嵌套的意义(《C 语言核心技术》中说预处理器最多允许 15 层嵌套,我未作测试,不知现在还是不是)。


然而嵌套也会带来不必要的麻烦,也就是前面说的重复导入,换句话说,就是会引发重复定义。

现在我希望自定义一个 print_format ,让我的每次打印输出都会有特定的 “Guan say: ” 开头。因此我在 a.h 文件中定义 print_format 函数:

// a.h
#include <stdio.h>
#include <stdarg.h>

void print_format(char*, ...);

void print_format(char* fmt, ...)
{
    char msg[1024*4] = {};
    va_list valist;
    va_start(valist, fmt);
    vsprintf(msg, fmt, valist);
    va_end(valist);
    printf("Guan say: ");
    printf("%s\n", msg);
}

然后需要在 b.h 中使用这个函数,b.h 里的内容如下:

// b.h
#include "a.h"

void say_a_word()
{
    print_format("%s", "今天天气很好呢!");
}

最后源码 a.c 中既需要使用 a.h 中的 print_format(),也需要使用 b.h 中的 say_a_word()。a.c 内容如下:

#include "a.h"
#include "b.h"

int main()
{
    print_format("%s", "开始说话。");
    say_a_word();
    print_format("%s", "说话结束。");
}

编译 a.c 出现错误:

Guan@Orchard:~/CC++/19/src$ gcc a.c 
In file included from b.h:1:0,
                 from a.c:2:
a.h:9:6: error: redefinition of ‘print_format’
 void print_format(char* fmt, ...)
      ^
In file included from a.c:1:0:
a.h:9:6: note: previous definition of ‘print_format’ was here
 void print_format(char* fmt, ...)
      ^

会有这种问题是因为 #include 支持嵌套使用,导致重复导入了 a.h 。解决方案是在 a.h 中添加防卫式声明:

#ifndef __A_H__
#define __A_H__
...
#endif

最后编译正常,运行正常,输出如下:

Guan@Orchard:~/CC++/19/src$ ./a.out 
Guan say: 开始说话。
Guan say: 今天天气很好呢!
Guan say: 说话结束。

(未完待续)

感谢

  • 参考《C 语言核心技术(第 2 版)》
  • 参考海同网校王正平老师的C 语言之预处理


本文由 Guan 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

还不快抢沙发

添加新评论