详解C语言—预处理

目录

1、预处理

(1)预定义符号介绍

(2)预处理指令 #define

#define 定义标识符: 

#define 定义宏:

#define 替换规则

(3)预处理操作符#

(4)预处理操作符##

 (5)带副作用的宏参数

 (6)宏和函数对比

2、命名约定

3、预处理指令 #undef

4、命令行定义

5、条件编译 

(1)单分支#if:

(2)多分支#if:

(3)判断是否被定义

(4)嵌套指令

 6、文件包含

头文件被包含的方式:

嵌套文件包含:

小结


1、预处理

(1)预定义符号介绍

__FILE__      //进行编译的源文件

__LINE__     //文件当前的行号

__DATE__    //文件被编译的日期

__TIME__    //文件被编译的时间

__STDC__    //如果编译器遵循ANSI C,其值为1,否则未定义
这些预定义符号都是语言内置的,程序员可直接使用。

#include <stdio.h>

int main()
{
	printf("%sn", __FILE__);
	printf("%dn", __LINE__);
	printf("%sn", __DATE__);
	printf("%sn", __TIME__);
	//printf("%dn", __STDC__);//当前VS是不支持ANSI C
	return 0;
}

(2)预处理指令 #define

#define 定义标识符: 

#define 宏名 值或代码片段

#define 是用来定义宏的预处理指令。宏是一种在源代码中定义的符号,可以用来代表一个常量、一个表达式或一段代码片段。宏的定义通常在源代码文件的顶部,以便在编译之前被预处理器处理。  

#define MAX 1000
#define reg register          //为 register这个关键字,创建一个简短的名字
#define do_forever for(;;)     //用更形象的符号来替换一种实现
#define CASE break;case        //在写case语句的时候自动把 break写上。
// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%stline:%dt 
                          date:%sttime:%sn" ,
                          __FILE__,__LINE__ ,  
                          __DATE__,__TIME__ )  

#define 定义宏:

带参数的宏:宏也可以带有参数,允许你创建可重用的代码片段。例如:

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

 SQUARE 宏接受一个参数x,并返回x的平方。在使用时,你可以这样调用宏:int result = SQUARE(5);,它会被展开为 int result = ((5) * (5));

 接下来我们看一个例子:

#define SQUARE(x)  x * x

如果我们写成这样不带括号, 对函数传入参数 5 + 1 :SQUARE( 5 + 1 );
得到的结果将是 11,而不是36,计算过程:5 + 1 * 5 + 1 得到结果为11。

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

当我们加上括号,计算过程:(5+1)*(5+1) 得到结果为36.  

这样定义看似没有问题了,我们再看一个例子: 

#define SQUARE(x) (x) + (x)
int main()
{
    int a = 5;
    printf("%dn" ,10 * DOUBLE(a));
    return 0;
}

 这种情况下计算过程为 10 * (5) + (5)); 得到结果为55,并不是我们想要的100。

这时再对参数整体添加一对括号即可解决问题:

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

通过这几个例子我们理解了为什么要加那些那些括号。 

所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。  

#define 替换规则

在程序中扩展#define定义符号和宏时,需要涉及几个步骤。

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
  2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。

注意:

宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。

当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

  • 解释:当预处理器搜索由#define定义的符号时,它并不会搜索或替换字符串常量中的内容。这是因为字符串常量在C中是不可更改的文本序列,不应受到#define定义的符号的影响。
  • #include <stdio.h>
    
    #define VALUE 42
    
    int main() {
        int x = VALUE;
        printf("The value of x is: %dn", x);
        
        printf("This is a string with VALUE: "%d"n", VALUE);
        
        return 0;
    }

    在上面的示例中,我们定义了一个名为VALUE的符号,它被定义为整数42。然后,我们在程序中使用了这个符号。请注意以下两个地方:

  • 在第5行,我们使用了VALUE来初始化整数变量x。在这里,#define定义的符号VALUE被替换为42,因此x的值是42

  • 在第7行,我们在字符串常量中包含了VALUE。在这里,VALUE并不会被替换为42,而是保持不变。因此,字符串中的内容是"This is a string with VALUE: "VALUE""

  • 所以,尽管#define定义的符号VALUE在程序中的某些地方被替换为其定义的值,但它并不会影响字符串常量中的内容。字符串常量中的文本保持不变,不受符号替换的影响。这是为了确保字符串常量的内容始终保持不变。

(3)预处理操作符#

我们由一个例子来引入讲解: 

int main()
{
	int a = 20;
	printf("the value of a is %dn", a);
	
	return 0;
}

如何通过#define实现上述代码中printf语句的相同功能呢?

通过前面的学习可以很轻松实现#define定义宏,代码如下: 

#define Print(n) printf("the value of n is %d",n)
int main()
{
	int a = 20;
	printf("the value of a is %dn", a);
	Print(a);
	return 0;
}

