C语言KR圣经笔记 4.3 外部变量

4.3 外部变量

一个C程序由一系列的外部对象组成,这些外部对象不是变量就是函数。“外部”这个形容词用于区别于“内部”,后者描述的是函数参数及其内部定义的变量。外部变量在所有函数之外定义,这样就可能会被很多函数使用。函数本身总是外部的,因为C不允许在函数内部定义函数。默认情况下,外部变量和函数有这样的属性:对同一个名称的所有引用(即使这个引用来自于独立编译的函数)全都指向相同的对象。(标准将这个属性称为外部链接。)在这个意义上来说,C 的外部变量类似于 FORTRAN 的 COMMON 块,或是 Pascal 最外层块里的变量。后面章节我们将会看到如何定义只在单个源文件内可见的外部变量和函数。

由于外部变量是全局可访问的,因此它们能作为函数参数及返回值的替代品,用来在函数之间交流数据。如果某个外部变量的名字以某种方式声明过,则所有函数都能通过引用该外部变量的名称来访问它。

如果有大量的变量必须在函数之间共享,外部变量会比长长的参数列表更加方便且高效。然而,正如第一章中所指出的,这个做法要谨慎使用,因为它对程序结构有害,并且会导致我们写出函数间存在过多数据关联(耦合)的程序。

外部变量的用处还体现在它们有更大的作用域(scope)和生命周期。自动变量是在函数内部的;它们在函数进入时出现,在函数退出时消失。另一方面,外部变量是永久的,所以在从一个函数调用到其他函数时,它们的值仍然保留着。这样的话,若两个函数必须共享一些数据,不管是谁调谁,如果将共享数据保存在外部变量中,总是会比通过参数来传入传出更方便。

我们来通过一个大些的例子来进一步审视下这个问题。现在要写个支持加减乘除操作符( + - * / )的计算器程序。计算器使用逆波兰表示法而不是中缀表示法,因为前者更容易实现。(逆波兰表示法用于某些便携式计算器,并且用在一些计算机语言中,如Forth 和 Postscript。)

在逆波兰表示法中,每个操作符后面跟着它的操作数;中缀表达式如:

(1 - 2) * (4 + 5)

要以下面的方式输入:

1 2 - 4 5 + *

括号是不需要的;只要我们知道每个操作符期望接收多少个操作数,这种表示法就不会产生歧义。

代码实现很简单。每个操作数都被推到一个栈上;当操作符到来时,正确数量的操作数(对二元操作符来说是2个)被出栈,并对它们应用这个操作符,然后再将结果推回到栈上。比如,在上面的例子中,首先推入栈的是 1 和 2,然后栈上内容被替换为它们的差 -1。接着,4 和 5 被推入栈,然后被它们的和 9 替换。 -1 和 9 又被它们的积,即 -9 所替换。当输入行到达末尾时,栈顶的值被出栈并打印出来。

因此,程序的结构就是一个循环,在每个操作符和操作数出现时,进行合适的处理:

while (下一个操作符或操作数不是EOF标识)if (是数)入栈else if (是操作符)对操作数出栈进行操作将结果入栈else if (是换行符)栈顶内容出栈并打印else错误

入栈和出栈操作是简单的,但当加入错误检测和恢复之后,这些代码就会变得很长,所以最好是把每个操作都放在单独的函数中,而不是在程序里到处重复相同的代码。另外还要有个函数,用来获取下一个操作符或操作数。

就还剩一个主要的设计决策没有讨论,就是把栈放在哪里,即哪些例程(函数)能直接访问它。可以设计成把它放在 main 里面,并将栈及其当前位置传给需要入栈和出栈的例程。但 main 不需要知道控制栈的变量;它只是做入栈和出栈操作。所以我们决定不将栈及其关联信息存入main, 而是将它们存入外部变量中,让 push 和 pop 函数能够访问,但 main 不能。

将这个概要设计翻译成代码十分简单。如果现在我们想象整个程序放在一个源文件中,看起来如下:

#include //多个
#define  //多个main里面用到的函数的声明
main() { ... }push 和 poo 用到的外部变量
void push(double f) { ... }
double pop(void) { ... }int getop(char s[]) { ... }被getop调用的例程

后面章节我们会讨论怎么将它拆分到两个或多个源文件中。

这里的 main 函数就是个大循环,里面包含了一个以操作符或操作数类型为分支条件的大switch;这种用法比3.4节所展示的例子更为典型。

