《计算机是如何工作的:人人都能懂的计算机软硬件工作原理》第 9 章 机器码与汇编语言 阅读笔记 9

《计算机是如何工作的:人人都能懂的计算机软硬件工作原理》第 9 章 机器码与汇编语言 阅读笔记 9

第 8 章研究了软件的基础:在处理器上运行的机器码,以及汇编语言(一种人类可读的机器码表示)​。

尽管所有的软件最终都必须转换成机器码的形式,但绝大多数软件开发人员都用更高、更抽象的语言编写程序。

本章将介绍高级编程。我们将概述高级编程,讨论各种编程语言中的常见元素,并研究几个示例程序。

9.1 高级编程概述

虽然可以用汇编语言(甚至可以用机器码)编写软件,但这既费时又容易出错,并且会使软件难以维护。

此外,汇编语言是特定于 CPU 架构的,所以,如果汇编语言开发人员想要在其他类型的 CPU 上运行其程序,就必须重新编写代码。

为了克服这些缺点,人们开发了高级编程语言,这就允许程序用独立于特定 CPU 的语言来编写,并且它们在语法上更接近人类语言。

许多语言都需要编译器------一种把高级程序语句转换为特定处理器机器码的程序。

使用高级编程语言,软件开发人员可以编写程序一次,然后针对不同类型的处理器进行编译,有时只需对源代码进行少量修改,甚至不用修改。

编译器的输出是一个目标文件,它包含了针对特定处理器的机器码。

另一种被称为链接器的程序用于把一个或多个目标文件转换成一个可执行文件,这样操作系统就可以运行这个可执行文件了。

链接器还可以在需要的时候引入其他已编译代码库。编译和链接过程如图 9-1 所示。

图 9-1 从源代码构建可执行文件

编译和链接的过程被称为打包。

但是,在常见的用法中,软件开发人员在说到编译代码时,其实际意思是指包含编译、链接和任何其他把代码转换成最终形式所需步骤的完整过程。

编译器通常会自动调用链接步骤,使其对软件开发人员的可见度降低。

9.2 C 和 Python 简介

学习高级编程最好的方法是研究编程语言并用这些语言编写一些程序。本章选择两种高级语言:C 和 Python。

这两种语言都很强大,也很有用,下面用它们来演示编程语言如何用不同的方式来提供类似的功能。我们先对它们进行简要介绍。

C 编程语言可追溯到 20 世纪 70 年代早期,当时它被用于编写 UNIX 操作系统的一个版本。

尽管 C 是一种高级语言,但它与底层机器码离得并不远,这使得它成为操作系统开发或其他直接与硬件交互的软件开发的绝佳选择。

20 世纪 80 年代出现了 C 的升级版,称为 C++。C 和 C++ 是功能强大的语言,几乎可以用来完成所有事情。

然而,这些语言很复杂且没有提供很多防止程序员错误的保护措施。对于需要与硬件交互的程序以及那些要求高性能的程序(比如游戏)来说,它们仍然是主流的选择。

C 对于教育目的也很有用,它提供了底层概念与高级概念之间的直接映射,这就是本章选择它的原因。

与 C 相比,Python 编程语言离底层硬件更远。Python 最早是在 20 世纪 90 年代发布的,历经多年越来越受欢迎。

它以易读且对初学者友好而闻名,同时它还能提供支持复杂软件设计所需的一切。

Python 有 "自带电池" 的理念,意思是 Python 的标准发行版包含了有用的功能库,开发人员能轻松地将其用于程序设计中。

Python 的直接属性使其成为教授编程概念的上佳选择。

现在,我们来看看大多数高级编程语言中都有的元素。我们的目标不是把你教成特定语言的程序员,而是让你熟悉编程语言中常见的思想。

请记住,高级编程语言中的功能是 CPU 指令的抽象。如你所知,CPU 提供访问内存、逻辑运算和控制程序流的指令。让我们来看看高级语言如何揭开这些底层功能。

9.3 注释

我们先介绍编程语言的一个特性,这个特性实际上并不会指示 CPU 做事情。几乎所有的编程语言都支持在代码中添加注释。

注释是源代码中对代码解读的文本。

注释是给其他开发人员看的,通常会被编译器忽略,它们对编译的软件没有任何影响。

在 C 编程语言中添加注释的方式如下所示:

Python 用 # 表示注释,如下所示:

