C语言预处理详解

目录

一、预定义符号

二、#define

        1. #define 定义标识符

        2. #define定义宏

        3.#define的替换规则

        4.#和##

                <4.1> #的使用

                <4.2> ##的作用

        5.带副作用的宏参数

        6.宏和函数的对比

        7.命名约定

三、#undef

四、命令行定义

五、条件编译

        1. #if、#else、#elif和#endif

        2. #ifdef和#ifndef

六、文件包含

        1.头文件被包含的方式

                <1.1>本地文件包含

                <1.2>库文件包含

        2.嵌套文件的包含

                <2.1>用#ifndef来避免

                <2.2>用#pragma once来避免


一、预定义符号

介绍一些C语言内置的预定义符号:

__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C ,其值为1,否则未定义

运用举例:

int main()
{
	printf("%s\n", __FILE__);
	printf("%d\n", __LINE__);
	printf("%s\n", __DATE__);
	printf("%s\n", __TIME__);
	return 0;
}

 运行效果:

         注:在VS编译器中是不遵循ANSI C的,如下图所示:

         但是在Linux环境下是遵循ANSI C的:

二、#define

#define在我们编译代码时并不少见,下面我们来详细看看其使用:

        1. #define 定义标识符

        使用语法为:

                #define  name  stuff

        >name是想定义的名字,stuff是定义的具体内容。

例如:

#define MAX 16		//定义一个数
#define FC "AE86"	//定义一个字符串
#define do_Forever for(;;)	//定义一个死循环语句

运行效果(该程序进入死循环一直在运行): 

该段代码在预处理之后#define定义的标识符全部被替换,等价于:

 所以在程序预处理时仅仅就是将#define所定义的标识符进行了替换。

        2. #define定义宏

        #define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。

下面是宏的声明方式:

#define name(parament-list)  stuff

        >name为宏,相当于名称。

        >其中parament-list是一系列将要传入的参数,用逗号(,)分隔开(即一个用逗号分隔开的符号表),它们可能出现在stuff中。

        >stuff是宏体,是具体的内容。

举例:

#define MAX(x,y) (x>y?x:y)
int main()
{
	int a = 10, b = 20;
	printf("%d和%d,%d大\n", a, b, MAX(a, b));
	return 0;
}

运行效果:

 其实在代码被预处理后,MAX(x,y)将被替换为(x,y?x:y),最终等同于以下代码(其中x,y分别被a,b替换):

 >注:在宏定义时左括号必须与name相邻,不然会被解读为stuff的一部分。

在这里提一点建议:我们在定义宏时要带足括号!

举个错误的例子:

 在这里我们发现输出的结果并不是我们预期的220,因为宏定义是被直接替换的所以等同于以下代码:

 我们发现由于乘的优先级大于加,所以导致了最终结果的输出错误。

所以我们在使用宏定义时一定要加足括号(这样可以避免因替换而引起的错误):

        3.#define的替换规则

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

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

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

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

注:

        >1.宏参数和#define定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归(即一个宏定义里不能再嵌套它本身)。

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

如:

#define M 8
int main()
{
	printf("Holle M");
	return 0;
}

在此代码中字符串“Holle M”中的“M”不会被替换。

        4.#和##

                <4.1> #的使用

在我们想将一个变量的名称转换为字符时应该怎么办呢?

例如:

int main()
{
	int a = 30;
	printf("The value of a is %d\n", a);
	int d = 2;
	printf("The value of a is %d\n", d);
	float f = 9.6f;
	printf("The value of a is %f\n", f);
	return 0;
}

像上面的代码,这么多的printf函数差的只不过是打印的格式(%d,%f)和对应的数值(a,d,f)我们是否可以将其简化呢?

这里我们可以在宏定义中使用#:#可以将一个宏参数转换为相对应字符串

#define PEINT(val,format) printf("The value of " #val " is " format"\n",val)
int main()
{
	int a = 30;
	PEINT(a, "%d");
	int d = 2;
	PEINT(d, "%d");
	float f = 9.6f;
	PEINT(f, "%f");
	return 0;
}