#include <stdio.h>
#include <stdlib.h> /* 用了atof() */#define MAXOP 100   /* 操作符或操作数的最大长度 */
#define NUMBER '0'  /* 表示遇到了数字 */int getop(char []);
void push(double);
double pop(void);/* 逆波兰计算器 */
main()
{int type;double op2;char s[MAXOP];while ((type = getop(s)) != EOF) {switch (type) {case NUMBER:push(atof(s));break;case '+':push(pop() + pop());break;case '*':push(pop() * pop());break;case '-':op2 = pop();push(pop() - op2);break;case '/':op2 = pop();if (op2 != 0.0) push(pop() / op2);elseprintf("error: zero divisor\n");break;case '\n':printf("\t%.8g\n", pop());break;default:printf("error: unknown coomand %s\n", s);break;}}return 0;
}

因为 + 和 * 的操作数是可交换的,因此操作数出栈和结合的顺序无关紧要,但对 - 和 / 来说,两个操作数必须区分谁左谁右。若写成

push(pop() - pop());	/* 错误 */

则 pop 两次调用的顺序是未定义的。为了保证顺序正确,有必要将第一个值出栈并保存到临时变量值中,正如main中代码所示。

#define MAXVAL 100	/* 数值栈的最大深度 */int sp = 0;			/* 下个一个空闲的栈位置 */
double val[MAXVAL];	/* 数值栈 *//* push:将f 推入数值栈 */
void push(double f)
{if (sp < MAXVAL)val[sp++] = f;elseprintf("error: stack full, can't push %g\n", f);
}/* pop: 从数值栈的栈顶出栈并返回其值 */
double pop(void)
{if (sp > 0)return val[--sp];else {printf("error: stack empty\n");return 0.0;}
}

如果变量定义在所有函数的外面,则变量是外部的。栈和栈索引必须被 push 和 pop 共享,因此就被定义在这些函数外面。但 main 本身不引用栈或栈索引——它们可以被隐藏。

现在让我们转到 getop 的实现上来,它用于获取下一个操作符或操作数。任务很简单。首先跳过空白字符和制表符。如果下一个字符不是数字或小数点,则将它返回。否则,收集数位字符串(可能包含小数点),并返回 NUMBER,用来标识收集到了一个数。

#include <ctype.h>int getch(void)
void ungetch(int)/* getop: 获取下一个操作符或者数字操作数 */
int getop(char s[])
{int i, c;while ((s[0] = c = getch()) == ' ' || c == '\t');s[1] = '\0';if (!isdigit(c) && c != '.')return c;		/* 非数字 */i = 0;if (isdigit(c))		/* 收集整数部分 */while (isdigit(s[++i] = c = getch()));if (c == '.')		/* 收集小数部分 */while (isdigit(s[++i] = c = getch()));s[i] = '\0';if (c != EOF)ungetch(c);return NUMBER;
}

getch 和 ungetch 是什么?通常情况下,程序无法确定它是否读入了足够的输入,直到它读过头了。以收集一个数所包含的字符为例:除非读到一个非数字字符,否则这个数就是不完整的。但这个时候程序已经多读了一个字符,一个不归他处理的字符。

如果能把不想要的字符 “取消读” ,就能解决这个问题。此后,每当程序多读了一个字符,它就能将该字符推回给输入,因此对后面的代码来说,就像该字符没被读过一样【即后面的代码再次能读到这个字符,而不会丢失】。幸运的是,通过写一对互相协作的函数,很容易模拟对一个字符的“取消读”。getch 负责分发下一个待处理的输入字符; ungetch 记住要推回给输入的字符,这样后续调用 getch 时会先返回这些字符,而不是直接读取新的输入。

让它们协作起来也简单。ungetch 把要推回的字符放到一个共享缓冲区中——一个字符数组。getch 在被调用时,如果缓冲区有内容,就从中读取,如果缓冲区是空的,则 getch 调用 getchar。还必须有个索引变量,记录缓存区中当前字符的位置。

由于缓冲区和索引被 getch 和 ungetch 共享,而且它们的值必须在函数调用间保持,故它们对两个例程而言必须都是外部的。这样我们可以按如下方式来编写 getch,ungetch 和它们的共享变量:

#define BUFSIZE 100char buf[BUFSIZE];	/* ungetch 的缓冲 */
int bufp = 0;		/* buf的下一个空闲位置 */int getch(void)		/* 获取一个(可能是被推回的)字符 */
{return (bufp > 0) ? buf[--bufp] : getchar();
}void ungetch(int c)	/* 推回一个字符给输入 */
{if (bufp >=  BUFSIZE)printf("ungetch: too many characters\n");elsebuf[bufp++] = c;
}

标准库包含了一个支持推回一个字符的函数 ungetc ,我们将在第七章讨论它。我们这里使用一个数组来做推回,是比一个字符更加通用的做法。

练习4-3、给出了基本框架后,扩展这个计算器就很简单了。增加取模(%)操作符以及对负数的支持。

