Bootstrap

C语言之预处理详解(#define宏定义、条件编译、#include文件包含)


前言

  我们编写的源程序在被编译前,需要进行预处理。C语言的预处理主要有三个方面的内容: 1.宏定义; 2.条件编译 ; 3.文件包含1,这三个在程序中以符号“#”开头,代表自己是预处理命令。宏定义主要是替换;条件编译决定程序的哪些部分可以被编译,哪些部分被舍弃;文件包含是将头文件中的内容包含进来。本文主要对预处理的三个部分进行了介绍,并给出了相应的例程方便大家理解。

1 宏定义

1.1 预定义符号

  C标准规定了一些预定义符号2,可以直接使用,比如以下符号:

__FILE__    //进行编译的源文件名(文件路径+文件名主干+文件后缀)
__LINE__    //文件当前的行号
__DATE__    //文件被编译的日期
__TIME__    //文件被编译的时间
__STDC__    //如果编译器遵循ANSI C,其值为1,否则显示未定义

以上预定义符号都是C语言内置的,可以直接打印出来:

/***************************************
程序功能:打印C语言内置预定义符号
时间:2021年6月2日11:09:55
****************************************/
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int main()
{
    printf("file:%s\n", __FILE__);
    printf("line:%d\n", __LINE__);
    printf("date:%s\n", __DATE__);
    printf("time:%s\n", __TIME__);
    //printf("stdc:%d\n", __STDC__);
    return 0;
}

运行结果:
在这里插入图片描述

  VS编辑器没有__STDC__符号,说明VS编译器没有严格遵循ANSI C标准,对于gcc,clang编译器则可以打印出这个符号,即gcc,clang编译器严格遵循ANSI C标准。下面举例说明预定义符号的用法。
  利用预定义符号可以用来打印程序运行日志2,这样我们就可以通过日志查看程序的运行情况,方便快速找到错误。下面给出一个打印日志的例子,将内容保存在一个文本文件里面,程序如下:

/***************************************
程序功能:打印日志
时间:2021年6月2日19:11:56

****************************************/
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int main()
{
    int i = 0;
    FILE* pf = fopen("log.txt", "w");
    if (pf == NULL)
        return;
    for (i = 0; i < 10; i++)
    {
        printf("%d\n", i);
        fprintf(pf, "%d:%s--%d--%s--%s\n", i,
            __FILE__, __LINE__, __DATE__, __TIME__);
    }

    fclose(pf);
    pf = NULL;
    return 0;
}

运行结果:
在这里插入图片描述

1.2 #define的使用

  #define可以用来定义标识符,也可以用来定义宏。定义的标识符有时也称为无参宏定义3,本文提到的宏是指有参数的宏定义。

1.2.1 定义标识符

  除了使用C语言内置的预定义符号,我们也可以利用#define定义一些标识符,这些标识符不占内存,只是一个临时的符号4,这些符号在程序预编译阶段会被替换。#define使用格式如下:

#define name stuff

  其中,name是标识符,stuff是被标识符name替换的内容,stuff可以是数字常量、字符、字符串、表达式,其中最常用的是数字4。下面介绍define 的几种使用场景。

(1)使用一个标识符代替一个数字常量

#define MAX 1000 // 用MAX代替1000

(2)为关键字创建一个简短的名字

#define reg register // 为register创建简单名字reg

(3)用更形象的符号来替换一种函数实现

#define do_forever for(;;)//用符号do_forever来替换for(;;)

(4)如果被的替换的stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。

#define DEBUG_PRINT printf("file:%s\tline:%d\t \
    date:%s\ttime : %s\n" ,\
        __FILE__, __LINE__,\
        __DATE__, __TIME__)

  有一点需要注意,请对比#define和typedef的区别,观察以下程序:

#include <stdio.h>
#define INT_PTR int*
typedef int* int_ptr;
int main()
{
    INT_PTR a, b;
    int_ptr c, d;

    return 0;
}

查看监视:
在这里插入图片描述

  可以看到变量b的类型是int,其他三个是int*类型。这是因为#define是查找替换,所以替换过后的语句是“int*a, b;”,这就不难看出b为什么是一个int类型变量,如果要让b也是指针,必须写成“int *a, *b;”。而我们使用typedef时不会出现这个问题,可以看到c、d都是整型指针。

定义标识符的注意事项
  在define定义标识符的时候,最好不要在最后加上分号“;”否则可能出现问题。比如下面的场景:

#define MAX 1000; 
if(N  == 10)
max = MAX; 
else 
max = 0; 

分析:以上程序会出现语法错误,这是因为MAX被替换后,原程序变成:

if(N  == 10)
max = 1000;;
else 
max = 0; 

  以上程序会出现语法错误,这是因为if后跟了两个语句,一个是“max = 1000;”,另一个是空语句“;”,这就是产生错误的原因。如果加上花括号就没问题了,修改后的程序如下所示:

#define MAX 1000; 
if(N  == 10)
{
max = MAX;
}
else 
max = 0; 

  很多文章都会提醒我们不要加分号,因为加分号很容易出错,这点务必请注意。

1.2.2 定义宏

  #define 机制有一个规定,允许把参数替换到文本中,这种实现通常称为(macro)也称定义宏(define macro)。与使用define定义的标识符相比,宏可以看做是具有参数的标识符。

下面是宏的声明方式:

#define name( parament-list ) stuff 

  其中name是宏的标识符,也称为宏名,我们在定义宏的时候,习惯于把宏名全部大写。parament-list 是一个由逗号隔开的参数符号表,这些参数可以出现在宏体stuff中。

注意: 参数列表的左括号必须与name紧邻。 如果两者之间有任何空白符5存在,参数列表就会被解释为stuff的一部分。

  下面演示宏的使用方法,我们定义一个宏,利用这个宏可以求一个数的平方。

/***************************************
程序功能:宏实现求一个数的平方
时间:2021年6月2日20:30:03

****************************************/
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#define SQUARE( x ) x * x

int main()
{
    printf("%d", SQUARE(5));

    return 0;
}

运行结果:
在这里插入图片描述

  SQUQRE是宏的标识符,这个宏有一个参数x。程序运行后,预处理器就会将printf("%d", SQUARE(5));替换为printf("%d", 5*5);,故程序最后运行结果为25。但是这个宏存在一个问题,如果我们给宏输入的参数是5+1,那么运行结果是多少呢?你可能觉得这段代码将打印36这个值,事实上,结果是11,如下所示:

printf("%d", SQUARE(5+1));

运行结果:
在这里插入图片描述

  以上说明宏的参数是直接被替换的,在替换过程中不进行任何计算,即printf("%d", SQUARE(5+1));被直接替换成printf("%d", 5+1*5+1);,这样就不难看出最终结果为什么是11了。产生这个问题的主要原因是被替换的表达式并没有按照预想的次序进行求值。为了解决这个问题,我们可以给宏体中的参数加上两个括号:

#define SQUARE( x ) (x) * (x)

运行结果:
在这里插入图片描述

  可以看到输出变成36了,符合预期结果,这是因为替换后的程序为printf("%d", (5+1)*(5+1));

为了说明括号的重要性,我们再来看一个宏定义:

/***************************************
程序功能:宏实现求一个数的2倍
时间:2021年6月5日15:08:54

****************************************/
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#define DOUBLE( x ) (x) + (x)

int main()
{
    printf("%d", 10 * DOUBLE(5));

    return 0;
}

运行结果:
在这里插入图片描述

  分析:定义中我们使用了括号,想避免之前的问题,但是这个宏可能会出现新的错误。程度看上去应该输出100,但结果却是55,这是因为程序被替换成了printf("%d", 10 *(5) + (5));,而乘法运算先于加法运算,所以结果为55。解决这个问题的方法是给整个宏体加上括号:

#define DOUBLE( x ) ((x) + (x))

运行结果:
在这里插入图片描述

  可以看到程序的运行结果符合我们的预期,经过以上的两个例子,相信我们都能意识到一个重要的东西,那就是定义宏时不要吝啬括号,所以我们以后定义宏时,给宏的每个参数都加上括号,最后再给整个宏体加上一个括号,这样就能避免在使用宏时由于参数中的操作符或邻近操作符之间产生不可预料的相互作用。

1.3 #define 替换规则

  经过前面的认真学习,我们知道#define定义的符号在预处理时会被替换,那具体的替换过程是怎样的呢?其实在程序的预处理阶段,需要涉及以下几个步骤。

步骤1:在调用宏时,首先对传给宏的参数进行检查,看看是否包含任何由#define定义的符号。如果包含这些符号,则这些符号首先被替换。

步骤2:经过步骤1替换替换后的宏体,随后被插入到程序中,即宏的标识符所在的位置,然后宏的参数名被参数的值替换。

步骤3:最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。



请注意以下几点:
  1. 宏参数和#define 定义中可以出现其他#define定义的变量
    例如:
/***************************************
程序功能:宏参数和#define 定义中可以出现其他#define定义的变量
时间:2021年6月5日16:13:44

****************************************/
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <math.h> // sin

#define PI 3.14
#define THETA (PI/2)
#define COS(x) (sin((x + THETA)))
int main()
{
    printf("cos(0) = %f\n", COS(0));
    return 0;
}

运行结果:
在这里插入图片描述

  1. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
/***************************************
程序功能:字符串常量的内容不被替换
时间:2021年6月5日16:19:15

****************************************/
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

#define ONE 1
#define TWO 2
#define THREE 3
int main()
{
    const char* str = "ONE TWO THREE";
    printf("%d %d %d\n", ONE, TWO, THREE);
    printf("%s\n", str);
    return 0;
}

运行结果:
在这里插入图片描述

  运行结果显示,程序中定义的字符串常量"ONE TWO THREE"的内容并没有被替换。

  1. 宏,不能出现递归

  因为宏在预编译阶段就会被替换掉,宏体也就不见了,所以不能进行递归调用。

1.4 使用#和##

  我们看别人的代码的时,可能会遇到使用#和##的情况,下面它们的用法进行简短的介绍。

(1)使用 # ,可以把一个宏的参数变成对应的字符或字符串:

/***************************************
程序功能:使用#将宏的参数转换为字符
时间:2021年6月5日16:32:51

****************************************/
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#define PRINT(x) printf("The value of " #x " is %d\n", x);
int main()
{
    float pi = 3.14;
    int a = 2;
    PRINT(pi);
    PRINT(a);
    return 0;
}

运行结果:
在这里插入图片描述

  可以看到,#x 使得传入的参数转换成字符,比如给宏PRINT传入参数a,则 #x 被替换成”a”,这样我们就相当于实现了将变量插入到字符串中,这种效果利用函数是无法实现的。

(2)利用##粘接宏的参数

/***************************************
程序功能:利用##粘接宏的参数
时间:2021年6月5日16:57:43

****************************************/
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#define CAT(var, num) var##num
int main()
{
    int x0 = 0;
    int x1 = 1;
    printf("%d\n", CAT(x, 0));
    printf("%d\n", CAT(x, 1));

    return 0;
}

运行结果:
在这里插入图片描述

  可以看到,在宏CAT中,利用##可以将两个宏参数,粘接在一起,并产生了一个合法的标识符。比如CAT(x, 0),就是将x和0粘接在一起形成了有效的标识符“x0”。

1.5 带有副作用的宏参数

  如果我们传给宏的参数带有副作用,并且宏参数在宏体内使用超过1次,那么就有可能出现意想不到的错误。所谓副作用就是,传递参数时改变了实参的值,比如:

x + 1;//不带副作用
x++;//带有副作用

  我们举例说明这种情况,比如我们想比较得出两个数中的较大值,可以通过函数来实现:

/***************************************
程序功能:利用函数求两个数的最大值
时间:2021年6月5日17:20:00

****************************************/
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int max(int a, int b)//定义函数
{
    return a > b ? a : b;
}

int main()
{
    int a = 10;
    int b = 20;
    int c = max(a++, b++);
    printf("a = %d, b = %d, c = %d\n", a, b, c);
    return 0;
}

运行结果:
在这里插入图片描述

  可以看到,我们给函数max传递参数时,实参a和b的值被改变了,所以a,b的输出分别为11、21。而对于后置++,是先使用后自加,所以实际上传递给函数max的参数是10和20,因此c的值为20。这说明,对于带有副作用的参数,该参数只在函数调用的时候求值一次,对于这样的求值结果,我们可以轻松的推测出来。




  我们再来看一下如何使用求两个数的最大值,并据此比较宏与函数的区别:

/***************************************
程序功能:利用宏求两个数的最大值(带副作用的宏参数)
时间:2021年6月5日17:50:51

****************************************/
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#define MAX(a, b) ((a)>(b)?(a):(b))

int main()
{
    int a = 10;
    int b = 20;
    int c = MAX(a++, b++);
    printf(" a = %d, b = %d, c = %d\n", a, b, c);

    return 0;
}

运行结果:
在这里插入图片描述
  可以看到运行结果与调用函数的输出结果不一样,a的最终值为11,b的最终值为22,c输出为21,宏为啥就输出了这么些玩意呢?相信看了我前面写的内容,各位分析这个问题应该不在话下,我就不再赘述了。



  算了,还是说一下吧。还是和前面分析的一样,请记住宏的结果是替换,所以我们首先把程序中使用宏的地方都给替换掉。int c = MAX(a++, b++);,就变成了下面的这句:

int c = ((a++)>(b++)?(a++):(b++));

  程序中首先执行(a++)>(b++),此时a的值变成了11,b的值变成了21,很显然结果为假,故可得c = b++,这样相当于先把b的值赋给c,c的值变成21,然后b再自加1,故b的最终值为22。

  通过以上我们知道了,使用宏的时候尽量避免使用带有副作用的宏参数

1.6 宏和函数对比

  前面介绍带有副作用的宏参数时,我们已经知道了同一个功能,可以使用函数实现,也可以通过宏实现。那两者有什么区别呢? 我们想实现一个功能是用宏好,还是用函数好呢?其实两个各有优劣:

宏与函数优劣对比:

  1. 使用函数存在调用和返回的时间开销,而使用宏的时候,宏代码会被插入到程序中,所以宏在运行速度方面更胜一筹。但是除非宏比较短,否则可能大幅度增加程序的长度。

  2. 由于宏在预编译阶段就被替换了,所以宏是没法调试的,函数一般都可以调试。

  3. 函数的参数必须声明为特定的类型,所以我们传递给函数的参数类型必须是确定的;而一个宏可以适用于很多类型,即宏的参数与类型无关,只要对参数的操作是合法的,宏就可以应用于任何类型的参数。但也是因为宏的这种类型无关性,导致宏不够严谨。参看下面展示宏类型无关性的程序:

/***************************************
程序功能:利用宏求各种类型数的最大值(宏类型无关性)
时间:2021年6月5日17:50:51

****************************************/
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#define MAX(a, b) ((a)>(b)?(a):(b))

int main()
{
    printf(" %d\n", MAX(2, 5));
    printf(" %f\n", MAX(8.89, 5.46));
    printf(" %c\n", MAX('c', 'd'));

    return 0;
}

运行结果:
在这里插入图片描述
  可以看到利用一个宏可以对比不同类型数据的大小,这就是宏类型无关性。

  1. 宏可能会带来运算符优先级的问题,导致程容易出现错,所以定义宏的时候尽量使用括号,防止优先级问题。(这点前面已经介绍过,不再赘述)

  2. 使用宏可以传类型参数,而函数做不到

/***************************************
程序功能:宏的参数可以传类型
时间:2021年6月6日17:36:52

****************************************/
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h> // malloc
#include <string.h>
#define MALLOC(n, type)\
    (type*)malloc(n*sizeof(type))

int main()
{

    char* pstr = MALLOC(15, char); //pstr指向的空间可以存放15个char型数据
    if (NULL == pstr)//判断pstr指针是否为空
    {
        perror("malloc");
        return 1;
    }
    strcpy(pstr, "hello world\n");
    printf(pstr);
    free(pstr);//释放pstr所指向的动态内存
    pstr = NULL;//防止野指针
    return 0;
}

运行结果:
在这里插入图片描述

  程序中,我们直接给宏MALLOC传递一个类型参数char,利用这个宏我们可以简化malloc的调用格式,使用宏MALLOC时只需传一个数和数据的类型。

1.7 #undef移除宏定义

  如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除,利用#undef可以移除一个已经定义的标识符或宏。

/***************************************
程序功能:移除define定义标识符
时间:2021年6月6日19:15:18

****************************************/
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#define MAX 100
int main()
{
    printf("%d\n", MAX);
#undef MAX // 移除宏定义MAX
#define MAX 150
    printf("%d\n", MAX);
    return 0;
}

运行结果:
在这里插入图片描述


2 条件编译

  我们编程时可能遇见这种情况,比如一些用于调试的代码,删除可惜,保留又碍事。解决这个问题,有两种方法,第一种方法我们可以将其注释掉,第二种方法是不让这段代码进行编译。第二种方法可以利用条件编译进行实现。

2.1 单分支条件编译

  常量表达式为,则编译分支内的代码;为,在预处理时,去除分支内的代码。

#if 常量表达式
 //... 
#endif 

例程:

/***************************************
程序功能:单分支条件编译示例
时间:2021年6月6日20:18:14

****************************************/
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#define __DEBUG__ 1

int main()
{

    #if __DEBUG__
    printf("今天天气好晴朗\n"); //第1个printf函数
    #endif // __DEBUG__

    #if 0
        printf("我不会被编译\n"); //第2个printf函数
    #endif // 0

    return 0;
}

运行结果:
在这里插入图片描述

  由于第1个printf函数的常量表达式为真,所以会被编译。第2个printf函数的常量表达式为0(假),所以不会被编译执行。

2.2 多分支条件编译

  这个和if...else语句使用类似,哪个常量表达式为真,编译哪个分支。

#if 常量表达式
 //... 
#elif 常量表达式
 //... 
#else 
 //... 
#endif 

例程:

/***************************************
程序功能:多分支条件编译
时间:2021年6月6日20:22:05

****************************************/
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#define __DEBUG__ 2
int main()
{
    #if __DEBUG__ == 1
    printf("第1个分支被编译\n");
    #elif __DEBUG__ == 2
    printf("第2个分支被编译\n");
    #elif __DEBUG__ == 2
    printf("第3个分支被编译\n");
    #else 
    printf("第4个分支被编译\n");
    #endif //__DEBUG__
    return 0;
}

运行结果:
在这里插入图片描述

  可以看到,第2分支和第3分支都为真,但只有第2个分支被编译执行了,这说明如果有一个分支为真,剩下的分支无论真假都不会再执行。

2.3 判断符号是否被定义

  根据符号symbol是否被#define定义,决定分支内的代码是否编译。有两种情况:

① 如果符号symbol被定义则编译

#if defined(symbol) 
//... 
#endif
#ifdef symbol 
//... 
#endif

② 如果符号symbol没有被定义则编译

#if !defined(symbol) 
//... 
#endif
#ifndef symbol 
//... 
#endif

例程:

/***************************************
程序功能:宏定义和条件编译结合
时间:2021年6月6日20:48:31

****************************************/
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

#define __DEBUG__

int main()
{
#if defined(__DEBUG__)
    printf("第1句被执行\n");
#endif //__DEBUG__

#ifdef __DEBUG__ 
    printf("第2句被执行\n");
#endif //__DEBUG__

#if !defined(__DEBUG__) 
    printf("第3句被执行\n");
#endif //__DEBUG__

#ifndef __DEBUG__ 
    printf("第4句被执行\n");
#endif //__DEBUG__

    return 0;
}

运行结果:
在这里插入图片描述
  由于__DEBUG__被定义了,所以第1、2分支被执行了。

2.4 嵌套指令

#if defined(OS_UNIX) 
    #ifdef OPTION1 
        unix_version_option1();
    #endif // OPTION1
    #ifdef OPTION2 
        unix_version_option2();
    #endif // OPTION2
#elif defined(OS_MSDOS) 
    #ifdef OPTION2 
        msdos_version_option2();
    #endif // OPTION2
#endif // OS_UNIX || OS_MSDOS

  我们可以根据前三种条件编译实现嵌套编译,读者自行尝试编程,这里不再赘述。


3 文件包含

3.1 文件包含中条件编译

  在编程开头总是会加上#include,我们现在已经知道,#include 指令可以使一个文件被包含进来,这样该文件的内容就会被加载到程序中。预处理器的具体做法是:先删除这条指令(#include),并用文件的内容替换这条指令。

  那如果一个源文件被包含10次,是不是这个源文件的代码就会被编译10次呢?如果真是这样,不就造成程序重复编译了吗?

  实际上,即使我们真滴将一个头文件包含10次,该内容也只会被编译一次,究竟是怎样实现的呢?答案即条件编译。

  比如对于头文件test.h,我们只需在头文件的开头加上#ifndef __TEST_H__#define __TEST_H__,然后在头文件的结尾加上#endif就行了。具体格式如下所示:

#ifndef __TEST_H__ 
#define __TEST_H__ 
//头文件的内容
#endif //__TEST_H__

  分析:我们第1次进入test.h文件中,符号__TEST_H__会被定义,接着会执行头文件的内容。当我们第2次再进入test.h文件中时,由于符号__TEST_H__已经被定义,分支内的代码不再被执行,所以即使多次引用头文件,test.h的内容也只会被编译一次。

  注意:符号的名字也有讲究,尽量和文件的名字相同,#endif后的注释不要省略,这是程序猿之间的默契。(相信你编代码久了,就明白了这样做的好处,所以跟着做就完事了。)

  防止头文件被重复编译,还有一个简便的方法,那就是在头文件开头加上下面这一句:

#pragma once

  #pragma是一个比较复杂的预编译语句,但不属于条件编译,想要了解原理的可以查阅《C语言深度解剖》这本书,这里不再介绍。

3.2 头文件被包含的方式

(1)本地文件包含
比如:

#include "test.h"

  使用双引号包含头文件,编译器首先在源文件所在目录下查找test.h文件,如果该头文件未找到,编译器就去标准路径查找头文件。如果依然找不到就提示编译错误。

(2)库文件包含
比如:

#include <stdio.h>

  我们一般用尖括号包含标准库的头文件,查找头文件时,编译直接去标准路径下去查找,如果找不到就提示编译错误。



  有的人可能会想,既然本地文件包含库文件包含,最终都会去标准路径中查找文件,那我无论什么头文件,全部使用双引号的形式进行引用不就行了?这种方法是可行的,但如果头文件位于标准路径下,那使用双引号查找的效率就低些。所以对于库文件,首选尖括号形式引用头文件。

  最后再说一句,头文件中不能定义全局变量,否则如果有多个文件包含了这个头文件,那程序在链接时会发生冲突。

参考资料


  1. 预处理命令_百度百科 (baidu.com). ↩︎

  2. C语言:条件编译及预定义符号知识详解-电子发烧友网 (elecfans.com). ↩︎ ↩︎

  3. #define用法详解 - shmilxu - 博客园 (cnblogs.com). ↩︎

  4. #define用法,C语言#define详解 (biancheng.net). ↩︎ ↩︎

  5. 空白符_百度百科 (baidu.com). ↩︎

;