但我们的输出语句是the value of n is 20,并不是我们传入的参数 a ,

那怎么修改 #define 把参数插入到字符串?

我们通过添加双引号将“参数前后字符串”分隔成“独立的两个字符串”。再在参数前添加 # 使其在宏展开时被替换为参数 n

#define Print(n) printf("the value of "#n" is %d",n)
int main()
{
	int a = 20;
	printf("the value of a is %dn", a);
	Print(a);
	return 0;
}

如果我们要输出不同格式,那怎么修改#define呢?

添加一个参数代替格式化字符即可解决。

#define Print(n,format) printf("the value of "#n" is " format "n",n)
int main()
{
	float f = 4.5f;
	printf("the value of a is %fn", f);
	Print(f, "%f");
	return 0;
}

(4)预处理操作符##

##可以把位于它两边的符号合成一个符号。

它允许宏定义从分离的文本片段创建标识符。

看下面的例子就明白了: 

#define CAT(x,y) x##y
int main()
{
	int Class110 = 110;
	printf("%dn", Class110);
	printf("%dn", CAT(Class, 110));
	return 0;
}

两种输出形式结果一样:  

 (5)带副作用的宏参数

当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能 出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。

例如:

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

我们看下面的例子:

#define MAX(a,b) ((a)>(b)?(a):(b))
int main()
{
	int a = 5;
	int b = 6;
	int c = MAX(a++, b++);
	printf("a=%dnb=%dnc=%dn", a, b, c);
	return 0;
}

 替换宏的文本:c=MAX( (a++)>(b++) ? (a++):(b++) ) 首先判断 ab 的大小,之后a与b自增:a=6 b=7b 大于 a 则 c 被赋值为MAX的返回值 b的值7 即( c=7 ), 赋值之后b进行自增,b=8

输出结果: 

(6)宏和函数对比

宏通常被应用于执行简单的运算。

比如:在两个数中找出较大的一个

#define MAX(a,b) ((a)>(b)?(a):(b))

int Max(int x, int y)
{
	return (x > y ? x : y);
}

那为什么不用函数来完成这个任务?

原因有二:

1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。

所以宏比函数在程序的规模速度方面更胜一筹

2. 更为重要的是函数的参数必须声明为特定的类型。

所以函数只能在类型合适的表达式上使用。反之这个宏可以适用于整形、长整型、浮点型等可以用于>来比较的类型。

宏是类型无关的

宏的缺点:当然和函数相比宏也有劣势的地方:

  1. 宏由于类型无关,也就不够严谨。
  2. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
  3. 宏是没法调试的。
  4. 宏可能会带来运算符优先级的问题,导致程容易出现错。
  5. 宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。比如下面的例子:
#define Malloc(num,type) (type*)malloc(num*sizeof(type))

int main()
{
	int* p = (int*)malloc(126 * sizeof(int));
	int* p = Malloc(126, int);
	return 0;
}

 宏和函数详细对比: 

2、命名约定

一般来讲函数和宏的使用语法很相似,所以语言本身没法帮我们区分二者 

一般习惯宏名全部大写,函数名不要全部大写。

#define MAX(x,y) ((x)>(y)?(x):(y))

int Max(int x, int y)
{
	return x > y ? x : y;
}

3、预处理指令 #undef

这条指令用于移除一个宏定义。  

#undef NAME
//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。

 示例:

#define MAX(x,y) ((x)>(y)?(x):(y))

int main()
{
	int c = MAX(3, 5);
	printf("%dn", c);
#undef MAX
	c = MAX(5, -5);
	printf("%dn", c);
	return 0;
}

 当我们运行时,程序报错

我们想要修改 c 的值,宏MAX已经失效了,所以程序报错“MAX”未定义。 

4、命令行定义

许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
例如:当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大写,我们需要一个数组能够大写。
在gcc环境下演示:

 

 gcc 文件名.c -D SZ=10 -o 文件名,通过这种操作在命令行中定义符号。

5、条件编译 

在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
#if 是一个预处理指令,用于根据指定的条件编译代码块。
#if 常量表达式
 //...这里是条件为真时要编译的代码
#endif

在这个结构中,#if 后面可以跟一个常量表达式,如果这个表达式的值为非零(true),那么就会编译 #if 和 #endif 之间的代码,否则,这部分代码会被预处理器直接忽略,不参与编译。

常量表达式是在预处理阶段求值的,它通常包括常量、宏和运算符,但不能包括任何需要运行时计算的东西。

(1)单分支#if:

#define DEBUG 1

#if DEBUG
   // 这里是调试时要编译的代码
#else
   // 这里是非调试时要编译的代码
