C语言预处理及宏和函数的区别与各自优劣点的详解

C语言预处理及宏和函数的区别与各自优劣点的详解

  • 一:#define
    • 1:#define定义标识符
      • 1.1:语法形式
      • 1.2:实例
    • 2:#define定义宏
      • 2.1:宏定义的介绍
      • 2.2:宏定义的替换规则引例
      • 2.3:宏定义的替换规则
    • 3:#define替换规则
    • 4:使用#和##
      • 4.1:#
      • 4.2:##
    • 5.带副作用的宏参数
    • 6:宏和函数对比
    • 7:#undef及命名约定
      • 7.1:#undef
      • 7.2:命名约定
  • 二:条件编译
    • 1:单分支与多分支的条件编译
      • 1.1 单分支
      • 1.2 多分支
    • 2:判断是否被定义
    • 3:嵌套指令
  • 三:文件包含
    • 1:#include<>与""
    • 2:嵌套文件包含
  • 四:预定义符号

一:#define

1:#define定义标识符

1.1:语法形式

#define name stuff

1.2:实例

#define MAX 1000
#define reg register          //为 register(寄存器)这个关键字,创建一个简短的名字
// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
                          date:%s\ttime:%s\n" ,\
                          __FILE__,__LINE__ , \
                          __DATE__,__TIME__ )
在#define定义标识符之后后面不能加;
例如:
#define M 100;
int main()
{
	int a = 0;
	int b = 0;
	if (a < 5)
		b = M;
	else
		b = -1;
	return 0;
}

此时会报错
C语言预处理及宏和函数的区别与各自优劣点的详解_第1张图片

C语言预处理及宏和函数的区别与各自优劣点的详解_第2张图片

当替换后:
int main()
{
	int a = 0;
	int b = 0;
	if (a < 5)
		b = 100;;
	else
		b = -1;
	return 0;
}
//也就是说else语句前不是if语句,而是空语句即;
所以报错中有:没有匹配if的非法else

那么,怎么书写宏才能够避免这种情况呢?

#define M(x) do{x=100;}while(0)
int main()
{
	int a = 0;
	int b = 0;
	if (a < 5)
		M(b);
	else
		b = -1;
	return 0;
}

此时,代码成功执行

宏替换后:

#define M(x) do{x=100;}while(0)
//这里while(0)后面不要加;
int main()
{
	int a = 0;
	int b = 0;
	if (a < 5)
		do
		{
		   b=100;
		}while(0);
	else
		b = -1;
	return 0;
}

C语言预处理及宏和函数的区别与各自优劣点的详解_第3张图片
我们可以再看一个例子

//如果不加换行符会报错
#define INIT_VAL(a,b) do{\
			a = 0;\
			b = 0; \
			}while(0)
//这里while(0)后面不要加;
int main()
{
	int x = 10;
	int y = 20;
	printf("before: x = %d, y = %d\n", x, y);
	if (1)
	{
		INIT_VAL(x, y);
	}
	else
	{
		x = 100;
		y = 100;
	}
	printf("after: x = %d, y = %d\n", x, y);
	return 0;
}

不加换行符会报错
C语言预处理及宏和函数的区别与各自优劣点的详解_第4张图片
C语言预处理及宏和函数的区别与各自优劣点的详解_第5张图片

宏替换后:

#define INIT_VAL(a,b) do{\
			a = 0;\
			b = 0; \
			}while(0)
//这里while(0)后面不要加;
int main()
{
	int x = 10;
	int y = 20;
	printf("before: x = %d, y = %d\n", x, y);
	if (1)
	{
		do
		{
		   x=0;
		   y=0;
		}while(0);
	}
	else
	{
		x = 100;
		y = 100;
	}
	printf("after: x = %d, y = %d\n", x, y);
	return 0;
}

所以,如果我们想用一个宏向目标代码处插入多条数据时,可以将这多条语句封装到do…while(0)语句中,此时这个语句中可以任意添加语句去成批替换

此处do…while(0)循环的作用:
(1)do…while(0)循环具有花括号语句:可以让宏在替换的时候能够替换多条语句
(2):while(0):让这个循环只执行一次,实现替换

这种结构被称为:do-while-zero结构

2:#define定义宏

2.1:宏定义的介绍

#define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义
宏(define macro)。
下面是宏的声明方式:
#define name( parament-list ) stuff
其中的 parament - list 是一个由逗号隔开的符号表,它们可能出现在stuff中。
注意:
参数列表的左括号必须与name紧邻。
如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分

2.2:宏定义的替换规则引例

//例如:
#define MAX(x,y) ((x)>(y)?(x):(y))
//反例:
#define SQUARE(x) x*x
int main()
{
	int a = 3;
	int r = SQUARE(a);
	printf("r=%d\n", r);
	r = SQUARE(a+2);
	printf("r=%d\n", r);
	return 0;
}