Python 不支持多行注释,程序员只需一行接一行地使用多个单行注释即可。

9.4 变量

内存访问是处理器的基本功能,因此它也是高级语言的必备功能。

编程语言访问内存的最基本方式是通过变量。

变量是一个确定的内存地址。

变量允许程序员给内存地址(或内存地址范围)指定一个 "名字"​,然后通过这个名字访问对应地址的数据。

在大多数编程语言中,变量都有 "类型"​,以说明它们所保存数据的种类。

例如,一个变量可能是整数类型或文本字符串类型的。

变量也有一个 "值"​,它是存储在内存中数据。

变量还有一个 "地址"​,尽管这通常对程序员而言是隐藏的,这个地址是变量值在内存中的存储位置。

最后,变量有 "作用域"​,意思是只有在程序的某些部分(即作用域)才能访问它们。

9.4.1 C 中的变量

我们来看一个 C 编程语言变量的例子:

这段代码声明了一个变量,变量名为 points,类型是 int,在 C 语言中这表示该变量保存整数。

该变量被分配了一个值,即 27。

当运行代码时,十进制数 27 被保存到某个内存地址,不过开发人员不用操心存储这个变量的具体地址。

当前,大多数 C 编译器把 int 视为32位的数,因此在运行时(程序执行的时间)为这个变量分配 4 个字节(4B×8bit/B=32bit),

而变量的内存地址指的就是第一个字节的地址。

现在声明第二个变量并为其赋值,然后看看这两个变量是如何分配内存的。

c 复制代码
// Two variables in C
int points = 27;
int year = 2020;

我们先后声明了两个变量:points 和 year。

它们都是整数,所以每个变量都需要 4 个字节的存储空间。变量在内存中的存储方式如表 9-1 所示。

表 9-1 变量在内存中的存储方式

表 9-1 中使用的内存地址只是例子,实际地址随硬件、操作系统、编译器等不同而不同。

注意地址增幅为 4,因为存储的是 4 字节整数。

已知变量前后的地址都用问号表示变量名和变量值,因为根据前面的代码,我们并不知道存储在那里的是什么。

如同 "变量" 这个名字所暗示的,变量的值是可以变化的。

在前面的 C 程序中,如果需要把 points 设置为其他值,我们只需简单地在后面的程序中这样写:

c 复制代码
// Setting a new points value in C
points = 31;

请注意,与前面的 C 代码片段不同,这行代码没有在变量名的前面指定 int 类型或任何其他类型。

我们只需要在最初声明变量时指定类型即可。

在这个例子中,变量已经在前面声明过了,所以这里只需给它赋值即可。

不过,C语言要求变量类型保持不变,因此,一旦 points 被声明为 int,那么就只能为它赋整数值。

如果试图赋其他类型的值,比如文本字符串,就会在代码编译时出错。

9.4.2 Python中的变量

并不是所有的语言都要求声明类型。例如,Python 就可以像下面这样声明并赋值变量:

python 复制代码
age = 22

在这个例子中,Python 可识别出数据类型为整数,因此程序员不用指定类型。

与 C 语言不同,变量类型可以随着时间变化,所以下列语句在 Python 中是有效的:

python 复制代码
age = 22
age = 'twenty-two'

让我们仔细看看在这个例子中实际发生了什么。

Python 变量没有类型,但赋给它的值是有类型的。

这是一个重要的区别:类型与值相关联,而不是与变量相关联。

Python 变量可以引用任何类型的值。

当变量被分配新值时,并不是变量的类型真的发生了变化,而是变量绑定了不同类型的值。

C 语言则与此相反,C 语言变量自身有类型,且只能保存该类型的值。

这个差异解释了为什么 Python 中的变量可以被赋予不同类型的值,而 C 中的变量则不可以。

9.5 栈和堆

当程序员使用高级语言访问内存时,后台管理内存的细节是模糊的,具体取决于使用的编程语言。

像 Python 这样的编程语言会让程序员几乎看不到内存分配的细节,而像 C 这样的语言则会暴露一些底层的内存管理机制。

不管这些细节是否暴露给程序员,程序通常使用两种类型的内存:栈和堆。

9.5.1 栈

栈 (stack) 是内存的一个区域,其运行模型为后进先出 (Last-In First-Out,LIFO)。

也就是说,最后入栈的项是最先出栈的。

