你真的了解C/C++程序预处理阶段干的这5件事情吗?
这篇博客详细讲解了C/C++程序被翻译成可执行程序需要经理的步骤。其中,预处理(也称为预编译)阶段干了5件事情,分别是:
1.删注释
程序员写注释,是给人看的,不是给计算机看的。所以,在预处理阶段会把注释都删掉。C/C++中有2种注释风格,分别是:
- C的注释风格:以
/*
开头,*/
结尾,不能嵌套注释。
/* 这里写注释 */
- C++的注释风格:以
//
开头,一直到本行末尾。
// 这里写注释
2.#define的替换
#define可以定义2种替换方式。
- 定义标识符。
如#define NUM 100
,如果遇到了NUM就会被替换成100。
printf("%d\\n", NUM); // 会被替换成printf("%d\\n", 100);
当然,如果想移除一个#define
定义的标识符,可以使用#undef
。
#undef NUM
- 定义宏。
如#define MAX(x, y) ((x)>(y)?(x):(y))
,如果遇到了MAX(…,…)就会替换掉。
int sum = MAX(10, 20); // 会被替换成int sum = ((10)>(20)?(10):(20));
以上的代码中,会把10传给x,把20传给y,然后根据把((x)>(y)?(x):(y))中的x换成10,y换成20,作为替换的结果。
宏和函数有很多相同点和不同点,关于这点的总结我会单独写一篇博客来阐述。
3.头文件包含
头文件包含有2种方式,分别是:
- #include <filename>
- #include “filename”
这两种方式对应的文件查找策略是不一样的。
- 如果使用<>,编译器会直接到库目录中查找该头文件。
- 如果使用"",编译器会先从工程目录下查找该头文件,如果找不到,再到库目录中查找。
那么,我们是不是可以写:#include "stdio.h"
?
确实是可以的,但是不建议。有2个原因:
- 这么写,编译器会先到工程目录中查找,找不到了再去库目录中查找,效率低下。
- 不便于区分是库里实现的头文件还是工程中实现的头文件。
建议:
- 使用<>来包含库里的头文件。
- 使用""来包含工程里程序员自己实现的头文件。
当编译器找到这个头文件后,就会用该头文件中的代码替换掉#include的位置。
需要注意的是:为了防止一个头文件被重复包含在同一个文件中,比如:
#include "test.h"
#include "test.h"
#include "test.h"
以上代码重复包含了3次test.h这个头文件,会导致其中的代码被拷贝3份,降低效率。所以一般会在头文件第一行加上#pragma once
,这句代码的作用是,哪怕被重复包含,也只会把里面的代码拷贝一次。当然,也可以使用条件编译指令来达到同样的效果,后面会讲。
4.条件编译
条件编译,即根据不同的条件,选择是否编译某一段代码。常见的条件编译指令有:
- #if #elif #else
- #ifdef #ifndef
- 以上的条件编译指令都需要使用#endif来结束。
#if
可以理解成if
,#elif
可以理解成else if
,#else
可以理解成else
,比如:
#if 1 == 1printf("hehe\\n");
#elif 2 == 2printf("haha\\n");
#elif 3 == 3printf("heihei\\n");
#elseprintf("hengheng\\n");
#endif
如果1==1
成立,就会printf("hehe\\n");
会被编译,其他语句不会被编译。如果1==1
不成立,2==2
成立,printf("haha\\n");
会被编译,其他语句不会被编译。如果2==2
仍然不成立,3==3
成立,printf("heihei\\n");
会被编译,其他语句不会被编译。如果以上都不成立,printf("hengheng\\n");
会被编译,其他语句不会被编译。这和if()
,else if()
,else
的逻辑是一样的。注意最后要用#endif
来结束。
#ifdef
后面的标识符如果已经被#define
定义,则后面的语句会被编译,否则不会被编译。注意最后也要使用#endif
来结束。
#ifndef
和#ifdef
恰恰相反,如果后面的标识符没有被#define
定义译,否则不会被编译。注意最后也要使用#endif
来结束。
比如:
#define __DEBUG__#ifdef __DEBUG__printf("debug\\n"); // 这条语句会被编译
#endif#ifndef __DEBUG__printf("not debug\\n"); // 这条语句不会被编译
#endif// 移除__DEBUG__的定义
#undef __DEBUG__#ifdef __DEBUG__printf("debug\\n"); // 这条语句不会被编译
#endif#ifndef __DEBUG__printf("not debug\\n"); // 这条语句会被编译
#endif
除此之外,#ifdef __DEBUG__
就等价于#if defined(__DEBUG__)
,#ifndef __DEBUG__
就等价于#if !defined(__DEBUG__)
。
前面讲过,如果要防止头文件被重复包含,可以使用#pragma once
,也可以使用条件编译。比如test.h
头文件中,只要这么写:
#ifndef __TEST_H__
#define __TEST_H__// 这里写头文件的内容#endif
假设被重复包含了,那么头文件中的内容就会被拷贝多份。假设被重复包含3次,就会像下面这样:
#ifndef __TEST_H__
#define __TEST_H__// 这里写头文件的内容#endif#ifndef __TEST_H__
#define __TEST_H__// 这里写头文件的内容#endif#ifndef __TEST_H__
#define __TEST_H__// 这里写头文件的内容#endif
有没有发现,由于第一次#ifndef
后面的语句会被编译,从而会#define
后面的__TEST_H
这个标识符,从而第2、3次的#ifndef
后面的语句都不会被编译。这就提高了编译的效率。
5.命令行编译
我们可以使用命令行编译来指定某些标识符的值。比如创建一个main.c:
#include <stdio.h>int main()
{int arr[SZ] = {0};int i = 0;for (i=0; i<SZ; i++){arr[i] = i;printf("%d ", arr[i]);}printf("\\n");return 0;
}
使用gcc编译这段代码时,可以指定SZ的值:
gcc main.c -D SZ=100
输出结果如下:
当然,也可以指定SZ为其他值:
gcc main.c -D SZ=10
输出结果也会跟着变。
总结
- 预处理阶段会把注释删掉。
- 预处理阶段会把
#define
定义的标识符和宏替换掉。 - 预处理阶段会把
#include
的头文件展开。<>
和""
对应的查找策略是不一样的。<>
会直接到库目录中去找,""会先到工程目录中找,找不到了再去库目录中去找。为了防止头文件被重复包含,建议使用#pragma once
或者条件编译。 - 预处理阶段根据一些条件来决定是否编译一段代码。常见的条件编译的指令有:
#if, #elif, #else, #ifdef, #ifndef, #endif
。条件编译都要用#endif来结束。#ifdef NUM
和#if defined(NUM)
等价,#ifndef NUM
和#if !defined(NUM)
等价。 - 防止头文件被重复引用,除了使用
#pragma once
,也可以使用#ifndef, #define, #endif
。 - 预处理阶段会根据命令行指定的参数来替换某些标识符,gcc需要使用
-D
选项来实现这一点。
感谢大家的阅读!