冰冰学习笔记:预处理指令


前言

一个程序从源代码到可执行程序要经过编译和链接的过程,最后生成可执行程序。编译过程又可以分为预编译,编译,汇编。在预编译期间编译器将把头文件包含,定义的符号替换,注释删除;在编译阶段将会把C语言代码翻译成汇编代码,并且进行词法分析,语法分析,语义分析,符号汇总;汇编阶段将把汇编命令变为二进制指令,形成符号表。完成所有的编译后,链接阶段将合并段表,将符号表合并并且重定位,最终生成我们的可执行程序。

接下来我们来详细了解以下预编译过程中的一些预编译指令。


一、预定义符号

什么是预定义符号呢?这些符号都是语言内置的,例如下列这些符号:

__FILE__       进行编译的源文件

__LINE__       文件当前的行号

__DATE__     文件被编译的日期

__TIME__      文件被编译的时间

__STDC__     如果编译器遵循ANSI C,值为1,否则未定义(VS环境下未定义)

这些符号有啥用呢,其实用这些符号我们可以生成代码日志,例如在写某一段代码后,记录一下这个代码书写的日期和时间,以及该代码的行号便可以使用这些符号

#include<stdio.h>
#include<windows.h>
int main()
{
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	int i = 0;
	for (; i < 10; i++)
	{
		fprintf(pf, "date:%s time:%s name:%s file:%s line:%d i=%d\n", 
__DATE__, __TIME__, __func__, __FILE__, __LINE__, i);

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

写入文件中的结果如图所示:

当然,我们只需要了解即可,实际应用的场景并不是很多。 

二、#define

#define是我们最常用的一种预处理指令,我们可以使用该指令定义任何我们需要的符号,我们在写扫雷,通讯录时便大量使用了该指令定义的符号,能让一些单一的数值变的可读性更强,并且修改起来更加方便。

2.1#define定义的标识符

#define定义的符号在预处理阶段就会被统一替换掉。

书写形式:

#define name  stuff

该表达式的意思为name的值与stuff一样,代码中使用name符号的地方,最终都会被替代为stuff。

举个例子:

#define MAX 100

int a=MAX;

该代码实际上与int a=100;一样。

当然,#define不仅可以定义常量,其还可以将关键字定义为其他符号,有点typedef的感觉。

那么有一个问题,#define定义的标识符后面需不需要加上分号";"呢?

答案是否定的,我们不能添加上。

有时可能不会出错,因为标识符后面的分号被自动解读为一个空语句,但是有时就不能。

例如在下面代码例2中,如果添加上";"便会出错。

例1代码并不会出错,原因是两个if是独立判断的,中间多一条空语句并不会有什么影响,但是在例2中,if else语句是匹配存在的,if语句后面只能跟一条语句,但是后面多了一句空语句,就导致后面的else语句无法匹配到if语句,因此代码出现错误。

2.2#define定义宏

宏是啥?在C语言中#define 机制包括了一个规定,允许把参数替换到文本中,这种实现形式称之为宏或定义宏。

声明方式:

#define name( parament-list )  stuff

例如:#define ADD(x,y)  ((x)+(y))

意思为将参数a,b放入到ADD(a,b),在预处理阶段便会替换成((a)+(b))

举个例子:

下列代码我们定义了一个宏来实现参数平方的计算

#define SQRT(x) x*x
int main()
{
	int a = 10;
	int c = SQRT(a);
	printf("%d\n", c);
	return 0;
}

该函数运行后能够正确计算出c的值为100。

在预处理阶段,上面的代码便会发生替换。

 

但是该宏的计算并不是总是对的,例如我们将SQRT(x)里面的参数更换为9+1;

那结果就不再是100,而是19.

为什么呢?

如上图所示,在预处理之后,代码变更为9+1*9+1,由于乘法运算优先于加法运算,这就导致先计算1*9,然后计算9+9+1,最终结果为19.

因此我们在定义宏的时候一定不能吝啬括号的使用,如果我们将每个参数都加上括号,就不会在产生因为运算符的优先级导致计算结果出错的问题。

所以我们前面的宏SQRT便可以写为#define SQRT(x) ((x)*(x))

2.3#define的替换规则

宏的替换遵循下列的规则:

1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。

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

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

注意:

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

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

 2.4 #和##

上文中我们说,宏定义的参数不会去字符串中查找,那我们如何将参数插入到字符串中呢?

也就是说我们能否把下面的代码封装成一个函数或者一个宏,让我们能够传不同的参数就可以打印不同形式而不是每次都得重新输入。

有人会说,写成这样

 

但是字符串中的x并不会替换,最终结果将为“The value of x is 10” 

那怎么办呢?

首先我们先学习一个知识

这两句代码都能打出“hello world!”吗?

答案是肯定的,两句代码输出结果一样

 

 也就是说"The value of x is %d\n"可以写为"The value of" "x" "is" "%d\n"

那我们能否把单独的"x"和"%d\n"替换呢?

C语言提供了一个符号,可以将宏参数变为对应的字符串,就是#

表达式:#value

表明将value处理为"value"

所以我们的代码可以这样写:

#define PRINT(x,y) printf("The value of " #x " is " y "\n",x)
int main()
{
	int a = 10;
	double c = 20.345;
	PRINT(a,"%d");
	PRINT(c, "%lf");
	return 0;
}

最终结果便可以根据类型来输出:

##操作符的作用是可以把它两边的符号合成一个符号。

#define CAT(name,num) name##num
int main()
{
	int sum5 = 10;
	printf("%d\n", CAT(sum, 5));
	//替换后变为
	//printf("%d\n",sum5);打印出10
	return 0;
}

2.5带副作用的的宏参数

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

举例来说:

#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{
	int a = 0;
	int b = a + 1;
	int c = a++;
	printf("%d %d\n", b, c);
	int d = MAX(b++, c++);
	printf("%d\n", d);

	return 0;
}

这个代码中,b=a+1=1;c=a++=1;虽然b和c的值都是1,但是在得到c的同时a的值也发生了变化,这就对a产生了副作用。

定义宏MAX计算两个数的较大值,在传入b++和c++时,表达式就被替换成了下面这样

实际计算时b和c的值都会发生变化,首先b和c都是1,在计算完((b++)>(c++))后,b变为2,c变为2,然后执行(c++),c变为3,最后结果为3

所以我们在给宏传递参数的时候应该避免传带来副作用的参数,当然,我们也可以使用函数来实现。

使用该函数,就不会产生结果的副作用。

2.6宏和函数的对比 

写到这里我们发现,宏和函数是有很多相似的地方的,我们习惯将宏的名全部大写,函数名则不用全部大写,现在我们对比以下函数和宏。

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

2.7#undef

该指令的意思为移除一个#define定义的符号

例:

三、条件编译

使用条件编译,我们可以在编译程序时选择或者放弃一段代码的编译。例如我们在调试程序时,有很多代码在正式发布后,是没有用的,删除后,后期进行维护又太麻烦,所以我们可以设置条件编译,测试版本可以调用,发布版本便不在调用。

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

四、文件包含

4.1本地文件包含

#include"test.h"

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

4.2库文件包含

#incldue<stdio.h>

查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。

当然,我们也可以使用双引号进行包含,然后现在文件目录查找,找不到再去标准路径查找,在一些工程量很小的代码中可以这样运行,但是如果一个代码的工程量非常大,文件很多,那就会浪费很多的时间在查找头文件上,所以还是不建议这样使用。

4.3嵌套文件包含

在很多情况下,我们的代码不会只在一个文件中书写,很多项目都是不同的开发人员分模块开发,因此难免会出现引用相同头文件的情况,无形之间就形成了嵌套文件的包含,那怎么避免呢?

我们可以使用条件编译,在每一个头文件开头加入如下代码:

什么意思呢?该代码的意思为如果没有定义__TEST_H__符号,我就定义__TEST_H__符号,然后将我的头文件包含进去。如果我发现之前包含过了,那么意味着该符号已经被定义,就不会执行#ifndef,头文件便不会再次包含,从而避免了嵌套包含的问题。

当然我们还有更简单的办法:

#pragma once

将该语句写入头文件中同样可以避免嵌套包含。


总结

预处理指令还有很多,例如#pragma,我们在更改结构体偏移量的时候使用过。今天所介绍的都是常用的命令,其余的不在过多介绍,有兴趣的可以参考《c语言深度解剖》。

  • 26
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 19
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bingbing~bang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值