你真的了解C/C++程序预处理阶段干的这5件事情吗?

这篇博客详细讲解了C/C++程序被翻译成可执行程序需要经理的步骤。其中,预处理(也称为预编译)阶段干了5件事情,分别是:

  1. 删除注释。
  2. #define定义的标识符和宏的替换。
  3. 头文件的包含。
  4. 条件编译。
  5. 命令行编译。

你真的了解C/C++程序预处理阶段干的这5件事情吗?_第1张图片

1.删注释

程序员写注释,是给人看的,不是给计算机看的。所以,在预处理阶段会把注释都删掉。C/C++中有2种注释风格,分别是:

  1. C的注释风格:以/*开头,*/结尾,不能嵌套注释。
/* 这里写注释 */
  1. C++的注释风格:以//开头,一直到本行末尾。
// 这里写注释

2.#define的替换

#define可以定义2种替换方式。

  1. 定义标识符。

#define NUM 100,如果遇到了NUM就会被替换成100。

printf("%d\n", NUM); // 会被替换成printf("%d\n", 100);

当然,如果想移除一个#define定义的标识符,可以使用#undef

#undef NUM
  1. 定义宏。

#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种方式,分别是:

  1. #include
  2. #include “filename”

这两种方式对应的文件查找策略是不一样的。

  1. 如果使用<>,编译器会直接到库目录中查找该头文件。
  2. 如果使用"",编译器会先从工程目录下查找该头文件,如果找不到,再到库目录中查找。

那么,我们是不是可以写:#include "stdio.h"

确实是可以的,但是不建议。有2个原因:

  1. 这么写,编译器会先到工程目录中查找,找不到了再去库目录中查找,效率低下。
  2. 不便于区分是库里实现的头文件还是工程中实现的头文件。

建议:

  1. 使用<>来包含库里的头文件。
  2. 使用""来包含工程里程序员自己实现的头文件。

当编译器找到这个头文件后,就会用该头文件中的代码替换掉#include的位置。

需要注意的是:为了防止一个头文件被重复包含在同一个文件中,比如:

#include "test.h"
#include "test.h"
#include "test.h"

以上代码重复包含了3次test.h这个头文件,会导致其中的代码被拷贝3份,降低效率。所以一般会在头文件第一行加上#pragma once,这句代码的作用是,哪怕被重复包含,也只会把里面的代码拷贝一次。当然,也可以使用条件编译指令来达到同样的效果,后面会讲。

4.条件编译

条件编译,即根据不同的条件,选择是否编译某一段代码。常见的条件编译指令有:

  1. #if #elif #else
  2. #ifdef #ifndef
  3. 以上的条件编译指令都需要使用#endif来结束。

#if可以理解成if#elif可以理解成else if#else可以理解成else,比如:

#if 1==1
	printf("hehe\n");
#elif 2==2
	printf("haha\n");
#elif 3==3
	printf("heihei\n");
#else
	printf("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 

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

输出结果也会跟着变。
输出结果

总结

  1. 预处理阶段会把注释删掉。
  2. 预处理阶段会把#define定义的标识符和宏替换掉。
  3. 预处理阶段会把#include的头文件展开。<>""对应的查找策略是不一样的。<>会直接到库目录中去找,""会先到工程目录中找,找不到了再去库目录中去找。为了防止头文件被重复包含,建议使用#pragma once或者条件编译。
  4. 预处理阶段根据一些条件来决定是否编译一段代码。常见的条件编译的指令有:#if, #elif, #else, #ifdef, #ifndef, #endif。条件编译都要用#endif来结束。#ifdef NUM#if defined(NUM)等价,#ifndef NUM#if !defined(NUM)等价。
  5. 防止头文件被重复引用,除了使用#pragma once,也可以使用#ifndef, #define, #endif
  6. 预处理阶段会根据命令行指定的参数来替换某些标识符,gcc需要使用-D选项来实现这一点。

你可能感兴趣的:(C语言,c语言,c++,开发语言,预处理,编译)