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

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

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️⃣用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。

所以宏比函数在程序的规模和速度方面更胜一筹 。

📚怎么理解呢:

从函数返回

78e67896072b40c3b185f29f74d9241b.png


📃函数调用的时间花费:

1.函数调用前准备( 传参、函数栈帧空间的维护)

2.主要运算

3.函数返回,返回值的处理,函数栈帧的销毁

涉及到函数栈帧的内容,传送门👉: http://t.csdnimg.cn/DtDhX

使用宏定义

69a3585729aa46cbafc35fba5ccfce9a.png


📃宏定义的时间花费:

2.主要运算(写成宏就把1,3步骤省略掉了) 不用建立函数栈帧,也就没有它的销毁

2️⃣更为重要的是函数的参数必须声明为特定的类型。


       所以函数 只能在类型合适的表达式 上使用。反之 这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型 。 宏是类型无关的

       如何理解:


c4f49963a4e241ce923757fa5514553a.png


由上面的两个原因,求两个数的较大值这个例子中,宏更有优势一些,使用宏定义可以省掉不必要去损耗的时间,那么宏是不是比函数更有优势呢?

宏的缺点: 当然和函数相比宏也有劣势的地方

1️⃣每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。

90364e93db3649948d95c37350785618.png


2️⃣宏是没法调试的

31a3fa05893a40cb9b146c88b4313779.png



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);
}


       以后功能比较简单的时候,可以采用宏来实现如果功能比较复杂,建议使用函数来实现


313c80a654f646609f3eee610dee6018.png


宏和函数的一个对比


属性

#define定义宏 函数
代码长度 每次使用时,宏代码都会被插入到程序中。除了非常 小的宏之外,程序的长度会大幅度增长 函数代码只出现于一个地方;每 次使用这个函数时,都调用那个地方的同一份代码
执行速度 更快 存在函数的调用和返回的额外开 销,所以相对慢一些
操作 符优 先级 宏参数的求值是在所有周围表达式的上下文环境里, 除非加上括号,否则邻近操作符的优先级可能会产生 不可预料的后果,所以建议宏在书写的时候多些括 号。 函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测。
带 有 副 作 用 的 参 数 参数可能被替换到宏体中的多个位置,所以带有副作 用的参数求值可能会产生不可预料的结果 函数参数只在传参的时候求值一 次,结果更容易控制。
参 数 类 型 宏的参数与类型无关,只要对参数的操作是合法的, 它就可以使用于任何参数类型. 函数的参数是与类型有关的,如 果参数的类型不同,就需要不同 的函数,即使他们执行的任务是 相同的。
调 试 宏是不方便调试的 函数是可以逐语句调试的
递 归 宏是不能递归的 函数是可以递归的


3.2.7 命名约定

一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。


那我们平时的一个习惯是:


1.把宏名全部大写
2.函数名不要全部大写


3.3 #undef

这条指令用于移除一个宏定义


#undef NAME //如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除

演示:

abd1426a6c4c465f909186f07b69b856.png



3.4 命令行定义

1.许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
例如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处。(假定某个 程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大些,我们需要一个数组能够大些。)


演示代码:



aa441f09674f4ddca35811f2256fb146.png

按ctrl+~键,看下图:按照下图先按住①再按②


058befec11da46849e1e20b673281e55.png


把终端调出来


bf5f3515c78c4f1c8b4ee15a1010a793.png


指定SZ(宏的大小)为10,即数组大小为10,那么依次打印1~10



337ca4e85a6d4693a3a29dc8f504426e.png

指定SZ(宏的大小)为100,即数组大小为100,那么依次打印1~100


c3462a2f30034e4a9b1eb2f1a8e8148a.png


编译指令:


//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;
}



727e72d09a434b35a4911caea61df1d8.png

常见的条件编译指令:


1.常量表达式

1.
#if 常量表达式
 //...
#endif
//常量表达式由预处理器求值
如:
#define __DEBUG__ 1
#if __DEBUG__
 //..
#endif

7d0a161a498149a6805c686b4d86e676.png


注意:预处理期间,其实处理都是文本呀,代码处理的过程中,编译指令是有的,不需要编译的,就把它删了,需要后面编译的代码会留着。


所以右图中int a=2;不需要删除的原因在这里。


b3838f01d2c644e4b76fb41d30d353f0.png

2.多个分支的条件编译

2.多个分支的条件编译
#if 常量表达式
 //...
#elif 常量表达式
 //...
#else
 //...
#endif

该编译放到这个代码里头,不该编译就删掉了




c828b524b2fc4157b45680cbc05c0f8d.png

76254608dbea4ae38671e8b56cf20a2e.png

104e38e2cb1d4cd1ab9194f8e105ec08.png



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)


2b016023f88246fdac8619aa3b413c26.png


#ifdef MAX


834c16d3933a41f0936a0b3feb9f0bdf.png


把宏注释掉,用ifdef


349308fe77214ab0a83426943f241844.png


同理可得!define和#ifndef:


#define


先来看没有用#define定义的时候,define(MAX)条件判断为假,!define(MAX)判断为真。


4cdc9c9a8a2f4ba29e9cc2c46d6274f5.png


下面是已经定义的情况


ddeb525708df44c38c4d3e778b5fa2f0.png


#ifndef


下面是 "#ifndef" 指令的基本语法:


