目录
预定义符号
预处理指令#define
#define定义符号
#define定义宏
#define替换规则
#和##(C语言预处理操作符)
#
##
带副作用的宏参数
宏和函数的对比
命名约定
在之前我们学习了一个文本文件.c生成一个可执行程序。今天我们详细讲解其中的编译下的预处理这个步骤的我们需要掌握的知识点。
预定义符号
标准C语言预定义的符号,在预处理阶段可以直接使用。 这些预定义符号都是语言内置的。
- __FILE__ 进行编译的源文件
- __LINE__ 文件当前的行号
- __DATE__ 文件被编译的日期
- __TIME__ 文件被编译的时间
- __STDC__ 如果编译器遵循ANSI C,其值为1,否则未定义
- __FUNCTION__ 文件当前的函数名字
#include<stdio.h>
int main()
{printf("%s\n", __FILE__);printf("%d\n", __LINE__);printf("%s\n", __DATE__);printf("%s\n", __TIME__);//printf("%s\n", __STDC__);//当前使用的是VS2022不遵循ANSI Cprintf("%s\n", __FUNCTION__);return 0;
}
- 换到gcc的底下就可以使用。printf("%s\n", __STDC__);
预处理指令#define
接着我们来介绍#define。#define是一种预处理指令。
#define定义符号
#define定义常量就是定义标识符。
语法:#define name stuff
- name是名字
- stuff是内容
- #define MAX 100
- #define M 3+5
- #define 仅仅只是替换符号,不进行计算。
- #defien在定义标识符的时候,不要在最后加上;
#define MAX 100
#define STR "abcdef"
#define INT int
#define M 3+5
int main()
{int a = MAX;INT b = 0;printf("%s", STR);int c = M;return 0;
}
除了上面示例以外还有特殊的写法。
#define MAX 100
#define STR "abcdef"
#define INT int
#define forever for(;;)
int main()
{int a = MAX;INT b = 0;printf("%s", STR);forever;//替换之后for (;;);//死循环return 0;
}
#define CASE break;case
int main()
{int n = 0;scanf("%d", &n);switch (n){case 1:CASE 2:CASE 3:}return 0;
}
//替换之后
#define CASE break;case
int main()
{int n = 0;scanf("%d", &n);switch (n){case 1:break; case 2:break; case 3:}return 0;
}
#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 MAX 1000;
#include<stdio.h>
int main()
{int a = MAX;return 0;
}#define MAX 1000;
#include<stdio.h>
int main()
{int a = 1000;;//空语句return 0;
}
像上面这种情况可能没有问题,但是建议不要加上 ; ,这样容易导致问题。 比如下面的场景,会出现语法错误:
#define MAX 1000;
#include<stdio.h>
int main()
{int a = 10;int b = 0;if (a>5)b = MAX;elsemax = 0;return 0;
}#define MAX 1000;
#include<stdio.h>
int main()
{int a = 10;int b = 0;if (a > 5)b = 1000;;//空语句这里就有问题了elsemax = 0;return 0;
}
#define MAX 1000;
#include<stdio.h>
int main()
{printf("%d", MAX);return 0;
}#define MAX 1000;
#include<stdio.h>
int main()
{printf("%d", 1000;);//语法错误return 0;
}
#define定义宏
宏是什么?
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(definemacro)。
语法
#define name( parament-list ) stuff
- name 是名字
- parament-list 是一个由逗号隔开的符号表,可能出现在stuff中,可以没有/一个/两个均可。
- parament-list 的参数可能会出现在 stuff 文本内容中。
- 参数列表的左括号必须与name紧邻。
- 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
- 所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用
#define ADD(x,y) x+y
#include<stdio.h>
int main()
{int a = 10;int b = 20;int c = ADD(a, b);printf("%d\n", c);return 0;
}
看了上面#define定义宏的使用,相信你很快就上手了。
但是这个简单的宏却存在重要错误隐患。
#define ADD(x,y) x+y
#include<stdio.h>
int main()
{int a = 10;int b = 20;int c = 4*ADD(a, b);printf("%d\n", c);return 0;
}
我们原本想计算4(a+b)的值,应该是90,但是结果确是60。因为替换之后的代码:
#define ADD(x,y) x+y
#include<stdio.h>
int main()
{int a = 10;int b = 20;int c = 4* a + b;//60printf("%d\n", c);return 0;
}
有人说将x+y加上括号(x+y)即可,但是这样真的就安全了吗?
#define ADD(x,y) (x*y)
#include<stdio.h>
int main()
{int a = 10;int b = 20;int c = 4*ADD(a+1, b+1);
// int c=4*(11*21)=4*(32)printf("%d\n", c);return 0;
}
我们原本想计算的是4((a+1)*(b+1))结果,但是没有达到我们想要的结果。为什么呢?
#define ADD(x,y) (x*y)
#include<stdio.h>
int main()
{int a = 10;int b = 20;int c = 4*(a+1*b+1);
//int c=4*(10+20+1)=4*(31)printf("%d\n", c);return 0;
}
综上所诉:宏在书写的时候尽量带上足够多的有效的括号。
所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。导致计算的顺序发生变化。
#define替换规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
- 首先,在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
- 其次,替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。
- 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程
特别提醒:
- 宏参数和#define 定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归。
- 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
#define M 100
#define ADD(x,y) ((x)*(y))//这里的文本内容是不能出现宏的:类似函数递归的这种情况的
#include<stdio.h>
int main()
{int a = 10;int b = 20;int c = 4 * ADD(M, b+10);//M首先会被替换printf("%d\n", c);printf("Master\n");//这里的M是不会被替换return 0;return 0;
}
#和##(C语言预处理操作符)
#
提问:如何把参数插入到字符串中?这里我们引入:#
# 就是把宏的参数插入到字符串中
首先,我们需要知道:字符串是有自动连接的特点的。
合并打印和分开打印都能达成一样的效果。
#include<stdio.h>
int main()
{printf("hello word\n");printf("hello"" word\n");return 0;
}
看下面这个代码,我们发现有非常多重复的地方,我们能简化代码,把重复的地方封装成函数。
#include<stdio.h>
int main()
{int a = 10;printf("the value of a is %d\n", a);int b = 20;printf("the value of b is %d\n", b);float f = 3.14f;printf("the value of f is %f\n", f);return 0;
}
我们发现并不能封装成一个函数,因为类型不同,打印的符号也不同。那能不能写一个宏呢?
宏当然可以解决,宏没有类型的限制。宏只负责替换,不管类型的。
(#n == "n" 替换到文本内容里面去,把一个宏参数变成对应的字符串)
//printf("the value of " "n" " is " "%d" "\n", n);
#define PRINT(n,format) printf("the value of "#n" is "format"\n", n);
#include<stdio.h>
int main()
{int a = 10;PRINT(a, "%d");//写一个宏,把需要打印的变量和类型传过去int b = 20;PRINT(b, "%d");float f = 3.14f;PRINT(f, "%f");return 0;
}
//在宏参数面前加# 不是让宏参数完整的替换,而是以字符串的形式替换过去
##
##可以把位于它两边的符号合成一个符号。 它允许宏定义从分离的文本片段创建标识符。
注: 这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。
#define CAT(v,n) v##n
#include<stdio.h>
int main()
{int value10 = 100;printf("%d\n", CAT(value, 10));return 0;
}
带副作用的宏参数
宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
#include<stdio.h>
#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{int a = 3;int b = 5;int m = MAX(a++, b++);printf("%d\n", m);//6printf("%d\n", a);//4printf("%d\n", b);//6return 0;
}
为什么和我们预期的不一样??因为宏的参数是直接替换进去的,不做任何其他处理。
#include<stdio.h>
#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{int a = 3;int b = 5;int m = MAX(a++, b++);//int m=((a++)>(b++)?(a++):(b++));// 3 > 5// 4 6// no 6// 7printf("%d\n", m);//6printf("%d\n", a);//4printf("%d\n", b);//7return 0;
}
宏和函数的对比
宏:通常被应用于执行简单的运算。比如在两个数中找出较大的一个。
#define MAX(a, b) ((a)>(b)?(a):(b))
宏的优势:
- 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。
- 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。宏是类型无关。
- 宏有时候可以做函数做不到的事情。
- 宏的参数可以出现类型,但是函数做不到
- 宏的操作符# 和 ## ,在函数里面是没有的
#define MALLOC(num,type) (type*)malloc(num*sizeof(type))
int main()
{int* p = MALLOC(10, int);////>....return 0;
}
宏的劣势:
- 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
- 宏是没法调试的。
- 宏由于类型无关,也就不够严谨。
- 宏可能会带来运算符优先级的问题,导致程容易出现错。
属性 | #define | 函数 |
代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长 | 函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码 |
执行速度 | 更快 | 存在函数的调用和返回的额外开销,所以相对慢一些 |
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号。 | 函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测。 |
带有副作用的参数 | 参数可能被替换到宏体中多个位置,所以带有副作用参数求值可能会产生不可预料的结果。 | 函数参数只在传参的时候求值一次,结果更容易控制。 |
参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型。 | 函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是不同的 |
调试 | 宏是不方便调试的 | 函数是逐语句逐过程调试的 |
递归 | 宏是不能递归的 | 函数是可以递归的 |
建议:如果逻辑比较简单,可以使用宏来实现。但是如果计算逻辑比较复杂,就使用函数。
C99之后,C++引入了内联函数的概念,后期我们学习了C++,inline内联函数具有函数和宏的双重特点,内联函数是函数,却又像宏一样,在调用的地方展开。
命名约定
一般来讲函数的宏的使用语法很相似。
所以语言本身没法帮我们区分二者。 那我们平时的一个习惯是:
把宏名全部大写 函数名不要全部大写
✔✔✔✔✔最后,感谢大家的阅读,若有错误和不足,欢迎指正!
代码------→【gitee:唐棣棣 (TSQXG) - Gitee.com】
联系------→【邮箱:2784139418@qq.com】