在#define中由于val在printf中前加了一个#,此时参数类型的val已变为字符串类型的“val”,就可以在printf函数中直接打印“val”了:

上图是运行时的效果(十分巧妙的将各种变量的名称转换为字符串了)。

                <4.2> ##的作用

##在宏定义中的作用就是将##左边和右边的符号合成一个符号:

举例:

#define CAT(a,b) (a##b)
int main()
{
	int Ae86 = 6;
	printf("%d\n", CAT(Ae, 86));
	return 0;
}

运行效果(就是将Ae和86结合了): 

 注:这样的连接必须产生一个合法的标识符,否则结果就是未定义的。

        5.带副作用的宏参数

我们在写代码时有些时候是有副作用的:

int main()
{
	int m;
	int a = 2;
	m = a + 1;//无副作用,给m赋值时,a的值不发生改变
	m = a++;//带有副作用,给m赋值时,a的值改变了
	return 0;
}

所以我们在给宏传参时也要注意到传入的参数是否带有副作用。

举个栗子:

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

在我们传入参数时将原变量的值前置++了,这就造成了传入的参数是带有副作用的。

最终输出的效果:

 因为宏定义经替换后代码等同于:

int main()
{
	int a = 1;
	int b = 3;
	int m = ((++a) > (++b) ? (++a) : (++b));
	printf("m=%d a=%d b=%d\n", m, a, b);
	return 0;
}

造成了a和b都进行了两次前置++,使其值都增加了2。

所以我们在传入宏参数时尽量避免传入带有副作用的参数,以免造成结果出乎意料。

        6.宏和函数的对比

我们从上述例子发现宏做的事情函数也可以做,比如:

#define MAX(x,y) ((x)>(y)?(x):(y))
int Cmp(int x, int y)
{
	return (x > y ? x : y);
}

在这两种比大小的方法中它们有什么区别呢?

我们可以运行比较一下:

int main()
{
	int a = 1;
	int b = 3;
	Cmp(a, b);
	MAX(a, b);
	return 0;
}

运行时我们转到反汇编:

发现在执行函数Cmp时,函数需要调用参数、创建函数栈帧、返回返回值等等步骤,而真正计算时所运行的就寥寥无几的代码,这大大减少了时间的利用率。

我们在来看看宏:

 而宏在计算时它所需运行的代码相当于函数少了不少,相当于进行简单计算,使用在使用宏时运行效率比函数更高。

所以可以知道宏相对于函数的优势:

        >用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。

        >更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。 所以函数只能在类型合适的表达式上使用。反之这个宏可以适用于整形、长整型、浮点型等可以用于>来比较的类型。宏是类型无关的。

        >宏可以做到函数做不到的事情:如#,传入类型参数(宏的参数是可以接收类型的(如int、char、short等等))。

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

        >每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。

        >宏是没法调试的。

        >宏由于类型无关,也就不够严谨

        >宏可能会带来运算符优先级的问题,导致程容易出现错。

        >宏不能进行递归,而函数可以。

        7.命名约定

由于函数和宏的使用语法很相似,所以语言本身无法帮我们区分两者。

所以平时命名遵循两种习惯(但有时也会有例外):

        >函数命名时不要全部大写。

        >宏命名时要全部大写。

三、#undef

在我们不需要被定义的宏时,可以使用#undef来将其取消。

举例:

#define M 10
int main()
{
	printf("%d", M);
#undef M
	printf("%d", M);
	return 0;
}

运行效果:

 这样M就被取消定义了。

四、命令行定义

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

举例:

int main()
{
	int arr[SZ];
	int i;
	for (i = 0; i < SZ; i++)
	{
		arr[i] = i;
	}
	for (i = 0; i < SZ; i++)
	{
		printf("%d", arr[i]);
	}
	return 0;
}

此时SZ未定义的:

我们可以在编译时赋予SZ值:

 注:该命令行定义操作是在Linux环境下进行的,在Windows环境中不好演示。

五、条件编译

我们在调试的时候可以可以选择性的选择要编译的代码呢?