#ifndef 宏名称
    // 如果宏名称未定义,则执行的代码
#endif

       如果名为 "宏名称" 的宏未定义,那么在预处理阶段将包含 "#ifndef" 块中的代码。如果该宏已定义,则会跳过块中的代码。


06250bc8d09d4af2bfe775bee0227394.png


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"


查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标 准位置查找头文件。 如果找不到就提示编译错误。

路径:自己工程当前的目录查找



5c4fad9d2475417aa23f2f76a355996e.png

Linux 环境的标准头文件的路径:

/ usr / include

VS 环境的标准头文件的路径:

C : \Program Files ( x86 ) \Microsoft Visual Studio 12.0 \VC\include
// 这是 VS2013 的默认路径

注意按照自己的安装路径去找。


db8d670259514845b59583331ebf46cd.png

库文件包含

#include <filename.h>


查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。


这样是不是可以说,对于库文件也可以使用 “” 的形式包含?


53b400c2502b457dbfbf91e1fd58e699.png


答案是肯定的, 可以。

但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。

3.6.2 嵌套文件包含


5abffd3e86cf49d999edd2fc7d7f8afc.png

comm.h和comm.c是公共模块。
test1.h和test1.c使用了公共模块。
test2.h和test2.c使用了公共模块。
test.h和test.c使用了test1模块和test2模块。
这样最终程序中就会出现两份comm.h的内容。这样就造成了文件内容的重复


如何解决这个问题? 答案:条件编译。


51f9ac7c101d48abbf04d81b0ac94714.png

解决思路:


①使用#ifndef条件编译


#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif   //__TEST_H__

7e88f9c12cf2427b8ee9ffc264b0fab8.png


②使用pragma once防止头文件被反复多次的包含


#pragma once


784517eb54f14cc89a6931008629f2c2.png

vscode编译器:



a6a9a77fcfe94635b30a1e66f0e5f8c0.png

以上①②两者写法均可防止文件重复包含。


注: 推荐《高质量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语言深度解剖》学习



相关文章
|
自然语言处理 监控 搜索推荐
CAP 快速部署项目体验评测
在体验过程中,我选择了 RAG 模板,整体部署较为顺畅,CAP 平台的一键部署功能简化了配置步骤。但也遇到了环境依赖、模型加载速度和网络配置等挑战。性能测试显示响应速度较快,高并发时表现稳定。二次开发使用 Flask 和 Vue,调试顺利,功能正常运行。建议 CAP 增加 NLP、推荐系统、IoT 应用和开源项目集成等模板,以提升模板库的丰富度。
|
9月前
|
人工智能 安全 搜索推荐
阿里云采购季:短信服务低至 0.01 元/条!
阿里云“上云采购季”,短信服务低至 0.01 元/条
373 3
|
人工智能 关系型数据库 MySQL
一键实现穿衣自由,揭秘淘宝AI试衣间硬核技术,AnalyticDB向量在线召回
云原生数据仓库AnalyticDB MySQL为淘宝AI试衣间提供高维向量低延时的在线向量召回检索服务。
一键实现穿衣自由,揭秘淘宝AI试衣间硬核技术,AnalyticDB向量在线召回
|
12月前
|
域名解析 负载均衡 安全
DNS技术标准趋势和安全研究
本文探讨了互联网域名基础设施的结构性安全风险,由清华大学段教授团队多年研究总结。文章指出,DNS系统的安全性不仅受代码实现影响,更源于其设计、实现、运营及治理中的固有缺陷。主要风险包括协议设计缺陷(如明文传输)、生态演进隐患(如单点故障增加)和薄弱的信任关系(如威胁情报被操纵)。团队通过多项研究揭示了这些深层次问题,并呼吁构建更加可信的DNS基础设施,以保障全球互联网的安全稳定运行。
百万级高并发mongodb集群性能数十倍提升优化实践(上篇)
本文是oppo互联网某百亿级数据量/百万级高并发mongodb集群线上真实优化案例,荣获mongodb中文社区2019年度一等奖。
百万级高并发mongodb集群性能数十倍提升优化实践(上篇)
|
负载均衡 Java Maven
微服务技术系列教程(23) - SpringCloud- 声明式服务调用Feign
微服务技术系列教程(23) - SpringCloud- 声明式服务调用Feign
228 0
|
存储 缓存 定位技术
如果遇到网络延迟问题,有哪些方法可以快速解决以保证视频源同步?
如果遇到网络延迟问题,有哪些方法可以快速解决以保证视频源同步?
|
消息中间件 JavaScript Java
消息队列 MQ产品使用合集之如何嵌入到Spring Boot中运行
消息队列(MQ)是一种用于异步通信和解耦的应用程序间消息传递的服务,广泛应用于分布式系统中。针对不同的MQ产品,如阿里云的RocketMQ、RabbitMQ等,它们在实现上述场景时可能会有不同的特性和优势,比如RocketMQ强调高吞吐量、低延迟和高可用性,适合大规模分布式系统;而RabbitMQ则以其灵活的路由规则和丰富的协议支持受到青睐。下面是一些常见的消息队列MQ产品的使用场景合集,这些场景涵盖了多种行业和业务需求。
remount of the / superblock failed: Permission denied remount failed
remount of the / superblock failed: Permission denied remount failed
358 0
|
数据挖掘 Serverless Python
在Python中计算标准差
在Python中计算标准差
1506 3