C语言 预编译详解


 编译C程序涉及很多步骤,第1个步骤就是预编译(预处理)阶段,预编译是在源代码编译之前做一些文本性质的操作。包括删除注释、执行预处理指令。为了观察预编译阶段所做的事,环境使用Linux系统下的GCC编译器
程序编译完整步骤可以查看这篇博客: https://blog.csdn.net/kjl167/article/details/124157077

一、 预定义符号

ANSI C定义了一些预定义符号,它们表示不同含义
__FILE__ :进行编译的源文件名
__LINE__ :文件当前行号
__DATE__ :文件被编译日期
__TIME__ :文件被编译时间
__STDC__ :如果编译器遵循ANSI C 其值为1,否则未定义
在这里插入图片描述

二、 #define 宏定义

#define是 C语言 和 C++ 中的一个预处理指令,其中的“#”表示这是一条预处理命令·。凡是以“#”开头的均为预处理命令,“define”为宏定义命令

2.1 定义不带参数的宏

用法:
#define 标识符(宏名) 对应值
作用:在预编译时,将标识符替换为对应值,对应值可以是任何文本内容
例如:

#define int_t int   //对关键字int起了一个别名,注意int_t不是类型,只是会替换为int  
#define do_forever for(;;) //用do_forever描述了一个无限for循环

在这里插入图片描述

注意:不应该在宏定义的尾部加上分号

例一
#define MAX 10;
int x = MAX;   //预编译后替换为: int x = 10;;  可以发现第二个;表示一条空语句,这种场景不会出错

例二 
#define MAX 10;
int y = 10;
int x = 0;
if(y >= 10)
	x = MAX;     //预编译后替换为:  x = 10;;   if没有加{ },后面不能跟两条语句,出错
else
	x = 20;

在这里插入图片描述

2.2 定义带参数宏

用法:
#define name(parameter-list) stuff
作用:在预编译时,将传入参数列表中参数替换到宏体中对应参数执行某些操作
name :宏名
parameter-list:参数列表
stuff:宏体
注意:参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表和宏体会被当作不带参数的宏中的替换值
在这里插入图片描述

2.2.1 宏的一些问题

测试1:当传入参数中有表达式
在这里插入图片描述
改进:将宏体中参数用小括号包裹起来
在这里插入图片描述

测试2:当表达式旁边还有其他运算符

在这里插入图片描述
改进:将宏体用小括号包裹起来
在这里插入图片描述
结论:所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中
的运算符或邻近运算符之间因为运算符优先级不同产生不可预料的相互作用。

2.2.2 小技巧

C语言支持:相邻字符串常量自动连接成一个字符串
例如:

#include <stdio.h>
int main()
{
	printf("Hello ""World\n"); //等同于printf("Hello World\n"); 
	printf("Hello World\n");
	return 0;
}

输出

Hello World
Hello World

我们可以将字符串常量作为宏参数,打印指定数据类型值

#include <stdio.h>
#define PRINT(FORMAT,VALUE) printf("The value is " FORMAT "\n",VALUE)

int main()
{
	PRINT("%d", 10); // 即 printf("The value is " "%d" "\n",10);
	PRINT("%lf", 5.55); //即 printf("The value is " "%lf" "\n",5.55);
	return 0;
}

输出:

The value is 10
The value is 5.550000

2.3 宏的替换规则

在程序中使用宏时,需要涉及几个步骤。

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义。如果是,它们首先被替换。
  2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
#define MAX 100
#define DOUBLE(X) ((X) + (X))
int main()
{
    /*
    调用宏首先对参数检查,由于参数MAX是#define定义,所以被替换即:  int n = DOUBLE(100);
    再将DOUBLE(100)替换即:  int n = ((100) + (100));
   */
    int n = DOUBLE(MAX); 
    return 0;
}

注意:

  1. 宏参数和#define 定义中可以包含其他#define定义的宏。但是宏不能出现递归。
  2. 当预处理器搜索#define定义的宏时候,字符串常量的内容并不被搜索。
#define NUM 100
#define MAX(X) ((X) > NUM ? (X):NUM)  //宏中可以包含其他宏,但是不能包含本身 