你可以把栈想象成一叠盘子。

当你往这一叠盘子上放新盘子时,新的盘子是放在顶部的。

当要从这个栈上取盘子时,最先拿掉的是顶部的盘子。

这并不是说栈中的内容只能按 LIFO 顺序访问(读或修改)​。

实际上,当前栈中的任何项在任何时候都可以被读取和修改。

但是,当从栈中移除不需要的项时,它们会以从上到下的顺序被丢弃,也就是说,最后入栈的项会最先被删除。

保存栈顶值的内存地址存储在 "栈指针" 处理器寄存器中。

当向栈顶添加一个值时,会调整栈指针的值以增加栈的大小,为新值腾出空间。

当从栈顶移除一个值时,也会调整栈指针的值以减小栈的大小。

编译器生成的代码利用栈来跟踪程序执行的状态,以及保存局部变量。

这个机制对高级语言程序员是透明的。

图 9-2 展示了 C 程序是如何使用栈来保存之前在表 9-1 中介绍的两个局部变量的。

图 9-2 栈用于保存 C 语言程序中的变量值

在图 9-2 中,首先声明 points 变量,并为其赋值 27,这个值保存在栈中。

接下来声明了 year 变量,并为其赋值 2020。

在栈中,第二个值位于前一个值 "之上"​。其他值将继续添加到栈顶,直到不再需要它们,此时会将它们从栈中移除。

请记住,图中的每个长条只表示内存中的一个已经分配了内存地址的位置,虽然图中没有显示这些地址。

你可能会惊讶地发现,在许多架构中,分配给栈的内存地址实际上会随着栈增长而减少。

在本例中,这就意味着 year 变量的地址低于 points 变量的地址。

栈的访问速度很快,非常适合用于有限范围的小容量内存分配。

独立的栈可供程序的每个执行线程使用。

我们将在第 10 章详细地讨论线程,但现在你可以把线程看作程序中的并行任务。

栈是有限资源,分配给栈的内存是有限的。在栈中存放过多的值会导致被称为 "栈溢出" 的故障。

9.5.2 堆

栈的作用是存放只需临时使用的小值。

对于较大的或需要持续较长时间的内存分配,更适合使用堆 (heap)。

堆是程序可用的内存池。

和栈不同,堆不采用 LIFO 模型,如何分配堆内存并没有标准模型。

栈是特定于线程的,而程序的任何线程都可以访问堆。

程序从堆中分配内存,并且这个内存的使用会一直持续,直到它被程序释放或者程序结束。

释放内存分配的意思就是把它释放回可用内存池。

当分配的内存不再被引用时,有些编程语言会自动释放堆内存,执行这个操作的一种常见方法被称为 "垃圾回收"​。

其他编程语言(比如 C 语言)则需要程序员编写代码来释放堆内存。当程序不再使用某块动态分配的内存,却没有释放它,就会发生内存泄漏。

在 C 编程语言中,用一种被称为指针的特殊变量来跟踪内存分配。

指针就是一种保存内存地址的变量。

指针值(内存地址)可以存储在栈内的局部变量中,这个值可以指向堆中的位置,如图 9-3 所示。

图 9-3 名为 data 的指针变量在栈内,它指向堆中的一个地址

图 9-3 中的代码片段声明了一个名为 data 的变量。这个变量的类型是 void*,意思是它是一个指向内存地址的指针(用*表示)​,

这个内存地址可以保存任何类型的数据(void 的含义是未指定类型)​。

由于 data 是局部变量,因此它在栈上分配。

下一行代码调用了 malloc,它是 C 语言中的一个函数,用于从堆中分配内存。

程序正在申请 512B 的内存,malloc 函数返回新分配内存中第一个字节的地址。

该地址存储在栈内的 data 变量中。我们最终得到了一个局部变量,它在栈内的某个地址处,这个变量保存的是从堆中分配的地址。

9.6 算术运算

由于处理器提供了执行算术运算的指令,因此高级语言也提供。

汇编语言需要特定命名的指令(例如 ARM 处理器上用于减法运算的 subs 指令)进行算术运算,

不同于汇编语言,高级语言通常包含表示常见算术运算的符号,因此在代码中执行算术运算就变得简单了。

包括 C 和 Python 在内的大量编程语言使用相同的运算符进行加减乘除运算,如表 9-2 所示。