练习4-4、增加打印栈顶元素但不出栈的命令,复制栈顶元素的命令,以及交换栈顶两个元素的命令。增加清除栈的命令。

练习4-5、增加对库函数如 sin,exp 和 pow 的访问。 见附录B第四节的 <math.h>。

练习4-6、增加处理变量的命令。(以单个字母作为名称,可以很容易提供26个变量)。增加最近打印值的变量。

练习4-7、写个例程 ungets(s),将整个字符串推回给输入。ungets应当知道 buf 和 bufp吗,或者它只知道 ungetch ?

练习4-8、假定永远不会推回超过1个字符。据此修改 getch 和 ungetch。

练习4-9、我们的 getch 和 ungetch 不能正确地推回 EOF。如果EOF需要被推回,先确定它们属性应当如何,然后实现你的设计。

练习4-10、还有一种方案是使用 getline 来读取整个输入行,这样就不需要 getch 和 ungetch 了。按这个方式来修订计算器。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/202726.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

解决 video.js ios 播放一会行一会不行

最近用video 进行m3u8视频文件播放&#xff0c;但是途中遇到了 安卓和电脑端都能打开&#xff0c;ios有时可以播放有时播放不了 出现问题原因&#xff1a; ios拿到视频流前需要预加载视频&#xff0c;如果当前视频流还没有打开过&#xff0c;ios拿不到视频流的缓存&#xff0c;…

【AI-Fix】解决地图展示包leafmap在Jupyter NoteBook中地图不显示的问题

1. 问题描述 新创建的环境想使用leafmap在Jupyter中进行地图展示&#xff0c;结果发现运行完成之后不显示&#xff0c;不论怎么重启都没有办法显示出来&#xff0c;以经验来看&#xff0c;多半是缺了包了。 于是去leafmap的官方文档查找原因&#xff0c;一开始并没有发现什么问…

C++ 拷贝构造函数

目录 拷贝构造函数概述 拷贝构造函数特性 拷贝构造函数概述 当我们定义好一个类&#xff0c;不做任何处理时&#xff0c;编译器会自动生成以下6个默认成员函数&#xff1a; 默认成员函数&#xff1a;如果用户没有手动实现&#xff0c;则编译器会自动生成的成员函数。 同样&…

怎么学C++

学习目标&#xff1a; 一篇文章教你怎么学 C 学习内容&#xff1a; 怎么学C 学习时间&#xff1a; 什么时候都行 学习产出&#xff1a; CSDN 技术博客 1 篇 主文 以下是学习C的一些基本步骤&#xff1a; 学习C语言基础&#xff1a;包括数据类型、运算符、控制语句等内容。 学…

JavaWeb(十)

一、JavaWeb概述 Web&#xff1a;全球广域网&#xff0c;也称为万维网(www)&#xff0c;能够通过浏览器访问的网站。 JavaWeb&#xff1a;使用 Java技术进行web互联网开发。 二、JavaWeb 技术栈 2.1、B/S 架构 B/S 架构&#xff1a;Browser/Server&#xff0c;浏览器/服务器…

解决找不到msvcr120.dll无法执行代码的4个方法,快来看看解决方法!

在计算机使用过程中&#xff0c;我们经常会遇到一些错误提示&#xff0c;其中最常见的就是“缺少xxx.dll文件”。而msvcr120.dll就是其中之一。那么&#xff0c;msvcr120.dll到底是什么呢&#xff1f;它又有什么作用呢&#xff1f;本文将从多个方面对msvcr120.dll进行详细的解析…

华为鸿蒙爆发真实力!原生应用媲美iOS,使用流畅度将提升20至30%

随着华为鸿蒙原生应用开发计划的启动&#xff0c;一场席卷全球的科技浪潮正在涌动。鸿蒙生态的快速发展&#xff0c;吸引了无数企业和开发者的关注&#xff0c;他们纷纷拥抱这个新兴的生态系统&#xff0c;共同构建一个更加繁荣的鸿蒙世界。 华为鸿蒙原生应用开发计划引爆全球…

Java中的并发编程:深入理解CountDownLatch

Java中的并发编程&#xff1a;深入理解CountDownLatch 本文将深入探讨Java中的并发编程&#xff0c;重点关注CountDownLatch的使用。通过理解这些概念和技术&#xff0c;我们可以编写出更高效、稳定的Java程序。 一、CountDownLatch简介 CountDownLatch是Java中的一个同步工具…

编程常见的问题

在现代社会中&#xff0c;编程已经成为一项非常重要的技能。随着科技的不断发展和普及&#xff0c;计算机已经渗透到我们生活的方方面面&#xff0c;从个人电脑、手机到智能家居、自动驾驶等。编程作为计算机科学的基础&#xff0c;为我们提供了解决问题和创造新事物的工具和方…

