3. 预处理详解
3.2.6 宏和函数对比
下面两种方式求两个数的较大值,谁优,谁劣?
#include<stdio.h> //函数的实现 int Max(int x, int y) { return x > y ? x : y; } //宏的实现 #define MAX(x,y) ((x)>(y)?(x):(y)) int main() { int a = 0; int b = 0; //输入 scanf("%d %d",&a,&b); //1.函数返回较大值 int m1 = Max(a, b); printf("%d\n",m1); //2.使用宏 int m2 = MAX(a, b);//等价于 ((a)>(b)?(a):(b)); printf("%d\n",m2); return 0; }
宏通常被应用于执行简单的运算
比如在两个数中找出较大的一个
#define MAX(a, b) ((a)>(b)?(a):(b))
那为什么不用函数来完成这个任务?
原因有二:
1️⃣用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。
所以宏比函数在程序的规模和速度方面更胜一筹 。
📚怎么理解呢:
从函数返回
📃函数调用的时间花费:
1.函数调用前准备( 传参、函数栈帧空间的维护)
2.主要运算
3.函数返回,返回值的处理,函数栈帧的销毁
涉及到函数栈帧的内容,传送门👉: http://t.csdnimg.cn/DtDhX
使用宏定义
📃宏定义的时间花费:
2.主要运算(写成宏就把1,3步骤省略掉了) 不用建立函数栈帧,也就没有它的销毁
2️⃣更为重要的是函数的参数必须声明为特定的类型。
所以函数 只能在类型合适的表达式 上使用。反之 这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型 。 宏是类型无关的
如何理解:
由上面的两个原因,求两个数的较大值这个例子中,宏更有优势一些,使用宏定义可以省掉不必要去损耗的时间,那么宏是不是比函数更有优势呢?
宏的缺点: 当然和函数相比宏也有劣势的地方
1️⃣每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
2️⃣宏是没法调试的
3️⃣ 宏由于类型无关,也就不够严谨
只要能够参与运算,那么传入任何参数都能适用 ,这是一把双刃剑。
4️⃣宏可能会带来运算符优先级的问题,导致程容易出现错
宏的劣势:当参数里面有表达式的时候,表达式传参传到宏的体内的时候,宏体内如果有相邻的操作符,这时候操作符优先级可能引起一些问题,导致程序错误。
函数不会有这个问题,即使传入一个表达式,也会把它的值算出一个结果,再传进去.
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。(因为类型是不可能作为参数给函数传参的,函数传参传的是变量、数组、指针等)
例子:
#define MALLOC(num,type) (type*)malloc(num*sizeof(type)) int main() { //函数传参 int* p = (int*)mallloc(10 * sizeof(int)); if (p == NULL) { perror("malloc fail!"); return; } //宏传参 int* p2 = MALLOC(10, int);//类型作为参数,传参方便多了 if (p2 == NULL) { perror("malloc fail!"); return; } MALLOC(10,float); }
以后功能比较简单的时候,可以采用宏来实现如果功能比较复杂,建议使用函数来实现
宏和函数的一个对比
属性 |
#define定义宏 | 函数 |
代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非常 小的宏之外,程序的长度会大幅度增长 | 函数代码只出现于一个地方;每 次使用这个函数时,都调用那个地方的同一份代码 |
执行速度 | 更快 | 存在函数的调用和返回的额外开 销,所以相对慢一些 |
操作 符优 先级 | 宏参数的求值是在所有周围表达式的上下文环境里, 除非加上括号,否则邻近操作符的优先级可能会产生 不可预料的后果,所以建议宏在书写的时候多些括 号。 | 函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测。 |
带 有 副 作 用 的 参 数 | 参数可能被替换到宏体中的多个位置,所以带有副作 用的参数求值可能会产生不可预料的结果 | 函数参数只在传参的时候求值一 次,结果更容易控制。 |
参 数 类 型 | 宏的参数与类型无关,只要对参数的操作是合法的, 它就可以使用于任何参数类型. | 函数的参数是与类型有关的,如 果参数的类型不同,就需要不同 的函数,即使他们执行的任务是 相同的。 |
调 试 | 宏是不方便调试的 | 函数是可以逐语句调试的 |
递 归 | 宏是不能递归的 | 函数是可以递归的 |
3.2.7 命名约定
一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。
那我们平时的一个习惯是:
1.把宏名全部大写 2.函数名不要全部大写
3.3 #undef
这条指令用于移除一个宏定义
#undef NAME //如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除
演示:
3.4 命令行定义
1.许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。 例如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处。(假定某个 程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大些,我们需要一个数组能够大些。)
演示代码:
按ctrl+~键,看下图:按照下图先按住①再按②
把终端调出来
指定SZ(宏的大小)为10,即数组大小为10,那么依次打印1~10
指定SZ(宏的大小)为100,即数组大小为100,那么依次打印1~100
编译指令:
//linux 环境演示 gcc -D ARRAY_SIZE=10 programe.c
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; }
常见的条件编译指令:
1.常量表达式
1. #if 常量表达式 //... #endif //常量表达式由预处理器求值 如: #define __DEBUG__ 1 #if __DEBUG__ //.. #endif
注意:预处理期间,其实处理都是文本呀,代码处理的过程中,编译指令是有的,不需要编译的,就把它删了,需要后面编译的代码会留着。
所以右图中int a=2;不需要删除的原因在这里。
2.多个分支的条件编译
2.多个分支的条件编译 #if 常量表达式 //... #elif 常量表达式 //... #else //... #endif
该编译放到这个代码里头,不该编译就删掉了
3.判断是否被定义
if defined和ifdef是相同的,都是用于检查某个标识符是否已经定义的预处理指令
它们在C和C++中是等效的。
使用 ifdef 或 if defined 可以根据某个标识符是否已经定义来进行条件编译。如果标识符已经通过 #define 或其他方式定义过,则执行 ifdef 或 if defined 后面的代码块;否则,忽略该代码块。
#define DEBUG_MODE #ifdef DEBUG_MODE // 调试模式下的代码 printf("执行调试代码\n"); // ... #endif
在上述示例中,#define DEBUG_MODE 定义了一个名为 DEBUG_MODE 的宏。在 #ifdef DEBUG_MODE 的代码块中,可以放置调试模式下需要执行的代码。如果 DEBUG_MODE 宏已经被定义,那么代码块中的代码将会被执行;否则,代码块将被忽略。
请注意,ifdef 和 if defined 仅用于在编译时进行条件判断,而不是在运行时。它们用于根据不同的编译配置或条件选择性地包含或排除代码块,从而实现更灵活的程序控制。
图解:
if defined(MAX)
#ifdef MAX
把宏注释掉,用ifdef
同理可得!define和#ifndef:
#define
先来看没有用#define定义的时候,define(MAX)条件判断为假,!define(MAX)判断为真。
下面是已经定义的情况
#ifndef
下面是 "#ifndef" 指令的基本语法:
#ifndef 宏名称 // 如果宏名称未定义,则执行的代码 #endif
如果名为 "宏名称" 的宏未定义,那么在预处理阶段将包含 "#ifndef" 块中的代码。如果该宏已定义,则会跳过块中的代码。
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
注意:上面条件编译只要有if,那么都用#endif来结束。
3.6 文件包含
我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样。
这种替换的方式很简单:
预处理器先删除这条指令,并用包含文件的内容替换。
这样一个源文件被包含10 次,那就实际被编译 10 次。
3.6.1 头文件被包含的方式:
本地文件包含
#include "filename"
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标 准位置查找头文件。 如果找不到就提示编译错误。
路径:自己工程当前的目录查找
Linux 环境的标准头文件的路径:
/ usr / include
VS 环境的标准头文件的路径:
C : \Program Files ( x86 ) \Microsoft Visual Studio 12.0 \VC\include // 这是 VS2013 的默认路径
注意按照自己的安装路径去找。
库文件包含
#include <filename.h>
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
这样是不是可以说,对于库文件也可以使用 “” 的形式包含?
答案是肯定的, 可以。
但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。
3.6.2 嵌套文件包含
comm.h和comm.c是公共模块。 test1.h和test1.c使用了公共模块。 test2.h和test2.c使用了公共模块。 test.h和test.c使用了test1模块和test2模块。 这样最终程序中就会出现两份comm.h的内容。这样就造成了文件内容的重复
如何解决这个问题? 答案:条件编译。
解决思路:
①使用#ifndef条件编译
#ifndef __TEST_H__ #define __TEST_H__ //头文件的内容 #endif //__TEST_H__
②使用pragma once防止头文件被反复多次的包含
#pragma once
vscode编译器:
以上①②两者写法均可防止文件重复包含。
注: 推荐《高质量C/C++编程指南》中附录的考试试卷(很重要)。
笔试题:
1. 头文件中的 ifndef/define/endif是干什么用的?
头文件中的ifndef/define/endif是用于防止头文件被重复包含,以避免编译错误。ifndef用于判断某个标识符是否已经被定义,如果未被定义,则继续执行define指令,定义该标识符,并执行后续的代码;如果已经被定义,则跳过后续的代码,直接执行endif指令。这样可以确保头文件只被包含一次。
2. #include 和 #include "filename.h"有什么区别?
#include <filename.h>是用于包含系统头文件,编译器会先在系统目录中查找该头文件;而#include "filename.h"是用于包含用户自定义的头文件,编译器会先在当前目录中查找该头文件,如果未找到,则会在系统目录中查找。
4. 其他预处理指令
#error #pragma #line ... 不做介绍,自己去了解。 #pragma pack()在结构体部分介绍。
参考《C语言深度解剖》学习