表 9-2 常用算术运算符

运算 运算符
加 +
减 -
乘 *
除 /

跨多种编程语言的另一个常见约定是用等于符号 (=) 表示赋值而不是表示相等。

也就是说,形如 x=5 的语句表示把 x 的值设置为 5。

因此,可以用自然的方式为算术运算的结果变量赋值,就如下列语句一样:

c 复制代码
cost = price + tax;
python 复制代码
cost = price + tax

到目前为止,我们的重点在于整数运算,这在计算中很常见。

但是,计算机和高级语言还支持所谓的浮点运算。

与整数不同,浮点值可以表示小数。

有些编程语言隐藏了这方面的细节,但是在内部,CPU 对浮点运算和整数运算采用了不同的指令。

在 C 语言中,浮点变量用浮点类型(比如 float 和 double)声明,如下所示:

c 复制代码
double price = 1.99;

Python 可以推断变量的类型,所以整数和浮点值都以同样的方式进行声明:

python 复制代码
year = 2020
price = 1.99

整数与浮点数之间的差异有时会导致意想不到的结果。举个例子,假设有如下 C 代码:

c 复制代码
int x = 5;
int y = 2;
int z = x / y;

你预期的 z 值是多少?事实证明,由于涉及的所有数都是整数,因此 z 的最终值是2,不是 2.5。

作为整数,z 不能保存小数数值。

现在,我们稍微修改一下代码,如下所示:

c 复制代码
int x = 5;
int y = 2;
float z = x / y;

注意,z 现在的类型是 float。你现在预期的 z 值是多少?

有意思的是,z 现在等于 2.0,仍然不是 2.5!这是因为除法运算发生在两个整数之间,所以其结果也是整数。

除法运算结果是 2,当它被赋值给浮点变量 z 时,其值为 2.0。

C 语言是很确切的,它被编译成与程序员所述非常相似的指令。

这对于那些想要精确控制任务处理的程序员来说非常棒,但对于那些 希望从其编程语言中 获得更直观行为的 程序员来说 就不总是那么好了。

Python 试图变得更有用:自动分配类型以允许在这种情况下获得小数结果。

如果我们用 Python 编写与之等效的代码,z 中保存的结果将为 2.5。

python 复制代码
x = 5;
y = 2;
z = x / y;

有些语言提供算术运算符,用缩写方式表示运算。例如,C 语言提供了递增(加 1)和递减(减 1)运算符,如下所示:

c 复制代码
x = x + 1;
x++;
x--;

注意

有趣的是,编程语言 C++ 的名字意在传递一种想法,即它是对 C 编程语言的改进。

Python 还为算术运算提供一些快捷运算符。+= 和 -= 运算符允许程序员对变量进行加减运算,例如:

python 复制代码
cats = cats + 3;
# 等同于
cats += 3;

+= 和 -= 运算符在 C 语言中也可以使用。

9.7 逻辑运算

如前所述,处理器非常善于执行逻辑运算,因为逻辑是数字电路的基础。

编程语言也提供了逻辑处理功能。

绝大多数高级语言提供两种类型的运算符来处理逻辑运算:按位运算符和布尔运算符。

前者处理整数的位,后者处理布尔(真/假)值。

这里的术语可能会令人困惑,因为不同的编程语言使用不同的术语。

Python 用 "按位" 和 "布尔" 描述,C 用 "按位" 和 "逻辑" 描述,

其他语言还使用其他术语。这里我们继续使用 "按位" 和 "布尔" 等说法。

9.7.1 按位运算符

按位运算符作用于整数值的每个位上,其结果为整数值。

按位运算符类似于算术运算符,但并不执行加法或减法运算,而是对整数的位执行 AND、OR,以及其他逻辑运算。

这些运算符按照第 2 章介绍的真值表工作,并行地对整数的所有位进行运算。

表 9-3 编程语言中常用的按位运算符

按位运算 按位运算符
AND &
OR |
XOR ^
NOT ~

许多编程语言(包括 C 和 Python)都使用表 9-3 所示的运算符进行按位运算。

让我们看看 Python 中的按位运算:

python 复制代码
x = 5
y = 3
a = x & y
b = x | y

上面代码的结果是 a 为 1,b 为 7。我们来看二进制中的这些运算(见图 9-4)​,弄清楚为什么是这样的。

图 9-4 对 5 和 3 进行 AND 和 OR 按位运算