#endif
  • 在这个例子中,如果 DEBUG 宏被定义且值不为零,那么将会编译第一部分的代码,否则编译第二部分的代码。这种条件编译的方式常用于在调试和发布版本之间切换代码,或者根据不同的平台选择性地包含或排除特定的代码块。
  • 需要注意的是,#if 只是一种条件编译的方式,而且它只能根据常量表达式的真假来进行选择性编译,不能用于运行时的条件判断。如果需要在运行时进行条件判断,应该使用 if 语句。

 在gcc环境中,我们可以看到经过预处理后,没有满足条件的语句,所以没有语句参与编译。

(2)多分支#if:

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

在gcc环境中,我们可以看到经过预处理后,只有满足条件的 printf("hehen"); 参与编译。

if-else 什么条件执行什么代码,#if-#else-#endif 什么条件编译什么代码 

(3)判断是否被定义

#if defined(symbol)
#ifdef symbol

#if ! defined(symbol)
#ifndef symbol

这些都是条件编译的预处理指令,用于根据符号是否被定义来选择性地编译代码块。

#if defined(symbol)& #ifdef symbol( #if defined(symbol) 的简写)

这个指令用于检查是否已经定义了名为 symbol 的宏。

如果 symbol 已经被定义,那么 #if defined(symbol) 就被视为真(非零),相应的代码块会被编译。

如果 symbol 没有被定义,那么 #if defined(symbol) 就被视为假(0),相应的代码块会被忽略。

#if !defined(symbol)& #ifndef symbol(#if !defined(symbol) 的简写)

这个指令用于检查是否没有定义名为 symbol 的宏。

如果 symbol 没有被定义,即条件为真,那么 #if !defined(symbol) 就被视为真(非零),相应的代码块会被编译。

如果 symbol 已经被定义,即条件为假,那么 #if !defined(symbol) 就被视为假(0),相应的代码块会被忽略。

示例:

define WIN 0//无论是0还是1,都是被定义

int main()
{
#if defined(WIN)
	printf("windowsn");
#endif


	return 0;
}

输出结果:

因为WIN被定义,所以运行时,printf("windowsn"); 参与编译:

我们还可以写成这种形式,效果与上面的代码一样。

#define WIN 1

int main()
{
#ifdef WIN
	printf("windowsn");
#endif

	return 0;
}

因为WIN被定义,所以运行时,printf("windowsn"); 参与编译: 

(4)嵌套指令

我们可以将#if defined&#ifdef 和#if嵌套使用: 

#if defined(OS_UNIX)
    #ifdef OPTION1
        unix_version_option1();
    #endif
    #ifdef OPTION2
        unix_version_option2();
    #endif
#elif defined(OS_MSDOS)
    #ifdef OPTION2
        msdos_version_option2();
    #endif
#endif
  1. 首先,代码检查是否定义了宏 OS_UNIX,如果定义了,表示代码正在运行在类Unix操作系统上。如果没有定义,表示不是Unix系统。

  2. 如果 OS_UNIX 宏被定义,那么代码会进入第一个条件分支,执行与Unix系统相关的代码。

    如果宏 OPTION1 也被定义,就会调用 unix_version_option1() 函数。                                 如果宏 OPTION2 也被定义,就会调用 unix_version_option2() 函数。
  3. 如果 OS_UNIX 宏没有被定义,表示不是Unix系统,那么代码会检查是否定义了宏 OS_MSDOS,如果定义了,表示代码正在运行在MS-DOS操作系统上。如果没有定义,表示不是MS-DOS系统。

  4. 如果 OS_MSDOS 宏被定义,那么代码会进入第二个条件分支,执行与MS-DOS系统相关的代码。

    如果宏 OPTION2 也被定义,就会调用 msdos_version_option2() 函数。

 6、文件包含

头文件被包含的方式:

本地文件包:

#include "filename"
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。 如果找不到就提示编译错误。

 Linux环境的标准头文件的路径:

/usr/include

 VS环境的标准头文件的路径:

C:Program Files (x86)Microsoft Visual Studio 12.0VCinclude
//这是VS2013的默认路径

库文件包含 :

#include <filename.h>
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
这样是不是可以说,对于库文件也可以使用 “” 的形式包含?
答案是肯定的,可以
但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。

嵌套文件包含:

如果出现这样的场景:

  • comm.hcomm.c是公共模块。
  • test1.htest1.c使用了公共模块。
  • test2.htest2.c使用了公共模块。
  • test.htest.c使用了test1模块和test2模块。
这样最终程序中就会出现两份 comm.h 的内容。这样就造成了文件内容的重复。
如何解决这个问题?
答案:条件编译。
每个头文件的开头写:
#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif   //__TEST_H__
或者:
#pragma once
就可以避免头文件的重复引入。

小结

希望这篇文章可以帮助你学习和复习预处理与条件编译相关知识,切记!!一定要动手操作!!

代码的理解从敲代码开始哦。只有自己实践过,知识才是属于你的!!!