详解C语言的预编译

Last Updated: 2023-12-12 04:52:35 Tuesday

-- TOC --

编译C/C++代码,首先就是预编译(preprocessing),这一步其实很独立,主要就是(递归)引入头文件(include header file),进行宏替换(macro replacement),和处理条件包含(conditional inclusion),生成一个编译单元。

预编译干的事情,都是文本选择和替换!都是在编译前,纯粹的文本处理工作。

源码中#开头的行,都属于预编译!

生成预编译后的中间文件(默认的.i后缀,表示intermediate):

$ gcc -E test.c -o test.i

-E,控制gcc只做预编译,输出预编译的结果到.i文件!如果不使用-o,默认输出到stdout。

理解C语言的编译,一定要在头脑中形成一个重要的概念,每一个.c文件是独立编译的,只是最后链接在一起而已!每个.c文件编译的时候,从上到下。

下面分块来详细说明C语言预编译过程所进行的操作,以及一些重要细节。

头文件(#include)

头文件包含有两种形式:<>"",它们的区别仅仅是起始搜索路径的不同。

头文件搜索路径

  1. 当前路径;
  2. gcc编译器-I指定的路径;(常用gcc编译参数
  3. 系统默认路径;

两种形式的起始搜索不同:

在头文件中,一般都能看到这样的定义:

#ifndef XXXXX_H
#define XXXXX_H 1
...
#endif

这是为了避免同一个头文件的重复包含!这个语法叫条件包含,见下文。

所谓包含,其实就是将头文件的内容在#include语句出现的地方进行替换展开。头文件中也会包含其它头文件,预处理允许这种递归包含替换,但一条include指令只能包含一个头文件!

头文件中一般会包含各种其它include和define,以及一些函数的declaration。这种机制保证了所有source file(.c文件)使用的macro和declaration是一致的,避免了一类bug的出现。也因此,只要头文件内容发生变化,所有依赖此文件的source file都需要重新编译。由于编译是从上到下进行的,有的时候,头文件引用顺序也会引入奇妙的编译问题。

宏定义(#define和#undef)

宏的作用也是文本替换,无论有无参数,都是简单的替换。

#define AA
#define BB 123
#define MAX(a,b) ((a)>=(b)?(a):(b))

AA是一个已被定义了的Macro,它只是被定义了,但没有替换内容。如果要在代码中替换AA,会被替换成空,什么都没有!使用这类没有替换内容的Macro,不影响代码编译,但可提高代码可读性,甚至会被编译器利用。而BB会在代码文本中被替换成123。

Macro Substitution只在Token上起作用,在字符串内或非token内都不会发生替换,比如BB,在"BB"(字符串内)和ABB(token内)出现时,都不会执行替换。

MAX(a,b)看起来像个Function,但也是替换,替换成一个表达式(inline code),加上那么多括号(优先级最高),是为了防止替换展开后的代码出现语义上的混乱。这样做的另一个好处,是不限制data type,如果是函数定义,就要限定参数的type。不过,这样的macro,在使用时也有

  1. 不正确的使用:MAX(a++,b++) // wrong,a和b有可能不支持这样的操作。
  2. 不正确的定义:#define MAX (a,b) ...,MAX如果要带参数,它与(符号之间不能有空格!

#undef很简单,就是去掉某个Macro的定义,后续代码再出现这个Macro,会报错。对于前面定义的带参数的MAX,undef时不用带参数:

#undef MAX

#define#undef配合,实现了在源码中的某一个范围内替换特定文本的功能。

当一个Macro要替换的text很长时,一行放不下,或放在一行可读性的很差,使用\在行尾,表示继续下一行,这叫line splicing

Line Splicing: Lines that end with the backslash character \ are folded by deleting the backslash and the following newline character. This occurs before division into tokens.

使用backslash定义macro的经典case

一个Macro的生命周期,从#define那一行开始,直到编译的source file的最后,或者遇到#undef。一个Macro的定义,可以使用前面已经定义过的的Macro。

几个预处理操作符,Preprocessor Operators,如###,介绍如下。

#操作符

在#define定义的macro function中,如果在参数前使用一个#操作符,表示将此参数文本用双引号括起来,即将参数名称转换成字符串。示例如下:

#include <stdio.h>
#define dprint(expr) printf(#expr " = %g\n", expr)

int main(){
    double a = 11;
    double b = 7;
    dprint(a);
    dprint(b);
    dprint(a/b);
    dprint(a+b);
    dprint(a*b/a);
    return 0;
}

输出:

$ gcc test.c -o test
$ ./test
a = 11
b = 7
a/b = 1.57143
a+b = 18
a*b/a = 7

在macro中:#expr == "expr"

这里还用到了C语言的一个特性,两个字符串放在一起时(中间可以有space),编译器自动做字符串拼接,比如:

#include <stdio.h>

int main(void){
    printf("abc""123""\n");
    printf("abc" "123"    "\n");
    printf("abc"
           "123"
           "\n");
    printf("abc" \
           "123" \
           "\n");
    return 0;
}

输出:

$ gcc -Wall -Wextra test.c -o test
$ ./test
abc123
abc123
abc123
abc123

##操作符

两个##操作符,用于在macro展开的时候,硬拼接macro function的参数。

#include <stdio.h>
#define CON(a,b) a##b

int main(void){
    char ka[] = "abcdefg";
    printf("%d\n", CON(666,123)); // int 666123
    printf("%s\n", CON(k,a)); // variable ka
    return 0;
}

输出:

$ gcc test3.c -o test3
$ ./test3
666123
abcdefg

硬拼接,不需要加上引号。注意,这是在预编译阶段执行的简单文本处理,还没真正开始编译呢...

Predefined Macros

常见的standard predefined macros有:

__FILE__
__LINE__
__DATE__
__TIME__
__STDC__
__cplusplus 
__func__
__VA_ARGS__

C++编译器,一定会定义__cplusplus这个宏!注意:这个Macro没有后面的__

COUNTER这个预定义的宏,值的了解一下。

Variadic Macro

可变参数数量的Macro!C99引入的特性,__VA_ARGS__。一个示例如下:

#include <stdio.h>

// ... --> ##__VA_ARGS__
#define pp(x, ...)   printf(x, ##__VA_ARGS__)

int main(void) {
    int b  = 2345;
    pp("b:%d %s\n", b, "haha..");
    pp("variadic macro\n");
    return 0;
}

...所代表内容为空时,编译器会自动将x后面的那个逗号去掉,以保证不会有语法错误。

使用__VA_ARGS__

条件包含(#if)

Conditional Inclusion,以前都说成了条件编译,看来是不准确的。条件包含,是用来控制预编译生成编译单元。

条件包含由各种#if组成的条件判断语句组成,逻辑并不复杂,符合条件的代码块,将会出现的.i文件中,并进入后续的编译环节。不符合条件的代码块,不会参与编译,.i文件中也不会出现。

示例:

/* Convenience macros to test the version of gcc. */
#undef __GNUC_PREREQ
#if defined __GNUC__ && defined __GNUC_MINOR__
# define __GNUC_PREREQ(maj, min) \
        ((__GNUC__ << 16) + __GNUC_MINOR__ >= ((maj) << 16) + (min))
#else
# define __GNUC_PREREQ(maj, min) 0
#endif

#if defined __GNUC__#if defined(__GNUC__)等价!

#if ...
...
#elif ...
...
#elif ...
...
#else
...
#endif

其它预编译

就一个#呢?

某行代码就一个#,没有任何效果。

#error [msg]

如果预编译遇到这一行,会抛出msg,作为错误提示。没有msg也可以,反正遇到#error,整个编译结束!!

msg不需要双引号括起来!

#warning [msg]

显示warning,编译继续...

#pragma directive

编译控制指令,具体的options与具体编译器相关。

In C programming, "pragma" is not an acronym. It is a keyword used to provide additional instructions or directives to the compiler. The word "pragma" itself is short for pragmatic information or pragmatic directive. It is typically followed by a specific directive that specifies certain compiler-specific instructions.

#line

#line用于强制指定新的行号和编译文件名,并对源程序的代码重新编号

用法:#line number newFilename //newFilename 可省略

#line编译指示的本质是,重定义__LINE__和__FILE__

用得很少...

本文链接:https://cs.pynote.net/sf/c/202111183/

-- EOF --

-- MORE --