程序预处理:全解版

本文深入探讨了C语言的预处理概念,包括#define定义标识符和宏,预处理符号#与##的作用,以及宏与函数的对比。同时讲解了条件编译的选择性编译和头文件包含的机制,帮助读者理解预处理在程序开发中的重要性和使用技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  • ANSI C的标准中,任何一种实现都存在两个不同的环境:翻译环境和执行环境;

而预处理就是在翻译环境进行的,预处理也称为预编译

      接下来就来详细讲讲预处理环节的细节  (为了方便观看代码,下面的对代码的解释和板书会采用全角输出的模式)


目录

 一、预定义符号

 二、#define

1、定义标识符号:

2、定义宏

3、#define替换规则

4、预处理符号#与##

  1、#

2、##

5、#define定义宏和函数对比

1、#define和函数的简单对比

2、表格对比

三、#undef

四、命令行定义

五、条件编译(选择性编译)

六、头文件包含

1、本地文件包含

2、库文件包含

七、嵌套文件包含


 一、预定义符号

c语言会内置预定义符号,这种标识符在c语言中没有特定的含义,可以用printf函数进行格式化输出。可作为用户标识符使用,让我们了解到当前文件更多的编译信息,但是使用不当也会使程序报错。

 他们的功能如下:

 二、#define

#define也是预处理指令的一种  ,c语言中可以用#define来定义标识符号

1、定义标识符号:

    可以直接将对应的用户关键字替换为#define所定义的内容,遵循替换原则

(这个相比都不陌生)在写链表或者其他的程序的时候我们经常使用#define来定义字母,来让程序后期的维护更加的安全快捷。

  首先我们可以想到的就是 用 #define去定义一个符号,例如:

#define  Max  100 //用#define定义整形
#define  exm   "abcdefg"  //用#define定义字符串

