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



相关文章
|
程序员 API 数据库
【Cmake工程 库相关教程 】深入理解CMake工程C/C++ 库管理技巧
【Cmake工程 库相关教程 】深入理解CMake工程C/C++ 库管理技巧
508 1
|
人工智能 机器人 人机交互
哥大华人开发人脸机器人,照镜子自主模仿人类表情超逼真
【4月更文挑战第3天】哥伦比亚大学研究人员开发了一款名为Emo的机器人,能观察并模仿人类面部表情,实现更自然的人机交互。Emo配备26个面部执行器和高分辨率摄像头,通过“自我建模”学习模仿表情,并能预测人类表情变化。这一创新有望改善人机理解和响应情绪的能力,应用于教育、医疗等领域,但也引发了关于情感依赖和伦理问题的讨论。
712 4
哥大华人开发人脸机器人,照镜子自主模仿人类表情超逼真
|
8月前
|
存储 机器学习/深度学习 弹性计算
阿里云4核8G服务器ecs.c9i.xlarge实例规格族详细介绍:CPU、网络、存储及费用整理
阿里云ECS计算型c9i实例ecs.c9i.xlarge,4核8G配置,搭载Intel Xeon Granite Rapids处理器,主频3.2GHz,全核睿频3.6GHz,网络带宽最高15Gbps,云盘IOPS最高20万,支持可信计算。适用于机器学习推理、数据分析、高性能计算等场景,价格约0.77元/小时,371.82元/月,年付享7.1折优惠。
561 4
|
9月前
|
人工智能 数据可视化 安全
WhatsApp Business API 怎么用?阿里云 Chat App 常见问题(持续更新)
全面解答阿里云 Chat App 消息服务高频问题,涵盖 WhatsApp Business 账号注册、模板审核、API 集成、多通道发送、计费规则、AI 机器人配置、安全合规等。出海企业必备指南,助您高效触达全球 30 亿用户。
1583 0
|
10月前
|
供应链 JavaScript 前端开发
诊所云平台源码,综合门诊部云his系统源码,诊所his系统
诊所云平台源码,自主版权,支持二次开发与合作交付。B/S架构,云端SaaS部署,兼容C/S应用。集成挂号、门诊、收费、药房、会员及医保对接等全功能模块,助力诊所数字化管理。
330 0
|
存储 人工智能 小程序
微信掌上医院是如何实现的?智慧掌上医院核心实现原理及关键技术解析
微信掌上医院(智慧医院)小程序源码,基于uni-app与Vue.js开发,集成预约挂号、在线支付、报告查询等功能,支持与医院HIS、医保系统对接,采用微服务架构与多重安全机制。
673 2
|
缓存 NoSQL 数据库
什么是缓存击穿
缓存击穿是指热点缓存key突然失效,导致大量并发请求直接冲击数据库,造成巨大压力。常见于高并发场景,如热门商品信息失效时。解决方法包括设置热点key永不过期、使用分布式锁、预热数据、熔断降级等,以保障系统稳定性。
1093 0
|
Java 数据库
MapStruct详细解析
总的来说,MapStruct是一个强大的对象映射工具,它可以大大提高开发效率,减少错误,值得在项目中使用。
493 17
|
安全 搜索推荐 PHP
一款轻量级的PHP链接发布页面源码
一款轻量级的PHP链接发布页面源码
398 6
|
Python
.repo/repo/main.py“, line 79 file=sys.stderr) SyntaxError: invalid syntax
.repo/repo/main.py“, line 79 file=sys.stderr) SyntaxError: invalid syntax
720 0