这是程序环境和预处理的下半篇文章。至此,关于c语言知识点:从编译到运行的过程已讲解完毕。传送🚪,上半篇: http://t.csdnimg.cn/hvxmr本章涉及的知识点: 宏和函数对比、命名约定、#undef、命令行定义、条件编译、文件包含以及其他预处理指令。
目录
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.函数调用前准备( 传参、函数栈帧空间的维护)2.主要运算
3.函数返回,返回值的处理,函数栈帧的销毁涉及到函数栈帧的内容,传送门👉: http://t.csdnimg.cn/DtDhX
使用宏定义
📃宏定义的时间花费:
2.主要运算(写成宏就把1,3步骤省略掉了) 不用建立函数栈帧,也就没有它的销毁
2️⃣更为重要的是函数的参数必须声明为特定的类型。
如何理解:
2️⃣宏是没法调试的
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。(因为类型是不可能作为参数给函数传参的,函数传参传的是变量、数组、指针等)
例子:
#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);
}
![](https://i-blog.csdnimg.cn/blog_migrate/9e246075108374e2188f42d1efbeb8b2.png)
属性 | #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 OPTION1unix_version_option1 ();#endif#ifdef OPTION2unix_version_option2 ();#endif#elif defined(OS_MSDOS)#ifdef OPTION2msdos_version_option2 ();#endif#endif
3.6 文件包含
3.6.1 头文件被包含的方式:
-
本地文件包含
#include "filename"
/ usr / include
C : \Program Files ( x86 ) \Microsoft Visual Studio 12.0 \VC\include// 这是 VS2013 的默认路径
![](https://i-blog.csdnimg.cn/blog_migrate/e59532f8fe4a4dde34cc6ec5f14bc97a.png)
- 库文件包含
#include <filename.h>
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
![](https://i-blog.csdnimg.cn/blog_migrate/b852a5975301bc352ebea396f5340efd.png)
3.6.2 嵌套文件包含
comm.h和comm.c是公共模块。
test1.h和test1.c使用了公共模块。
test2.h和test2.c使用了公共模块。
test.h和test.c使用了test1模块和test2模块。
这样最终程序中就会出现两份comm.h的内容。这样就造成了文件内容的重复
如何解决这个问题? 答案:条件编译。
![](https://i-blog.csdnimg.cn/blog_migrate/914e70192c8823f3ae9f73eb47bf85f0.png)
解决思路:
①使用#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语言深度解剖》学习
本章完。