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语言预编译过程所进行的操作,以及一些重要细节。
头文件包含有两种形式:<>
或""
,它们的区别仅仅是起始搜索路径的不同。
头文件搜索路径
-I
指定的路径;(常用gcc编译参数)两种形式的起始搜索不同:
<>
:从2开始搜索,跳过当前路径。""
:从1开始搜索。在头文件中,一般都能看到这样的定义:
#ifndef XXXXX_H
#define XXXXX_H 1
...
#endif
这是为了避免同一个头文件的重复包含!这个语法叫条件包含,见下文。
所谓包含,其实就是将头文件的内容在#include
语句出现的地方进行替换展开。头文件中也会包含其它头文件,预处理允许这种递归包含替换,但一条include指令只能包含一个头文件!
头文件中一般会包含各种其它include和define,以及一些函数的declaration。这种机制保证了所有source file(.c文件)使用的macro和declaration是一致的,避免了一类bug的出现。也因此,只要头文件内容发生变化,所有依赖此文件的source file都需要重新编译。由于编译是从上到下进行的,有的时候,头文件引用顺序也会引入奇妙的编译问题。
宏的作用也是文本替换,无论有无参数,都是简单的替换。
#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,在使用时也有坑:
MAX(a++,b++) // wrong
,a和b有可能不支持这样的操作。#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.
一个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
硬拼接,不需要加上引号。注意,这是在预编译阶段执行的简单文本处理,还没真正开始编译呢...
常见的standard predefined macros有:
__FILE__
__LINE__
__DATE__
__TIME__
__STDC__
__cplusplus
__func__
__VA_ARGS__
- https://gcc.gnu.org/onlinedocs/cpp/Standard-Predefined-Macros.html
- https://gcc.gnu.org/onlinedocs/cpp/Common-Predefined-Macros.html
C++编译器,一定会定义__cplusplus
这个宏!注意:这个Macro没有后面的__
。
COUNTER这个预定义的宏,值的了解一下。
可变参数数量的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后面的那个逗号去掉,以保证不会有语法错误。
Conditional Inclusion,以前都说成了条件编译,看来是不准确的。条件包含,是用来控制预编译生成编译单元。
条件包含由各种#if
组成的条件判断语句组成,逻辑并不复杂,符合条件的代码块,将会出现的.i文件中,并进入后续的编译环节。不符合条件的代码块,不会参与编译,.i文件中也不会出现。
#if
后面跟的是表达式,而#ifdef
后面是一个macro!#ifdef
和#if defined
的区别,前者只能判断一个macro,后者实际上可以是很复杂的表达式,由defined来判断某个macro是否有定义,而判断macro具体值的时候,则可以使用==
,!
,&&
和||
。示例:
/* 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
#
呢?某行代码就一个#
,没有任何效果。
如果预编译遇到这一行,会抛出msg,作为错误提示。没有msg也可以,反正遇到#error,整个编译结束!!
msg不需要双引号括起来!
显示warning,编译继续...
编译控制指令,具体的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
orpragmatic directive
. It is typically followed by a specific directive that specifies certain compiler-specific instructions.
#line
用于强制指定新的行号和编译文件名,并对源程序的代码重新编号
用法:#line number newFilename //newFilename 可省略
#line
编译指示的本质是,重定义__LINE__和__FILE__
用得很少...
本文链接:https://cs.pynote.net/sf/c/202111183/
-- EOF --
-- MORE --