【电路笔记】-交流电路中的电阻器

交流电路中的电阻器 文章目录 交流电路中的电阻器1、概述2、交流电路中的电阻器示例 13、交流电路中的电阻器示例2 电阻器也可用于交流电源&#xff0c;其中消耗的电压、电流和功率以有效值给出。 1、概述 在之前的文章中&#xff0c;我们研究了电阻器及其连接&#xff0c;并使…

Leetcode刷题笔记题解(C++):BM11 链表相加(二)

思路&#xff1a;先对两个链表进行反转&#xff0c;反转求和注意进位运算&#xff0c;求和完成之后再进行反转得到结果 /*** struct ListNode {* int val;* struct ListNode *next;* ListNode(int x) : val(x), next(nullptr) {}* };*/ #include <cstddef> class Soluti…

Excel 删除空白行

目录 一. 方式一: 筛选删除二. 方式二: 定位条件三. 方式三: 隐藏非空白行&#xff0c;删除空白行 一. 方式一: 筛选删除 选中空白行对应的列&#xff0c;按下Ctrl Shift L&#xff0c;给列添加过滤条件。过滤出空白行&#xff0c;然后删除即可。 二. 方式二: 定位条件 按下…

【Qt】QLineEdit显示输入十六进制,位数不足时按照规则填充显示及每两个字符以空格填充

问题 在实际开发中&#xff0c;有时候需要对输入进行限制&#xff0c;一是更加合理&#xff0c;二是防止出现误操作。 比如&#xff1a; 使用Qt进行应用程序开发时&#xff0c;对单行编辑框QLineEdit控件&#xff0c;设置只可输入十六进制。 限制输入的方式常用且经典的是使用…

Linux常用指令详解

目录 前言&#xff1a; Linux的目录结构 Linux常用指令简介 whoami指令 ls指令 pwd指令 cd指令 tree指令 touch指令 mkdir指令 rmdir指令与rm指令 man指令 cp&#xff08;copy&#xff09;指令 mv&#xff08;move&#xff09;指令 cat指令 重定向及重定向的类型…

Redis——某马点评day03——part2:秒杀业务异步优化

异步秒杀思路 原本的流程是如下所示&#xff0c;必须从开始到创建订单成功才会返回响应。就像饭店里面从下单到上菜都是一个人在服务&#xff0c;就导致服务员利用率很低&#xff0c;后一个顾客要等到前一个顾客上完菜才可以下单。 最简单的优化就是加员工&#xff0c;一次性…

前端实现手机短信验证码倒计时效果

实现效果&#xff1a;实现按钮倒计时10s后可重新发送验证码 一、思路 1、禁用按钮&#xff0c;调用后端接口&#xff0c;获取验证码 2、setTimeOut(() > {},1000)延迟1s执行&#xff0c;time - 1&#xff0c;返回文案&#xff0c;9s 3、迭代处理&#xff0c;调用自身函数&a…

6.1810: Operating System Engineering 2023 <Lab3: page tables>

一、本节任务 实验环境&#xff1a; 二、要点 如何防止程序破坏内核或其他进程空间&#xff1f;隔离地址空间&#xff0c;进程只能读写自己的内存空间。 在保证隔离的同时&#xff0c;如何将多个地址空间复用到一个物理内存上&#xff1f;虚拟内存/页表。操作系统通过页表来为…

DDSP-SVC-3.0完全指南:一步步教你用AI声音开启音乐之旅

本教程教你怎么使用工具训练数据集推理出你想要转换的声音音频&#xff0c;并且教你处理剪辑伴奏和训练后的音频合并一起&#xff0c;快来试试看把&#xff01; 1.使用的工具 要想训练ai声音&#xff0c;首先需要有各种工具&#xff0c;还需要我们提供你需要训练的声音&#…

Avalonia中如何将View事件映射到ViewModel层

前言 前面的文章里面我们有介绍在Wpf中如何在View层将事件映射到ViewModel层的文章&#xff0c;传送门&#xff0c;既然WPF和Avalonia是两套不同的前端框架&#xff0c;那么WPF里面实现模式肯定在这边就用不了&#xff0c;本篇我们将分享一下如何在Avalonia前端框架下面将事件…

陀螺仪LSM6DSV16X与AI集成(2)----姿态解算

陀螺仪LSM6DSV16X与AI集成.2--姿态解算 概述视频教学样品申请完整代码下载欧拉角万向节死锁四元数法姿态解算双环PI控制器偏航角陀螺仪解析代码上位机通讯加速度演示陀螺仪工作方式主程序演示 概述 LSM6DSV16X包含三轴陀螺仪与三轴加速度计。 姿态有多种数学表示方式&#xff…