int n = MAX(10);  //替换为: int n = ((10) > 100 ? (10):100);
printf("NUM = %d\n",NUM); //替换为: printf("NUM = %d\n",100);   字符串中NUM没有被替换

2.3.1 # 作用

# :在宏中,#可以将宏参数名(不是参数值)转换为一个字符串
#x 替换为: “x”
#y 替换为: “y”

#include <stdio.h>
#define PRINT(FORMAT,VALUE) printf("The value of " #VALUE  " is " FORMAT "\n",VALUE)

int main()
{
    int x = 10;
	double d = 1.23;
	PRINT("%d", x); // 即预编译替换为 printf("The value of “ "x“ " is " "%d" "\n",x); 
	PRINT("%lf", d); //即预编译替换为 printf("The value of " “d” " is " "%lf" "\n",d);
	return 0;
}

输出

The value of x is 10
The value of d is 1.230000

2.3.2 ## 作用

## :在宏中, ##可以把自己两边的符号连接成一个符号
p##f 替换为: pf

#include <stdio.h>
#define JOIN(X,Y) X##Y
int main()
{
	int result = 10;
    int i = JOIN(re,sult); // JOIN(re,sult) -> re##sult 即 int i = result; 
    printf("%d\n",i); 
	return 0;
}

输出

10

2.4 带副作用的宏参数

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

x+1 //不带副作用
x++ //具有副作用,增加了x的值,当这个表达式下次执行时,产生一个不同的结果

MAX宏可以证明具有副作用的参数所引起的问题,观察下面代码,你认为它将打印什么

#include <stdio.h>
#define MAX(X,Y) ((X) > (Y) ? (X) : (Y)) 
int main()
{
        int x = 5;
        int y = 8;
        int z = MAX(x++,y++);
        printf("x=%d y=%d z=%d\n",x,y,z);
        return 0;
}

输出

x=6 y=10 z=9
说明:  MAX(x++,y++) 预编译替换为: ((x++) > (y++) > (x++) : (y++)) , x++和y++都是后缀++所以先比较后++,
所以x=5 y=8 条件表达式为假(比较后x=6,y=9),执行y++,由于是后置++所以先返回9,然后y=10

2.5 宏与函数区别

宏常用于执行简单的计算,比如在两个表达式(数)中寻找其中较大(较小)一个

#define MAX(X,Y) ((X) > (Y) ? (X) : (Y)) 

相较于函数完成这个功能,宏有2个优势:

  1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹
  2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型以及其他任何可以用>运算符来比较的类型,即宏是类型无关的

有些任务函数不能完成,函数参数无法是类型,而宏的参数可以出现类型

#define MALLOC(n,type) ((type *)malloc((n) * sizeof(type)))  

int * p = MALLOC(25,int);  // 替换为 int * p = ((int *)malloc((25) * sizeof(int)));  
char * p = MALLOC(10,char);  // 替换为 char * p = ((char *)malloc((25) * sizeof(char)));  

在这里插入图片描述

2.6 宏的命名约定

#define宏的行为和函数相比存在一些不同的地方,上文做了总结。由于这些不同之处,因此让程序员知道一个标识符究竟是宏还是一个函数非常重要。不幸的是,使用函数或宏的语法是完全一样的,所以语法本身并不能帮助你区分这两者

一个常见的约定是把宏名字全部大写

#define MAX(X,Y) ((X) > (Y) ? (X) : (Y)) 

value = MAX(a,b); //命名约定可以使MAX身份一清二楚

2.7 #define 与 typedef区别

C语言支持用typedef关键字对各种数据类型定义新名字

typedef int int_t;  //int_t是一个类型
int_t i = 10;

有的人喜欢使用#define方式

#define int_t int  //这里int_t只是符号,不是类型,在预编译后会被替换
int_t i = 10;//预编译后即 int i = 10; 

强烈不推荐使用#define方式,因为它不能正确处理指针类型

typedef int * int_p;
int_p x,y; //这里x、y都是int*指针类型

#define int_p int *
int_p x,y; //预编译替换为:int * x,y;   这里x为int*指针类型,而y为int类型

三、 #undef

宏的作用域从定义位置开始,到文件结束。可以使用#undef移除一个宏定义
用法:
#undef 宏名
在这里插入图片描述

四、命令行定义

