C语言-入门-宏定义(十七)

x33g5p2x  于2022-08-17 转载在 其他  
字(4.6k)|赞(0)|评价(0)|浏览(341)

预处理

编译一个C语言程序的第一步骤就是预处理阶段,这一阶段就是宏发挥作用的阶段。C预处理器在源代码编译之前对其进行一些文本性质的操作,主要任务包括删除注释、插入被#include进来的文件内容、定义和替换由#define 定义的符号以及确定代码部分内容是否根据条件编译(#if )来进行编译。”文本性质”的操作,就是指一段文本替换成另外一段文本,而不考虑其中任何的语义内容。宏仅仅是在C预处理阶段的一种文本替换工具,编译完之后对二进制代码不可见

C语言在对源程序进行编译之前,会先对一些特殊的预处理指令作解释(比如之前使用的#include文件包含指令),产生一个新的源程序(这个过程称为编译预处理),之后再进行通常的编译,为了区分预处理指令和一般的C语句,所有预处理指令都以符号“#”开头,并且结尾不用分号,预处理指令可以出现在程序的任何位置,它的作用范围是从它出现的位置到文件尾。习惯上我们尽可能将预处理指令写在源程序开头,这种情况下,它的作用范围就是整个源程序文件,C语言提供了多种预处理功能,如宏定义、文件包含、条件编译等。合理地使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。

注意点:

  1. 宏名一般用大写字母,以便与变量名区别开来,但用小写也没有语法错误
  2. 对程序中用双引号扩起来的字符串内的字符,不进行宏的替换操作
  3. 宏名和参数列表之间不能有空格,否则空格后面的所有字符串都作为替换的字符串.
  4. 带参数的宏在展开时,只作简单的字符和参数的替换,不进行任何计算操作。所以在定义宏时,一般用小括号括住相关计算,这样会避免运算符优先级的问题
  5. 在编译预处理用字符串替换宏名时,不作语法检查,只是简单的字符串替换。只有在编译的时候才对已经展开宏名的源程序进行语法检查
  6. 宏名的有效范围是从定义位置到文件结束。如果需要终止宏定义的作用域,可以用#undef命令
  7. 定义一个宏时可以引用已经定义的宏名
  8. 可用宏定义表示数据类型,使书写方便

宏定义

被定义为“宏”的标识符称为“宏名”。在编译预处理时,对程序中所有出现的“宏名”,都用宏定义中的字符串去代换,这称为“宏代换”或“宏展开”。宏定义是由源程序中的宏定义命令完成的。宏代换是由预处理程序自动完成的。在C语言中,“宏”分为有参数和无参数两种。##不带参数的宏定义:#define 标识符 字符串 其中的“#”表示这是一条预处理命令。凡是以“#”开头的均为预处理命令。“define”为宏定义命令。“标识符”为所定义的宏名。“字符串”可以是常数、表达式、格式串等。

#include <stdio.h>

// 源程序中所有的宏名PI在编译预处理的时候都会被3.14所代替
#define PI 3.14

 // 根据圆的半径计radius算周长
 float girth(float radius) {
    return 2 * PI *radius;
}

int main ()
 {
    float g = girth(2);

    printf("周长为:%f", g);
    return 0;
}

带参数的宏定义

C语言允许宏带有参数。在宏定义中的参数称为形式参数,在宏调用中的参数称为实际参数。对带参数的宏,在调用中,不仅要宏展开,而且要用实参去代换形参#define 宏名(形参表) 字符串

// 第1行中定义了一个带有2个参数的宏average,
 #define average(a, b) (a+b)/2

int main ()
  {
  // 第4行其实会被替换成:int a = (10 + 4)/2;,
      int a = average(10, 4);
  // 输出结果为:7是不是感觉这个宏有点像函数呢?
      printf("平均值:%d", a);
     return 0;
 }

宏名和参数列表之间不能有空格,否则空格后面的所有字符串都作为替换的字符串.

宏函数的巧用

我们知道函数虽然可以传递参数,但是却不能把类型作为参数传递,有时我们为了实现函数的复用性,就要使用STL模板,但是我们这个时候还有另外一种选择,就是写成宏函数
一个开辟内存的函数

#define Malloc(type,size) (type*)malloc(sizeof(type)*size)

这个时候,我们只有把类型,容量作为参数传递进行,就可以开辟各种类型的内存了

int *p=Malloc(int,100); //开辟int类型的内存
char *q=Malloc(char,100); //开辟字符类型的内存

传递数组

由数组作为函数参数传递时,会失去其数组特性,也就是无法使用sizeof()函数计算出数组的大小,比如我们写一个排序函数,排序时我们不仅需要知道数组的首地址,还需要知道数组的大小,但是仅仅把数组名作为参数传递时,无法得知其数组大小,这时我们的函数就需要传递第二个参数,也就是数组的大小,于是函数就要写成Sort(int *a,int size).但宏函数就可以解决这个问题

#define InsertSort(list)\
{\
	int s=sizeof(list)/4;\
	int i,j;\
	for(i=2;i<=s;i++)\
	{\
		list[0]=list[i];\
		for(j=i-1;list[j]>list[0];j--)\
				list[j+1]=list[j];\	
		list[j+1]=list[0];\		
	}\ 
}
int main()
{
	int num[]={0,2,5,7,3,1,8,0,8,22,57,56,74,18,99,34,31,55,41,12,9,4};
	InsertSort(num);
	for(int i=1;i<sizeof(num)/4;i++)	
		printf("%d ",num[i]);
	return 0;
}

宏定义相关作用符

换行符 “”

我们定义宏语句或者宏函数时不可能总是一条语句呀,那要是有很多条语句时怎么办?都写在一行吗?这样显然代码就不美观,可读性不好,所以有多条语句时,我们就在每行末尾(除了最后一行)加上"",代表换行的意思

#include"stdio.h"
#define Print   printf("这是第1条语句\n");\
 		    	printf("这是第2条语句\n");\
 		    	printf("这是第3条语句\n")
 		    	
#define Show(str1,str2,str3)\
{\
	printf("%s\n",str1);\
	printf("%s\n",str2);\
	printf("%s\n",str3);\	
}

字符串化符 “#”

"#"是“字符串化”的意思,将出现在宏定义中的#是把跟在后面的参数转换成一个字符串

#define Print(str)\
{\
	printf(#str"的值是%d",str);\	
}

片段连接符"##"

“##”是一种分隔连接方式,它的作用是进行强制连接。, 简单来说就是类似字符串拼接的方式, 但是这种方式是可以执行的

#define TEXT_A "Hello, world!"
#define msg(x) puts( TEXT_ ## x )
msg(A);

无论标识符 A 是否定义为一个宏名称,预处理器会先将形参 x 替换成实参 A,然后进行记号粘贴。当这两个步骤做完后,结果如下:

puts( TEXT_A );

现在,因为 TEXT_A 是一个宏名称,后续的宏替换会生成下面的语句

puts( "Hello, world!" );

案例:

#define trace(x, format) printf(#x " = %" #format "\n", x)
#define trace2(i) trace(x##i, d)

int main(int argc, char* argv[])
{
    int i = 1;
    char *s = "Hello";
    float y = 2.0;

    trace(i, d);                // 相当于 printf("i = %d\n", i)
    trace(y, f);                // 相当于 printf("y = %f\n", y)
    trace(s, s);                // 相当于 printf("s = %s\n", s)

    int x1 = 1, x2 = 2, x3 = 3;
    trace2(1);                  // 相当于 trace(x1, d)
    trace2(2);                  // 相当于 trace(x2, d)
    trace2(3);                  // 相当于 trace(x3, d)

    return 0;
}

可变参数

#define PRINT(fmt, ...) printf(# fmt, ##__VA_ARGS__)
#define SHOW_LIST(...) printf(# __VA_ARGS__)
int main(int argc, char* argv[])
{

    SHOW_LIST(HELLO, 250, 3.14); //输出:HELLO, 250, 3.14
    PRINT(Hello); //输出:Hello

    return 0;
}

常见的预处理指令

预处理指令使用注意事项
1)预处理功能是C语言特有的功能,它是在对源程序正式编译前由预处理程序完成的,程序员在程序中用预处理命令来调用这些功能。
2)宏定义可以带有参数,宏调用时是以实参代换形参,而不是“值传递”。
3)为了避免宏代换时发生错误,宏定义中的字符串应加括号,字符串中出现的形式参数两边也应加括号。
4)文件包含是预处理的一个重要功能,它可以用来把多个源文件连接成一个源文件进行编译,结果将生成一个目标文件。
5)条件编译允许只编译程序中满足条件的程序段,使生成的目标程序较短,从而减少了内存的开销并提高了程序的效率
6)使用预处理功能便于程序的修改、阅读、移植和调试,也便于实现模块化程序设计。

