从0开始学c语言-40-预处理详解

CSDN话题挑战赛第2期
参赛话题:学习笔记

 上一篇:从0开始学c语言-39-程序环境(翻译环境和执行环境的简单介绍)_阿秋的阿秋不是阿秋的博客-CSDN博客

 接续上一篇

目录

3. 预处理详解

3.1 预定义符号

3.2 #define

3.2.1 #define 定义标识符

3.2.2 #define 定义宏

​3.2.3 #define 替换规则

 3.2.4 #和##(有讲如何替换)

3.2.5 带副作用的宏参数

3.2.6 宏和函数对比

命名约定

3.3 #undef

3.4 命令行定义

3.5 条件编译

3.6 文件包含

3.6.1 头文件被包含的方式:

3.6.2 嵌套文件包含

4. 其他预处理指令


3. 预处理详解

3.1 预定义符号

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

这些预定义符号都是语言内置的。

可以自己尝试着打印几个。

printf("file:%s line:%d\n", __FILE__, __LINE__);

3.2 #define

3.2.1 #define 定义标识符

语法:
 #define name stuff

        把name定义为stuff,当这个名字出现在源文件的任何地方,它就会被替换为定义的值。

        由于它们被定义为字面值常量,所以这些名字不能出现在有些一般变量可以出现的场合。(如:赋值符的左边)。这些定义的名字一般都用大写,用于提醒这并非普通的变量。

例如:

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

        提问:在define定义标识符的时候,要不要在最后加上 ; ?

#define MAX 1000;
if(condition)
 max = MAX;
else
 max = 0;

        以上这段语句就会出问题,MAX;就相当于1000;;,会有一个空语句。

3.2.2 #define 定义宏

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

宏的申明方式:

#define name( parament-list ) stuff

        其中的 parament-list 是一个由逗号隔开的符号表(允许把参数替换到文本stuff中),它们可能出现在stuff中。(可以理解把parament-list为函数参数,也就是后面stuff需要的参数)

注意:

        参数列表的左括号必须与name紧邻。 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。

比如:

#define SQUARE( x ) x * x

        这个宏接收一个参数 x ,如果在上述声明之后,写这么一段函数。请思考这段代码的结果。

#define SQUARE( x ) x * x
int main()
{
	printf("%d\n", SQUARE(5));
	printf("%d\n", SQUARE(5+1) * 5);
	return 0;
}

        要特别注意我们上面提到的define的介绍,define定义的是会被替换到程序中,而不是运算后再替换。

所以这段代码相当于

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

总结:

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

这样定义的define 还有比较好的用法,如图,在switch语句中省去了一些功夫。


3.2.3 #define 替换规则

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

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

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

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

注意:

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

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

 3.2.4 ###(有讲如何替换)

        看一段这样的代码。猜猜打印结果。

int main()
{
	char* p = "hello ""bit\n"; //不可以写成char* p = "hello ","bit\n";
	printf("hello"" bit\n");  //可以写成printf("hello"," bit\n"); 但是会警告参数过多
	printf("%s", p);
	return 0;
}

​        

         可以看到就算没在一个双引号里,也能够正常打印出来。所以,字符串是有自动连接的特点的。(后面会用到)

        那么现在问题来了,你不觉得define定义的宏很像函数吗?而且还有缺陷,在使用的时候会直接替换,如果写不好会容易出很多问题。那么为什么c语言创作者还要弄一个define定义的宏呢?那自然是有它自己独特的魅力。

        所以,我们用下面这段代码来阐述。

         要用函数打印这样一段代码并不难实现,但是如果我写成这样。

void print(int x)
{
	printf("a is %d", x);
}
int main()
{
	int a = 10;
	print(a);
	int b = 20;
	print(b);
	return 0;
}

        期望能够不改变函数内容就实现打印出 b is 20 ,那是不能做到的。

#的作用

        但是我们的define可以做到。

#define print(X,FORMAT) printf(" "#X" is "FORMAT" \n",X) 
int main()
{
	int a = 10;
	print(a,"%d");
	float b = 20.9f;
	print(b,"%f");
	return 0;
}

上面这段代码稍不注意就忘记哪个引号和哪个引号是对应的了,实际上上面这段代码就相当于