我们推测答案应该是9跟25,但是:
C语言预处理及宏和函数的区别与各自优劣点的详解_第6张图片
这说明宏定义跟函数有所不同,函数是先计算a+2得出5后再传递给函数的形参,但是宏却不一样

那么这么结果到底是怎么得出的呢?

#define SQUARE(x) x*x
int main()
{
	int a = 3;
	int r = 3*3;
	printf("r=%d\n", r);//3*3=9
	r = 3+2*3+2;
	printf("r=%d\n", r);//3+2*3+2=11
	return 0;
}

这样我们就能解释11是怎么得出的了

事实上,宏定义就是这种替换方式,宏定义在编译阶段就已经替换进了代码中

也就是说编译后的代码中是没有类似于这种代码的int r = SQUARE(a);
而是替换成了int r = 3*3;
而:#define SQUARE(x) x*x就直接被删除掉了

那么我们到底应该怎么改呢?

2.3:宏定义的替换规则

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

改为:

#define SQUARE(x) ((x)*(x))

int main()
{
	int a = 3;
	int r = SQUARE(a);
	printf("r=%d\n", r);
	r = SQUARE(a + 2);
	printf("r=%d\n", r);
	return 0;
}

C语言预处理及宏和函数的区别与各自优劣点的详解_第7张图片
可见,此时准确得出了我们想要的结果
那么此时替换为了什么呢?

#define SQUARE(x) ((x)*(x))
int main()
{
	int a = 3;
	int r = ((3)*(3));
	printf("r=%d\n", r);//9
	r = ((3+2)*(3+2));
	printf("r=%d\n", r);//25
	return 0;
}

可见,加括号后的确可以防止操作符优先级的干扰,那么到底要加多少括号才能万无一失呢?
建议是能加的地方都加上括号.

接下来我们再来看一个例子:

#define DOUBLE(x) (x) + (x)
定义中我们使用了括号,想避免之前的问题,但是这个宏可能会出现新的错误。
int a = 5;
printf("%d\n", 10 * DOUBLE(a));

这将打印什么值呢?
看上去,好像打印100,但事实上打印的是55.
我们发现替换之后:
printf("%d\n", 10 * (5) + (5));

由于:乘法运算先于宏定义的加法,所以出现了55
这个问题的解决办法是在宏定义表达式两边加上一对括号就可以了。

即这样

#define DOUBLE(x) ((x) + (x))
我们发现替换之后:
printf("%d\n", 10 * ((5) + (5)));50

3:#define替换规则

#define 替换规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先
被替换。
2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上
述处理过程。
注意:
1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

4:使用#和##

4.1:#

#:作用:把参数插入到字符串中

使用方法:

int main()
{
	int a = 20;
	printf("the value of a is %d\n",a);

	int b = 15;
	printf("the value of b is %d\n",b);

	float f = 4.5f;
	printf("the value of f is %f\n",f);
	return 0;
}
我们有这么一个需求,可不可以定义一个函数print完成a,b,f三者的打印任务呢?
答案是:不可以,
因为函数的参数必须声明为特定的类型。
所以函数只能在类型合适的表达式上使用。

也就是说:
函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是相同的。

不过宏可以完成这个任务

我们先来介绍关于字符串的一个很容易被忽视的细节

int main()
{
	char* p = "hello ""world\n";
	printf("hello ""world\n");
	printf("%s", p);
}

C语言预处理及宏和函数的区别与各自优劣点的详解_第8张图片
从中我们发现字符串是有自动连接的特点的。

	printf("the value of a is %d\n",a);
	printf("the value of b is %d\n",b);
	printf("the value of f is %f\n",f);
	这三个printf函数中这三者的不同点就是
	1:字符串中的a,b,f字符
	2:占位符%d,%d,%f不同

	又因为:字符串是有自动连接的特点的
	所以我们想:
	printf("the value of" "a" "is" "%d" "\n",a);
	printf("the value of" "b" "is" "%d" "\n",b);
	printf("the value of" "f" "is" "%f" "\n",f);
	能不能将这两个不同点当成参数传入呢?

所以我们这样去写
从下面的代码中我们就能够看出#的作用来
也就是:把参数插入到字符串中

#define PRINT(n,format) printf("the value of "#n" is " format "\n",n)
// 当n为a时:  #n:"a"
printf("the value of ""a"" is " "%d" "\n",a);
// 当n为b时:  #n:"b"
printf("the value of ""b"" is " "%d" "\n",b);
// 当n为f时:  #n:"f"
printf("the value of ""f"" is " "%f" "\n",f);
int main()
{
	int a = 20;
	PRINT(a,"%d");

	int b = 15;
	PRINT(b,"%d");

	float f = 4.5f;
	PRINT(f, "%f");
	return 0;
}