宏定义和函数的区别

我们常用到的比较大小,宏定义:

#define MAX( a, b) ( (a) > (b) (a) : (b) )

自定义函数实现就是:

int max( int a, int b)
{
    return (a > b a : b)
}

区别:

  1. 最直观的来讲,自定义的函数已经指定了类型,只能比较两个整数的大小,却不能再去比较浮点数的大小,或者两个ASCII码的大小;而宏定义是不会限定类型的,只要比较的类型一致即可。
  2. 自定义接口在程序运行时,会产生临时的堆空间,有临时的空间消耗,如果是递归的话,需要的临时栈空间可能更多;宏定义是在程序运行时,会将宏定义这段代码插入到程序中执行,会有额外的代码段。
  3. 自定义接口在调用时,实际的开销要比代码段大,规模更大; 而宏比自定义函数在程序的规模和速度方面,比自定义函数更胜一筹。

点赞 -收藏-关注-便于以后复习和收到最新内容有其他问题在评论区讨论-或者私信我-收到会在第一时间回复在本博客学习的技术不得以任何方式直接或者间接的从事违反中华人民共和国法律,内容仅供学习、交流与参考免责声明:本文部分素材来源于网络,版权归原创者所有,如存在文章/图片/音视频等使用不当的情况,请随时私信联系我、以迅速采取适当措施,避免给双方造成不必要的经济损失。感谢,配合,希望我的努力对你有帮助^_^

相关文章