我们将其打印,结果如下:

 (原则上,只要#define定义的内容符合代码语法规则,就可以定义,例如整形,字符串,甚至是一段代码)

2、定义宏

#define允许把参数替换到文本当中,这种实现通常称为宏:

下面是宏申明的格式

#difne  name ( paramen_list )   stuff   其中parament_lsit 是用逗号隔开的参数列表

(需要注意的是,参数列表左边的括号必须和name紧邻,如果留有空白的话就变成了前面所讲的情况)

举个例子:例如我想求一下  一个int a  和 int b 两个整形的较大值


#include<stdio.h>
#define  Max(x,y) (x>y?x:y)

int main()
{
   int a=1; 
   int b=2;
   int m=Max(a,b);
   printf("%d\n",m);
   return 0;
}

最后运行结果为 : 2.

     上面提到define的替换性,可见,我们所定义的整形或者字符串在define看来是没有数据类型的;所以在定义宏的时候,不会直接将某些表达式的结果传入宏当中,而是将这个表达式整体传过去进行计算。如果是这种情况的话,就难免会产生表达式计算优先级的问题;

举个简单的例子:

#include<stdio.h>
#define CALC(x) x*x

int main()
{
	int m = CALC( 6);
	printf("%d\n", m);
	return 0;
}

我们写一个这样的代码,我们传入6,那么用宏来计算之后的值必然是36;
---->但是如果,我们将b改成  3+3 ,结果又会是什么呢?结果如下:

 虽然3+3的值也为6,但是传入的时候由于是整体代入,所以最终CALC(3+3)就被替换成了
   int  m  =  3 + 3 *3 + 3。这种优先级的问题也就体现出来了。

但是居然遇到这种问题,那必然就有解决的方法:
针对于优先级的问题,我们可以想到用括号来解决,例如将上述的 #define后面的内容改为

#define   CALC(x)  (x)*(x)

 这样一来问题就解决了,但是如果我们经常使用#define定义宏之后,其实还会发现参数列表右括号右边的表达式,最好在最外层也带上括号:

#define   CALC(x)  ((x)*(x))

 目前这个平方的宏暂时看不出来什么猫腻,因为这些代码一般都是内层替换的时候出现的问题;我们换个代码来看看:

#include<stdio.h>
#define JIA(x)  (x)+(x)
int main()
{
	printf("%d\n", 2 * JIA(2));
	return 0;
}

按理来说JIA(2)的结果应该为4,但是打印后的结果确为》》6 ,不难分析,是#define将整体替换为了

printf ("%d\n", 2 * 2 + 2);   

这就体现了在宏的外层加上括号的重要性(不管是外层还是内层!

#include<stdio.h>
#define JIA(x)  ((x)+(x))
int main()
{
	printf("%d\n", 2 * JIA(2));
	return 0;
}

3、#define替换规则

接下来详细了解一下#define的替换规则

1、在调用宏时,首先要对参数进行检查,查看是否包含#define定义的字符,如果是,它们首先被替换

2、替换文本随后会被插入到程序中原来的文本位置,对于宏,参数名被他们的值所替换。

3、最后再次对文件结果进行扫描,查看是否还包含#define定义的标识符,如果存在,就重复上述操作。

注意:

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

2、当预处理器搜索#define定义的符号的时候,字符串常量里的内容不被搜索,例如:

   #define   MAX  100     

   char arr[ 10 ]  =  "abcdefgxxxMAX";  //其中的MAX不被#define搜索和替换

4、预处理符号#与##

  1、#

简单直入,在二、3的替换规则末尾,也就是上面两行内容中提到,#define不能搜索字符串常量里面的内容,那么问题来了,如果某一天需要去替换字符串里面的内容的时候该怎么办?

这就要说说#和##的作用了————将参数插入到字符串当中。

我们先来看看这样的一串代码:

#include<stdio.h>
int main()
{
   printf("hello world\n");
   printf("hello"" world");
   return 0;
}

它的运行结果都为:hello  world
我们可以看出来:两个紧邻的字符串最后合成了一个字符串。
其实在敲代码的时候,不免遇到这样的场景:有一些整形变量如下

int a = 1;  int  b = 3 ;  int c=4.......

我们是否能在一个for循环内使用一个printf函数将如下内容打印下来:

the value of a is 1;

the value of b is 3;

the value of c is 4;

...........以此类推

如果不用for循环,用一个一个用printf函数打印的时候,过程不免冗杂:

int a =1;    float b=2.3f;     int c =3;

printf("the value of a is %d",a);

printf("the value of a is %f",b);

printf("the value of a is %d",c);

对于一个for循环,我们发现,一个函数时解决不了问题的,所以宏的重要性就体现出来了:

#define print(x,y)  printf("the value of  "#x" is "format"\n",x);

对上述内容进行一个解释:由于#define不能搜索字符串里面的内容,所以我们的printf里面的内容用多个字符串("the value of"  #x  " is "" format"  "\n")替代,但是由前面的内容可以看到,如果x为我们上述的a,b,c等变量的时候,他就会把x替换为相应的值,而不是直接替换成用户变量(如果传入a,那么#x就会变成"a"),这个时候就要用到#了。

在这里#的作用就是直接将x对应的字符传过去,而不是值的替换,例如我们传入print(a,"%d"):

结果为: printf("the value of " "a" " is " "%d" "\n",x);

即:        printf("the value of a is %d\n",x);

我们依次传入 print(变量名,数据类型);就能将其打印出来,这样就方便了很多。

2、##

##可以将位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符:

有个变量的名为  int  jinitaimei =6;

#define  cat(x,y)  x##y

printf("%d\n",cat( ji , nitaimei ));

输出结果为6;

(标识符必须合法,否则会显示标识符未定义)

5、#define定义宏和函数对比

1、#define和函数的简单对比

从上面的例子不免看出来,越是复杂运算,宏所带来副作用越大,所以宏在非必要时一般都用来执行简单的计算,宏被用来实现量大但是计算过程简单的代码时非常适合不过的;

那为什么不用函数来完成这个任务?
原因有二:
       1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。
           所以宏比函数在程序的规模和速度方面更胜一筹。
       2. 更为重要的是函数的参数必须声明为特定的类型。
           所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点               型等可以用于>来比较的类型。
       3.宏是类型无关的。
宏的缺点:当然和函数相比宏也有劣势的地方:
       1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
       2. 宏是没法调试的。
       3. 宏由于类型无关,也就不够严谨。
       4. 宏可能会带来运算符优先级的问题,导致程容易出现错。
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。
除此之外,在命名上,宏一般采用全大写的形式,而函数则采用一般的驼峰式,或者是下划线加驼峰式的组合式(多为小写,便于区分宏和函数)。

2、表格对比

#define和函数对比
属性 #define        函数
代码长度
每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长
函数代码只出现于一个地方;每次使用这个函数时,都调用那个 地方的同一份代码
执行速度            更快
存在函数的调用和返回的额外开销,所以相对慢一些
操作符优先级宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生 不可预料的后果,所以建议宏在书写的时候多些括号。函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测
副作用参数
参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果
函数参数只在传参的时候求值一次,结果更容易控制。
参数类型
宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型
函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是
相同的
调试不方便调试可以逐语句调试
递归不能递归可以递归

三、#undef

用于移除#define定义的宏;我们来看这样的一个例子:

 

 我们在上面引用define定义MAX 100 ,然后打印MAX,发现MAX可用,但是在接下来的一行使用了#undef MAX后,后面再去打印就显示语法错误----未定义标识符,可见在这个地方定义的MAX已经被移除了。

四、命令行定义

许多 C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
例如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大些,我们需要一个数组能够大些。)
举个例子:
#include <stdio.h>
int main()
{
    int arr [size];
    int i = 0;
    for(i = 0; i< size i ++)
        arr[i] = i;
    for(i = 0; i< size; i ++)
        printf("%d " ,arr[i]);
    printf("\n" );
    return 0;
}

在编译的时候根据不同的需求,来命令size的大小,以确保程序在不同运行环境下的稳定性。

五、条件编译(选择性编译)

在编译的时候我们可以进行选择性编译,使用一下条件编译指令

1、单分支

#if 常量表达式

   // .....

#endif

  // .....

如:
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif  

2、多分支

#if 常量表达式 
//... 
#elif 常量表达式 
//...  
#else
//... 
#endif
3.判断是否被定义
#if defined(symbol) 
#ifdef symbol 
#if !defined(symbol) 
#ifndef symbol  
4.嵌套指令
#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

单分支语句举例   运行结果为  0 1 2 3 4 5 6 7 8 9

#include <stdio.h>
#define __DEBUG__
int main()
{
 int i = 0;
 int arr[10] = {0};
 for(i=0; i<10; i++)
 {
 arr[i] = i;

 #ifdef __DEBUG__  //判断条件,如果表达式结果为1则参与编译
   printf("%d\n", arr[i]);
 #endif   
 }
 return 0;
}

看上面这个例子,如果__DEBUG__被定义,则编译执行printf语句,否则不编译

多分支举例    运行结果为   3;

#include<stdio.h>
#include<stdlib.h>
#define MAX 100
int main()
{
#if 1>2
    printf("1");
#elif 2>3 
    printf("2");
#elif  4>3
    printf("3");
#endif
    system("pause");
    return 0;
}

判断是否被定义: 结果为MAX has been defined

#include<stdio.h>
#include<stdlib.h>
#define MAX 100
int main()
{
#if defined(MAX)
    printf("MAX has been defined\n");
#endif
#if defined(min)  //仅仅只看你是否定义,不会显示未定义标识符
    printf("???");
#endif
    system("pause");
    return 0;
}

(也可以将#if defined (MAX)改为

#ifdef  MAX  这两者是等价的)

此外,#ifndef MAX则等价于 #if !defined(MAX)

加了一个n和!,意思就变为了:如果MAX没定义就真,执行语句就参与编译

这里的嵌套使用和我们c语言中的if else if语句用法差不多,这里不再赘述。

六、头文件包含

我们知道,工程里面的源文件通过编译器转化为目标文件后,通过链接器和链接库 链接形成可执行程序。

#include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样
这种替换的方式很简单:
  • 预处理器先删除这条指令,并用包含文件的内容替换。
  • 这样一个源文件被包含10次,那就实际被编译10次。

1、本地文件包含

#include"filename"

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

2、库文件包含

#include<filename.h>

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

七、嵌套文件包含

一个复杂的项目往往需要很多个公共文件模块来辅助完成,这样子就避免不了重复包含而造成问价内容重复的问题。

如何解决: 用我们前面所用到的条件编译。

即每个头文件开头和结尾分别加上

#ifndef __TEST_H__

#define __TEST_H__

  //文件内容

#endif 

或者

#pragma once  //这种方法是最简洁的

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值