printf("   "#X"  is "FORMAT" \n", X);
printf(" " "a" " is " "%d" " \n", a);

使用#,#x相当于“x”,只能在宏里面用。

因为#号的作用是不直接将参数进行替换,而是将参数以字符串的形式打印。

FORMAT则不需要#,而是直接把“%d”替换过去。(注:参数部分写%d不加双引号是不可以的,因为%d、%f等,只有在输出输出格式的时候编译器才认识它。所以这大概就是为什么不需要#修饰的原因了,因为本身已经具有双引号。)

         所以,现在你就会看到宏定义还是有自己独特的用处的。函数也不是那么万能。

## 的作用 

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

 例如:请判断这段代码替换后所代表的意思。

#define ADD_TO_SUM(num, value) sum##num += value	
int main()
{
	int sum5 = 0;
    ADD_TO_SUM(5, 10);			
	return 0;
}

        如果不知道从何下手,请回顾之前写过的这段内容。

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

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

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

        首先我们检查参数,发现参数中并未包含由#define定义的符号,所以我们进入下一步:

替换宏到文本中,同时把宏当中的参数名用相应的值替换

        所以代码会变成这样。

#define ADD_TO_SUM(num, value) sum##num += value	
int sum5 = 0;
sum5 += 10;    //作用是:给sum5增加10.		

        这种用法似乎显得多余,但是我想到了它为什么会存在的原因。我们知道在给一些变量和函数命名的时候大多时候会根据他的功能或者意义来命名。那么在做大工程的时候,就难免遇到开头一样的函数或者变量,那么这时候的##就显得有那么点作用了,可以把同一类开头或者结尾的变量用##连接,看起来就没那么眼花缭乱了

3.2.5 带副作用的宏参数

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

        那么具体就猜猜这段代码运行的结果。

         注:如果不会替换,请向上翻翻,刚复习过如何替换。

#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
int main()
{
	int x, y, z;
	x = 5; y = 8; z = MAX(x++, y++);
	printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么?
	return 0;
}

          这里我们遵循上面的替换原则,写成下面这段代码。        

 output:x= 6 y=10 z=9

3.2.6 宏和函数对比

        宏通常被应用于执行简单的运算。比如在两个数中找出较大的一个。
#define MAX(a, b) ((a)>(b)?(a):(b))
那为什么不用函数来完成这个任务?
原因有二:
        1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。 所以宏比 函数在程序的规模和速度方面更胜一筹
        2. 更为重要的是函数的参数必须声明为特定的类型。所以 函数只能在类型合适的表达式上使用 。反之这个宏怎可以适用于整形、长整型、浮点型等可以用> 来比较的类型。 宏是类型无关的 。(譬如:上面代码中的找最大数,如果数据有很多不同类型需要比较,那么宏是一个好的选择)
当然和宏相比函数也有劣势的地方:
        1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
        2. 宏是没法调试的。
        3. 宏由于类型无关,也就不够严谨。
        4. 宏可能会带来运算符优先级的问题,导致程序容易出现错。 宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型 ,但是函数做不到( 函数是传值或者传址调用 )。
        关于宏参数可以出现类型这件事,我们看这段代码。
#define MALLOC(num, type)\
 (type *)malloc(num * sizeof(type))
...
//使用
MALLOC(10, int);//类型作为参数
//预处理器替换之后:
(int *)malloc(10 * sizeof(int));

然后更详细的对比请看表格

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

命名约定

一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。
那我们平时的一个习惯是:
把宏名全部大写
函数名不要全部大写

3.3 #undef

        这条指令用于移除一个宏定义。
#define Max(a, b) ( (a) > (b) ? (a) : (b) ) 
#define S 10
int main()
{
#undef S
#undef Max(a,b) 

	int z;
	int k = S;
    z = Max(1, 2);
	return 0;
}

        比如这样写,你就会收获一堆警告,因为你已经把自己定义的变量移除了。

3.4 命令行定义

        这个过程不好用vs编译器实现,所以这里只是简单介绍一下。

        许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。例如这样一段代码,你并没有定义ARRAY_SIZE的值,所以程序运行起来会报错。

#include <stdio.h>
int main()
{
    int array [ARRAY_SIZE];
    int i = 0;
    for(i = 0; i< ARRAY_SIZE; i ++)
   {
        array[i] = i;
   }
    for(i = 0; i< ARRAY_SIZE; i ++)
   {
        printf("%d " ,array[i]);
   }
    printf("\n" );
    return 0; }

        那么在VIM或者gcc或者linux环境中使用这样一段命令行定义,就可以实现程序的正常运行。

gcc programe.c -D ARRAY_SIZE=10 

        那么这样的指令有什么用呢? 

        当我们根据同一个源文件要编译出同一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大些,我们需要一个数组能够大些。)

3.5 条件编译

        在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
比如说:
调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。

例如:下面这段代码

#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__
		printf("%d\n", arr[i]);//为了观察数组是否赋值成功。 
#endif //__DEBUG__
	}
	return 0;
}

        我们为了观察数组是否赋值成功,在这个语句前后写了条件指令,在运行后没有问题,就可以在直接删掉define定义的语句(注:不可定义成0,因为这个判断条件是,#ifdefine后的符号是否有定义,而不是【是否为真】,#if后的才是判断真假为条件),这就是选择性的放弃编译这段代码。(因为一步步调试麻烦,便写了我们并不需要的printf语句来观察是否赋值成功,在确认代码没问题后,就可以从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

这里放图,看看意思就行。

1·逗小白常用

2·简单的多分支条件编译

3·符号是否被定义

#ifdef 和 #if defined 是一样的意思

4·和!的结合应用

注意!的位置

3.6 文件包含

补充:头文件的包含:类型的定义、函数的声明。(声明可以多次、定义不能多次)

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

3.6.1 头文件被包含的方式:

本地文件包含
    #include "filename"
    /*先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标
准位置查找头文件。如果找不到就提示编译错误。*/
库文件包含
    #include <filename.h>
    /*查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。*/

Linux环境的标准头文件的路径:
    /usr/include
VS环境的标准头文件的路径:
    C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include
是不是对于库文件也可以使用 双引号“”   的形式包含?
        答案是肯定的,可以
        但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。

3.6.2 嵌套文件包含

        有时候会出现一个头文件多次引用的问题,这样会降低我们程序的运行效率。

        为了解决这个问题,我们可以在每个头文件的开头写:

#ifndef __TEST_H__ (其实就是test.h头文件)
#define __TEST_H__
//头文件的内容
#endif

或者
#pragma once

        这个条件指令还是很好理解的,如果已经有这个头文件就不再#define这个头文件,如果没有便定义上。

注:
推荐《高质量 C/C++ 编程指南》中附录的考试试卷(很重要)。

4. 其他预处理指令

        后续我再补充这些指令的作用。暂时还不怎么用到呢。

#error
#pragma
#line
#pragma pack()
...

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值