首先来看图 9-4 中的 AND 按位运算,回忆一下第 2 章的内容,AND 意味着当两个输入都是 1 时,结果为 1。

这里一次查看一列的位。如你所见,对于两个输入来说,只有最右边的位都为 1,所以 AND 的结果是二进制的 0001 或十进制的 1。

因此,前面的代码为 a 赋值 1。

对于 OR 运算,只要有一个输入(或两个输入都)为 1,结果就为 1。

在本例中,两个输入右边的 3 列位中每一列都至少有一位为 1,所以结果是二进制的 0111 或十进制的 7。因此,前面的代码为 b 赋值 7。

9.7.2 布尔运算符

高级编程语言中的另一种逻辑运算符是布尔运算符。布尔运算符处理布尔值,其结果也是布尔值。

我们先花点时间谈谈布尔值。布尔值指 "真" 和 "假"​。不同的编程语言用不同的方式表示 "真" 和 "假"​。

布尔变量是一个确定的内存地址,其中保存了一个布尔值(​"真" 或 "假"​)​。

例如,在 Python 中我们可以用布尔变量来跟踪某商品是否在销售:item_on_sale=True。

表达式可以计算为 "真" 或 "假"​,且其结果不用存储到变量中。

例如,根据 item_cost 变量的值,表达式 item_cost > 5 在运行时可以计算为 "真" 或 "假"​。

布尔运算符允许对布尔值执行逻辑运算,比如 AND、OR 和 NOT。

例如,可以用 Python 的 AND 布尔运算符来检查两个条件是否都为真:item_on_sale AND item_cost > 5。

AND 左右两边的表达式的计算结果都是布尔值,所以整个表达式的计算结果也是布尔值。

这里,C 和 Python 使用不同的运算符,如表 9-4 所示。

表 9-4 C 和 Python 编程语言中的布尔运算符