C语言预处理及宏和函数的区别与各自优劣点的详解_第9张图片
不过,这里只有当字符串作为宏参数的时候才可以把字符串放在字符串中

4.2:##

##可以把位于它两边的符号合成一个符号。
它允许宏定义从分离的文本片段创建标识符。
注:
这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。

例子:

#define PT(x,y) x##y
int main()
{
	int Hello0716 = 2024;
	printf("%d\n", PT(Hello,0716));
	return 0;
}

C语言预处理及宏和函数的区别与各自优劣点的详解_第10张图片

5.带副作用的宏参数

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

例如:
int a = 10;
int b = ++a;//给b赋值时改变了a,也就是产生了副作用
int b = a + 1;//无副作用
x + 1;//不带副作用
x++;//带有副作用

例子:

#define MAX(a,b) ((a)>(b)?(a):(b))
int c = ((a++) > (b++) ? (a++) : (b++));:a,b,c分别等于什么?
int main()
{
	int a = 5;
	int b = 6;
	//int c = MAX(a++,b++);
	int c = ((a++) > (b++) ? (a++) : (b++));
	//         5       6               7
	//这个最后位置的b++这个整体表达式的值是7,这个7赋值给c
	//  7      6                       8
	printf("c=%d\n", c);//7  注意c不是有歧义
	printf("a=%d\n", a);//6
	printf("b=%d\n", b);//8
	return 0;
}

6:宏和函数对比

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

7:#undef及命名约定

7.1:#undef

#undef
这条指令用于移除一个宏定义
#undef NAME
如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。

7.2:命名约定

一般来讲函数的宏的使用语法很相似。
所以语言本身没法帮我们区分二者。

那我们平时的一个习惯是:

把宏名全部大写
函数名不要全部大写

二:条件编译

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

1:单分支与多分支的条件编译

1.1 单分支

#define M 1

int main()
{
#if M
	printf("hello");
#endif
	return 0;
}

//等同于注释:
#if 0
int main()
{
	return 0;
}
#endif

1.2 多分支

#define I 2
int main()
{
	#if (I==1)
		printf("(1)执行");
	#elif (I==2)
		printf("(2)执行");
	#else
		printf("(3)执行");
	#endif
	return 0;
}

注意:
1.#if后面必须有#endif
2.#endif前面必须有#if

2:判断是否被定义

判断是否被定义(只关心是否被定义过,而不关心具体的真/)
 一定不要忘记#endif
 
被定义过则执行
 
#if defined(symbol)   #endif
#ifdef symbol        #endif

 未被定义过才执行
 
#if !defined(symbol)     #endif
#ifndef symbol         #endif

3:嵌套指令

#define FIRST 0
#define SECOND 0
#define OPTION1 1
#define OPTION2 2
#define OPTION3 3
void do_option1()
{
	printf("do_option1\n");
}
void do_option2()
{
	printf("do_option2\n");
}
void do_option3()
{
	printf("do_option3\n");
}
int main()
{
	#if defined(FIRST)
		#ifdef OPTION1
			do_option1();
		#endif
		#ifdef OPTION2
			do_option2();
		#endif
	#elif defined(SECOND)
		#ifdef OPTION3
			do_option3();
		#endif
	#endif
}

C语言预处理及宏和函数的区别与各自优劣点的详解_第11张图片

三:文件包含

1:#include<>与""

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

这样是不是可以说,对于库文件也可以使用 “” 的形式包含?
答案是肯定的,可以。
但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。

2:嵌套文件包含

在头文件中写入这两种代码中的任意一种都可以防止头文件被重复引用

//防止头文件被重复引用
#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif

//或者
#pragma once
头文件中的 ifndef / define / endif的用处:
防止头文件被重复引用

#include  和 #include "filename.h"的区别:
<>:只在库的标准目录下进行查找
"":先在当前目录下进行查找,如果查找不到,就去库的标准目录下进行查找

四:预定义符号

这些预定义符号都是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__);
	//printf("%d\n",__STDC__);//当前VS不遵守ANSI_C(标准C)
	//C:\Users\86157\Desktop\gitee总\c - code\C_preprocessing_compilation\C_preprocessing_compilation\test.c
	//90
	//Jul 18 2023
	//13:55 : 48
	return 0;
}

在这里插入图片描述
在这里插入图片描述

以上就是C语言预处理及宏和函数的区别与各自优劣点的详解
的全部内容,希望能对大家有所帮助

你可能感兴趣的:(C语言学习与总结,c语言,windows,开发语言)