【C进阶】C程序是怎么运作的呢?-- 程序环境和预处理(下)

news/2025/2/16 1:03:32/
前言:
这是程序环境和预处理的下半篇文章。至此,关于c语言知识点:从编译到运行的过程已讲解完毕。传送🚪,上半篇: http://t.csdnimg.cn/hvxmr
本章涉及的知识点: 宏和函数对比、命名约定、#undef、命令行定义、条件编译、文件包含以及其他预处理指令。

目录

3. 预处理详解

3.2.6 宏和函数对比

3.2.7 命名约定

3.3 #undef

3.4 命令行定义

3.5 条件编译

1.常量表达式 

2.多个分支的条件编译

3.判断是否被定义

4.嵌套指令

 3.6 文件包含

3.6.1 头文件被包含的方式:

3.6.2 嵌套文件包含

4. 其他预处理指令


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语言深度解剖》学习

本章完。


http://www.ppmy.cn/news/1267432.html

相关文章

亚马逊云科技:向量数据存储在生成式人工智能应用程序中的作用

生成式人工智能深受大众喜爱&#xff0c;并且由于具备回答问题、写故事、创作艺术品甚至生成代码的功能&#xff0c;推动了行业的转变&#xff0c;那么如何才能在自己的企业中充分地利用生成式人工智能等应运而生问题。许多客户已经积累了大量特定领域的数据&#xff08;财务记…

OkHttp: 拦截器和事件监听器

文章目录 1. 拦截器1. 拦截器链2. 实际案例1. 注册为应用拦截器2. 注册为网络拦截器 3. 如何选择用哪种拦截器1. 应用拦截器2. 网络层拦截器3. 重写请求4. 重写响应 4. 可用性 2. 事件监听器1. 请求的生命周期2. EventListener使用案例3. EventListener.Factory4. 调用失败的请…

高通平台开发系列讲解(USB篇)MBIM协议详解

文章目录 一、MBIM协议二、MBIM 消息类型三、基本控制消息构成3.1、MBIM OPEN MSG FORMAT3.2、MBIM CLOSE MSG FORMAT3.3、MBIM_COMMAND_MSG3.4、MBIM_COMMAND_DONE3.5、MBIM_INDICATE_STATUS_MSG四、MBIM Message(UUID+CID)4.1、UUID_BASIC_CONNECT

【启扬方案】启扬储能管理平板助力储能电站实现智能且高效化运行

在储能领域&#xff0c;储能电站扮演着重要角色&#xff0c;储能电站技术的应用贯穿于电力系统发电、输电、配电、用电的各个环节。实现电力系统削峰填谷、可再生能源发电波动平滑与跟踪计划处理、高效系统调频&#xff0c;增加供电的可靠性。 但随着储能电⼒系统建设发展得越来…

C语言—每日选择题—Day45

第一题 1. 以下选项中&#xff0c;对基本类型相同的指针变量不能进行运算的运算符是&#xff08;&#xff09; A&#xff1a; B&#xff1a;- C&#xff1a; D&#xff1a; 答案及解析 A A&#xff1a;错误&#xff0c;指针不可以相加&#xff0c;因为指针相加可能发生越界&…

Oracle(2-17) RMAN Maintenance

文章目录 一、基础知识1、Retention Policy 保留政策2、Recovery Window - Part 1 恢复窗口-第1部分3、Cross Checking 交叉检查4、The CROSSCHECK Command CROSSCHECK命令5、OBSOLETE VS EXPIRED 过时与过期6、Deleting Backups and Copies 删除备份和副本7、The DELETE Comma…

【Flink名称解释一】什么是cataLog

Catalog 提供了元数据信息&#xff0c;例如数据库、表、分区、视图以及数据库或其他外部系统中存储的函数和信息。 数据处理最关键的方面之一是管理元数据。 元数据可以是临时的&#xff0c;例如临时表、或者通过 TableEnvironment 注册的 UDF。 元数据也可以是持久化的&#x…

玻色量子袁为出席中国移动第四届科技周量子计算算法与应用分论坛

9月12日&#xff0c;中国移动第四届科技周“量子计算算法与应用”分论坛在北京成功举办&#xff0c;中国移动研究院院长黄宇红发表致辞&#xff0c;中国移动未来研究院院长崔春风全程主持。玻色量子作为光量子计算领域真机测试与场景应用的标杆企业应邀出席&#xff0c;玻色量子…