答案是肯定的,C语言为我们提供了不少条件编译指令:

        1. #if、#else、#elif和#endif

        我们可以用 #if、#else、#elif和#endif指令来选择性的编译我们的代码,#if和#elif后面接将要判断的条件,如果为真就编译其内部的代码,反之则跳过。其中#if、#elif、#else同时满足条件时只执行其中的一条(和if和else if一样)。

举个栗子:

根据上述选择编译的条件,在预处理结束后,被编译的代码只有:

 再来个栗子:

根据上述选择编译的条件,在预处理结束后,被编译的代码只有:

        当然我们也可以在#if后面加上defined()来判断一个宏是否有被定义(括号中是将要判断是否被定义的宏),如果被定义则为真值,反之为假。

例如:

 此时MAX并未被定义,所以编译后代码为:

现在我们定义MAX:

 再次预处理后,代码为:

 即使MAX被定义为0,也不影响它是被定义过的,经#if defined()判断后也是真值。

当然我们也可以在defined前加上!(即#if !defined)这样如果未被定义为真值,被定义则为假(与#if !defined相反):

 此时预处理后的代码为:

        2. #ifdef和#ifndef

         #ifdel和#ifndef也是用来判断有无被定义的条件编译语句,其中#ifdel等价于#if defined(),#ifndef等价于#if defined()。

举例举例:

#define M 0
int main()
{
#ifdef M
	printf("hehe\n");
#endif

#ifndef M
	printf("haha\n");
#endif

	return 0;
}

预处理后代码为:

六、文件包含

我们在写代码之前都需要各种头文件,都会使用#include来包含,下面我们来具体看看文件包含:

        1.头文件被包含的方式

                <1.1>本地文件包含

        本地文件我们在使用#include时用引号(" ")来包含文件名

例如:

#include"test.h"

        编译代码时查找本地文件策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件(查找两次)

如果找不到就提示编译错误。 

                <1.2>库文件包含

        库文件我们在使用#include时用尖括号(< >)来包含文件名

例如:

#include<stdio.h>

         编译代码时查找库文件策略:直接从标准位置查找头文件(查找一次),如果找不到就提示编译错误。 

我们从本地文件和库文件的查找方式可以看出,查找本地文件方式包含了查找库文件的方式,所以库文件也可以用引号(" ")来包含,但是我们并不建议这样做:

        但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。

        2.嵌套文件的包含

         如上图:我们在test.c中引用test.h头文件时头文件包含了另外两个头文件(add.h和sum.h),而这两个头文件都包含了一个相同的头文件(long.h)。此时我们编译代码后预处理后的代码就会两次调用long.h文件里的内容造成代码的重复。试想一下如果这是一个大项目,每个程序员都有成千上万的头文件被重复嵌套包含,这就会造成很严重的冗余问题。

        那么我们要如何避免这种情况呢?

                <2.1>用#ifndef来避免

                创建一个头文件test.h:

                现在我们重复包含了五次头文件"test.h":

                在预处理过后重复包含多次test.h的内容:

                 现在我们使用#ifndef来处理头文件:

                 加了#ifndef后,在头文件代码被一次预处理包含时__TEST_H__是未被定义的,所以编译#ifndef内部的代码:声明宏__TEST_H__并声明函数add。在后续头文件代码被预处理包含时,由于__TEST_H__被定义了,就不会再编译其内部代码了,这样就很好的避免了头文件被重复包含的问题:

                 在预处理过后test.c的内容就被包含了一次。

                <2.2>用#pragma once来避免

                除了使用#ifndef我们还可以用#pragma once来更简便的避免头文件被重复包含的问题(只需在头文件开头加上#pragma once):

                 在预处理过后的效果:

         注:#pragma once 是比较新的用法可能在一些老编译器下不能使用


        本期的博客到这里又要和各位看客说再见啦,后期的博客将会主要以数据结构与算法为主,另外也会补充一些C语言的知识。如果各位觉得本期博客对自己有帮助可以点击收藏哦~

那就写到这里,我们下一期见~

  • 7
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

1e-12

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

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

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

打赏作者

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

抵扣说明:

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

余额充值