运算 C 语言 Python
与 && and
或 `
非 ! not
短路求值 &&、`
优先级(高→低) ! > && > `
操作数判定 0 为假,非 0 为真 False、None、0、空序列/空映射为假,其余为真

当我们讨论返回布尔值的运算符时,不能遗漏比较运算符。

比较运算符用来比较两个值并把比较结果评估为 "真" 或 "假"​。

例如,> 运算符比较两个数并确定一个是否大于另一个。

表 9-5 展示了在 C 和 Python 中都使用的比较运算符。

比较含义 C 语言 Python 说明
等于 == == C 中不能写成 =,= 是赋值
不等于 != != Python 旧写法 <> 已不推荐
大于 > > ---
小于 < < ---
大于等于 >= >= ---
小于等于 <= <= ---
身份/地址比较 无专门语法 is、is not Python 中 is 比较是否是同一对象,类似比较地址
成员比较 无 in、not in Python 专用,C 没有对应运算符

在前面的例子 item_cost > 5 中,你已经看到了其中一个运算符的用法。

注意,对于等于运算符,C 和 Python 都使用双等号,因为单等号表示赋值。

这就意味着 x==5 是一个比较运算,需返回 "真" 或 "假"​,而 x=5 是一个赋值操作,表示把 x 的值设置为 5。

9.8 程序流

布尔运算符和比较运算符用于计算表达式是否为真,但这本身并不是很有用,因为我们需要的是一种回应方式!

程序流或控制流语句可以改变程序的行为以响应某些条件。让我们来看一些跨编程语言的常见程序流结构。

9.8.1 if 语句

if 语句(通常与 else 语句一起使用)允许程序员在条件为真时执行一些操作。

反过来,else 语句允许程序在条件为假时执行另一些不同的操作。下面是一个 Python 示例:

本例中,if 语句 ❶ 检查 age 变量引用的值是否小于 18。

如果是,它会输出一条消息,指示用户非常年轻 ❷。

如果 age 为 18 或更大的值 ❸,那么 else 语句告诉系统输出另一条消息 ❹。

下面是同一 "检查年纪" 的逻辑,这次用 C 语言编写:

在 C 语言例子中,请注意 if 语句后面使用的花括号 ❶❷。这些括号标记了响应 if 应执行的代码块。

在 C 语言中,代码块可以由多行代码构成,不过当代码块由单行组成时可以忽略花括号。

Python 不使用花括号来分隔代码块,它使用缩进格式表示。

在 Python 中,具有相同缩进级别(例如缩进 4 个空格)的连续行被视为同一个代码块的组成部分。

Python 还包含 elif 语句,其含义为 "else if"​。仅当某 elif 语句前面的 if 或 elif 语句为假时,才处理该 elif 语句。

同样的功能在 C 语言中可用 else 加上 if 来实现:

注意,由于这里的代码块都是单行的,所以省略了花括号。

9.8.2 循环

有些时候,程序需要反复执行某个操作。

while 循环允许代码重复运行,直到满足某个条件。

在下面的 Python 例子中,while 循环被用于输出从 1 到 20 的数字:

初始时,变量 n 被设置为 1。

当 n 小于或等于 20 时才开始运行 while 循环。

由于 n 初始为 1,符合要求,因此 while 循环运行,输出 n 的值,然后将 n 加 1。

现在,n 等于 2,代码回到 while 循环的顶部。

这个过程一直持续到 n 等于 21,此时它不再满足 while 循环的要求,所以循环结束。

下面是用 C 语言实现的相同功能:

在这两个例子中,while 循环的循环体使 n 的值递增。

实际上,还有一种更简洁的方式可以实现这一功能。

for 循环允许对某个范围内的数字或一组值进行迭代,以便程序员对其中每个数字执行一些操作。

这里给出一个 C 语言的例子:输出数字 1 到 10。

for 循环声明 x 并将其初始值设置为 1 ❶,

表明当 x 小于或等于 10 时循环将继续 ❷,

最后声明 x 在循环体运行后递增 ❸。

通过把这些信息全部放入单行的 for 语句中,我们可以轻松地查看循环运行的条件。

for 循环的循环体只输出 x 的值 ❹。

Python 的 for 循环则采用不同的方法,它允许程序对一组值中的每一个重复执行操作。下面的 Python 示例输出列表中的动物名:

首先,声明一个动物名称列表变量,并命名为 animal_list。

在 Python 中,列表是一个有序的值集合。

接着,在 for 循环中,对 animal_list 中的每一项,代码块都要运行一次,且每次代码运行时,列表中的当前值都被赋给 animal 变量。

因此,第一次运行循环体时,animal 等于 cat,程序输出 cat。接下来依次输出 dog 和 mouse。

9.9 函数

循环允许一组指令连续运行多次。但是,还有一种情况也很常见,即程序多次运行一组特定指令,且不一定是循环。

这样的指令可能需要从程序的不同部分、在不同的时间、用不同的输入和输出来调用。

当程序员意识到同一代码需要出现在多个地方时,他们可能会把这些代码编写成函数。

函数是一组程序指令,它可以被其他代码调用。函数可选地接收输入(称为 "参数"​)并返回输出(称为 "返回值"​)​。

不同的高级语言使用不同术语称呼函数,例如子例程、过程或方法。

在某些情况下,这些不同的名称实际上传递了略有差异的含义,但在这里,我们坚持用术语 "函数"​。

把字符串转换成小写、将文本输出到屏幕、从互联网下载文件都是可以以函数的形式使用可重用代码执行的例子。

程序员希望避免多次输入相同的代码,因为多次输入相同的代码意味着维护了相同代码的多个副本,且增加了程序总的代码量。

这违反了 "不重复自己" (Don't Repeat Yourself DRY) 的软件工程原则,该原则鼓励减少重复性代码。

函数是封装的另一个例子。我们在之前的硬件部分看到过封装,这里我们再次看到它,这次是软件封装。

函数封装了代码块的内部细节,并提供了使用该代码的接口。

想要使用函数的开发人员只需了解其输入和输出,无须完全了解该函数的内部工作方式。

9.9.1 定义函数

函数在使用前必须先定义。一旦定义了函数,你就可以通过调用来使用它。

​"函数定义" 包括了函数名、输入参数、函数的程序语句(称为函数体)​,在某些语言中还有返回值类型。

这里有一个 C 语言函数的例子,即给定半径,计算圆的面积的函数:

开始的 double 类型 ❶ 表示函数返回一个浮点数(double 是 C 语言中的一种浮点类型)​。

函数名为 areaOfCircle ❷,旨在传递函数的功能------在本例中为计算圆的面积。

函数接收一个名为 radius 的输入参数❸,其类型也是 double。

位于开始和结束花括号之间的是函数体,它准确定义了函数的功能。

我们声明了一个名为 area 的局部变量,它的类型也是 double。

函数用 π×radius2 计算面积,并把结果赋给 area 变量。

最后,函数返回 area 变量的值 ❹。

注意,area 变量的作用域是有限的,它不能在函数外被访问。

当函数返回时,局部变量 area 被丢弃(它可能存储在栈中)​,但它的值可能通过处理器寄存器被返回给调用者。

下面是一个类似的面积函数,只不过这次是用 Python 编写的。

让我们比较一下这两个函数。两个函数都用 π×radius2 计算面积,然后返回结果值。

两个函数也都接收了一个名为 radius 的输入参数。C 语言版本显式定义返回类型为 double,radius 的类型也是 double,

而 Python 语言版本没有要求声明类型。Python 中函数定义以 def 关键字开始。

9.9.2 调用函数

虽然在程序中定义了函数,但函数不一定会被运行。函数定义只是让代码在需要时可以供其他代码调用。这种调用被称为 "函数调用"​。

调用代码传递所有需要的参数,并把控制权转给函数。

然后,函数执行其代码并把控制权(以及所有输出)返还给调用者。

下面演示一下如何在 C 语言中调用示例函数:

这是在 Python 中的调用:

当函数返回时,由调用代码把返回值存储在某个地方。

两个例子都声明用变量 area1 和 area2 保存函数调用的返回值。

在两种语言中,area1 是 12.56,area2 是 4678.4744。

实际上,调用代码可以忽略返回值,不将它们赋给变量,但考虑到本函数的目的,这不是十分有用。

图 9-5 演示了调用函数时是如何把控制权暂时移交给被调用函数的。

图 9-5 调用一个函数

在图 9-5 中,左边的 Python 代码调用 area_of_circle 函数,向它传递输入参数 radius 的 值2.0。

然后,左边的代码进入等待状态,直到右边的函数完成其工作。

一旦右边的函数返回,左边的代码就把返回值存储到变量 area1 中,然后继续执行。

9.9.3 使用库

尽管程序员确实可以自定义函数,但编程的一个重要部分是如何充分利用其他人已经编写好的函数。

编程语言通常包括大量函数,它们称为该语言的 "标准库"​。

这里,​"库" 是指给其他软件使用的代码集合。

C 和 Python 都有标准库,以提供函数完成诸如输出到控制台、处理文件和处理文本等功能。

Python 的标准库使用特别广且备受推崇。

虽然并非总是如此,但一种语言的绝大多数实现都涵盖了该语言的标准库,所以程序员可以依赖这些函数。

除了标准库,其他函数库也可以供编程语言使用。

开发人员编写库给其他人使用,并以源代码或编译文件的形式进行共享。

这些库有时候是非正式共享的,某些编程语言用众所周知且被接受的机制来发布库。

一组共享的库称为包,共享这种包的系统称为包管理器。

C 语言有几种可用的包管理器,但没有一个是被 C 程序员广泛接受成为标准的。

Python 包含的包管理器称为 pip。pip 使得为 Python 安装社区开发软件库变得容易,它常常被 Python 开发人员使用。

相关推荐
秋饼1 小时前
Spring AI 2.0 结构化输出企业级实战:从 BeanOutputConverter 源码到生产级解析容错
java·ai·技术分享·后端开发
Cc.Y1 小时前
Java零基础入门:深入Java核心:文件IO与序列化全解
java·开发语言·python
xier_ran1 小时前
【infra之路】AI 编译器学习笔记(六):重拾 C++——lambda 与 MLIR 中的回调
c++·笔记·学习
拿本唠嗑AI研究1 小时前
本体工程实战指南:从建模到可维护的完整落地路径
java·人工智能·python·jena·ontology
志尊宝1 小时前
Vue3 零基础每日笔记(078):首屏加载速度优化汇总——清单化方案
笔记
lambo mercy2 小时前
Day3:结构化输出
java·数据库·oracle
程序员清风2 小时前
最短路径算法:BFS、Dijkstra 与 Floyd
java·算法·宽度优先
jimy13 小时前
c++的 Type Traits (一)
开发语言·c++
智能直播3 小时前
RK3528 V4L2 框架 UVC 接口自动获取 H.264 / MJPEG / YUYV 格式:从内核采集到硬解分发
java·网络·实时音视频·视频编解码·h.264