许多C编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。当根据同一个源文件要编译一个程序的不同版本的时候,这个特性很有用。假定某个程序中声明了一个某个长度的数组,如果机器内存有限,这个数组必须很小,但是另外一个内存充沛机器上,数组能够大些。如果数组是用类似下面的形式进行声明“

int array[ARRAY_SIZE];

在GCC编译器下可以使用 -D name=stuff,将name的值定义为stuff
在这里插入图片描述

五、 条件编译

 在编译一个程序的时候,选定或忽略源文件中某条语句(某组语句)是很常见的。只用于调试程序的语句就是一个很明显的例子。它们不应该出现在程序的产品版本中,但我们可能并不想把这些语句从源代码中删除,因为在需要一些维护性修改时,可能需要重新调试这个程序,此时还需要这些语句,条件编译可以实现这个目的。

条件编译:满足条件编译某些代码,不满足条件不编译某些代码

5.1 #if #endif

语法:
#if constant-expression
  statements
#endif

说明:constant-expression必须为常量表达式,如果它的值为非0,statements部分正常编译,否则预编译阶段删除它们。
之所以要常量表达式,因为这个是在预编译阶段进行条件判断,如果是变量,变量只有在程序执行阶段才赋值的。
在这里插入图片描述

5.2 #if #elif #else #endif

语法:
#if constant-expression
  statements
#elif constant-expression
  statements
#else
  statements
#endif

说明:#elif子句出现次数不限,constant-expression(常量表达式)值非0编译下面statements部分,如果都不满足编译else对应statements部分,#else是可选的

#include <stdio.h>
#define VERSION 1
#if VERSION == 1
	int ver = 1;
#elif VERSION == 2
    int ver = 2;
#elif VERSION == 3
    int ver = 3;
#else
	int ver = -1;
#endif
int main()
{
	printf("version: %d\n",ver);
	return 0;
}

5.3 是否被定义

#if defined(symbol)
#ifdef symbol
说明:上面两条语句等价
功能:当symbol符号存在,则编译#if与#endif之间语句
在这里插入图片描述

#if !defined(symbol)
#ifndef symbol
说明:上面两条语句等价
功能:当symbol符号不存在,则编译#if与#endif之间语句
在这里插入图片描述

5.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

说明:这个例子中,根据操作系统类型选择不同的处理方案,在预处理指令前面加空白符,形成缩进,有利于提高可读性

六、 文件包含

#include 指令可以使另外一个文件的内容被包含到本文件内编译,就像它实际出现于#include指令的位置一样。这种替换的方式很简单:预处理器先删除这条指令,并用包含文件的内容取而代之一个头文件被包含10次,那就实际被编译10次
在这里插入图片描述

6.1 文件包含两种方式

编译器支持两种不同方式#include文件包含:库函数头文件和自定义头文件

库函数头文件:
#include <filename>
编译器直接去标准位置去查找,如果找不到就预编译错误,在UNIX(Linux)系统下标准位置为:/usr/include

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

对于库函数头文件和自定义头文件使用双括号或尖括号方式都可以,它们区别在于:1. 查找方式不同 2. 通过库函数头文件<> 自定义头文件" " 这种约定可以判断一个头文件是库函数头文件还是自定义头文件

6.2 嵌套文件包含

在这里插入图片描述
头文件a.h和b.h都包含x.h文件,test.c文件又分别包含a.h、b.h文件,当预编译test.c文件时,x.h文件被包含2次
这种嵌套包含在绝大多数情况下出现在大型程序中,它往往需要很多头文件,因此发现这种情况并不容易,为了解决这个问题,可以使用条件编译。如果所有头文件都像下面这样编写,就可以解决问题:

#ifndef _HEADERNAME_H
	#define _HEADERNAME_H
	//头文件内容
#end

当头文件第一次被包含时,_HEADERNAME_H未定义,条件判断为真,使用宏定义_HEADERNAME_H,并包含头文件内容。如果头文件再次被包含,条件判断为假,头文件内容不会再次被包含。
_HEADERNAME_H :按照头文件名进行取名,以避免其他头文件使用相同的名字而引起冲突。如头文件add.h, 则 _ADD_H

  • 30
    点赞
  • 222
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值