UCSC-C-语言编程笔记-全-
UCSC C 语言编程笔记(全)
001:课程概述

概述
在本节课中,我们将要学习这门C语言入门课程的核心目标、适用人群以及选择C语言作为教学语言的原因。课程由加州大学圣克鲁兹分校工程学院的伊恩·波尔教授设计,旨在为所有人提供编程基础。
为什么学习编程?
编程已成为一项通用技能。编程应被纳入课程体系,与阅读、写作和算术并列。在新世界中,我们需要理解如何正确地表达能在计算机上运行的算法。这是一项至关重要的技能。
为什么选择这门课程?
这门课程专为所有人设计。无论你是计算机科学初学者、其他领域的科学家,还是人文学者,都能从中受益。
以下是不同背景学习者的收获:
- 计算机科学初学者:将获得关于语法、运算符优先级等概念的严谨介绍,为学习更高级的编程打下基础。
- 其他领域科学家:将能够编写各种函数来建模科学理论,这对从事现代科学研究至关重要。
- 人文学者:将学习算法的工作原理,并能够完成诸如让计算机创作诗歌等任务。
课程将教授循环编写等核心技能,同时允许学习者在适合其背景和兴趣的问题解决环境中进行实践。
为什么选择C语言?
市面上有许多针对Java、Python等语言的入门课程。这些语言在许多方面非常适合教学。从其他语言开始学习或转学过来并无不妥。
然而,即使你已经接触过Python或Java,学习C语言也大有裨益。C语言由丹尼斯·里奇于1972年在贝尔实验室发明,是一种久经考验、广泛应用于工业界的语言,被称为系统实现语言。
通过本课程学习C语言,你将了解底层细节。许多其他语言(如Java或Python)隐藏了部分细节,这在一定程度上降低了学习难度。但如果你想深入学习,或更深入地理解计算机的运行机制,就需要掌握像C这样贴近硬件的语言,它能提供更深层次的理解。
课程内容与形式
如果你决定继续学习这门课程,你将接触到以下内容:
- 详细的视频讲座:教授将演示C语言的原理和操作。
- 教材节选:来自教授与阿尔·凯利合著的C语言书籍。
- 定期测验:用于检验你的理解程度。
- 同行评审的编程作业:你可以在此展示新技能,并观摩其他学习者的成果。
此外,教授鼓励大家积极参与讨论论坛,期待看到所有学习者,无论经验丰富与否,都能用C语言创造出精彩内容。
总结

本节课我们一起学习了这门C语言课程的设计初衷、广泛适用性以及选择C语言教学的优势。课程旨在通过严谨的教学和丰富的实践,帮助不同背景的学习者掌握编程核心思想与C语言基础,为后续深入学习或应用打下坚实基础。
002:C语言的历史



在本节课中,我们将要学习C编程语言的起源和发展历程,了解它为何能成为现代计算机科学中如此重要的基石。
C语言的诞生
C语言由丹尼斯·里奇发明。他于1972年创造了这门语言。
C语言的目的
发明C语言的主要目的是为了编写一个操作系统。这个操作系统后来变得非常著名,至今我们仍在使用它,它就是Unix。
开发背景
在丹尼斯·里奇与肯·汤普森共同进行的研究中,Unix系统被开发出来。当时,这项研究是在贝尔实验室进行的。完成后,它被作为一个免费产品发布。
Unix系统的优势与影响
以下是Unix系统在当时具备的几个关键优势:
- 它允许人们在小型计算机上构建一个高效的分时系统。
- 它使得多用户能够同时使用一台小型计算机。
- 它拥有许多优点,并且正好赶上了小型计算机时代的开端。
对于那些拥有像IBM或Burroughs生产的、价值数百万美元的大型计算机的用户来说,Unix通常不是他们的选择。相反,Unix是为当时正在发展的、由惠普和数字设备公司等厂商制造的新型计算机而设计的。
C语言与Unix的深远影响
后来,这个系统变得非常重要,因为它也能在小型个人计算机上运行。事实上,它是Linux操作系统的基础,并且是Macintosh操作系统底层的核心机制。
这是一个非常重要的事实,其重要性体现在丹尼斯·里奇和肯·汤普森都因此获得了计算机科学领域的最高奖项——图灵奖。
本节课中,我们一起学习了C语言的发明者、发明时间及其最初目的——为Unix操作系统而生。我们了解了Unix系统的开发背景、其免费发布的特性,以及它在小型计算机和多用户环境中的优势。最后,我们看到了C语言和Unix系统如何深刻地影响了后来的Linux和macOS,并因此获得了计算机界的最高荣誉。理解这段历史,有助于我们认识C语言为何能成为一门强大且持久的编程语言。
003:编译、调试和运行程序 - 第一部分

概述
在本节课中,我们将要学习进行C语言编程所需的基本工具,包括编译器、编辑器以及编写、编译和运行一个简单C程序的基本流程。
工具介绍
学习C语言编程,无论你使用什么操作系统,都需要一些基本工具。
编译器
首先,你需要一个C编译器。一个广泛可用且可以安装在大多数计算机系统上的免费编译器是GNU编译器,其命令是 gcc。
编辑器
其次,你需要一个编辑器。一个更简单的纯文本编辑器更好。当然,你也可以使用像Microsoft Word这样的软件,但前提是你必须以纯文本格式保存文件。Microsoft Word是一个智能编辑器,包含各种特殊字符,而C编译器无法识别这些字符,这会导致编译错误。
不同系统的工具选择
在不同的操作系统上,工具选择有所不同。
- 通用选择:GNU编译器(gcc)是一个跨平台的优秀选择。
- Windows系统:你可以直接从微软获取非常好的工具,其中最好的工具可能是某个版本的Visual Studio。
课程前提与建议
本课程不会详细介绍不同编译器的细微差别或安装步骤。这些步骤是必需的,但在许多情况下,安装过程并不复杂,通常只需要获取安装包并按照说明操作即可。如果你对此不熟悉或感到不适,应该寻求帮助。
因此,本课程的预设前提是:你已经安装好了一个C编译器和一个编辑器。
演示环境说明
在整个课程中,你将看到我使用我的家用Mac电脑进行演示。我将使用Mac的终端窗口。为什么使用终端窗口?因为它是一个非常简单的编程环境,非常基础,本质上是Unix操作系统。你们中的一些人可能通过学校或自己的设备拥有Unix操作系统。有些Unix系统可以安装在Windows下,而Macintosh系统则已经预装了。
编辑与编译流程
在我们的演示环境中,我们将使用 vi 编辑器来编写代码或数据文件,并使用GNU编译器命令 gcc 来编译我们的代码。
编写代码的基本流程如下:
- 构思与手写:我总是建议,在为一个特定问题或你感兴趣的事情编写代码时,先用纸和笔写下代码。这让你有时间思考,甚至可以进行我们所谓的“手工模拟”。你不仅要写下来,还要确保它在视觉上看起来像你认为的样子,像一个语法正确的C程序。
- 手工检查:然后逐步检查它,至少是一个简化版本,看看它是否给出了你期望的结果。你也可以从一个你将修改的示例程序开始。
以下是一个非常简单的示例程序:
int main(void) {
return 0;
}
- 使用编辑器编写:在纸上构思好后,我打开编辑器(例如使用命令
vi sample.c)来编写代码。通常,在为C编译器编写代码时,我会使用后缀.c,这向编译器表明这是C代码,并且当它存放在文件夹或目录中时,我可以识别出它是C源代码。 - 编译与调试:接下来,我将使用
gcc编译器来确保程序按预期工作。我们会检查是否有任何语法错误。修复所有语法错误后,我们需要重新编译。修复过程需要再次使用编辑器,然后不断运行程序。如果我们得到错误的答案,就回到编辑步骤:修复,重新编译,运行。 - 生成可执行文件:最终,我们应该得到我们喜欢的、正确的行为。然后,在GNU编译器中,你会看到一个输出文件自动出现,这就是你的运行时可执行代码,你可以将其视为机器代码。默认情况下,这个文件是
a.out。使用a.out作为可执行文件的问题是,每当你编译一个正确的C程序时,a.out文件都会被替换。
为了避免这个问题,你可以通过以下两种方式之一来处理:
- 将其重命名为一个可识别的名称,例如
sample.exe。 - 从一开始就使用GNU命令的
-o选项来指定输出文件名。命令格式为:gcc -o sample sample.c。这样,输出就会保存在sample这个文件中。

总结
本节课我们一起学习了进行C语言编程所需的核心工具:编译器和编辑器。我们了解了在不同操作系统下的工具选择,并概述了从构思、手写代码到使用编辑器编写、使用编译器编译和调试,最终生成可执行文件的完整流程。记住,从一个简单的程序开始,并耐心地编译、运行和调试,是学习编程的有效方法。在下一节中,我们将实际演示在我的Macintosh环境中完成所有这些步骤。
004:包含文件

概述
在本节课中,我们将学习C语言编程中一个完整的开发周期:编辑、编译和运行程序。我们将通过一个具体的例子,演示如何修改现有代码、处理编译错误和警告,并最终生成一个可执行的程序。
编辑源代码
上一节我们介绍了编译的基本概念,本节中我们来看看如何编辑和修改源代码。
在我的工作目录 C program/week1 中,存放着本课程第一周的一些代码文件。使用Unix命令 ls 可以查看该目录下的文件。
ls
目录中包含 .c 源文件、已编译的可执行文件(如 add2.exc)以及中间文件(如 a.out)。我们可以直接运行已编译的程序,例如执行 add2.exc 来计算两个浮点数的和。
现在,假设我想修改程序,使其能够输入三个浮点数。我将基于现有的 add2.c 文件创建一个新文件 add3.c。
vi add3.c
使用文本编辑器(如vi)是编程中的关键技能。虽然本课程不深入讲解特定编辑器的命令,但你需要熟悉一种编辑器的基本操作。在更复杂的集成开发环境(IDE,如Visual Studio)中,通常会提供代码模板来帮助你快速开始。
以下是修改代码的基本步骤:
- 将提示信息从“input two floats”改为“input three floats”。
- 在变量声明部分增加第三个浮点数变量
c。 - 在
scanf函数中增加读取第三个变量的格式说明符%f和对应的变量&c。 - 在
printf函数中增加输出第三个变量的格式说明符%f和对应的变量c。
在修改过程中,我故意引入两个错误来演示编译过程:
- 在
printf的格式字符串中漏掉一个右引号。 - 在
printf中声明了输出三个值(%f %f %f),但只提供了两个变量(a, b)。
完成编辑后,保存并退出编辑器。
编译与调试
现在,我们进入编译阶段,使用 gcc 编译器将源代码 add3.c 编译成可执行文件 add3。
gcc -o add3 add3.c
编译器会输出错误和警告信息:
- 错误:提示括号或引号不匹配。这是因为我们漏掉了右引号,导致编译器无法正确解析字符串。
- 警告:提示格式字符串中的转换说明符数量与提供的参数数量不匹配。这是因为
printf需要三个值,但我们只给了两个。
首先,我们修复致命的语法错误(漏掉的引号)。重新打开文件并修正后,再次编译。
gcc -o add3 add3.c
这次编译通过了,但之前提到的警告仍然存在。我们暂时忽略警告,直接运行生成的可执行文件。
./add3
输入 3.4, 5.6, 6.6。程序输出结果为 3.4 + 5.6 + 0.0 = 6.6。虽然总和(6.6)是前两个数的和,但第三个变量 c 显示为 0.0,这是因为 scanf 成功读取了值给 c,但 printf 没有接收到 c 变量来输出,所以输出的是变量 c 的初始值。
这个结果印证了编译器的警告:输出行为不符合预期。现在,我们修复这个逻辑错误,在 printf 中补上变量 c。
修正代码后,第三次进行编译。
gcc -o add3 add3.c
这次编译成功,且没有任何警告。再次运行程序。
./add3
输入 1.1, 2.2, 3.3。程序正确输出 1.1 + 2.2 + 3.3 = 6.6。
此时,查看目录,会发现已经成功生成了 add3.exc 可执行文件。
总结
本节课中,我们一起学习了一个完整的C程序开发周期:
- 编辑:使用文本编辑器创建或修改源代码(
.c文件)。 - 编译:使用编译器(如
gcc)将源代码转换为机器代码。此过程会检查语法错误和部分逻辑问题。 - 调试:根据编译器的错误和警告信息,定位并修复代码中的问题。错误会阻止程序生成,警告则提示潜在问题。
- 运行:执行生成的可执行文件,验证程序功能是否符合预期。

这个“编辑-编译-调试-运行”的循环是软件开发的核心流程。你所使用的具体工具(编辑器、编译器、终端)可能因操作系统(Unix、Mac、Windows)而异,但核心流程和概念是相通的。无论是简单的命令行环境还是功能丰富的IDE,掌握这个流程都是学习编程的基础。
005:第一个程序

在本节课中,我们将学习C语言的起源、特点,并通过分析经典的“Hello, World!”程序来了解C程序的基本结构。
丹尼斯·里奇与C语言的诞生
上一节我们了解了编程语言的基本概念,本节中我们来看看C语言的创造者及其背景。
丹尼斯·里奇出生于1941年,于2011年去世。他是一位美国计算机科学家,与同事肯·汤普森共同创造了C编程语言和Unix操作系统。
他一生获得了多项荣誉,包括:
- 1983年的图灵奖(计算机科学领域的诺贝尔奖)。
- 汉明奖章。
- 国家技术奖章。
此外,他与布莱恩·克尼汉合著的《C程序设计语言》一书,被广泛认为是C语言的权威指南。
C语言的特点
了解了创造者后,我们来看看C语言本身有哪些关键特性。
C语言由丹尼斯·里奇于1972年在贝尔实验室创造。它具有以下使其强大且实用的特点:
- 小巧:与许多现代语言相比,C语言需要理解的核心概念和关键字数量更少。例如,C++的关键字数量至少是C的三倍。
- 低级:C语言提供了高效率和对机器资源的直接访问能力,这使得它非常适合用于构建操作系统(如Unix)。
- 高效:C语言代码可以高效地翻译成各种机器的机器码,因此有时被称为“可移植的汇编语言”。
- 起源:C语言最初被设计为一种“系统实现语言”,用于替代其前身B语言和BCPL语言,以构建小型操作系统。它的设计深受Algol 60语言的影响,许多现代编程概念都起源于Algol 60。
第一个C程序:“Hello, World!”
现在,让我们通过C语言中最著名的入门程序来具体认识其结构。这个程序源自《C程序设计语言》一书。
以下是经典的“Hello, World!”程序代码:
#include <stdio.h>
int main() {
printf("Hello, World!\n");
return 0;
}
接下来,我们将逐部分解析这段代码的含义。
以下是该程序中各组成部分的详细说明:
#include <stdio.h>:这是一个预处理指令。它告诉编译器包含标准输入输出库的头文件,这样我们才能使用如printf这样的输入输出函数。int main():这是程序的主函数。每个C程序都从这里开始执行。int表示这个函数会返回一个整数值。{和}:这对花括号定义了main函数的代码块范围,即函数体的开始与结束。printf("Hello, World!\n");:这是一个函数调用语句。printf是标准库中用于打印输出的函数。它打印引号内的字符串Hello, World!。字符串中的\n是一个转义字符,代表换行。语句末尾的分号表示该语句结束。return 0;:此语句结束main函数,并向操作系统返回值0。按照惯例,返回0通常表示程序成功执行完毕。}:这个花括号标志着main函数体的结束。
语法与标点的重要性
在编写程序时,语法的准确性至关重要,这与写文章不同。
C语言编译器对语法要求极其严格。标点符号的正确使用是语法的关键部分,包括:
- 分号:用于标记语句的结束。
- 花括号:用于定义代码块的范围,必须成对出现。
- 圆括号:用于函数调用和表达式,也必须匹配。
- 引号:用于定义字符串。
如果这些符号不匹配或缺失,编译器将报错并拒绝编译代码。因此,在初学阶段,养成仔细检查标点符号的习惯非常重要。


本节课中我们一起学习了C语言的创造者丹尼斯·里奇、C语言小巧、高效、低级的核心特点,并通过剖析“Hello, World!”程序掌握了C程序的基本结构,包括头文件包含、主函数、输入输出语句以及标点符号的关键作用。
006:示例-圆代码

概述
在本节课中,我们将学习一个结合了输入和输出的C语言程序。这个程序将演示如何从用户那里获取信息,进行计算,然后输出结果。我们将通过一个计算圆面积的经典示例来理解这些概念。
程序结构与元素
上一节我们介绍了基本的程序结构。本节中我们来看看一个更完整的程序,它包含了用户交互。

程序需要捕获用户输入的信息,并利用这些信息进行计算和响应。我们将编写一个简单的程序来计算圆的面积,单位使用米。
让我们再次查看代码,熟悉如何编写一个简单的C程序及其构成元素。后续课程会详细讲解每个部分。
代码详解
和之前一样,程序以标准注释开头,用于提高可读性。这个注释说明程序用于计算圆的面积。
#include <stdio.h> 是预处理指令,它引入了 printf 和 scanf 函数。scanf 是用于输入的基本例程。在大多数情况下,默认输入来自键盘。
这里我们看到另一个指令 #define。它执行一个有趣的操作。按照惯例,我们定义一个预处理器令牌 PI,使其值为六位有效数字的3.14159,这是双精度浮点数通常能存储的精度。这同样是为了文档化和提高程序可读性。
接着是 main 函数,以一个复合语句的左花括号开始。
第一个语句是声明语句,我们定义了两个标识符 area 和 radius。标识符可以是普通的单词,只要不是像 double 这样的关键字。我们将 area 和 radius 初始化为 0.0,使用 0.0 可以清楚地表明我们处理的是双精度常量,使用 0 也是允许的。
然后我们提示用户输入。如果我们省略这个提示,屏幕会停止等待,用户将不知道需要做什么。这被称为提示信息。
提示用户输入半径。scanf 函数需要特殊的 & 操作符,这表示变量 radius 的地址。我们将在课程后期详细解释这个概念,因为它涉及到地址、内存和指针。
接下来是面积计算,公式是 area = PI * radius * radius。C语言中没有简单的平方表示法(如 **),所以我们使用重复乘法。
最后,我们打印计算结果,并以 return 0; 结束程序。
程序测试
让我们确保这个程序可以运行。我已经编译了它,现在执行它。
程序提示“Enter radius”。我们测试半径为 1.0 的情况,结果是 3.14159,这是正确的。调试程序时,我们首先使用已知答案的简单测试数据。
再次运行程序,输入半径 2.5,结果是 19.634937,同样正确。
总结
本节课中我们一起学习了如何编写一个同时包含输入和输出的简单C程序。我们通过计算圆面积的例子,了解了使用 scanf 进行输入、使用 printf 进行输出,以及利用 #define 定义常量的方法。关键在于,对于 scanf 函数,除了使用格式字符串(如 %lf 对应双精度浮点数),还需要使用变量的地址(即 & 操作符)。


007:示例-马拉松距离转换

在本节课中,我们将学习一个完整的C语言程序示例。这个程序将马拉松的距离从英里和码转换为公里。通过这个例子,我们将深入理解C语言的基本结构,包括注释、预处理指令、变量声明、数据类型、表达式计算以及格式化输出。
程序概述与结构
上一节我们介绍了简单的输出,本节中我们来看看一个更完整的程序。输出是编程中至关重要的组成部分,正如那句著名的格言“垃圾进,垃圾出”。在大多数情况下,无法产生结果的程序是没有用处的,因此输出至关重要。
让我们来看一个将马拉松距离从英里和码转换为公里的程序。
/* 马拉松距离转换程序
最后更新: [日期]
作者: [姓名]
用途: 将26英里385码的马拉松距离转换为公里
*/
#include <stdio.h>
int main(void) {
int miles = 26;
int yards = 385;
double kilometers;
kilometers = 1.609 * (miles + yards / 1760.0);
printf("\nA marathon is %lf kilometers.\n\n", kilometers);
return 0;
}
代码详解
注释的作用
程序开头的部分是注释。注释是会被编译器忽略的文本。既然会被忽略,我们为什么还需要注释呢?原因在于我们需要让代码具有可读性。首先,需要有人来维护程序,他们必须理解程序的用途和开发历史。因此,我们通常喜欢注明程序最后更新或编码的时间、作者,以及关于程序用途的总体说明。
预处理指令
C代码中首先出现的是所谓的预处理指令,即 #include。尖括号内的 stdio.h 是一个头文件(.h 扩展名表示头文件)。这意味着这个文件中的代码将自动包含到我们的程序中。它之所以被称为“预处理”指令,是因为它发生在C代码被编译之前。额外添加的代码是我们执行 printf 函数所必需的。没有它,printf 将被视为未声明。
主函数
然后我们开始实际的代码,以关键字 int 开头,这表示一个名为 main 的函数。在我们入门课程中,几乎所有程序都会有一个 main 函数,程序从这里开始执行,这是C语言系统所理解的。
int是一个关键字,表示程序返回一个整数。void是另一个关键字,在这里表示函数没有参数。
代码块与变量声明
接下来我们看到一个花括号 {。开括号意味着代码块的开始。在编程语言中,有很多成对出现的符号。我们已经看到星号*、尖括号<>、圆括号()需要匹配。确保正确匹配括号对于语法正确至关重要。这个花括号有一个对应的闭括号 },这被称为复合语句,是一系列可执行语句的集合。
在代码块内,我们有两个整型变量。这被称为声明,并且是带有初始化的声明。
miles被初始化为26。yards被初始化为385。
一个好的程序员会选择有意义的标识符名称。miles 和 yards 就是标识符。为什么选择这样的名字而不是 M 和 Y 呢?虽然 M 和 Y 可能也可以,但使用像 miles 或 yards 这样的实际单词可以在不增加成本的情况下为程序添加文档,使程序更具可读性。
然后,结果变量 kilometers 将被声明为 double(双精度浮点)类型。
计算公式与类型转换
计算公式是:kilometers = 1.609 * (miles + yards / 1760.0);
这里需要特别强调一点,这一点很重要,我们后面会再次强调。我们没有选择使用 1760 而是用了 1760.0,原因是 yards 是一个整数。如果这里使用 1760(整数),那么 yards / 1760 将进行整数除法,385 / 1760 的结果将是 0。
我们通过使用 1760.0(一个双精度浮点常量)来避免这个问题。这样,整个表达式会进行隐式类型转换,取更广泛的类型(这里是 double),使整个表达式成为该类型。我们以后还会学到,也可以使用类型转换来实现。另一个技巧是写成 1.0 * yards / 1760。
输出语句与程序返回
接下来是 printf 打印语句。在打印语句中,首先有一个控制格式的字符串。再次注意,我们看到匹配的双引号。之前有一次我漏掉了一个引号,结果出现了语法错误。你应该看看如果漏掉一个匹配的符号会发生什么,有时会得到非常奇怪且难以理解的语法错误。
这个格式字符串的意思是:打印一个换行符(意味着在屏幕上换行),然后打印“A marathon is”,接着 %lf 表示使用长浮点数(即 double)格式打印 kilometers 的值,再打印“kilometers.”,最后打印两个换行符。然后,kilometers 的值被解释并以长浮点数格式打印出来。
最后是 return 0;。因为我们声明 main 函数返回一个整型值,所以这里返回 0。这个 0 会神秘地返回给操作系统,0 是一个表示程序正确结束的代码。
程序执行结果
让我们执行这个程序。确实,我们得到了正确的结果:A marathon is 42.185969 kilometers.
总结

本节课中我们一起学习了一个比之前程序更复杂的C语言示例。它包含了注释、预处理指令、main 函数、变量声明与初始化、数据类型、涉及类型转换的表达式计算以及格式化输出。这个例子提供了许多关键概念,我们将在后续课程中更详细地探讨这些内容。请仔细研究这个程序,它对于理解C语言基础非常有帮助。
008:简单输入输出 - 华氏度转摄氏度
在本节课中,我们将学习如何编写一个简单的C语言程序,用于将华氏度温度转换为摄氏度。这个程序将演示C语言中的变量声明、基本算术运算以及使用scanf和printf进行输入输出的方法。
程序目标
我们将编写一个程序,要求用户输入一个华氏度温度值(整数),程序将计算并输出对应的摄氏度温度值(整数)。这个程序是学习C语言输入输出和表达式计算的经典入门示例。

代码结构与解释
以下是完整的程序代码,我们将逐部分进行解释。
#include <stdio.h>
int main(void) {
int fahrenheit;
int celsius;
printf("Please enter your Fahrenheit as an integer: ");
scanf("%d", &fahrenheit);
celsius = (fahrenheit - 32) / 1.8;
printf("%d Fahrenheit is %d Celsius.\n", fahrenheit, celsius);
return 0;
}
1. 包含头文件与主函数
每个C程序都需要一个main函数作为入口点。我们使用#include <stdio.h>来引入标准输入输出库,以便使用printf和scanf函数。
2. 变量声明
我们声明了两个整型变量fahrenheit和celsius,分别用于存储用户输入的华氏度温度和计算得到的摄氏度温度。
3. 获取用户输入
程序使用printf函数向用户显示提示信息。接着,使用scanf函数读取用户输入的整数。%d是格式化字符串,表示读取一个整数。&fahrenheit中的&符号是取地址运算符,它告诉scanf将读取的值存入变量fahrenheit所在的内存地址中。
4. 温度转换计算
温度转换的核心是以下公式:
celsius = (fahrenheit - 32) / 1.8
在代码中,我们直接使用这个公式进行计算。需要注意的是,常量1.8是一个双精度浮点数,因此整个表达式会以浮点数方式进行计算,结果再被转换为整数赋值给celsius变量。
5. 输出结果
计算完成后,我们使用printf函数输出结果。格式化字符串"%d Fahrenheit is %d Celsius.\n"中的两个%d会被变量fahrenheit和celsius的值依次替换。
6. 程序结束
return 0;语句表示main函数正常结束,并向操作系统返回状态码0。
运行示例
编译并运行此程序,你将看到类似以下的交互过程:
Please enter your Fahrenheit as an integer: 32
32 Fahrenheit is 0 Celsius.
Please enter your Fahrenheit as an integer: 212
212 Fahrenheit is 100 Celsius.
Please enter your Fahrenheit as an integer: 92
92 Fahrenheit is 33 Celsius.
总结
本节课中,我们一起学习并实现了一个华氏度转摄氏度的C语言程序。通过这个简单的例子,我们掌握了以下几个核心概念:
- C程序的基本结构(
main函数)。 - 如何使用
int类型声明变量。 - 如何使用
printf进行输出和scanf进行输入,特别是&取地址运算符的用法。 - 如何编写和执行一个包含算术运算的表达式。
- 理解程序中可能发生的隐式类型转换。


这个程序是经典教材《C程序设计语言》中早期示例的简化版本,是迈向更复杂C语言编程的坚实一步。
009:简单输入输出 - 英里转换

概述
在本节课中,我们将学习如何编写一个C语言程序,将马拉松的距离从英里和码转换为公里。我们将通过这个具体的例子,深入理解变量声明、数据类型、算术表达式以及运算符优先级等核心概念。
程序目标与公式
我们的目标是编写一个程序,计算马拉松距离对应的公里数。已知马拉松距离为26英里385码。转换公式如下:
公里数 = 1.609 * (英里数 + 码数 / 1760.0)
这个公式将英里和码先统一转换为英里(小数形式),再乘以转换系数1.609得到公里数。
代码解析:变量声明与初始化
上一节我们介绍了程序的目标和核心公式,本节中我们来看看如何用代码实现。首先,我们需要声明并初始化存储数据的变量。
int miles = 26, yards = 385;
double kilometers;
在这段代码中:
int表示变量miles和yards是整数类型。double表示变量kilometers是双精度浮点数类型,用于存储带小数的计算结果。- 声明时直接赋值(如
= 26)称为“初始化”。选择miles和yards这类有意义的变量名,有助于提高程序的可读性和可维护性。
核心:算术表达式与数据类型
声明变量后,我们需要进行计算。计算过程体现在一个赋值表达式中,这里包含了本节课最重要的知识点:数据类型如何影响运算结果。
kilometers = 1.609 * (miles + yards / 1760.0);
让我们分解这个表达式:
- 首先计算
yards / 1760.0。这里使用了1760.0而非1760,这至关重要。因为1760.0是一个浮点数(double),所以除法yards / 1760.0是浮点数除法,结果是一个小数(约0.21875)。如果写成yards / 1760,C语言会执行整数除法,结果会被截断为0,导致最终计算错误。 - 然后将上一步的结果与
miles相加:miles + (yards / 1760.0 的结果)。 - 最后将总和乘以
1.609,得到公里数,并赋值给kilometers变量。
括号 () 明确了运算的优先级,确保先进行除法,再进行加法。
关键概念总结
以下是本程序涉及的关键编程概念:
- 变量与数据类型:
int用于整数,double用于浮点数。正确选择类型是保证计算精度的前提。 - 运算符与运算规则:除法运算符
/的行为取决于操作数的类型。整数相除得到整数(舍去小数),而只要有一个操作数是浮点数,就会进行浮点数除法。 - 表达式求值顺序:可以通过括号
()来控制复杂表达式的计算顺序,避免歧义。
练习建议
为了巩固理解,建议你修改这个程序,使其能够接收用户输入的任意英里和码数值,然后输出对应的公里数。这类似于我们之前做过的华氏度转摄氏度程序,结合两者的思路,你应该可以轻松完成这个练习。


总结
本节课中我们一起学习了如何构建一个单位转换程序。我们重点掌握了:
- 如何声明和初始化
int和double类型的变量。 - 如何编写包含混合数据类型的算术表达式,并理解了整数除法与浮点数除法的根本区别。
- 如何使用括号来控制表达式的计算顺序。
理解这些基础概念对于后续学习更复杂的C语言编程至关重要。
010:字符集和标记

概述
在本节课中,我们将学习如何编写一个语法正确的C程序。我们将关注构成C程序的基本元素——字符和标记,并理解编译器如何解析它们。通过一个简单的加法程序示例,我们将了解常见的语法错误及其修正方法。
字符与标记:程序的基本构成单元
上一节我们介绍了编程的基本概念,本节中我们来看看C程序的具体构成。
一个C程序由一系列字符组成,就像一篇英文文章。然而,与可以容忍拼写和标点错误的文章不同,C程序中的拼写错误、标点错误等会被编译器捕获,导致程序无法运行。一个好的编译器会提供清晰的错误信息,帮助我们定位问题。
首先,C程序将字符组合成我们称为“标记”的单元。标记是词法分析的基本单位。
以下是C程序中常见的几种标记类型:
- 注释:注释以
/*开始,可以包含任意字符,直到遇到*/结束。例如:/* 这是一个注释 */。如果忘记写结束标记,注释会一直延续下去,导致编译错误。 - 预处理器指令:以
#开头,通常位于行首。例如#include指令告诉编译器包含特定的库文件,如我们常用的#include <stdio.h>。其他重要的标准库还包括数学库<math.h>、字符处理库<ctype.h>和时间库<time.h>等。 - 标识符:用于命名变量、函数等。标识符可以是单个字母(如
a,b)或多个字母的组合。main是一个特殊的标识符,它指明了程序开始执行的位置。 - 运算符:执行操作的符号。例如,
+是加法运算符(在特定上下文中也可能是正号),&是取地址运算符,而&&是逻辑与运算符。 - 标点符号:在语言中具有特定用途的符号,如开括号
{、闭括号}和分号;。这些符号必须正确使用,否则会导致语法错误。
一个程序必须语法正确。我们写下程序后,由编译器检查其语法。例如,如果在 int main() 后面错误地使用了圆括号 ( 而不是花括号 {,编译器就会报错。编译器通常会指出它认为错误开始的位置,并给出(希望是)有帮助的英文错误信息。我们必须理解错误发生的位置并加以修正。
比语法正确更难的是让程序语义正确,即程序能按预期工作。这通常需要我们反复运行、修改和测试程序。
示例程序:理解标记的解析与执行
上一节我们介绍了各种标记,本节中我们通过一个实际代码来看看编译器如何解析和执行这些标记。
接下来我们将看到一段实际代码,其中包含了上述标记。编译器会解析这些标记,编译程序,然后执行它。
这是一个简单的程序,它使用 scanf 读取两个浮点数,计算它们的和,并使用 printf 显示结果。
#include <stdio.h>
int main() {
float a, b, sum;
printf("请输入两个数字:");
scanf("%f %f", &a, &b);
sum = a + b;
printf("两数之和为:%f\n", sum);
return 0;
}

总结
本节课中我们一起学习了C程序的基础构成。我们了解到程序由字符组成,并组合成注释、预处理器指令、标识符、运算符和标点符号等不同类型的标记。编译器的任务是检查这些标记是否符合C语言的语法规则。编写正确的程序首先需要避免语法错误,然后通过测试确保其语义正确,即能完成预定的功能。理解这些基本元素是编写任何C程序的第一步。
011:注释
概述
在本节课中,我们将要学习C语言中一个非常基础但极其重要的概念:注释。注释是程序员在代码中添加的说明性文字,它们不会被编译器执行,但对于代码的文档化和可读性至关重要。


什么是注释?
在C编程语言中,注释是一种特殊的语法,用于在源代码中添加说明、解释或备注。这些文字会被编译器完全忽略,不会影响程序的执行。
注释的重要性
上一节我们介绍了注释的基本概念,本节中我们来看看为什么注释如此重要。注释的核心价值在于提高代码的文档化和可读性。为了能够维护、修改程序并理解其用途,我们希望程序是“可读的”。
注释的语法
以下是C语言中两种主要的注释语法:
1. 多行注释(经典风格)
这种注释以 /* 开始,以 */ 结束。在这两个符号之间的所有内容都会被编译器视为注释并忽略。这种注释可以跨越多行。
语法公式:
/* 这里是注释内容,
可以跨越多行 */
2. 单行注释(现代风格)
这种注释以两个斜杠 // 开始。从 // 开始直到该行结束的所有内容都会被编译器视为注释。这种风格更现代,通常更不容易出错。
语法公式:
// 这里是单行注释
注释的处理过程
为了理解注释,我们可以回忆一下C编译器的工作流程。首先,预处理器会处理源代码。随后,词法分析器(tokenizer) 会扫描代码,并丢弃所有注释内容。因此,注释在效果上等同于空白。
实践示例
让我们通过一个具体的程序 circle.c 来看注释的实际应用。
/* 计算圆的面积 */
#include <stdio.h>
#define PI 3.14159
int main() {
double radius, area;
// 提示用户输入半径
printf("Enter radius: ");
scanf("%lf", &radius);
// 计算面积
area = PI * radius * radius; // 面积公式:π * r²
// 输出结果
printf("Area is %.3f square meters\n", area);
return 0;
}
在这个例子中,我们使用了两种注释风格。开头的多行注释描述了程序的目的。在代码内部,我们使用单行注释来解释每一步操作,例如提示输入、计算公式和输出结果。
编译并运行这个程序没有问题。例如,输入半径 2,会得到面积 12.566 平方米。

总结
本节课中我们一起学习了C语言的注释。我们了解到注释是用于提高代码可读性和可维护性的非执行文本。C语言支持两种主要注释风格:经典的多行注释 /* ... */ 和现代的单行注释 // ...。虽然注释会被编译器完全忽略,但它们是编写清晰、易于理解代码的关键工具。
012:C语言关键词

概述
在本节课程中,我们将学习C语言中的“关键词”。关键词是语言中具有特殊含义的保留字,不能用作普通的标识符(如变量名或函数名)。理解这些关键词是掌握C语言语法的基础。
关键词的定义与作用
在C语言中,关键词有时也被称为“保留字”。它们由语言标准定义,具有特定的功能,因此程序员不能将它们用于其他目的,例如作为变量名。
需要注意的是,有些看起来像关键词的标识符实际上并不是关键词。一个常见的误解是认为 main 是一个关键词,因为它频繁出现,作为C程序执行的入口点。然而,main 只是一个普通的函数标识符。同样,由 # 符号引入的预处理器指令,如 #include 或 #define,虽然在编译系统中具有特殊用途,但它们也不属于这里讨论的保留关键词范畴。
关键词的主要类别
以下是C语言关键词的一些主要类别及其简要说明。
数据类型关键词
这类关键词用于声明C语言内置的(或称“原生”)数据类型。
int: 用于声明整数类型变量。int类型的长度通常是一个机器字长,在现代大多数机器上是4字节。它可以被signed、unsigned、short、long等修饰符修饰。char: 用于声明字符类型变量。字符类型是语言中非常重要的基本类型。double、float: 用于声明浮点数(小数)类型。long: 通常用作修饰符,与int、double等结合使用,表示更长的数据类型。enum: 用于定义枚举类型。
关于 int 类型,其取值范围在典型的有符号类型中大约是 -20亿到 +20亿;在无符号类型中,范围是 0 到 4294967295。
流程控制关键词
这类关键词用于控制程序的执行流程。
break: 跳出当前循环或switch语句。continue: 跳过当前循环的剩余部分,直接开始下一次循环。for、while、do: 用于构建循环结构。if、else: 用于构建条件判断语句。goto: 用于无条件跳转到程序中的指定标签。注意:goto语句被认为是一种不良的编程实践,因为它会破坏代码的结构,使程序难以理解和维护。事实上,许多现代语言已不再支持goto。在本课程中,我们将避免使用它。return: 用于结束当前函数的执行。如果后面跟有表达式,则将该表达式的值返回给函数的调用者。我们在之前的简单程序中已经多次见过它。
类型定义与复合类型关键词
这类关键词用于创建新的类型名称或定义复杂的数据结构。
typedef: 用于为现有类型(特别是复合类型)定义新的别名。struct: 用于定义结构体,这是一种将多个不同类型变量组合成一个单一类型的方式。结构体在本课程的后续部分(B部分)将被大量使用。union: 用于定义联合体,它允许在相同的内存位置存储不同的数据类型。联合体的使用频率较低,通常可以找到替代方案来避免使用它。
存储类别说明符关键词
这类关键词用于指定变量的存储方式和生命周期。
auto: 用于声明自动变量(在函数内部默认的变量类型)。注意:auto在现代C语言中已基本废弃,很少使用,因为函数内局部变量默认就是auto。register: 建议编译器将变量存储在CPU寄存器中,以期提高访问速度。注意:现代编译器在优化方面通常比程序员做得更好,因此register关键字已不再需要,也不建议使用。static: 用于声明静态变量或函数,它具有特殊的用途(如保持变量的值在函数调用之间不变,或限制函数/变量的作用域)。静态变量是你会遇到的重要概念。
C与C++关键词数量的对比
C语言的关键词列表相对较小,大约包含30个,其中大部分是核心概念。相比之下,C++语言的关键词数量要多得多。
如果我们查看C++的关键词表,会发现它包含了C的关键词作为一个子集,但总数远超C。粗略计算,现代C++拥有大约三倍于C的关键词数量。这在一定程度上也反映了C++语言比C更为复杂。当然,编写C++程序并不要求使用全部语言特性。我们之所以从C语言开始学习,正是因为它的核心相对简洁明了。

总结
本节课我们一起学习了C语言中的关键词。我们了解到关键词是语言保留的、具有特定功能的单词,不能用作普通标识符。我们将其分为几个主要类别进行了介绍:数据类型关键词(如 int, char)、流程控制关键词(如 if, for, return)、类型定义关键词(如 typedef, struct)以及存储类别说明符(如 static)。同时,我们也指出了 goto、auto、register 等关键词在现代编程中的局限性或废弃状态。最后,通过对比C和C++的关键词数量,我们理解了C语言语法核心的相对简洁性。熟悉这些关键词是编写正确C程序的第一步。
013:标识符详解


在本节课中,我们将深入学习C语言中的标识符。我们将探讨标识符的构成规则、命名规范,以及如何通过良好的命名习惯来提高代码的可读性和可维护性。
标识符的定义与作用
上一节我们介绍了代码的基本构成,本节中我们来看看标识符的具体定义和作用。
在编写代码时,我们会遇到被称为“标识符”的记号。例如,在代码 int main() 中,int 是一个关键字,而不是标识符。main 是一个标识符,它是程序执行起始函数的名称。括号 () 不是标识符的一部分,它们用于函数调用。
标识符的构成规则
了解标识符的构成规则是正确命名的基础。以下是标识符的语法定义。
标识符在C语言中由以下部分构成:
- 一个字母或下划线字符(必须至少有一个)。
- 其后可以跟随零个或多个字母、下划线或数字。
用语法公式可以表示为:
identifier = (letter | '') (letter | '' | digit)*
这意味着标识符可以任意长,但通常我们看到的标识符长度不会超过10到12个字符。
以下是符合规则的标识符示例:
k:常用于表示辅助整数。_printf:这可能是系统程序员在设计printf函数时使用的标识符。two_words:使用下划线分隔两个单词是一种命名约定。twoWords:将两个单词连在一起,并将第二个单词首字母大写,是另一种标准约定。my_DNA23:在第一个字符之后,可以使用数字。
以下是不符合规则的标识符示例:
#me或me#:字符#不允许出现在标识符中。-x:这会被解释为表达式“负x”,而不是标识符。23myDNA:标识符的第一个字符不能是数字。
总结规则:数字可以出现在除首字符外的任何位置;下划线和字母可以出现在任何位置,包括首字符。
良好的标识符命名实践
良好的标识符命名有助于为程序提供文档,并提高代码的可读性。
以下是一些好的标识符选择示例:
main,printf,sqrt:这些是标准库中常见的标识符。superman:如果代码与超人角色相关,这个名称能提供清晰的意象。radius:清晰地表示了半径。i,j,k:在编程和数学社区中,约定俗成地用于计数或表示整数。
当然,必须为标识符选择正确的用途。
以下是一些可能不好的标识符选择:
grX33,_pp_25:我们无法理解它们可能代表什么,因此不具备可读性和文档性。i_am_four_words:虽然意思明确,但混合了两种命名约定(下划线与大小写),在一定程度上削弱了其清晰度。
有些标识符的好坏取决于上下文:
data:可能不够具体,也许需要heightData或weightData。x:如果它表示XY平面上的一个坐标,则非常合适。p,q:在逻辑编程社区中,通常用于表示布尔变量,因此在特定上下文中可能是合适的。
代码示例分析
最后,让我们通过一个简单的程序来观察这些命名规范的实际应用。
以下是一个计算圆面积的代码片段:
#include <stdio.h>
#define PI 3.14159
int main() {
double area, radius;
printf("Enter the radius: ");
scanf("%lf", &radius);
area = PI * radius * radius;
printf("Area: %f\n", area);
return 0;
}
请注意代码中的标识符:
main:是一个标识符。PI:在C语言中,这是一个预处理器命令中的标识符。按照惯例,常量通常使用大写字母命名。area和radius:它们的含义清晰,我们能在程序中理解其用途。double:是一个关键字。printf和scanf:是标准输入输出库中的标准标识符。
这个程序遵循了命名约定,选择了良好的标识符,从而使其易于阅读和理解。
总结

本节课中我们一起学习了C语言标识符的核心知识。我们明确了标识符是由字母、下划线和数字按特定规则组成的名称,用于标记变量、函数等代码元素。我们掌握了标识符的构成语法,并理解了通过遵循社区命名惯例(如使用有意义的名称、合理运用下划线或驼峰命名法、常量全大写等)来选择良好的标识符,是编写可读、可维护代码的关键实践。
014:运算符优先级详解

概述
在本节课中,我们将深入学习C语言中运算符的优先级规则。理解运算符优先级对于正确计算表达式至关重要。我们将通过完整的优先级表格和具体示例,帮助你掌握如何确定表达式的计算顺序。
运算符优先级表
上一节我们介绍了运算符的基本概念,本节中我们来看看完整的运算符优先级规则。下图展示了C语言的完整运算符优先级表。

该表格由William Swanson制作,它清晰地描述了C语言读取表达式的顺序。例如,在表达式 A = 4 + B * 2 中,包含多个运算符。乘法运算符 * 的优先级高于加法运算符 +,因此 B * 2 会先被计算,然后其结果再与4相加。
优先级与结合性
运算符优先级决定了计算的先后顺序,而结合性则决定了当多个相同优先级的运算符出现时,计算是从左到右还是从右到左进行。
以下是主要的运算符优先级层级及其结合性说明:
-
第一级:基本表达式运算符
- 这些运算符具有最高的优先级。
- 包括后缀自增
++、后缀自减--、数组索引[]和函数调用()。
-
第二级:一元运算符
- 优先级仅次于基本表达式运算符。
- 包括解引用
*、取地址&、一元正号+、一元负号-、逻辑非!、前缀自增++、前缀自减--、类型转换(type)和sizeof运算符。 - 注意:一元正号
+和一元负号-比二元加法和减法运算符绑定得更紧密,优先级更高。
-
第三级及以后:二元与三元运算符
- 接下来是乘法
*、除法/和取模%运算符。 - 然后是加法
+和减法-运算符。 - 之后是关系运算符(如
<,>)、相等性运算符(==,!=)、位运算符和逻辑运算符。 - 还有一个特殊的三元条件运算符
? :,它接受三个操作数。
- 接下来是乘法
-
最低优先级:赋值与逗号运算符
- 赋值运算符(如
=,+=,-=等)的优先级很低。 - 逗号运算符
,的优先级最低。它强制表达式从左到右依次求值,有时用于确保特定的执行顺序。
- 赋值运算符(如
符号重载与上下文
C语言中存在符号重载的情况,同一个符号在不同上下文中有不同含义。理解上下文是关键。
以下是常见的符号重载示例:
&作为一元运算符时表示“取地址”,但在二元运算符中表示“按位与”。*作为一元运算符时表示“解引用”,但在二元运算符中表示“乘法”。<<和>>可以是“位左移/右移”运算符,但在流操作中(如C++)有不同用途。逻辑运算符&&和||则是独立的符号。
示例与练习
为了确保你理解这些规则,让我们通过一些示例来练习。
以下是一些表达式及其计算过程的说明:
3 * 5的结果是15。这是简单的乘法。3 * 5 + 2中,乘法*优先级高于加法+,因此先计算3 * 5得到15,再加2,最终结果为17。- 可以使用括号
()来显式覆盖默认的优先级。3 * (5 + 2)会先计算括号内的5 + 2得到7,再乘以3,结果是21。而(3 * 5) + 2则与默认顺序一致。 - 表达式
+8中的+是一元正号运算符,其结果就是8。 3 / 5是整数除法,结果为0(因为商为0,余数被丢弃)。3.0 / 5中,3.0是双精度浮点数常量,因此执行浮点数除法,结果为0.6。
理解这些区别对于编写正确的程序非常重要。
总结
本节课中我们一起学习了C语言运算符的完整优先级规则。我们回顾了从最高优先级的基本表达式运算符到最低优先级的逗号运算符的各个层级,并强调了结合性的概念。我们还指出了C语言中符号重载的现象,即同一符号在不同上下文中有不同含义。最后,通过一系列示例,我们巩固了如何应用优先级规则来解析和计算表达式。掌握这些规则是写出正确、可预测的C语言代码的基础。


015:表达式与优先级

概述
在本节课中,我们将要学习C语言中表达式的求值规则,特别是运算符的优先级和结合性。理解这些概念对于编写正确且可预测的代码至关重要。
表达式求值基础
C语言拥有丰富的运算符。了解这些运算符如何组合以及它们被应用的顺序非常重要。我们将通过一些简单的案例来帮助你掌握核心概念。核心思想是:了解运算符的工作原理,知道其优先级,并理解其结合性。
我们首先声明三个简单的变量。在C语言中,我们会这样写:
int a = 1;
int b = 2;
int c;
这里的 = 是初始化符号。变量 a 被初始化为1,b 被初始化为2,而 c 未被初始化。在大多数系统中,c 的初始值会是0。这样我们就有了三个变量用于后续的代码示例。
赋值表达式
以下是一个使用这些变量的简单表达式:
c = a + b;
这里的 = 表示赋值。因此,这是一个赋值语句。可以理解为:将表达式 a + b 的值赋给 c。分号 ; 表示操作的结束,它标志着一个序列点。该行上的所有计算完成后,程序才会继续执行下一行。
我们知道 a 被初始化为1,b 被初始化为2,因此我们预期 c 会变成3。这是一个赋值表达式,其内部包含一个二元加法运算符 +。它表示 c 被赋予 a + b 的值。
在C语言中,括号 () 对于表达式求值至关重要,因为它们总是会覆盖表达式中任何其他部分的优先级。当你感到困惑,或者表达式非常复杂时,使用括号可以清晰地表明你期望的求值顺序。正如我们刚才所说,c 变成了3,这应该很直接。
运算符优先级
C语言中大约有16个优先级的运算符。优先级非常低的运算符(例如赋值运算符)往往最后执行。然而,某些操作,如逻辑非(使用感叹号 ! 表示)等一元运算符,具有非常高的优先级。自增 ++ 和自减 -- 运算符也是如此。
算术运算符处于中间优先级。在算术运算符中,乘法(使用星号 * 表示)的优先级高于简单的二元加法 + 和减法 -。请注意,你还可以使用一元减号 - 和一元加号 +。区分它们的唯一方法是根据上下文或括号。像这样的一元运算符比对应的二元加法和减法具有更高的优先级。二元运算符需要两个参数,而一元运算符只需要一个参数。
乘法、除法 / 以及取余运算符 %(有时也称为模运算符)具有相同的较高优先级,都高于加法和减法。这一点很重要,因为当这些运算符在表达式中混合使用时,你必须知道哪个先执行。
运算符结合性
你需要了解的另一个概念是结合性。运算符是如何结合的?例如,当你有一个表达式 a + b + c,这里有两个运算符。哪个先执行?是先计算 b + c 然后加上 a,还是先计算 a + b 然后加上 c?事实证明,这些运算符是从左到右结合的。绝大多数运算符都是左结合的。
为了清晰地展示基于结合性的隐式求值顺序,我们可以用括号完全括起这个表达式:( (a + b) + c )。
现在,看另一个表达式:a = b = c = 3。赋值运算符是从右到左结合的。这意味着 c = 3 首先执行,然后将这个表达式的值赋给 b,最后赋给 a。最终,所有变量都会变成3。


总结
本节课中,我们一起学习了C语言表达式的求值规则。我们了解了如何使用括号来明确求值顺序,掌握了运算符优先级的概念(例如乘除法优先于加减法),并理解了运算符的结合性(大多数从左到右,赋值从右到左)。请尝试理解并吸收这些知识,确保你能编写这类代码并知道如何求值。接下来,我们将继续学习涉及不同运算符的更复杂表达式。
016:表达式和求值进阶

概述
在本节课中,我们将学习更复杂的C语言表达式求值。我们将探讨运算符在不同上下文中的多重含义、自增自减运算符、复合赋值运算符,以及如何使用括号来明确运算顺序。理解这些概念对于编写正确且易于理解的代码至关重要。
运算符的多重含义
上一节我们介绍了基本的运算符,本节中我们来看看运算符在不同上下文中的不同含义。初学者常犯的一个错误是忘记某些符号(如斜杠 /)在不同情境下代表不同的操作。
- 斜杠
/在表达式中通常表示除法,但它可以是整数除法或浮点数除法,具体取决于操作数的类型。 - 取余运算符
%(模运算符)在日常编程中不常用,因此需要特别记住其用法。 - C语言还有一些独特的运算符,例如自增
++和自减--。 - 此外,还有基于赋值的运算符,如普通赋值
=,以及复合赋值运算符如+=、-=。需要注意的是,==是逻辑相等运算符,而非赋值。
这些特性并非C语言独有,许多受C语言启发的语言(如Java、C++)也采用了类似的运算符。
使用括号明确优先级
当表达式变得复杂且容易混淆时,为了让自己和阅读、维护代码的人清晰理解,请使用括号 ()。
括号可以覆盖运算符内置的优先级和结合性。例如,表达式 A * B + C 会先计算 A * B(因为 * 的优先级高于 +),然后加上 C。但如果你希望先计算 B + C,就必须使用括号:A * (B + C)。
需要注意的细节
以下是表达式求值中一些常见的“陷阱”:
- 整数除法:整数除法会丢弃余数。例如,
3 / 7的结果是0,而7 / 3的结果是2。 - 浮点数除法:如果操作数中有一个是浮点数,则进行浮点数除法。例如,
5 / 2.0的结果是2.5。 - 模运算符:模运算符
%返回整数除法的余数。例如,3 % 7是3,7 % 3是1。
自增与自减运算符
自增 ++ 和自减 -- 运算符非常实用,它们可以简化代码。
以下是前缀和后缀形式的区别:
- 前缀自增:
A = ++B;- 含义:先执行
B = B + 1,然后将B的新值赋给A。 - 如果
B初始为5,执行后B变为6,A也被赋值为6。
- 含义:先执行
- 后缀自增:
A = B++;- 含义:先将
B的当前值赋给A,然后执行B = B + 1。 - 如果
B初始为5,执行后A被赋值为5,然后B变为6。
- 含义:先将
注意:后缀自增运算符的优先级高于前缀自增。
代码实验与分析
理解理论的最佳方式是实践。以下是一段用于实验的代码,你可以尝试编写或运行类似的代码来验证你的理解。
#include <stdio.h>
int main() {
int a = 5, b = 7, c, d = 0;
// 基础减法
c = a - b;
printf("a = %d, b = %d, c = %d, d = %d\n", a, b, c, d);
// 整数除法
c = 5 / 7; // 结果为 0
d = 7 / 5; // 结果为 1
printf("After integer division: c = %d, d = %d\n", c, d);
// 一元运算符与二元运算符
c = -5 - 7; // 等价于 (-5) - 7,结果为 -12
printf("After unary and binary minus: c = %d\n", c);
// 后缀自增
c = a++ + b; // a(5) + b(7) = 12 赋给 c,然后 a 变为 6
printf("After post-increment: a = %d, b = %d, c = %d\n", a, b, c);
// 复合赋值运算符
d += 5; // 等价于 d = d + 5,假设之前 d 是 -12,则 d 变为 -7
printf("After += operation: d = %d\n", d);
return 0;
}
运行上述代码,你将得到以下输出结果:
a = 5, b = 7, c = -2, d = 0
After integer division: c = 0, d = 1
After unary and binary minus: c = -12
After post-increment: a = 6, b = 7, c = 12
After += operation: d = -7
请仔细分析每一行代码,确保机器的执行结果符合你的预期。


总结
本节课我们一起深入学习了C语言中更复杂的表达式求值。我们探讨了运算符的多重含义、括号在控制运算顺序中的关键作用、整数除法与模运算的细节,以及前缀/后缀自增自减运算符的区别。通过编写和运行实验代码,你可以巩固这些概念,并避免在未来的编程中陷入常见的“陷阱”。务必理解这些内容,因为它们很可能是课程考核的重点。
017:声明与赋值

概述
在本节课中,我们将学习C语言程序的结构,重点探讨声明、赋值运算符以及它们与表达式的关系。所有这些概念都与类型紧密相连,因此我们将在接下来的几个小节中详细讨论C语言的类型系统。
程序结构与声明
上一节我们介绍了程序的基本结构,本节中我们来看看声明的具体形式。声明通常出现在代码块或程序的头部。
一个简单的声明格式如下:类型 后跟一个 标识符,并以分号结束。
int a;
更复杂的形式是声明并同时进行初始化(注意,这不是赋值):
int a = 2;
这意味着变量 a 被声明为 int 类型,并初始化为整数值 2。变量 a 后续可以被赋予不同的值,只要这些值符合 int 类型的范围。
我们还可以在一个声明语句中初始化多个变量,这是一个用逗号分隔的列表:
int a = 3, b = 5;
类型系统简介
C语言有多种类型。我们已经见过的有 int(整数类型)和 double(双精度浮点类型)。后续还会遇到其他简单类型,如 char、float、unsigned,以及更复杂的类型(例如涉及字符串的类型)。目前,我们专注于 int 和 double 这两种基本类型:
int用于精确表示整数。double是浮点类型,用于表示具有很大范围的科学计数法数字。
声明与赋值实例分析
理解了声明的基本形式后,让我们通过一个具体的代码示例来观察声明和赋值是如何协同工作的。
以下是一段已经编写好的代码,它展示了基本声明和赋值的用法:
#include <stdio.h>
int main() {
// 声明并初始化三个整型变量
int a = 5, b = 7, c = 6;
// 声明并初始化一个双精度浮点型变量
double average = 0.0;
printf("a = %d, b = %d, c = %d\n", a, b, c);
// 计算平均值并赋值给 average
average = (a + b + c) / 3.0;
printf("average = %f\n", average);
return 0;
}
在这段代码中:
- 在
main函数代码块的开头,我们声明并初始化了三个int类型变量:a,b,c。 - 同时,我们声明并初始化了一个
double类型变量average。将其初始化为0.0是一个良好的编程习惯,可以避免因系统未初始化变量而可能导致的错误。 - 第一个
printf语句用于输出已赋值的变量。 - 关键部分在于计算平均值的表达式:
average = (a + b + c) / 3.0;。(a + b + c)本身是一个整数表达式。- 除数我们使用了
3.0(一个双精度浮点数),而不是3(一个整数)。这是因为在C语言中,两个整数相除会进行整数除法,结果会被截断为整数,可能导致精度丢失。而使用3.0会使整个表达式(a + b + c) / 3.0按照双精度浮点数规则进行计算。 - 因此,整个
(a + b + c) / 3.0是一个双精度浮点表达式,其结果被赋值给同样是double类型的变量average。这里不需要进行类型转换。
- 最后,程序输出计算得到的平均值。
程序运行后,输出结果为:
a = 5, b = 7, c = 6
average = 6.000000
正如预期,我们得到了浮点数结果 6.000000。
关于类型转换的说明:表达式
(a + b + c) / 3与(a + b + c) / 3.0的结果有本质区别。前者是整数除法,后者是浮点数除法。类型转换是一个重要且微妙的话题,我们将在后续课程中深入探讨。

总结
本节课中我们一起学习了C语言程序结构中的核心元素:声明与赋值。
- 我们了解了简单声明和带初始化的声明的语法。
- 我们认识了
int和double这两种基本数据类型。 - 我们通过一个计算平均值的实例,看到了声明、初始化、赋值以及表达式求值是如何在程序中结合使用的,并初步接触了整数除法与浮点数除法的区别。
- 我们强调了初始化变量的良好习惯,并预告了类型转换这一重要主题。
018:基本类型与sizeof运算符


在本节课中,我们将学习C语言中的基本数据类型,并了解如何使用sizeof运算符来获取这些类型在特定系统上所占用的内存大小。理解这些概念是编写高效且可移植代码的基础。
基本数据类型简介
上一节我们介绍了C语言编程的概览,本节中我们来看看其核心的基本数据类型。C语言包含多种类型,例如后续会学到的指针类型、结构体类型和枚举类型。但目前,我们将专注于一组相对简单的类型。
我们的早期计算将大量使用int、double和char类型。直观上,char可以表示特殊字符,如‘a’、‘N’或数字字符‘5’,这与数值5不同。它也可以表示不可打印的控制字符,如换行符\n。字符通常代表ASCII字符表中的内容,包括大小写字母、标点符号、运算符、数字以及换行符等特殊字符。
整数类型int与我们自小学以来使用的数字类似,例如0、-3或+77。
双精度浮点数double则更类似于科学计数法表示的数字,例如1.245或3.2E5(其中E表示指数,即10的5次方,所以结果是320,000)。
类型修饰符
接下来,我们将看到基本类型可以拥有进一步的修饰符。两个关键的修饰符是unsigned和long。
unsigned修饰符仅适用于整数类型,其含义是排除负数,只表示非负整数。
long修饰符可同时应用于整数和浮点数类型。它的作用是扩展数值的表示范围。例如,典型的int范围大约是±20亿,而long int可以提供更大的范围,以满足涉及更大数字的计算需求。
对于浮点数,我们实际上有三种类型:float、double和long double。在典型的实现中,它们的长度依次增加,因此能够存储越来越大的数字(以及更高的精度)。
计算机内存的表示限制
然而,计算机表示数字的方式与数学概念不同。例如,数学家概念中的π是一个无限不循环小数,但计算机的内存资源是有限的。内存以字节为单位组织,一个字节(byte)是内存的常规单位,通常由8个比特(bit)组成。
八个比特只能存储有限的信息。本质上,它们可以存储2的8次方种状态。计算机是二进制的,它们以某种电子或磁性方式存储比特(除非使用某些高级的量子计算机)。我们所有的现代数字计算机都使用以2为基数的比特来表示任何需要表示的内容。
例如,一个二进制序列00010100,从右向左,每一位代表2的幂次(2^0, 2^1, 2^2, 2^3...)。计算这个序列的值是20(0*2^7 + 0*2^6 + 0*2^5 + 1*2^4 + 0*2^3 + 1*2^2 + 0*2^1 + 0*2^0 = 16 + 4 = 20)。如果不是计算机,我们很容易算错。
类型大小与系统相关性
在典型机器上,一个int占用4个字节,可以用来表示大约从-20亿到+20亿的整数。更精确地说,其最大值是2^31 - 1(因为4字节对应32位二进制数,其中一位用于符号位,剩下31位用于数值部分,所以范围大约是±2^31)。
当你编写代码时,需要知道什么是可表示的。你可以使用C语言的一个内置运算符(关键字)sizeof来查明一个类型存储所需的大小。我们将演示这一点。
C语言的一个特点是,某些属性可能依赖于具体的系统。这是因为C语言试图针对底层计算机实现高效运行。它并不追求在所有平台上都有完全相同的语义,因为有些计算机的字长很大,有些则很小。C语言希望能在int大小为2字节的计算机上高效运行,同样也能在int大小为16字节的超级计算机上运行。
因此,你必须了解并意识到你正在特定的系统上运行。如果你是一个经验丰富的程序员,甚至可以通过编程来确保你的系统拥有适合你计算任务的数据类型大小,并且你可以使用sizeof运算符动态地查看这些大小。
使用sizeof运算符
以下是如何使用sizeof运算符的示例。你可以对类型名或变量名使用sizeof。
#include <stdio.h>
int main() {
printf("Size of long int: %zu bytes\n", sizeof(long int));
printf("Size of char: %zu bytes\n", sizeof(char));
char c;
printf("Size of variable c: %zu bytes\n", sizeof(c)); // 也可以对变量使用
printf("Size of float: %zu bytes\n", sizeof(float));
printf("Size of double: %zu bytes\n", sizeof(double));
printf("Size of long double: %zu bytes\n", sizeof(long double));
return 0;
}
在演示者使用的Macintosh系统上执行该程序,得到以下结果:
int:4字节long int:8字节char:1字节float:4字节double:8字节long double:16字节
你应该在你使用的任何系统上尝试运行这段代码,以了解其类型大小。
总结


本节课中我们一起学习了C语言的基本数据类型(char, int, double)及其修饰符(unsigned, long)。我们理解了计算机内存以有限字节存储数据的本质,以及由此带来的数值表示限制。最重要的是,我们掌握了sizeof运算符的用法,它可以返回类型或变量在当前系统上占用的内存字节数,这是编写可移植和高效C程序的关键工具。记住,类型的具体大小可能因系统而异,使用sizeof是获取准确信息的可靠方法。
019:char类型详解 🎼


在本节课中,我们将要学习C语言中的基础数据类型之一:char。char类型让我们能够处理大量的文本处理任务。
概述
char是构成字符串的基本元素。虽然我们稍后会详细学习字符串,但可以提前了解,字符串本质上是一个char类型的数组,并在末尾有一个哨兵字符(通常是零值)。因此,char是一个基础数据类型。
char字面量与ASCII码
一个char字面量或常量可以包含小写字母等字符。在代码中,它被表示为单引号包裹的形式,例如 'x'。
数字也可以作为字符,例如 '7'。如果没有引号,7 将被视为整型字面量。
此外,我们已经使用过一些非打印字符,这些字符不会直接显示在屏幕上,但会执行某些操作。最常见的例子是 \n,它表示换行。
你不需要记住所有字符的编码,但可以在大多数资料或网上找到标准ASCII码表。该表包含128个字符,其中包含可打印字符和一些能触发计算机系统特定操作的非打印字符。
例如,大写字母 'A' 的整数值是 65。在ASCII表中,大写字母是连续排列的,因此 'B' 是66,'C' 是67,依此类推,直到 'Z'(90)。同样,小写字母 'a' 的整数值是 97,一直连续到 'z'(122)。
这可能会让人有些困惑:当数字被用作字符时,它们也有对应的整数值。字符 '0' 的整数值是 48。十进制数字字符从 '0' 到 '9' 是连续排列的,对应的整数值范围是48到57。
键盘上的其他字符,如标点符号、运算符(如 $)等,在ASCII表中也都有对应的整数值。除非你有超强的记忆力,否则通常需要查阅表格。当然,你可以直接使用引号表示它们,编译器会处理其内部的整数值。
这些字符都可以存储在一个字节(8位)中,因此可以用一个char变量存储任何ASCII值。
非打印字符
非打印字符非常重要,它们能在计算机系统上触发特定操作。
\n:这是一个特殊的非打印字符。你不能直接使用n,因为那会被打印为字母“n”。反斜杠\起到了“转义”的作用。\n的整数值是10,它的作用是在屏幕上换到新的一行。\a:这也是一个非打印字符,整数值是7。如果在程序中使用它,系统会响铃或发出提示音。
以上就是可以使用的字符集。接下来,让我们通过一个简单的程序来演示如何使用char类型。
示例程序
我编写了这个程序,以一种简单的方式展示char的用法,并演示字符如何被打印。
#include <stdio.h>
int main() {
char c = 'a';
printf("The character %c has an ASCII value %d\n", c, c);
printf("Three consecutive characters: %c, %c, %c\n", c, c+1, c+2);
printf("\a\a\a");
return 0;
}
在这个程序中:
- 变量
c被声明为char类型,并被赋值为字符'a'。 - 第一个
printf语句使用%d格式说明符将c作为整数打印,因此会显示其ASCII值。接着,它又使用%c格式说明符将其作为字符打印。 - 第二个
printf语句展示了字符值是连续的。它打印了c、c+1和c+2。虽然c+1和c+2是整数表达式,但它们会被解释为字符。因此,程序会利用ASCII表来决定打印什么内容。 - 第三个
printf语句包含了三个\a(响铃字符)。如果你在本地系统上运行此程序,应该会听到三声不同的提示音或响铃。\a是一个非打印字符。当然,字符串中的\n也是一个非打印字符,它由printf函数解释并执行换行操作。如果我将\a放在这里,并尝试用%c打印它,那么在屏幕上将看不到任何输出,但会触发系统响铃。
让我们运行这个程序(假设已编译):
The character a has an ASCII value 97
Three consecutive characters: a, b, c
程序输出显示,字符 c(即 'a')的ASCII值是97,正如本课开头所述。当作为整数打印时,显示97;作为字符打印时,显示“a”。三个连续的字符被打印为“a”、“b”、“c”,符合预期。至于那三声响铃,虽然屏幕上没有打印出内容,但在本地运行时我听到了“叮、叮、叮”的声音,这正是打印那三个非打印字符 \a 的效果。
总结

本节课中,我们一起学习了C语言中的 char 数据类型。我们了解了字符字面量的表示方法、ASCII码表的结构(包括可打印字符和如 \n、\a 这样的非打印字符),并通过一个示例程序演示了如何声明 char 变量、如何以整数和字符两种格式打印它,以及如何利用字符的整数值进行连续字符的操作。理解 char 类型是后续处理字符串和文本数据的基础。
020:int类型详解

概述
在本节课中,我们将深入学习C语言中最重要的数据类型之一:int(整数类型)。我们将探讨int的不同变体、它们在内存中的存储方式、表示范围以及在实际编程中的使用注意事项。
数据类型的重要性
上一节我们介绍了数据类型的基本概念,本节中我们来看看int类型的具体细节。int是C语言中用于处理整数数据的基础数据类型。掌握int是理解更复杂数据类型(如浮点类型)的关键。在学习编程时,专注于像int这样典型且重要的部分,能帮助你高效地掌握大部分核心知识。
int类型及其变体
int类型有几个重要的变体,它们提供了不同的数据范围和存储方式。
以下是int的主要变体:
- short:使用更少字节,表示范围较小。
- long:使用更多字节,表示范围更大。
- unsigned:仅表示非负整数(正数和零),从而扩大了正数的表示范围。
整数的表示与范围
在现代典型机器上,一个int通常存储在32位(即4个字节)中。这意味着它可以表示大约正负21亿范围内的整数。具体范围是:
- 最大正数:2,147,483,647
- 最小负数:-2,147,483,648
这种表示范围的不对称性源于计算机中常用的二进制补码表示法。在这种表示法中,最高位(最左边的位)用于表示符号(0为正,1为负)。
对于unsigned int,由于所有位都用于表示数值本身,其表示范围变为从0到大约42亿。
字面量表示法
在代码中为不同整数类型赋值时,可以使用特定的后缀来明确指定字面量的类型。
以下是不同整数类型字面量的表示方法:
35:普通int35L或35l:long类型35U或35u:unsigned类型35UL或35ul:unsigned long类型
类型使用与常见陷阱
了解你正在使用的数据类型对于输入、输出和运算都至关重要。混合使用不同类型时,必须清楚每个操作数的数据域。
一个经典的编程错误与除法运算符/有关。在C语言中,/的行为取决于操作数的类型。
以下是整数除法与浮点数除法的区别:
- 整数除法:当两个操作数都是整数时,
/执行整数除法,结果会丢弃余数。例如:2 / 3的结果是0。 - 浮点数除法:当至少一个操作数是浮点数(如
float或double)时,/执行浮点数除法,结果会保留小数部分。例如:2.0 / 3、2 / 3.0或2.0 / 3.0的结果都是0.666...。
在计算整数数据的平均值时,如果忘记将其中一个操作数转换为浮点类型,就很容易犯这个错误。
代码示例与分析
让我们通过一段代码来具体说明这些概念。你可以运行并修改这段代码来加深理解。
#include <stdio.h>
int main() {
short short_a = 5;
int normal_a = 67;
unsigned unsigned_a = 67u;
long long_a = 67l;
printf("short_a = %hd, 除以整数 2 得:%d\n", short_a, short_a / 2);
printf("short_a = %hd, 除以浮点数 2.0 得:%f\n", short_a, short_a / 2.0);
printf("67 作为字符打印是:%c\n", normal_a);
printf("我的机器上:\n");
printf("short 类型的大小 = %lu 字节\n", sizeof(short_a));
printf("int 类型的大小 = %lu 字节\n", sizeof(normal_a));
printf("unsigned int 类型的大小 = %lu 字节\n", sizeof(unsigned_a));
printf("long 类型的大小 = %lu 字节\n", sizeof(long_a));
return 0;
}
运行这段代码,你可能会看到类似以下的输出:
short_a = 5, 除以整数 2 得:2
short_a = 5, 除以浮点数 2.0 得:2.500000
67 作为字符打印是:C
我的机器上:
short 类型的大小 = 2 字节
int 类型的大小 = 4 字节
unsigned int 类型的大小 = 4 字节
long 类型的大小 = 8 字节
输出分析:
short_a / 2执行整数除法,结果为2。short_a / 2.0执行浮点数除法,结果为2.5。- 整数
67对应的ASCII字符是大写字母C。 sizeof运算符显示了不同数据类型在当前机器上所占的字节数,这与之前的描述一致。

总结
本节课中我们一起学习了C语言的int整数类型。我们了解了int及其变体(short、long、unsigned)的用途、表示范围和字面量写法。我们重点分析了整数除法与浮点数除法的关键区别,这是一个常见的编程陷阱。最后,通过代码示例,我们直观地验证了这些理论概念,并学会了如何使用sizeof运算符查看类型大小。理解这些基础知识是进行有效C语言编程的重要一步。
021:浮点数详解

在本节课中,我们将要学习C语言中的浮点数类型。我们将了解浮点数的不同种类、它们的表示方式、精度限制以及如何在程序中进行输入和输出。
浮点数类型
上一节我们介绍了整数类型,本节中我们来看看浮点数类型。浮点数用于表示带有小数部分的数字。
C语言中有三种浮点数类型:float、double和long double。这三种类型通常按精度从低到高排序,但请注意,C语言标准并不保证这一点,具体实现可能因系统而异。
浮点数的范围和精度
以下是三种浮点类型的典型特性:
float:这是最短的类型。其范围通常从 10⁻³⁸ 到 10³⁸。它可以提供大约6位有效数字。例如,地球人口约70亿,可以表示为7.0e9或0.7e10。double:在大多数机器上,double类型的大小是float的两倍(通常为8字节)。其范围与float类似,但能提供大约15位有效数字,足以表示宇宙中的原子数量。long double:当实现时,long double通常比double更大。它可能保持相同的指数范围,但提供更多的有效数字。例如,在某些IBM实现中,它提供31位有效数字。
C语言编译器保证:double 的精度至少与 float 一样高,long double 的精度至少与 double 一样高。在早期系统中,这三种类型可能相同;而在现代超级计算机上,它们可能有明显区别。
声明变量与书写字面量
你可以声明任何这些类型的变量。float 最节省空间,double 是典型选择,long double 则提供额外的精度。
书写浮点数字面量(常量)时,必须包含小数点或指数部分,否则会被视为整数。以下是表示数字1.0的几种方式:
1.01.0.1e1
其中,0.1e1 表示 0.1 × 10¹。使用 1.0 最为清晰,不易混淆。
理解精度的重要性
理解浮点数的精度限制至关重要。在计算中,浮点数并不等同于数学中的实数。
在数学中,你可以无限精确地表示π(3.14159...)。但在标准的 double 表示中,你只能得到大约15位有效数字,因此会损失一些精度。
精度问题可能在实际计算中显现。例如,考虑以下计算:
1.0e12 + 0.1
由于 1.0e12 这个数非常大,加上 0.1 后,结果可能没有任何变化,因为 0.1 被大数“淹没”了。这意味着在某些表达式中,小数的加法可能不产生效果。
在数值分析等高级课程中,专家需要跟踪精度损失。此外,某些在十进制下看起来精确的数字(如 1.333...),在转换为浮点数使用的二进制内部表示时,可能无法保持相同的精度。
浮点数的输入与输出
最后,我们需要了解如何对这些浮点类型进行输入和输出操作,这意味着需要知道格式转换字符。
以下是可用的转换字符:
%e或%E:以科学计数法打印数字,包含小数部分和指数部分(例如1.234560e+02)。%f:以十进制小数形式打印数字,不包含指数部分(例如123.456)。%g或%G:这是一种更灵活的格式。它会根据数值自动选择%e或%f中能产生最短表示形式的那一种。
在下一节中,我将运用这些概念编写一小段代码,演示如何打印 double 类型数据,以及如何使用数学库(包含 sqrt、sin 等函数)。你也可以提前查阅资料,了解数学库的使用方法。

本节课中我们一起学习了C语言的浮点数类型,包括 float、double 和 long double 的区别、表示方法、精度限制以及输入输出的格式控制。理解这些概念对于进行精确的数值计算至关重要。
022:逻辑运算符、表达式和短路求值

在本节课中,我们将学习逻辑运算符、逻辑表达式以及C语言中一个重要的概念——短路求值。我们将通过一个决定是否带雨伞的程序来理解这些概念。
上一节我们介绍了控制流的基本概念,本节中我们来看看如何使用逻辑运算符来构建更复杂的条件判断。
程序概述:是否带伞
这个程序将根据用户是否在室外以及天气是否下雨,来决定是否建议携带雨伞。我们使用两个变量:
outside:1 代表在室外(真),0 代表在室内(假)。weather:1 代表下雨(真),0 代表不下雨(假)。
程序的核心逻辑是:只有当我们在室外(outside 为真)并且正在下雨(weather 为真)时,才需要带伞。这正是一个逻辑“与”运算。
逻辑“与”运算符
逻辑“与”运算符在C语言中用 && 表示。其运算规则是:只有当运算符两边的表达式都为真时,整个表达式的结果才为真。
以下是逻辑“与”运算的真值表:
- A 为真,B 为真:
A && B结果为 真。 - A 为真,B 为假:
A && B结果为 假。 - A 为假,B 为真:
A && B结果为 假。 - A 为假,B 为假:
A && B结果为 假。
在我们的程序中,条件判断可以写为:
if (outside && weather) {
printf("Please use an umbrella.\n");
} else {
printf("Dress normally.\n");
}
只有当 outside 和 weather 都为 1(真)时,才会执行打印“请带伞”的语句。其他三种情况都会执行“正常着装”。
短路求值
C语言在计算逻辑表达式时采用“短路求值”策略。对于逻辑“与”运算 A && B:
- 如果表达式 A 的求值结果为假,那么无论表达式 B 的结果是什么,整个
A && B表达式的结果都已经是假。 - 因此,编译器将不会再去计算表达式 B。
这被称为短路求值。它不仅能提升程序效率(避免不必要的计算),在某些情况下还能防止因计算B而可能引发的错误(例如,当B包含除以零的操作时)。
在我们的例子中,如果 outside 为 0(假,表示在室内),程序会立即判定整个条件为假,直接跳转到 else 分支,而完全不会去检查 weather 变量的值。
运算符优先级
关于 && 运算符,还需要了解它的优先级。它的优先级非常低,低于所有的算术运算符(如 +, -, *, /)和关系运算符(如 >, <, ==)。
这意味着,如果逻辑表达式的参数中包含数学运算,这些数学运算会先于逻辑“与”运算被执行。你通常不需要刻意记忆优先级表,在编写复杂表达式时,使用括号 () 来明确指定运算顺序是最清晰、最安全的方式。
例如:
if ((x + 5) > 10 && (y - 2) < 5) {
// 先计算 x+5 和 y-2,再进行大小比较,最后进行逻辑与判断
}


本节课中我们一起学习了逻辑“与”运算符 && 的用法及其真值表,并通过一个生活化的例子理解了C语言中“短路求值”的工作原理。记住,短路求值是一种重要的优化和安全机制。最后,我们提到 && 的优先级较低,在复杂表达式中使用括号可以确保逻辑清晰无误。
023:条件语句-if和if-else

概述
在本节课中,我们将要学习C语言中的条件语句,特别是if和if-else语句。这是编写非线性程序的基础,它允许程序根据特定条件决定执行不同的代码路径。
到目前为止,我们编写的程序都是线性的,即从第一行开始,顺序执行到return 0;结束。但并非所有程序都如此简单。能够测试某个条件,并根据结果决定执行不同的操作,是非常有用的功能。本节课我们将学习最简单的形式:在两个备选方案中选择其一。这是编程的基础,后续我们还将学习循环结构。
第一个非线性程序示例
我们将编写第一个使用if语句的程序。这个程序的核心是一个if-else语句。程序非常简单:它要求用户输入一个整数速度值,然后根据输入的速度值,判断用户是否会收到超速罚单。
以下是程序代码:
#include <stdio.h>
int main() {
int speed;
printf("Enter your speed as an integer: ");
scanf("%d", &speed);
if (speed < 65) {
printf("\nNo speeding ticket.\n");
} else {
printf("\nSpeeding ticket.\n");
}
return 0;
}
假设我已经编译好了这个程序。在程序底部,它会提示“Enter your speed as an integer:”。如果我输入78,程序会输出“Speeding ticket.”。如果我再次运行程序并输入62,程序则会输出“No speeding ticket.”。
if-else语句详解
现在,让我们更详细地探讨如何正确使用if-else表达式。
if-else语句的一般语法结构如下:
if (expression) {
// 条件为真时执行的语句
} else {
// 条件为假时执行的语句
}
其中的expression可以是任意表达式,但通常是我们所说的关系表达式。这个表达式会被求值。如果其结果为0,则被视为假;如果结果为非零值,则被视为真。
在我们刚刚编写的程序中,关系表达式是speed < 65。例如,如果你输入速度50,那么50 < 65为真,表达式求值结果为1,程序将执行printf("\nNo speeding ticket.\n");。同样,如果你输入的速度恰好是65或更大,那么speed < 65不为真,表达式求值结果为0,程序将执行else分支的代码,输出“Speeding ticket.”。
这个条件可以修改。假设你犯了个错误,认为恰好65不算超速(在加州,许多高速公路限速65英里/小时)。那么条件可以改为speed <= 65。这是一个不同的关系,它包含了65。这样,输入65时条件依然为真。
关系运算符
C语言提供了四种基本的关系运算符:
<(小于)>(大于)<=(小于或等于)>=(大于或等于)
你应该在高中数学中熟悉它们。了解它们的优先级也很重要。关系运算符的优先级低于算术运算符(如+、-、*、/、%)。这意味着,如果你看到表达式speed < 65 + i,子表达式65 + i会先被计算,然后再与speed进行比较。这相当于你给65 + i加上了括号。虽然你不必显式地加括号,但必须理解优先级规则。不过,为了代码更清晰,显式地加上括号可能更好。
这种写法意味着先进行加法运算,加上一个增量i,然后再与speed比较。这个i可能是交警给予的宽容增量,例如在你的社区可能是4英里或更多。这样设置条件可能更符合实际收到超速罚单的情况。
简单的if语句
顺便提一下,还有一种更简单的条件语句:只有if。在if语句中,语法是if (expression) { statement; }。如果表达式求值为真(非零),则执行后面的语句;如果求值为假(0),则跳过该语句。
实际上,你可以将if-else语句拆分成两个独立的if语句来实现类似功能,但这通常不够优雅,并且效率稍低,因为你需要对关系表达式(如speed < 65)求值两次。


总结
本节课中,我们一起学习了C语言中if和if-else条件语句的基础知识。我们了解了如何编写非线性程序,使程序能够根据条件(通常是关系表达式的结果)做出决策。我们探讨了if-else语句的语法、关系运算符的使用及其优先级,并比较了if-else与单独使用if语句的区别。这是控制程序流程的第一步,为后续学习更复杂的循环和分支结构打下了基础。
024:迭代语句 - while循环

概述
在本节课中,我们将要学习C语言中一个至关重要的概念:迭代。迭代是使计算机变得强大的核心,它允许程序重复执行一系列操作,从而处理海量数据或复杂任务。我们将重点探讨最基本的迭代语句——while循环,理解其语法、执行逻辑以及如何避免常见错误。
什么是迭代?
如果所有计算都简单到只需几行指令就能一步步完成,那么我们可能就不需要计算机了。计算机之所以强大,是因为它能在极短时间内执行数百万甚至数万亿次操作,这是人类或机械计算器无法做到的。因此,掌握迭代是编写有用程序的关键。
上一节我们介绍了程序的基本结构,本节中我们来看看如何让程序“重复工作”。
while循环:语法与语义
while语句是最基础的迭代形式。理解它将有助于后续学习更复杂的for循环。
其基本语法结构如下:
while (表达式) {
// 语句块
}
以下是其执行语义(即工作原理):
- 首先,检查表达式的值。
- 如果表达式的结果为0(在C语言中代表“假”),则跳过整个循环体中的语句,直接执行循环之后的代码。
- 如果表达式的结果为非0(代表“真”),则执行循环体
{}内的所有语句。 - 执行完循环体后,流程会跳回第一步,重新检查表达式的值。
- 这个过程会一直重复,直到某次检查发现表达式结果为0,循环才会终止。
一个经典的while循环示例
让我们通过一个具体例子来理解。以下代码将打印数字0到9:
int i = 0; // 初始化一个计数器
while (i < 10) { // 测试条件:i 是否小于 10?
printf("i = %d\n", i); // 循环体:打印当前 i 的值
i++; // 循环体:将 i 的值增加 1。这是改变循环状态的关键步骤!
}
代码执行流程分析:
i初始值为0。表达式i < 10为真,进入循环。- 打印
i = 0,然后将i自增为1。 - 跳回测试
i < 10(此时i=1),为真,继续循环。 - 此过程重复,依次打印1, 2, 3...直到
i变为10。 - 当
i等于10时,表达式i < 10为假,循环终止。
这个简单的循环包含了复杂循环的所有核心特征:一个控制变量(i)、一个测试条件(i < 10)和循环内部执行的动作(打印和自增)。
while循环的执行流程图
传统上,我们用流程图来可视化循环的执行逻辑。下图展示了while循环的流程:
+-------------------+
| 开始 while 循环 |
+-------------------+
|
v
+-------------------+
| 计算表达式值 |
| (例如:i < 10) |
+-------------------+
|
+--------+--------+
| |
值为0 (假) 值非0 (真)
| |
v v
+-----------+ +-----------+
| 退出循环, | | 执行循环体 |
| 执行后续代码 | | 内的语句 |
+-----------+ +-----------+
|
v
+-----------+
| 返回并重新 |
| 测试表达式 |
+-----------+
流程说明:程序进入while语句后,先进行条件测试。若为假则退出;若为真则执行循环体,执行完毕后返回并再次测试条件,形成循环。
重要注意事项:避免无限循环
在循环中,必须确保循环条件有朝一日会变为假。如果循环条件永远为真,程序就会陷入无限循环。
例如,如果在上面的例子中忘记写i++;这条语句,i的值将永远为0,i < 10也永远为真,循环会永不停止地打印i = 0。
无限循环会耗尽计算机资源,通常意味着程序中存在逻辑错误,需要仔细检查循环体内的代码是否正确地改变了影响测试条件的变量。
总结
本节课中我们一起学习了C语言中while循环的核心知识。我们了解到:
- 迭代是编程的核心,使计算机能高效处理重复任务。
while循环的语法是while (表达式) { 语句块 }。- 其执行逻辑是:先判断,后执行,执行完再返回判断,直到条件不满足。
- 一个典型的循环包含初始化、条件测试和状态更新三个部分。
- 必须小心避免因忘记更新循环变量而导致的无限循环。



掌握while循环为理解所有迭代结构打下了坚实基础。在接下来的课程中,我们将学习另一种更紧凑的迭代语句——for循环。
025:while循环与字符计数程序详解

在本节课中,我们将详细解析一个使用while循环进行字符分类统计的C语言程序。我们将学习如何读取输入、判断字符类型以及使用逻辑表达式进行计数。
概述
我们将创建一个程序,用于统计输入文本中空格、数字、字母以及其他字符的数量。程序的核心是使用getchar()函数读取字符,并通过一系列if-else if条件判断进行分类计数。
代码结构详解
上一节我们讨论了程序的基本逻辑,本节中我们来看看具体的代码实现细节。
首先,程序需要为每种待统计的字符类型创建变量。由于初始时没有任何计数,所有变量都将被初始化为0。
int blanks = 0, digits = 0, letters = 0, others = 0;
接下来,程序使用变量c来存储读取到的字符的整数值。在C语言中,字符本质上是介于0到127之间的整数,对应标准的ASCII字符表。熟悉这个字符表对编程很有帮助。
程序从标准库中调用一个非常方便的函数getchar()。该函数定义在stdio.h头文件中,无需参数,其作用是从标准输入(通常是键盘或重定向的文件)获取一个字符。
#include <stdio.h>
...
int c;
while ((c = getchar()) != EOF) {
// 处理字符
}
这里,我们通过一个while循环持续读取字符,直到遇到文件结束符(EOF)。EOF是一个预定义的常量,通常值为-1,它位于ASCII字符范围之外,用于标识输入流的结束。
字符分类逻辑
在while循环内部,我们使用一个复杂的if-else if语句链来判断字符类型。虽然这个while循环体被写成了复合语句(用花括号包裹),但从语法上讲,即使内部只有一个if-else if结构,不写花括号也是可以的,使用花括号主要是为了风格清晰。
以下是判断逻辑的核心部分:
if (c == ' ') {
++blanks;
} else if (c >= '0' && c <= '9') {
++digits;
} else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
++letters;
} else {
++others;
}
- 第一个条件判断字符是否为空格。
- 第二个条件判断字符是否为数字(‘0’到‘9’)。在ASCII表中,这些数字字符的编码是连续递增的。
- 第三个条件判断字符是否为字母。这里使用了一个更复杂的逻辑表达式。需要注意的是,在ASCII表中,小写字母‘a’到‘z’是连续的,大写字母‘A’到‘Z’也是连续的。因此,我们可以用两个子条件通过逻辑或(
||)连接起来进行判断。
关于逻辑表达式,需要理解运算符优先级和“短路求值”:
- 逻辑与(
&&)的优先级高于逻辑或(||)。 - “短路求值”意味着:在逻辑或表达式中,如果第一个子条件为真,整个表达式已确定为真,第二个子条件将不再被计算。在逻辑与表达式中,如果第一个子条件为假,整个表达式已确定为假,第二个子条件也将不再被计算。这提高了程序效率。
程序输出与运行
所有字符处理完毕后,程序简单地打印出各类字符的统计结果。
printf("blanks = %d, digits = %d, letters = %d, others = %d\n", blanks, digits, letters, others);
运行此程序时,可以使用输入重定向功能使其从一个文件中读取数据,而不是从键盘输入。例如,对程序自身的源代码文件运行此程序,可能会得到类似这样的输出:blanks = 196, digits = 17, letters = 302, others = 152。
“其他字符”的计数可能包括换行符、制表符等不可打印字符,以及所有既不是空格、数字,也不是字母的可打印字符。
总结



本节课我们一起学习了如何构建一个字符统计程序。我们掌握了使用while循环和getchar()读取输入流,利用ASCII码值的连续特性以及if-else if语句链和逻辑表达式对字符进行分类,并通过变量递增完成计数。理解“短路求值”有助于编写更高效的逻辑判断代码。这个程序是学习基础I/O操作和流程控制的经典示例。
026:while循环代码示例
概述
在本节课中,我们将深入学习C语言中的while循环。while循环是迭代的核心结构,我们将通过一个简单的“情人节”程序来演示其工作原理。我们将分析代码的关键部分,并理解如何通过循环控制重复执行一段代码。
程序示例:情人节表白程序
我们将编写一个程序,它询问用户“你有多爱对方”,并根据输入的数字,重复打印“非常”这个词,最后以“爱你”结束。这个程序旨在演示while循环的基本用法。
以下是程序的核心代码结构:

#include <stdio.h>
int main() {
int repeat;
printf("How strong is your love? (1-10): ");
scanf("%d", &repeat);
printf("I love you ");
while (repeat > 0) {
printf("very ");
repeat--;
}
printf("much.\n");
return 0;
}
代码解析
上一节我们介绍了程序的目标,本节中我们来看看代码的具体实现细节。
变量声明与初始化
程序首先声明了一个整型变量repeat。这个变量将决定循环重复的次数,即“非常”这个词被打印的次数。
用户输入
程序使用printf和scanf函数与用户交互。scanf(“%d”, &repeat)语句用于读取用户输入的一个整数,并将其存储在变量repeat中。
while循环结构
while循环是C语言中最基本的循环结构。其语法格式为:
while (condition) { statement; }
只要condition(条件)为真,循环体内的statement(语句)就会重复执行。
在我们的程序中,循环条件是 repeat > 0。只要repeat的值大于0,循环就会继续。
循环体与迭代控制
循环体内执行两个操作:
printf(“very “);:打印单词“very”。repeat--;:这是一个自减运算符,它将变量repeat的值减1。这是控制循环次数的关键,它确保循环在执行指定次数后,条件repeat > 0会变为假,从而终止循环。
循环后的输出
当while循环结束后,程序执行最后的printf(“much.\n”);语句,完成整个表白消息的输出。
程序执行示例
让我们看看程序在不同输入下的运行结果。
以下是几种可能的输入和对应的输出:
- 输入
10:输出为 “I love you very very very very very very very very very very much.”(打印10次“very”)。 - 输入
1:输出为 “I love you very much.”(打印1次“very”)。 - 输入
0:输出为 “I love you much.”(不进入循环,直接打印“much”)。 - 输入负数(如
-5):输出为 “I love you much.”(条件repeat > 0初始即为假,不进入循环)。


总结
本节课中我们一起学习了while循环的实践应用。我们通过一个有趣的情人节程序,理解了如何声明循环控制变量、编写循环条件、在循环体内执行操作并更新控制变量。while循环是迭代的基础,掌握它对于理解更复杂的循环结构(如for循环)至关重要。你可以尝试修改这个程序,创造属于自己的版本。
027:for语句及其while模拟

概述
在本节课中,我们将要学习C语言中的for循环语句。我们将了解for循环的基本语法和工作原理,并将其与之前学过的while循环进行比较,理解两者之间的等价关系。
for循环简介
上一节我们介绍了while循环,本节中我们来看看一个更紧凑的循环语句——for循环。
for循环的行为与while循环非常相似,但语法更简洁。你可以将其流程理解为:进入循环,执行测试。如果测试结果为假(零值),则退出循环并执行下一条语句;否则,执行循环体内的语句,然后返回并再次测试,如此循环,直到测试结果为假时退出。
for循环示例
以下是一个简单的for循环示例:
for (i = 1; i <= 5; i++) {
printf("count %d sheep\n", i);
}
关键字for后面跟着括号,括号内由三个用分号分隔的表达式组成。在本例中,循环体会执行打印语句。其概念很简单:循环开始时i被初始化为1。然后测试i是否小于等于5。如果条件为真,则执行循环体,打印“count 1 sheep”。在循环体执行完毕后、下一次测试之前,会执行第三个表达式i++,将i增加为2。接着进行新的测试。第一个初始化表达式只在进入循环时执行一次,而测试和增量表达式则在每次循环迭代时都会执行。
因此,我们将看到输出:count 1 sheep, count 2 sheep... 直到count 5 sheep。当i等于6时,测试条件i <= 5为假,循环终止,程序继续执行后面的语句。
for循环的通用语法
现在让我们看看for语句的通用语法。
for (E1; E2; E3) {
statement;
}
关键字for后面是表达式1(E1)、分号、表达式2(E2)、分号、表达式3(E3)。然后是要执行的语句,通常是一个用花括号括起来的复合语句块。
- E1 最常用作初始化器,就像简单示例中的
i = 1。 - E2 始终是循环终止条件。如果循环需要在有限次数内结束,这个条件最终必须为假,否则会导致“无限循环”。
- E3 通常是增量或减量操作。终止条件通常涉及一个整数变量,通过递增或递减该变量来最终满足终止条件。
for循环与while循环的等价关系
理解for循环如何映射到while循环非常重要。
for循环的等价while形式如下:
E1;
while (E2) {
statement;
E3;
}
以下是映射关系:
- 表达式E1作为单独的语句首先执行(通常是初始化)。
- while循环的测试条件是E2。
- 执行循环体语句。
- 在循环体内部(通常作为复合语句的一部分)执行E3(通常是增量/减量操作)。
由此可见,for语句更简洁,它将所有相关元素组合到了一个语句中。原本需要多条语句完成的事情,用for语句写起来语法上更简单。但两种语句可以互相替代。
详细示例分析
让我们做一个更详细的例子。
int sum = 0;
int n = 5;
for (i = 1; i < n; i++) {
sum = sum + i; // 也可写作 sum += i;
}
printf("sum = %d\n", sum);
我们将求和变量sum初始化为0,将终止条件变量n设为5。for循环从i=1开始,测试条件是i < n(注意是小于,不是小于等于),每次循环i自动递增。
循环将从1开始,递增到5时条件i < 5为假,因此不会导致无限循环。循环内部,sum被替换为sum + i。请注意,这也可以使用复合赋值运算符sum += i来完成。
循环结束后(注意printf语句没有缩进,与for处于同一层级),我们将打印结果。
执行此代码,你将看到最终sum等于10,i等于5。循环内部过程如下:
- 第一次:
sum变为1(i=1)。 - 第二次:
sum变为3(i=2)。 - 第三次:
sum变为6(i=3)。 - 第四次:
sum变为10(i=4)。 - 当
i变为5时,不满足测试条件i < 5,循环退出。
练习建议
你应该练习使用for循环。可以尝试以下操作:
- 将上面这个循环写成一个真正的程序。
- 在循环内外添加打印语句来观察变量变化。
- 修改循环,尝试使用递减操作而不是递增。


总结
本节课中我们一起学习了C语言的for循环。我们了解了for循环的语法结构,包括初始化、测试和增量三个表达式。我们通过示例分析了其执行流程,并掌握了for循环与while循环之间的等价转换关系。for循环提供了一种更紧凑的方式来编写具有明确初始化、条件和更新步骤的循环。
028:for语句代码示例

概述
在本节课中,我们将学习一个使用for循环语句的简单程序。这个程序会读取一个文件,并统计其中的空格、数字以及字符总数。我们将通过重定向功能来运行这个程序,并详细解析for循环的语法结构。
一个简单的for循环程序
我们将编写一个与之前while循环类似的程序。这个程序的目标是统计文件中特定字符的数量。
以下是程序的核心逻辑:
- 统计空格数量。
- 统计数字数量。
- 统计字符总数。
我们将使用整型变量c来存储从文件中读取的每一个字符。
for循环的语法细节
现在,我们来看看这个for循环中一个有趣的语法点。
for ( ; (c = getchar()) != EOF; ++nc)
在这个for语句中,我们将表达式1留空了。你只能看到一个分号。当表达式1为空时,意味着没有初始化步骤。我们不需要在这里进行初始化,因为字符总数nc已经在循环外被初始化为0,并且它不参与循环条件的判断。
循环条件的判断方式与while语句相同:getchar()函数从标准I/O库中读取一个字符,通常返回其ASCII值。当遇到文件结束符(EOF,通常定义为-1)时,循环终止。
在每次循环的末尾,字符总数nc会自增1(从0开始计数)。
字符判断与计数逻辑
接下来,程序会判断读取到的字符类型。
以下是判断和计数的代码结构:
if (c == ' ')
++nb;
else if (c >= '0' && c <= '9')
++nd;
我们首先测试字符是否为空格。如果不是空格,则测试它是否为数字(‘0’到‘9’之间)。这里的判断逻辑和之前一样,你可以根据需要扩展这个程序,使其更复杂。
虽然这里的if和else if各自只控制一条自增语句,不需要用花括号{}包裹成复合语句,但有时使用花括号是良好的编程习惯,可以使代码结构更清晰。
在代码格式上,我通常使用三个空格来缩进,以显示控制流的层级。有些人使用四个或五个空格,也经常使用制表符(Tab)。在这个例子中,for语句控制着if语句,而if语句控制着增加空格数的子语句,else if则控制着增加数字数的子语句。
输出结果与验证
当循环因到达文件末尾而退出时,我们就可以打印统计结果了。
让我们来演示一下程序是如何工作的。我将通过重定向功能,将我们刚才编写的源代码文件本身作为输入。
运行程序后,输出结果显示:共有141个字符,16个空格,字符总数为543。
为了验证数字统计的准确性,我们可以手动检查一下文件中的数字数量。经过清点,文件中确实有16个数字,这证明我们的程序运行正确。


总结
本节课我们一起学习了如何使用for循环语句来构建一个字符统计程序。我们分析了for循环中表达式留空的语法,实践了字符的条件判断与计数,并通过重定向验证了程序的正确性。建议你尝试自己编写这个程序,并可以进一步扩展其功能,例如增加对其他字符类型(如字母)的统计。
029:奇特运算符 - 条件运算符和逗号运算符

在本节课中,我们将学习C语言中两个较为特殊的运算符:条件运算符(三元运算符)和逗号运算符。我们将了解它们的语法、语义、使用场景以及需要注意的细节。
条件运算符(三元运算符)
上一节我们介绍了各种运算符,本节中我们来看看一个独特的运算符——条件运算符。它是C语言中唯一的三元运算符,其语法形式为 表达式1 ? 表达式2 : 表达式3。
其工作原理是:首先计算表达式1,该表达式通常是一个关系或逻辑表达式,其结果为0(假)或非0(真)。如果表达式1的结果为真(非0),则整个条件表达式的值为表达式2的值;如果为假(0),则整个表达式的值为表达式3的值。
以下是一个使用示例:
c = (a < b) ? a : b;
这段代码等价于一个if-else语句:
if (a < b) {
c = a;
} else {
c = b;
}
条件运算符的优势在于简洁,可以用一行代码完成简单的条件赋值。它的优先级非常低,在C语言的16个优先级中排在第14位(倒数第三),这意味着在复杂表达式中,它通常最后被计算。
逗号运算符
了解了条件运算符后,我们再来看看另一个特殊的运算符——逗号运算符。它的语法形式为 表达式1, 表达式2。
逗号运算符的语义是:
- 首先计算
表达式1。 - 然后计算
表达式2。 - 整个逗号表达式的最终值是
表达式2的值。
逗号运算符是一个序列点,这意味着表达式1的所有副作用(例如赋值、自增等)都必须在计算表达式2之前完成。这对于保证程序执行顺序的确定性非常重要,因为C语言中许多运算符(如加法+)的操作数计算顺序是未定义的,编译器可能为了优化而改变顺序。
逗号运算符的优先级是所有运算符中最低的,甚至低于赋值运算符。
以下是一个逗号运算符的示例:
c = (a = 0, b = 1);
执行后,变量a被赋值为0,变量b被赋值为1,整个括号表达式的值为1(即b的值),因此变量c也被赋值为1。
逗号运算符最常见的、也是被广泛接受的用法是在for循环中,用于同时初始化多个变量。
以下是for循环中使用逗号运算符的惯用示例:
int sum, i;
for (sum = 0, i = 1; i <= 50; i++) {
sum += i;
}
在这个循环中:
sum = 0, i = 1是初始化部分,它利用逗号运算符同时将sum初始化为0,将i初始化为1。i <= 50是循环条件。i++是每次迭代后的步进操作。
这个循环的功能是计算从1到50的整数和。
总结
本节课中我们一起学习了C语言中两个特殊的运算符。
- 条件运算符(
? :) 是唯一的三元运算符,用于根据条件选择两个表达式中的一个,其优点是代码简洁。 - 逗号运算符(
,) 用于顺序执行两个表达式,并以第二个表达式的值作为结果。它是一个序列点,能保证执行顺序,最常见的用途是在for循环的初始化部分。


理解这两个运算符的语法和语义,有助于你编写更简洁、高效的C语言代码。
030:三元运算符代码示例

概述
在本节课中,我们将学习如何使用三元运算符来改进一个判断超速罚单的程序。我们将把之前使用switch语句的程序,改写为使用嵌套三元运算符的版本,使其能更灵活地处理速度范围,而非固定的几个数值。
上一节我们介绍了三元运算符的基本语法,本节中我们来看看如何在实际代码中应用它,特别是处理多个条件的情况。
代码解析与改写
以下是原程序的逻辑:它根据用户输入的速度,判断属于哪种罚单情况。原程序使用switch语句,但只能精确匹配65、70、90这三个值,不够灵活。
我们将使用嵌套三元运算符来实现基于范围的判断。核心思路是:
- 首先判断速度是否小于65(不超速)。
- 如果不满足条件1,则进一步判断速度是否小于等于70(普通超速)。
- 如果以上都不满足,则属于严重超速。
对应的嵌套三元运算符代码如下:
int result = (speed < 65) ? 65 : ((speed <= 70) ? 70 : 90);
这段代码的含义是:如果 speed < 65 为真,则 result 为65;否则,判断 speed <= 70,如果为真则 result 为70,否则 result 为90。
接下来,我们根据result的值来输出相应的罚单信息。由于三元运算符已经确保了result的值只会是65、70或90,因此switch语句中可以不再需要default分支。
以下是完整的程序逻辑步骤:
- 提示用户输入速度。
- 使用上述嵌套三元运算符计算一个代表罚单类型的值(65, 70, 90)。
- 使用
switch语句根据这个值打印对应的罚单信息。
程序测试
让我们运行程序来测试一下。在程序底部,我们会输入不同的速度值进行验证。
- 输入速度45,程序输出“没有罚单”。
- 输入速度98,程序输出“高额罚单”。
- 输入速度66,程序输出“普通罚单”。
测试结果表明,程序能正确根据速度范围输出对应的结果。
总结
本节课中我们一起学习了如何利用嵌套三元运算符来简化多条件判断的逻辑。通过将之前的switch案例改写成基于范围判断的版本,我们使程序变得更加灵活和健壮。这个例子展示了三元运算符在使代码更简洁、表达更清晰方面的优势。

你可以尝试修改这个程序,例如增加对非法速度(如负数)的检查,并让default分支来处理这种意外情况,这将是一个很好的练习。
031:break、continue与switch语句

在本节课中,我们将学习C语言中用于控制程序流程的另外两个重要语句:break和continue,并重点介绍switch语句的结构与用法。这些工具能帮助我们编写更清晰、更高效的条件分支代码。
控制流语句:switch
上一节我们介绍了基础的if-else条件判断。本节中,我们来看看switch语句。从某种程度上说,switch是if-else的一种泛化,因为你可以用if-else实现几乎相同的功能。但对于某些特定的控制流程,switch语句在结构上更为优雅,生成的代码也更具可读性。
switch语句基于一个整型表达式进行判断。这个表达式可以是一个简单的整数变量i,然后根据i的不同值(通常是较小的值),执行不同的代码块。
让我们看一个例子。
以下是switch语句的基本语法示例:
switch (i) {
case 1:
a = 2 * i;
break;
case 2:
a = 3 * i;
break;
default:
a = 0;
}
在这个例子中:
- 关键字
switch后跟着一个整型表达式i,在进入switch之前,i必须已被赋值。 - 我们定义了三个
case(情况)。每个case以关键字case开头,后跟一个整型常量(例如1),然后是一个冒号。 - 冒号后面是一系列在该
case匹配时要执行的语句。 - 每个
case块通常以break;语句结束。break的作用是:执行到这里时,直接跳出整个switch语句,继续执行右大括号}之后的代码。这是switch的标准写法。
关于“贯穿”行为
如果没有break语句,就会发生“贯穿”行为。例如,如果删除了case 1:后面的break,那么当i等于1时,程序会执行a = 2 * i;,然后继续执行case 2:中的代码a = 3 * i;,直到遇到下一个break或switch结束。这通常不是我们想要的结果,因此需要谨慎使用。
default 分支
default是一个特殊的关键字,代表所有未被前面case匹配到的值。它类似于if-else链中的最后一个else。如果switch表达式的值不匹配任何case,程序就会跳转到default分支执行。如果代码中没有default分支,那么switch语句将不执行任何操作,直接结束。
语法要点总结
switch后的表达式必须是整型(如int、short、long、char),不能是浮点型。case标签必须是整型常量。switch语句体必须用大括号{}包围。- 通常,每个
case分支都以break;结尾,以防止“贯穿”。 - 使用
default分支来处理所有其他情况是一个好习惯。

本节课中我们一起学习了break和continue在循环中的基本作用,并深入探讨了switch语句的语法、执行流程以及break在其中防止“贯穿”的关键作用。掌握这些控制语句,能让你更灵活地编写条件分支逻辑。
032:函数定义

概述
在本节课中,我们将要学习C语言中一个至关重要的概念:函数。我们将了解函数是什么、为什么它们如此重要、如何定义函数以及如何调用函数。理解函数是编写结构化、可重用和易于维护代码的关键。
什么是函数?📚
上一节我们介绍了编程的基本结构,本节中我们来看看构成复杂程序的基石——函数。
函数可以被看作是代码的“段落”。正如段落将句子组织成连贯的思想一样,函数将单个语句组织起来,以实现一个特定的、连贯的目的。我们其实从一开始就在使用函数,例如程序入口 int main(void),以及输入输出函数 printf 和 scanf。
函数之所以重要,主要有以下几个原因:
- 代码复用:函数是代码复用的核心。标准库(如
stdio.h)中的函数可以被无数次调用,无需重写。 - 封装:函数是一种封装技术。它将复杂的代码逻辑打包成一个独立的单元,隐藏内部细节,只通过接口(函数名和参数)与外界交互。
- 结构化:大型系统由数百万行代码组成。通过将系统分解为一系列相互调用的函数,我们可以更好地组织和管理代码。
- 易于测试:一个设计良好的函数通常只做一件事,并且代码量控制在一页以内。这使得我们可以独立地、有效地测试这个功能单元。
如何设计函数?✍️
在开始编写函数之前,了解一些设计原则很有帮助。
以下是编写函数时应考虑的几个要点:
- 单一职责:一个函数应该只做好一件事。例如,
sqrt函数只计算平方根,rand函数只生成随机数。 - 代码长度:函数的代码量最好控制在一页之内。这类似于一个段落的长度,便于理解和测试。
- 明确接口:函数应有清晰的输入(参数)和输出(返回值)。理想情况下,我们可以为函数定义前置条件(调用函数前必须满足的条件)和后置条件(函数执行后保证会达成的结果),这有助于测试。
函数的语法结构📝
了解了函数的重要性与设计原则后,我们来看看在C语言中定义函数的具体语法。
一个标准的函数定义包含以下几个部分:
返回类型 函数名(参数列表) {
// 变量声明(可选)
// 可执行语句
}
各部分解释如下:
- 返回类型 (
type):指定函数执行完毕后返回的数据类型,例如int、float。如果函数不返回任何值,则使用void。 - 函数名 (
function_name):函数的标识符,应具有描述性,如printf、calculateArea。 - 参数列表 (
parameter_list):传递给函数的值,是一个用逗号分隔的变量声明列表。可以为空,写作void或留空。 - 函数体 (
body):由一对花括号{}包围的代码块。内部可以包含变量声明和一系列可执行语句。
函数定义示例🎯
现在,让我们通过一个简单的例子来具体看看如何定义和调用一个函数。
以下是一个函数定义的例子:
void write_address(void) {
printf("123 Main Street\n");
printf("North Pole, H0H 0H0\n");
}
- 返回类型:
void,表示该函数不返回任何值。 - 函数名:
write_address。 - 参数列表:
(void),表示该函数不需要任何参数。 - 函数体:包含两个
printf语句,用于打印地址。
这个函数封装了“打印地址”这个操作。当我们需要在程序的不同位置打印这个地址时,只需调用这个函数即可,无需重复编写 printf 语句。
调用该函数的方式非常简单:
write_address();
只需写下函数名,后跟一对括号和分号。我们可以想象在 main 函数中调用它,那么这两行地址就会被打印出来。


总结
本节课中我们一起学习了C语言的核心概念——函数。我们了解到函数是代码的“段落”,它通过封装特定功能来实现代码的复用、结构化和易于测试。我们学习了函数的设计原则,并掌握了函数定义的标准语法:返回类型 函数名(参数列表) { 函数体 }。最后,通过一个打印地址的简单示例,我们实践了如何定义和调用一个 void 类型的无参函数。理解并熟练运用函数,是迈向编写高质量C语言程序的重要一步。
033:函数代码示例

概述
在本节课中,我们将要学习C语言中一个极其重要的概念——函数。我们将通过一个具体的代码示例,了解如何将一段代码封装成函数,以及这样做如何使程序结构更清晰、更易于维护和复用。
函数的重要性
函数可能是整个课程中最重要的主题。它们将你的编程能力从“书写句子”提升到“书写段落”的层次。一旦你能够编写一个良好的函数,你就有能力编写任何类型的代码。
示例程序:爱的表达
为了说明函数的概念,我们首先来看一个未使用函数的简单程序。这个程序使用while循环来重复表达“非常”这个词,以传达爱的程度。迭代(循环)是控制程序流程的关键语句,而while循环是其最简单的形式。
以下是该程序的代码:
#include <stdio.h>
int main() {
int repeat;
printf("How much? ");
scanf("%d", &repeat);
int count = repeat;
while (count > 0) {
printf("very ");
count--;
}
printf("much.\n");
return 0;
}
这个程序的核心部分是while循环。如果我希望在其他代码中重用这段逻辑,就需要将其封装起来。
函数的封装
上一节我们看到了一个使用循环的程序,本节中我们来看看如何将这段逻辑封装成一个独立的函数。
函数封装的第一步是创建函数定义。一个完整的函数定义包括声明部分和实现代码。如果只有声明部分并以分号结尾,那只是一个函数声明。函数的“签名”至关重要,它定义了函数的特征:返回类型、参数列表和函数名。
以下是将上述循环逻辑封装成函数后的代码:
#include <stdio.h>
// 函数定义
void write_very(int count) {
while (count > 0) {
printf("very ");
count--;
}
}
int main() {
int repeat;
printf("How much? ");
scanf("%d", &repeat);
// 调用函数
write_very(repeat);
printf("much.\n");
return 0;
}
在main函数中,我们调用了write_very函数。这里,变量repeat的值作为参数count传递给函数。在C语言中,参数是“按值传递”的,这意味着函数内部对count的修改(例如递减到0)不会影响外部变量repeat的原始值(如果输入是8,repeat之后仍然是8)。我们将在后续课程中更详细地讨论这一点。
封装带来的好处
通过封装,main函数变得非常简洁。我们可以编写多个函数来完成复杂程序的不同部分,然后在main函数中调用它们。这为我们的代码提供了一种清晰的结构,极大地帮助了程序的文档编写和调试。
让我们测试一下这个函数版的程序。运行后,输入一个数字,例如5,程序会输出:“I love you very very very very very much.”。
请尝试运行并修改这个程序,确保你理解了函数封装的概念。我们将在接下来的讨论中更多地使用它。


总结
本节课中我们一起学习了C语言函数的基础知识。我们通过一个具体的例子,了解了如何将一段代码(特别是循环逻辑)封装成一个独立的函数。我们认识了函数定义、函数签名以及参数传递(按值传递)的基本概念。最重要的是,我们看到了使用函数如何让主程序变得更简洁、更具结构性,这是构建复杂、可维护程序的关键一步。
034:return语句

概述
在本节课中,我们将要学习C语言中return语句的使用。return语句是理解函数工作原理的重要组成部分,它用于控制程序流程并从函数中返回一个值。
return语句简介
return语句是一个流程控制关键字。我们从最初的程序开始就已经见过它,在main函数的结尾处,我们总是写return 0;。这是一种约定,虽然可以不用,但显式地使用它被认为是更好的编程风格。
return语句的语法
return语句的语法有两种形式:一种是return;(不带表达式),另一种是return后跟一个表达式,例如return 表达式;。
以下是三个简单的例子:
return;return 0;return (a + b);
请注意,表达式(a + b)的括号不是必需的,但为了代码清晰,通常都会加上。即使是一个简单的整数常量如0,人们也倾向于加上括号。
return语句在函数中的使用
上一节我们介绍了语法,本节中我们来看看return语句在函数中的具体应用。
下面是一个简单的函数示例,它计算一个数的立方:
double cube(double x) {
return (x * x * x);
}
这个函数名为cube,它接收一个double类型的参数x,计算x * x * x,并通过return语句返回一个double类型的值。如果返回的表达式的类型与函数声明的返回类型不匹配,C语言会进行类型转换,但转换规则可能很复杂,通常应尽量避免。
函数调用与返回值
了解了函数如何返回值后,我们来看看如何调用它并使用其返回值。
我们可以这样调用cube函数:
a = cube(3.5);
在这个例子中,3.5被传递给cube函数,函数内部计算3.5 * 3.5 * 3.5,然后将结果值返回并赋值给变量a。
需要特别注意的是,如果我们这样调用:
a = cube(3);
整数3会先被转换为double类型3.0,然后进行计算。最终变量a得到的将是double类型的27.0,而不是整数27。
总结

本节课中我们一起学习了C语言的return语句。我们了解了它的两种语法形式,看到了它在函数中如何用于返回值,并通过示例理解了函数调用时参数传递和返回值的过程。return语句是函数与程序其他部分进行数据交换的关键机制。
035:函数原型

概述
在本节课中,我们将要学习C语言中的函数声明与原型。理解如何在使用函数之前声明它们,以及原型在程序编译过程中的重要作用。
函数声明与原型
上一节我们介绍了函数定义,本节中我们来看看函数声明与原型。
到目前为止,我们使用的函数定义同时也充当了声明。但如果编译器在遇到函数调用时,尚未看到该函数的定义,就会报错。例如,如果没有使用 #include <stdio.h> 包含标准输入输出库,直接调用 printf 函数,编译器会提示该函数未声明。
因此,在使用任何函数(包括库函数)之前,我们需要先知道它的声明。
解决这个问题的方法是,可以在函数被定义之前先声明它,这被称为函数原型。例如,我们可以这样写:
double cube(double);
这行代码就是一个函数声明。之后,在 main 函数中,我们就可以调用 cube 函数。编译器会期望在程序的其他地方找到该函数的实际代码(即定义)。
通常,函数定义可以放在 main 函数之后。这样写程序完全可以正常运行。或者,在更复杂的多文件项目中,函数的定义可能位于另一个单独的文件中。
这种声明,也称为原型,可以只包含参数类型,也可以包含参数名称。以下是两种等价的写法:
double cube(double);
double cube(double x);
省略参数名也是允许的。
函数原型有时也被称为函数签名。当你后续学习Java或C++等语言时,这个概念会变得非常重要,因为这些语言支持函数重载,即允许同名函数拥有不同的签名(参数列表)来实现不同的功能。不过,在学习C++之前,你无需担心这一点。
此外,还有一种特殊的签名用于处理可变参数列表。正如我们已经知道的,调用 printf 函数时,我们可以提供一个格式字符串,然后提供一系列变量。因为它是一个可变参数列表,所以在声明中可以用省略号 ... 来表示。当然,printf 等函数的这些声明都定义在 stdio.h 等头文件中,通过 #include 指令引入后,你就可以使用它们了。


总结
本节课中我们一起学习了C语言的函数声明与原型。我们了解到,在使用函数前必须先声明,函数原型可以只包含类型或同时包含参数名,并且理解了原型在多文件编程和后续学习其他语言中的重要性。
036:函数原型代码示例

概述
在本节课中,我们将通过一个打印平方与立方表的示例,来学习如何使用函数原型(prototype)来组织C语言代码。我们将看到如何声明函数、定义函数,以及如何在一个程序中有效地使用它们。
函数原型与声明
上一节我们讨论了函数的基本概念,本节中我们来看看如何在实际代码中使用函数原型。
函数原型告诉编译器一个函数的存在、它的返回类型以及它接受的参数类型。这允许我们在定义函数之前就调用它。在原型中,参数名是可选的,但参数类型是必需的。
以下是函数原型的语法示例:
double square(double);
double cube(double);
这里,我们声明了两个函数:square 和 cube。它们都接受一个 double 类型的参数,并返回一个 double 类型的值。
主程序结构
现在,让我们看看主函数 main 如何利用这些声明的函数。
主程序使用了一个嵌套的 for 循环来生成表格。外层循环控制表格的行数(从1到n),内层循环以0.1为步长计算每个值的平方和立方。这种结构虽然简单,但体现了计算机高效执行重复计算的能力。
以下是主程序的核心循环结构:
for (i = 1; i <= n; ++i) {
for (j = 0.0; j < 1.0; j += 0.1) {
x = i + j;
// 调用 square 和 cube 函数
}
}
函数定义
在程序文件的末尾,我们提供了 square 和 cube 函数的实际定义。这是编译器最终理解函数如何工作的地方。
以下是函数定义的示例:
double square(double x) {
return x * x;
}
double cube(double x) {
return x * x * x;
}
程序运行示例
当我们运行程序并输入 n=2 时,程序会输出从1.0到2.9之间,以0.1为间隔的每个数的平方和立方值。
例如,输出结果会包含:
- 1.0 的平方是 1.0,立方是 1.0。
- 1.5 的平方是 2.25,立方是 3.375。
- 2.0 的平方是 4.0,立方是 8.0。
代码组织风格
关于代码组织,有两种常见的风格:
- 将函数原型放在文件顶部(
main函数之前),而将函数定义放在main函数之后。 - 直接将完整的函数定义放在
main函数之前。
两种方式都是可行的。本示例采用了第一种风格,即先声明后定义,这有助于提高代码的可读性和模块化。


总结
本节课中我们一起学习了如何通过函数原型来结构化C语言程序。我们实践了声明函数、在 main 函数中调用函数、以及最终定义函数的具体实现。通过打印平方与立方表的例子,我们看到了如何利用循环和函数来高效地组织代码逻辑。记住“分而治之”的原则,先声明后使用,是编写清晰、可维护程序的好习惯。
037:按值调用解释的函数变量

在本节课中,我们将要学习C语言中一个极其重要的核心概念:函数调用中的“按值调用”。我们将通过一个具体的例子,详细解释函数参数是如何工作的,以及为什么理解这一点对编程至关重要。
函数与按值调用
上一节我们介绍了函数的基本概念,本节中我们来看看函数调用时参数的具体行为。在C语言中,函数参数的传递方式是“按值调用”。
这个概念的核心在于:当调用一个函数时,传递给函数的实际参数的值会被复制一份,这份副本被赋给函数内部的形式参数。函数内部对形式参数的任何修改,都不会影响到外部实际参数的值。
一个简单的求和函数
为了更好地理解,我们来看一个计算从1加到n的求和函数。
int sum_to_n(int n) {
int sum = 0;
for ( ; n > 0; n--) {
sum += n;
}
return sum;
}
这个函数名为 sum_to_n,它接收一个整数参数 n。函数内部定义了一个局部变量 sum 并初始化为0。然后使用一个 for 循环,只要 n 大于0,就将当前的 n 加到 sum 上,同时将 n 减1。最后,函数返回计算得到的 sum 值。
按值调用的执行过程
以下是函数被调用时,按值调用的具体执行步骤:
-
计算实参表达式:首先,计算调用函数时传入的实际参数(或表达式)的值。例如,调用
sum_to_n(5)时,实参就是5。实参也可以是一个复杂的表达式,如sum_to_n(3 * m + 2)。 -
类型转换:如果必要,将计算出的实参值转换为函数形式参数所声明的类型。在我们的例子中,形式参数
n是int类型。如果实参是double类型(如3.5),它会被转换为int类型(3)。 -
值复制:这是按值调用的关键步骤。将转换后的值复制给函数内部的形式参数
n。你可以将其想象为在函数内部创建了一个名为n_local的局部变量,并执行了n_local = 实参值的操作。 -
执行函数体:函数体开始执行,使用这个局部副本
n_local进行计算。注意,即使函数内部的循环将n递减到了0,这个操作也仅仅作用于内部的副本n_local。 -
返回结果:当执行到
return语句时,函数结束。return后面的表达式会被计算,并在必要时转换回函数的返回类型(本例中是int),然后将这个值返回给调用者。 -
无显式返回:如果函数执行到代码块末尾都没有遇到
return语句,则相当于在最后隐式地执行了一个return。
关键点与示例
让我们通过一个具体的调用场景来巩固理解:
int main() {
int m = 5;
int result = sum_to_n(m); // 调用函数
printf(“从1加到%d的和是:%d\n”, m, result);
printf(“调用后m的值仍然是:%d\n”, m);
return 0;
}
在这个例子中:
- 变量
m的值是5。 - 调用
sum_to_n(m)时,m的值5被复制给了函数内部的参数n。 - 函数内部,
n从5递减到0,但这只是内部副本的变化。 - 函数返回计算结果
15(1+2+3+4+5)。 - 回到
main函数,打印result得到15,打印m的值,它仍然是5,没有受到函数内部操作的影响。
总结


本节课中我们一起学习了C语言函数调用的核心机制——按值调用。我们明白了,函数参数传递的本质是值的复制。函数内部操作的是实参值的一个独立副本,因此不会改变原始实参变量的值。这是一个基础且至关重要的概念,请务必通过编写代码、运行示例并观察结果来加深理解。掌握按值调用,是理解后续更复杂参数传递方式(如指针)的基石。
038:函数定义和作用域规则

概述
在本节课中,我们将要学习C语言中的作用域规则。作用域规则决定了程序中变量和函数的标识符在何处可以被访问,以及它们的生命周期。理解这些规则对于编写结构清晰、资源高效的程序至关重要。
作用域规则简介
上一节我们介绍了函数的基本概念,本节中我们来看看作用域规则。作用域规则涉及声明,这些声明通常出现在代码块的头部。代码块可以有不同的结构,例如嵌套的代码块和并行的代码块。
简单代码块的作用域
以下是简单代码块中变量作用域的例子。
int main(void) {
int i = 5; // 变量i的声明和初始化
// 变量i的生命周期从此处开始,在整个main函数块内可用
{
int j = i + 2; // 变量j的声明和初始化
// 在此内层块中,可以同时访问i和j
printf("i = %d, j = %d\n", i, j); // 输出:i = 5, j = 7
}
// 退出内层块后,变量j的生命周期结束,不再存在
printf("i = %d\n", i); // 输出:i = 5
// printf("j = %d\n", j); // 如果尝试访问j,会导致语法错误
return 0;
}
对于简单变量,这种生命周期管理可能不关键。但对于涉及大量存储的变量(如大型数组),这会影响内存资源的使用效率,在大型程序中变得非常重要。
嵌套代码块与名称隐藏
更复杂的情况是嵌套代码块中同名变量的声明。
int main(void) {
int i = 5; // 外层块变量i_outer
{
int i = 3; // 内层块变量i_inner
// 内层声明会隐藏外层同名变量
printf("inner i = %d\n", i); // 输出:inner i = 3
// 此处无法直接访问外层变量i(值为5)
}
// 退出内层块后,内层变量i消失,外层变量i恢复可见
printf("outer i = %d\n", i); // 输出:outer i = 5
return 0;
}
当重用名称时,程序会优先使用最局部(即最内层)代码块中的标识符。在C语言中,一旦内层块隐藏了外层变量,就无法在内层块中直接访问被隐藏的外层变量(C++提供了相关机制,但C语言没有)。
并行代码块的作用域
我们还有一种结构是并行的代码块。
int main(void) {
// 块1
{
int a = 10;
// 变量a的生命周期仅限于此块内
}
// 块2
{
int a = 20; // 这是一个全新的变量,与块1中的a无关
// 变量a的生命周期仅限于此块内
}
return 0;
}
在并行块中,每个块内的变量拥有各自独立的生命周期。块1中变量的生命周期从其声明处开始,到块1结束时终止。块2中的变量则拥有另一段完全独立的生命周期。
函数调用与作用域
函数调用时也会创建独立的作用域。
void myFunction() {
int localVar = 100; // 局部变量
// localVar的生命周期仅存在于函数myFunction执行期间
}
int main(void) {
myFunction();
// printf("%d\n", localVar); // 错误:此处无法访问myFunction的局部变量
return 0;
}
当函数被调用时,其内部声明的变量被放置在称为“栈”的内存区域中。编译器通过栈来管理这些变量的存储空间。目前我们所见的所有变量存储管理都与代码块及其内部的声明相关。后续课程中,我们将看到另一种基于“堆”的存储管理方式。


总结
本节课中我们一起学习了C语言的作用域规则。我们了解到:
- 变量的可访问性(作用域)和存在时间(生命周期)由其声明所在的代码块决定。
- 内层代码块可以声明与外层同名的变量,此时会隐藏外层变量。
- 并行代码块中的变量互不影响,拥有各自独立的作用域。
- 函数内部声明的变量是局部的,其生命周期仅限于函数执行期间。
理解这些规则有助于我们更好地组织代码,有效管理程序的内存使用。
039:存储类代码示例

在本节课中,我们将通过一个具体的代码示例来探索C语言中不同存储类的概念。我们将学习extern、static、auto和const关键字如何影响变量的生命周期和作用域。
概述
上一节我们介绍了存储类的基本概念。本节中,我们来看看一个具体的代码示例,它演示了extern、static、auto和const这些关键字的实际用法。这个示例本身可能没有直接的实用价值,但它是一个很好的工具,可以帮助我们理解不同存储类的语义和行为。
代码解析
以下是示例代码的核心部分,我们将逐一分析。
全局变量与 extern
首先,我们声明了一个全局变量rips,并使用extern关键字。extern意味着这个变量在整个程序执行期间都存在,并且可以被任何函数访问。
extern int rips = 0;
实际上,这里直接使用int rips = 0;效果是相同的。extern明确表示这是一个全局变量。
静态局部变量 static
接下来,在函数f内部,我们定义了一个静态局部变量called。
static int called = 0;
static关键字意味着这个变量在函数第一次被调用时初始化为0。之后每次调用该函数,called的值都会保留上一次退出时的状态,而不是重新初始化为0。在函数中,我们执行called++,因此每次调用,called的值都会递增。
自动变量 auto
在main函数中,我们使用auto关键字声明了变量i。
auto int i = 1;
auto是默认的存储类,通常可以省略。它表示变量i是自动的,即进入main函数时被创建在栈上,并在退出时销毁。
常量限定符 const
同时,我们使用const声明了一个常量MAX。
const int MAX = 10;
const不是一个存储类,而是一个类型限定符。它表示MAX的值在初始化后不能被改变。这是现代C和C++中用于增强类型安全的一个特性。
程序执行流程
现在,让我们看看main函数中的逻辑。
i被初始化为1。- 循环条件
i < MAX意味着i将从1递增到9。 - 在循环体内:
- 打印当前的
i和全局变量rips的值。 - 调用函数
f。 - 函数
f会更新静态变量called,并修改全局变量rips(rips += called)。
- 打印当前的
- 每次循环,
i递增,rips的值根据called的累计值增加。
以下是程序执行时变量变化的逻辑推演:
- 第一次循环:
i=1,rips初始为0。调用f,called从0变为1,rips变为0+1=1。 - 第二次循环:
i=2,rips=1。调用f,called从1变为2,rips变为1+2=3。 - 第三次循环:
i=3,rips=3。调用f,called从2变为3,rips变为3+3=6。 - 依此类推,最终当
i=9时,循环结束。函数f总共被调用了9次。
理解与练习
这个示例的变量交互看起来可能有些复杂。如果感到困惑,最好的方法是亲自动手实践。
以下是帮助你深入理解的一些建议:
- 将这段代码复制到你的编译环境中并运行它,观察输出结果。
- 尝试修改代码,例如增加新的函数,在其中使用不同的存储类。
- 通过打印语句跟踪每个关键变量(
rips,called,i)在每次函数调用和循环迭代时的值。
通过这样的练习,你可以巩固对以下概念的理解:
- 全局变量 (
extern):整个程序生命周期可见。 - 静态局部变量 (
static):函数内持久存在,保持上次调用的值。 - 自动变量 (
auto):默认,在代码块内创建和销毁。 - 常量 (
const):值不可变。
总结


本节课中我们一起学习了C语言存储类的一个综合代码示例。我们看到了extern如何定义全局变量,static如何让局部变量在函数调用间保持状态,auto作为默认存储类的含义,以及const用于定义常量。虽然这个示例本身较为特殊,但它是理解变量作用域和生命周期的绝佳练习。掌握这些概念对于编写结构清晰、可维护的C语言程序至关重要。
040:简单递归

概述
在本节课中,我们将要学习递归这一核心编程概念。我们将通过一个简单的倒计时程序,对比迭代和递归两种实现方式,来理解递归的基本结构和原理。
从迭代到递归
上一节我们介绍了使用循环进行迭代的方法。本节中我们来看看如何用递归实现相同的功能。
迭代的核心是使用循环语句重复执行一段代码。例如,我们可以使用 while 循环实现一个倒计时程序。
以下是迭代版本的倒计时代码示例:
while (n > 0) {
printf("T minus %d\n", n);
n--;
}
printf("Blast off!\n");
这段代码从初始值 n 开始,每次循环打印当前值并将其减1,直到 n 不大于0时,打印“发射!”。
递归的基本思想
理解了迭代后,我们现在来探讨递归。递归是一种函数调用自身的技术。
递归程序通常包含两个关键部分:基准情形和递归情形。基准情形是递归终止的条件,而递归情形则包含函数对自身的调用。
递归倒计时程序
以下是使用递归实现相同倒计时功能的代码:
void recursive_countdown(int n) {
if (n == 0) { // 基准情形
printf("Blast off!\n");
} else { // 递归情形
printf("T minus %d\n", n);
recursive_countdown(n - 1); // 递归调用
}
}
在这个函数中:
- 如果
n等于0,我们到达基准情形,打印“发射!”。 - 否则,我们处于递归情形,打印当前倒计时数字,然后以
n-1为参数再次调用recursive_countdown函数。
递归与数学归纳法
如果你有数学背景,可能会发现递归与数学归纳法非常相似。两者都基于一个基准情形和一个能够推导出下一个情形的步骤。
使用递归编写程序有时能使逻辑更清晰,更容易调试或证明其正确性,因为其结构直接反映了问题的定义。

总结
本节课中我们一起学习了递归的基本概念。我们通过对比迭代和递归版本的倒计时程序,理解了递归的两个核心组成部分:基准情形和递归情形。我们还了解到递归与数学归纳法的联系,以及它在某些情况下能使代码更清晰易懂的优点。在接下来的课程中,我们将看到更多有趣且实用的递归程序示例。
041:递归与迭代实现阶乘

在本节课中,我们将学习如何使用两种不同的编程范式——迭代和递归——来实现阶乘计算。我们将通过具体的代码示例来对比这两种方法,并理解它们各自的实现逻辑。
递归的基本思想
上一节我们介绍了递归的简单概念。递归可以替代迭代来解决问题。有时你可能更倾向于使用其中一种方案,但了解如何运用两种方案都很重要。
阶乘的数学定义
现在,我们将展示两种方案来计算阶乘。阶乘是一个数学量,用于生成特别大的数字。它的定义是:1 * 2 * 3 * 4 * ... * n。这个数字会变得非常大。
因此,我们将使用 long int 类型。如果使用更短的类型,我们无法计算较大的阶乘。实际上,long 类型通常只允许计算到 20 的阶乘。超过 20 会发生整数溢出,导致结果不正确,因为在大多数机器上,long 类型只有 8 字节(64 位)可用。对于整数类型,只能表示到 20 的阶乘。
迭代实现阶乘
首先,我们来看一个应该很熟悉的方法:迭代实现阶乘。我们将答案累积在变量 long f = 1 中。
以下是迭代的标准结构:从 f = 1 开始,i 从 1 递增到 n,在每次迭代中累积 f,通过将 f 乘以 i。迭代结束后,你将得到 1 * 2 * 3 * ... * n 的结果,即阶乘的定义,然后返回 f。这个迭代过程应该很直接,对你来说应该没有问题。
迭代阶乘的代码示例:
long factorial_iterative(int n) {
long f = 1;
for (int i = 1; i <= n; i++) {
f = f * i;
}
return f;
}
递归实现阶乘
现在,让我们看看递归版本。这有点新颖。我们称之为递归阶乘。
同样,它返回 long 类型。n 不需要是 long 类型,因为我们实际上无法计算非常大的数字。我们首先检查基本情况:当 n == 1 时,根据定义,阶乘就是 1,所以我们返回 1。
如果不是基本情况,我们就进行递归。这类似于我们的倒计时程序,但这里我们计算的是 n 乘以一个较小值的递归阶乘。这是归纳情况的一般形式:n * factorial_recursive(n-1)。
你需要说服自己,这能给出与迭代完全相同的结果。
递归阶乘的代码示例:
long factorial_recursive(int n) {
if (n == 1) {
return 1; // 基本情况
} else {
return n * factorial_recursive(n - 1); // 递归情况
}
}
对比两种实现
我在这里进行了对比。我让用户输入一个数字,然后先迭代计算,再递归计算,以展示我们会得到相同的结果。
我已经编译了这个程序,让我们运行它。我们先试一个小数字。例如,1 的阶乘是 1,2 的阶乘是 2(1*2=2),3 的阶乘是 6。递归版本也得到相同的结果。
我们再试一个稍大的数字,比如 10。这时数字开始变大。你可以看到,因为屏幕宽度不够,递归版本和迭代版本的最终结果(10 的阶乘)是相同的。你可以看到数字如何增长,例如乘以 10 后,结果是前一个结果的 10 倍。就像 3 的阶乘是 6,4 的阶乘是 24(46),5 的阶乘是 120(524),依此类推。
这是一个非常标准的数学例子。如果你看到一个数学表达式,经常会看到这类函数以归纳或递归的方式定义。因此,这有一个自然的映射,让你可以轻松地、算法化地将这个想法转录成递归代码。
总结

本节课中,我们一起学习了如何使用迭代和递归两种方法来实现阶乘计算。我们理解了阶乘的数学定义,并通过代码示例对比了两种实现方式的逻辑。递归方法通过定义基本情况和递归情况,优雅地映射了数学归纳定义,而迭代方法则通过循环累积结果。两种方法都能正确计算阶乘,但在不同场景下各有优势。
042:递归与斐波那契数列

概述
在本节课中,我们将学习递归函数的另一个经典示例:斐波那契数列。我们将探讨其递归定义,并展示如何用迭代方式实现相同的功能。通过对比两种实现方式,我们将理解递归在某些情况下可能带来的性能问题。
斐波那契数列的定义
斐波那契数列是一个著名的整数序列。其定义如下:序列的第 n 个元素等于第 n-1 个元素与第 n-2 个元素之和。序列的初始值为:第 0 个元素是 0,第 1 个元素是 1。
用公式可以表示为:
F(n) = F(n-1) + F(n-2)
其中 F(0) = 0,F(1) = 1。
根据这个公式,我们可以推导出后续的值:
- F(2) = 0 + 1 = 1
- F(3) = 1 + 1 = 2
- F(4) = 1 + 2 = 3
- 以此类推。
这个序列的特点是,尽管起始数字很小,但后续数值会呈指数级增长。
迭代实现方法
上一节我们介绍了斐波那契数列的数学定义。本节中我们来看看如何用循环(迭代)的方式计算它。
以下是用 C 语言实现的迭代版本。我们使用 long 类型来存储结果,因为数列的值很容易超过普通整型的范围。
long fib(long n) {
long i, f2 = 0, f1 = 1;
if (n == 0) return 0;
for (i = 2; i <= n; i++) {
long temp = f2 + f1; // 计算新的F(n)
f2 = f1; // 更新F(n-2)为旧的F(n-1)
f1 = temp; // 更新F(n-1)为新的F(n)
}
return f1;
}
代码逻辑如下:
- 初始化
f2为 F(0) = 0,f1为 F(1) = 1。 - 从 i=2 开始循环,直到 i 等于 n。
- 在每次循环中,将
f2和f1相加得到新的斐波那契数。 - 更新
f2和f1的值,为下一次计算做准备。 - 循环结束后,
f1中存储的就是 F(n) 的值。
这是一种高效的线性时间计算方法。
递归实现方法
斐波那契数列的数学定义本身就是递归形式的,因此用代码实现递归版本非常直观。
以下是递归实现的代码:
long fib_recursive(long n) {
if (n <= 1) {
return n; // F(0)=0, F(1)=1
}
return fib_recursive(n - 1) + fib_recursive(n - 2);
}
代码逻辑遵循定义:
- 基线条件:如果
n小于或等于 1,直接返回n。这对应着 F(0)=0 和 F(1)=1。 - 递归步骤:否则,返回
F(n-1)与F(n-2)之和。这通过递归调用自身来实现。
需要注意的是,每次递归调用会产生两个新的递归调用,导致调用次数呈指数级增长。
性能对比与分析
我们编写一个程序来对比两种方法的计算结果和运行效率。
以下是测试代码的核心部分:
#include <stdio.h>
int main() {
long i;
for (i = 0; i <= 15; i++) {
printf("F(%ld) - 迭代: %ld, 递归: %ld\n", i, fib(i), fib_recursive(i));
}
return 0;
}
对于较小的 n 值(例如 15),两种方法都能瞬间给出正确且一致的结果。
然而,当 n 值变大时,差异开始显现。尝试计算 F(45):
- 迭代方法依然非常快速,因为它只进行了大约 45 次加法运算。
- 递归方法则变得极其缓慢。屏幕输出会出现明显的卡顿,计算
F(45)可能需要数秒甚至更长时间。
如果尝试计算 F(50),递归版本可能会因为耗时过长而变得不切实际。
递归性能问题的根源
为什么递归实现会这么慢?原因在于其巨大的计算冗余。
为了计算 F(n),递归函数需要计算 F(n-1) 和 F(n-2)。而计算 F(n-1) 又需要计算 F(n-2) 和 F(n-3)。注意,F(n-2) 被计算了两次。这种重复计算随着递归深度增加而爆炸性增长。
计算 F(n) 所需的递归调用次数大致是 2^n 量级。对于 n=45,这意味著可能超过 10亿 次函数调用。每次函数调用都需要在栈上分配内存、传递参数、跳转指令,这些开销虽然单次很小,但累积起来就造成了巨大的性能瓶颈。
总结
本节课中我们一起学习了斐波那契数列的两种实现方式:
- 迭代法:使用循环,效率高,时间复杂度为 O(n),是计算斐波那契数的推荐方法。
- 递归法:代码简洁,直接反映数学定义,易于理解。但对于斐波那契数列这类问题,会产生指数级的时间复杂度 O(2^n),导致在 n 较大时性能急剧下降。


这个例子揭示了一个重要的编程启示:递归虽然是一种强大而优雅的工具,但在使用时必须仔细分析其时间复杂度和可能产生的重复计算问题。 对于存在重叠子问题的情况(如斐波那契数列),通常迭代法或结合记忆化(缓存已计算结果)的递归法是更优的选择。
043:指针和简单数组 05_01_01

概述
在本节课中,我们将学习C语言中处理聚合数据的关键概念——数组。这是C语言编程基础课程A部分的最后一个主要主题。掌握数组后,你将在某种意义上成为一名严肃的程序员。我们将从数组的基本声明和内存结构开始,理解其索引机制,并学习处理数组数据的标准方法。
数组的基本概念与声明
在计算机真正开始使用的头二十年(即20世纪40年代末到60年代初),像Fortran和Algol这样的语言就已经有了聚合数据类型——数组。本节中,我们来看看如何在C语言中处理简单的一维数组。
到目前为止,当我们需要处理多个数据时,必须写出单独的声明,例如 int a, b, c;,或者使用类似 int a1, a2, a3; 的命名方式。但这种方式限制很大。例如,如果我们需要处理非常大的数据集,就像我们作业中提到的,要检查数千头象海豹的平均体重,我们不可能写出数千个单独命名的变量,这既难以处理,也过于局限。
我们的解决方案是使用以下形式的声明:
int data[100];
与普通声明一样,它包含一个类型(可以是 double、long、char 等标准类型)、一个标识符(用于指示我们正在处理什么数据,这里使用通用术语 data)和一个大小(必须是一个已知的整型常量)。之所以需要常量大小,部分原因在于此时的内存分配通常发生在栈上。
数组的内存结构与索引
当编译器处理这个声明时,它会为我们生成一个一维数组结构。其元素是 data[0], data[1], data[2], ..., data[99]。
C语言中的数组从0开始索引。有些编程语言的数组从1开始,但在C语言中,0是数组的基地址。在某种意义上,data 本身就是一个内存地址。索引0指的是第一个元素,而最后一个元素的索引是数组大小减1(本例中为99)。
实际上,带索引的方括号 [] 执行了一个地址计算。这个计算从 data 在内存中的位置开始。例如,一个普通的 int a 变量可能存储在内存地址2004。但对于数组,内存计算包含两个部分。理解这是一种获取地址的方式非常重要,因为后续我们将讨论数组和指针的关系。在C语言中进行更复杂的工作时,你必须理解寻址和指针。但对于简单的操作,数组索引是一个直观的抽象。
数组中的每个元素都可以存储一个 int 值,在大多数机器上,这需要4个字节。
数组处理的强大之处与标准范式
数组之所以如此有用,是因为大多数处理任务并非针对少量数据,而是涉及存储、检索和处理非常庞大的数据集,这些数据集通常达到百万级别。
请看下面这个简单的四语句循环:
for (i = 0, sum = 0; i < size; ++i) {
sum = sum + data[i];
}
我们在这里进行计算,对数组中的所有元素求和。循环结束时,我们就累加了这个集合中的所有数据。这个结果可能用于计算平均值,也可能有其他用途。
关键在于,这一个循环结构可以处理我们拥有的任意数量的数据。这就是它强大的地方。我们可以将其视为处理存储在数组中的数据时的标准范式。
只要你理解并能运用这个范式,你基本上就具备了成为一名严肃程序员的基础。


总结
本节课中,我们一起学习了C语言中数组的基础知识。我们了解了如何声明一个数组(例如 int data[100];),理解了数组在内存中的结构是从0开始索引的连续空间,并且掌握了使用 for 循环处理数组数据的标准方法。数组是处理聚合数据、应对大规模数据集的核心工具,掌握它是迈向更高级C语言编程的重要一步。
044:初始化数组

概述
在本节课中,我们将要学习C语言中数组的初始化方法,并重点探讨字符串的概念及其内部表示。字符串在C语言中并非独立的数据类型,而是字符数组,理解其初始化方式对于后续的编程实践至关重要。
数组初始化基础
上一节我们介绍了数组的基本概念,本节中我们来看看如何为数组赋予初始值。当我们声明一个简单变量时,可以在声明时使用等号进行初始化。然而,数组是一组通过索引访问的变量集合,其初始化方式有所不同。
以下是数组初始化的几种方法:
- 完整列表初始化:声明数组时,可以使用花括号
{}包含一个逗号分隔的值列表进行初始化。例如int data[5] = {1, 2, 3, 4, 5};,这将使data[0]为1,data[1]为2,依此类推。 - 部分初始化与自动补零:如果初始化列表中的值少于数组元素个数,剩余的元素会被自动初始化为0。例如
int data[5] = {1, 2};,则data[0]为1,data[1]为2,data[2]到data[4]均为0。 - 缩写初始化:若希望数组所有元素初始化为同一个值(如0),可以简写为
int data[5] = {0};。 - 自动确定数组大小:声明数组时可以省略大小,编译器会根据初始化列表自动确定。例如
int data[] = {1, 2, 3};等价于int data[3] = {1, 2, 3};。
深入理解字符串
了解了普通数组的初始化后,我们聚焦于一个特殊的字符数组——字符串。字符串在C语言中是以空字符(\0)结尾的字符数组。
字符串的内部表示是一个连续的字符序列,以空字符作为结束标志。例如,字符串常量 "AB" 在内存中的实际表示是三个字符:'A'、'B'、'\0'。
这里需要注意字符 '0' 和空字符 '\0' 的区别:
'0':这是一个字符,其ASCII码值为48。'\0':这是一个空字符(null character),其ASCII码值为0,用于标记字符串的结束。它也可以写作0(不带引号)。
字符串初始化示例
让我们通过一个具体例子来巩固对字符串初始化的理解。考虑以下声明:
char str[] = "A B C";
这个声明等价于以下字符数组初始化:
char str[6] = {'A', ' ', 'B', ' ', 'C', '\0'};
以下是该字符串数组 str 在内存中的内容:
str[0]:字符'A'str[1]:空格字符(ASCII码32)str[2]:字符'B'str[3]:空格字符str[4]:字符'C'str[5]:空字符'\0'(哨兵字符)
这个空字符被称为“哨兵”(sentinel)或“守卫”(guard),因为在对字符串进行循环处理(如计算长度、复制)时,程序通过检测这个字符来判断字符串是否结束。


总结
本节课中我们一起学习了C语言数组的初始化方法,包括完整列表、部分初始化及自动确定大小等技巧。我们深入探讨了字符串的本质——以空字符 \0 结尾的字符数组,并通过实例分析了字符串常量在内存中的实际存储形式。理解这些概念是后续进行字符串操作和更复杂数组处理的基础。
045:数组 - 成绩代码示例
在本节课中,我们将学习如何使用一维数组来处理数据。我们将通过一个具体的代码示例来演示如何计算一组学生成绩的平均分。这个示例将展示数组的声明、初始化、遍历以及基本运算。

概述
上一节我们介绍了数组的基本概念。本节中,我们来看看如何将数组应用于实际编程任务。我们将分析一段代码,它接收一系列成绩数据,计算其平均值,并输出结果。这段代码的核心思想可以扩展到处理任意大小的数据集合。
代码解析与教程
1. 定义数组大小与初始化
首先,我们需要确定数组的大小。在C语言中,以这种方式声明的数组会使用栈内存,因此其大小必须是一个常量。
以下是定义数组并初始化的步骤:
const int SIZE = 5;:定义一个常量SIZE来表示数组的大小。int grades[SIZE] = {78, 67, 92, 83, 88};:声明一个名为grades的整型数组,并用花括号内的值进行初始化。数组索引从grades[0]开始,到grades[SIZE-1]结束。- 如果初始化时提供的值少于数组大小(例如只列出3个),则剩余的元素会被自动初始化为0。
2. 声明求和变量
为了计算平均值,我们需要一个变量来累加所有成绩的总和。我们希望平均值是浮点数,以避免整数除法导致的截断误差,因此求和变量应声明为double类型。
double sum = 0.0;
3. 使用循环遍历数组
遍历数组是处理集合数据的核心操作。我们使用一个for循环来依次访问数组中的每个元素。
以下是循环结构的解析:
for (int i = 0; i < SIZE; ++i):这是一个标准的迭代循环。变量i通常用作循环索引。int i = 0:从数组的第一个元素(索引0)开始。i < SIZE:循环条件,只要i小于数组大小就继续执行。++i:每次循环后,索引i自动加1,指向下一个元素。
- 循环体内部可以执行各种操作。在本例中,我们首先打印每个成绩。
printf(“%d\t”, grades[i]);
4. 计算总和与平均值
在遍历数组的过程中,我们可以同时累加成绩来计算总和。完成遍历后,用总和除以元素个数即可得到平均值。
计算过程的代码如下:
// 在循环体内累加成绩
sum += grades[i]; // 等价于 sum = sum + grades[i];
// 循环结束后计算平均值
double average = sum / SIZE;
这个程序的核心逻辑(声明、遍历、计算)可以应用于任意大小的数组,只要我们有相应的数据。
5. 程序运行与输出
我们已经编译并运行了该程序。对于给定的成绩数据 {78, 67, 92, 83, 88},程序输出各成绩并用制表符分隔,然后输出计算出的平均分。
运行结果示例:
78 67 92 83 88
Average: 81.6
总结

本节课中我们一起学习了如何运用一维数组解决实际问题。我们分析了计算成绩平均分的完整代码,涵盖了数组的常量大小定义、初始化、使用for循环遍历、数据累加以及最终的平均值计算。理解并能够修改此类程序,意味着你已经掌握了使用数组处理聚合数据的关键技能,这是成为合格程序员的重要一步。
046:什么是指针

概述
在本节课中,我们将要学习C语言中一个核心且重要的概念:指针。我们将探讨数组在内存中是如何存储的,以及数组与指针之间的紧密联系。理解这些概念对于深入掌握C语言至关重要。
数组在内存中的存储方式
上一节我们介绍了数组的基本概念,本节中我们来看看数组在计算机内存中是如何具体存储的。
数组在内存中占据一块连续的存储空间。它从一个被称为基地址的位置开始,然后根据所需元素的数量,连续地占用后续的内存空间。编译器会自动为我们分配这些内存。
一个简单的数据类型,比如整型 int。当我们声明 int a; 时,意味着在内存中创建一个变量。这个变量需要占用足够存储一个整数值的字节数。在现代计算机上,一个 int 通常占用4个字节(32位)。
当我们执行 a = 3; 时,意味着在这4个字节的内存单元中,存放了二进制表示的数值3。这块内存有一个具体的地址,我们可以将其想象为一个编号,例如地址 7006。
我们可以将计算机内存想象成数十亿个并排的“邮箱”,每个邮箱都有一个唯一的地址编号,里面可以存放特定类型的数据(如整数、字符等)。
数组与地址计算
现在,让我们转向数组。假设我们有一个整型数组 int data[4] = {2, 4, 6, 8};。
这代表了四个连续的整型变量。如果编译器将数组的基地址同样分配在 7006,那么存储情况如下:
data[0](值为2)存储在地址7006开始的4个字节中。data[1](值为4)存储在地址7010开始的4个字节中(7006 + 4)。data[2](值为6)存储在地址7014开始的4个字节中(7006 + 2*4)。data[3](值为8)存储在地址7018开始的4个字节中(7006 + 3*4)。
以下是计算数组中某个元素地址的公式:
地址 = 基地址 + 索引 * 数据类型大小(字节)
这种计算就是地址运算。理解这个概念可能需要一些时间,对于只想进行基础编程的学习者,知道索引能正常工作可能就足够了。但若想深入计算机科学,理解地址的概念是必不可少的。
指针变量
存在一种特殊的变量类型,称为指针变量。它是一种派生类型。
指针的声明方式如下:首先指明它指向的基本数据类型(例如 int),然后使用星号 *,最后是指针变量的名称(常用 p 或 ptr)。
例如:int *p; 声明了一个可以指向整型数据的指针变量,但它本身存储的是一个内存地址。
假设 a 是一个普通的整型变量:int a = 3;。
我们可以进行指针初始化:int *p = &a;。
这里的 & 是“取地址”运算符。这行代码的意思是:初始化指针 p,让它存储变量 a 在内存中的地址。沿用之前的例子,p 中存储的值就是 7006(具体值由编译器决定)。
现在,我们可以区分两个概念:
p本身:它的值是内存地址(例如7006)。*p(解引用p):*是“解引用”运算符。它表示“获取指针p所指向地址中存储的值”。因此,*p的值就是整数3。


总结
本节课中我们一起学习了指针的基础知识。我们了解了数组在内存中的连续存储方式及其地址计算方法。更重要的是,我们引入了指针的概念,明白了指针是一种存储内存地址的变量,通过 & 运算符可以获取变量的地址,通过 * 运算符可以访问指针所指向地址的值。这些概念初学可能有些抽象,但在接下来的课程中,我们将通过实际编写代码来观察地址和指针的值,从而巩固对这些核心思想的理解。
047:指针、数组与地址代码示例

在本节课中,我们将通过一个具体的代码示例,深入理解指针、数组和内存地址之间的关系。我们将学习如何声明指针、获取变量的地址、解引用指针,并观察变量在内存中的布局。
上一节我们介绍了指针的基本概念,本节中我们来看看一个结合了数组和指针的实际代码示例。
代码示例:计算成绩总和与平均值
以下程序首先计算一个成绩数组的总和与平均值,然后打印出相关变量和数组在内存中的地址信息。
#include <stdio.h>
int main() {
int grades[] = {78, 67, 92, 83, 88};
double sum = 0.0;
double *ptr_to_sum = ∑
int i;
printf("成绩为:\n");
for (i = 0; i < 5; i++) {
printf("%d\t", grades[i]);
sum += grades[i];
}
printf("\n");
printf("平均成绩为:%.2f\n\n", sum / 5);
printf("sum变量的地址是:%p\n", &sum);
printf("sum变量的地址(长整型无符号数)是:%lu\n", (unsigned long)&sum);
printf("sum的值是:%f\n", sum);
printf("\n");
printf("ptr_to_sum存储的地址是:%p\n", ptr_to_sum);
printf("ptr_to_sum存储的地址(长整型无符号数)是:%lu\n", (unsigned long)ptr_to_sum);
printf("ptr_to_sum指向的值是:%f\n\n", *ptr_to_sum);
printf("grades数组的起始地址是:%lu\n", (unsigned long)grades);
printf("grades数组第五个元素的地址是:%lu\n", (unsigned long)&grades[4]);
printf("grades数组占用的内存范围是:%lu 到 %lu\n", (unsigned long)grades, (unsigned long)(grades + 5));
return 0;
}
代码解析与核心概念
以下是程序关键部分的详细解释:
1. 指针声明与初始化
double *ptr_to_sum = ∑ 这行代码声明了一个指向 double 类型变量的指针,并用 sum 变量的地址对其进行了初始化。& 是取地址运算符。
2. 指针的解引用
*ptr_to_sum 表示对指针 ptr_to_sum 进行解引用,即获取该指针所指向内存地址中存储的值。在这个例子中,它等同于变量 sum 的值。
3. 地址的打印格式
%p:用于以十六进制格式打印指针(内存地址)。%lu:用于以长整型无符号十进制数格式打印地址,便于我们直观地比较地址数值。
4. 数组的内存布局
程序最后打印了数组的起始地址和最后一个元素的地址。由于每个 int 类型通常占用4个字节,一个有5个元素的数组会占用20个连续字节的内存空间。因此,&grades[4] 的地址值会比 grades 的地址值大20。
程序输出与内存分析
运行上述程序,你可能会看到类似以下的输出(具体地址值因系统和编译器而异):
成绩为:
78 67 92 83 88
平均成绩为:81.60
sum变量的地址是:0x7ffee3a5b904
sum变量的地址(长整型无符号数)是:140732796753156
sum的值是:408.000000
ptr_to_sum存储的地址是:0x7ffee3a5b904
ptr_to_sum存储的地址(长整型无符号数)是:140732796753156
ptr_to_sum指向的值是:408.000000
grades数组的起始地址是:140732796753168
grades数组第五个元素的地址是:140732796753188
grades数组占用的内存范围是:140732796753168 到 140732796753188
从输出中我们可以观察到:
ptr_to_sum中存储的地址与sum变量的地址完全相同。- 对
ptr_to_sum解引用得到的值(408.0)正是所有成绩的总和,与变量sum的值一致。 grades数组的起始地址(140732796753168)与sum的地址(140732796753156)很接近,但不同,说明它们是内存中不同的变量。- 数组最后一个元素的地址(140732796753188)比起始地址大20,印证了数组在内存中是连续存储的,每个
int元素占4个字节。
总结


本节课中我们一起学习了如何通过代码探索指针、数组和内存地址。我们实践了指针的声明、初始化与解引用操作,并使用 %p 和 %lu 格式说明符查看了变量的内存地址。通过比较数组元素的地址,我们直观地理解了数组在内存中的连续存储特性。虽然这些底层细节初看起来可能很复杂,但通过亲手编写和修改这样的示例代码,是掌握C语言内存管理概念的有效方法。理解这些原理对于深入学习C语言乃至C++至关重要。
048:模拟按引用调用

在本节课中,我们将要学习C语言中一个至关重要的概念:如何模拟“按引用调用”。理解这个概念需要你建立对计算机内存、地址和指针的抽象模型。
概述
C语言默认使用“按值调用”来传递函数参数。这意味着传递给函数的只是变量值的副本,函数内部对参数的修改不会影响函数外部的原始变量。然而,许多编程任务需要函数能够直接修改调用者环境中的变量值。C语言通过传递变量的地址(即指针)来模拟“按引用调用”,从而实现这一功能。本节课我们将深入探讨其原理与实现方法。
按值调用回顾
上一节我们介绍了按值调用的基本概念。在按值调用中,函数接收的是实际参数值的副本。
- 过程:调用函数时,实参的值被复制到形参对应的内存位置。
- 结果:函数内部对形参的任何操作都只作用于这个副本。当函数执行完毕,原始的实参变量保持不变。
其核心行为可以概括为:函数内部操作的是局部副本,不影响外部原始数据。
模拟按引用调用的原理
既然按值调用无法修改外部变量,那么如何实现修改呢?本节中我们来看看C语言的解决方案:传递地址。
C语言允许我们通过传递变量的内存地址来模拟按引用调用。这涉及到两个关键操作符:
- 取地址操作符
&:用于获取一个变量的内存地址。- 代码示例:
&variable
- 代码示例:
- 解引用操作符
*:用于访问指针所指向地址中存储的值。- 代码示例:
*pointer
- 代码示例:
一个常见的应用就是 scanf 函数。当我们写 scanf(“%d”, &size); 时,就是将变量 size 的地址传递给 scanf。scanf 函数通过这个地址,直接向 size 所在的内存位置写入新的整数值,从而改变了调用环境中的 size 变量。
核心机制:我们传递一个地址(而非值)给函数。函数通过解引用这个地址,直接读写原始变量所在的内存位置。
实践:编写交换函数
为了具体理解,让我们尝试编写一个交换两个整数值的函数 swap。
错误的按值调用实现
首先,我们看看如果使用按值调用会发生什么。
void swap(int i, int j) {
int temp = i;
i = j;
j = temp;
}
以下是这个函数内部发生的过程:
- 函数接收
i和j的值副本。 - 在函数内部,
i和j的副本成功地交换了值。 - 但是,当函数结束时,这些局部副本被销毁。调用时使用的原始变量(例如
a和b)完全没有被触及,它们的值保持不变。
因此,这个版本的 swap 无法工作。
正确的模拟按引用调用实现
要使 swap 函数真正生效,我们需要传递变量的地址,并在函数内部通过指针来操作。
void swap(int *i, int *j) {
int temp = *i;
*i = *j;
*j = temp;
}
以下是这个正确版本的分析:
- 参数声明:
int *i, int *j声明i和j为指向整数的指针。 - 函数内部操作:
int temp = *i;:解引用指针i,获取其指向地址中的值,存入temp。*i = *j;:解引用指针j,获取其值,并赋值给i所指向的地址。*j = temp;:将temp的值赋值给j所指向的地址。
- 函数调用:调用时必须传入变量的地址:
swap(&a, &b);。
这个版本是C语言中实现“按引用调用”功能的典范。如果你理解了这个函数,就掌握了在C语言中编写此类函数的关键。
实现步骤总结
本节课中我们一起学习了在C语言中模拟按引用调用的完整流程。让我们总结一下必要的步骤:
- 函数声明:将函数的形参声明为指针类型(例如
int *ptr)。 - 函数体操作:在函数内部,使用解引用操作符
*来访问或修改指针所指向的值。 - 函数调用:调用函数时,使用取地址操作符
&传递你想要修改的变量的地址。
遵循以上三步,你就能写出可以改变调用者环境中变量值的函数。
总结


本节课我们深入探讨了C语言中“按引用调用”的模拟机制。我们首先回顾了“按值调用”的局限性,然后引入了通过指针和地址操作来模拟“按引用调用”的核心思想。通过分析一个失败的 swap 函数和一个成功的 swap 函数,我们清晰地对比了两种传递方式的区别。最后,我们总结了实现模拟按引用调用的三个关键步骤。理解并掌握这一机制,对于编写高效、灵活的C程序至关重要。建议你通过编写一些简单的函数来练习,以确保完全理解这个过程。
049:数组作为参数

概述
在本节课中,我们将学习如何将数组作为参数传递给函数。这是处理大量数据时一个非常强大且高效的工具。我们将通过两个具体的例子来展示其用法:一个用于计算数组元素的平均值,另一个用于打印数组的所有元素。
将一维数组作为参数
上一节我们介绍了数组的基本概念,本节中我们来看看如何将数组传递给函数。这看起来很熟悉,因为我们在主函数中已经使用过一维数组,并执行了打印成绩和计算平均值的操作。现在,我们将把这些操作封装成函数,并展示为什么这样做非常有效。
我们将创建两个函数:一个用于计算成绩的平均值,另一个用于打印所有成绩。为了计算平均值,我们需要知道成绩的数量,并传入成绩数组的基地址。
我们假设如果存在10个成绩,那么成绩数组中就会有10个条目,并且所有内容长度都合适。这里有一个可能出错的地方:例如,你传入的数组可能包含过多的元素。
计算平均值的函数
以下是计算平均值函数的逻辑。我们需要一个计数参数 int howMany,还需要一个参数来累加成绩的总和,就像之前一样。
这里我们再次看到了一个非常自然的惯用写法:for (i = 0; i < howMany; i++)。因为数组从零开始,所以循环条件是 i < howMany。然后我们递增索引,并累加总和,将每个元素都加进去。
无论有多少个元素,这种方法都适用,这再次体现了使用编程语言的强大之处:它允许你用相对较少的代码处理任意数量的数据,只要你使用了这些迭代的惯用形式。
最后,因为总和是整数,而我们需要返回一个双精度浮点数,所以我们执行 return (double) sum / howMany;。
代码示例:计算平均值
double computeAverage(int grades[], int howMany) {
int i;
int sum = 0;
for (i = 0; i < howMany; i++) {
sum += grades[i];
}
return (double) sum / howMany;
}
打印数组的函数
我们的第二个函数,打印成绩的函数,甚至更简单。同样是相同的设置:我们传入需要处理的成绩数量 howMany 和成绩数组 grades[]。
在函数内部,我们使用一个计数变量 i 来遍历数组。使用一个制表符分隔的格式字符串,打印出这些成绩。同样,我们可以打印出任意数量的成绩。
代码示例:打印数组
void printGrades(int grades[], int howMany) {
int i;
printf("There are %d grades:\n", howMany);
for (i = 0; i < howMany; i++) {
printf("%d\t", grades[i]);
}
printf("\n");
}
在主函数中使用
现在,主函数中的逻辑变得非常简单。我们不再需要把所有代码都嵌入在主函数里。我们可以创建数组(可以称之为 grades、data 或 myGrades),然后将其连同表示大小的参数 size 一起传入函数。
首先,我们调用函数打印出所有成绩。接着,在第二行,我们计算并输出平均成绩。整个过程非常清晰。
代码示例:主函数调用
int main() {
int myGrades[] = {98, 85, 92, 88, 91};
int size = 5;
printGrades(myGrades, size);
double avg = computeAverage(myGrades, size);
printf("The average grade is: %.2f\n", avg);
return 0;
}

总结
本节课中,我们一起学习了如何将一维数组作为参数传递给C语言函数。我们创建了两个函数:computeAverage 用于计算数组元素的平均值,printGrades 用于打印数组的所有元素。通过这种方式,我们可以将处理数据的逻辑封装起来,使代码更清晰、更易于复用,从而高效地处理大量数据。请习惯编写这种以数组为参数的简单函数,它是处理数据集的强大工具集。
050:数组与冒泡排序

概述
在本节课中,我们将学习一个非常重要的编程主题:排序算法。我们将重点介绍一种名为“冒泡排序”的简单排序算法。虽然冒泡排序对于小型数组完全适用,但对于大型数组则被认为效率较低。排序是计算机科学中最核心的功能之一,因为大量数据(例如大城市电话簿中的数百万条记录)需要被组织,以便高效地搜索和查找信息。
排序的重要性
排序之所以关键,是因为它使数据搜索变得高效。试想一个包含数百万条无序电话号码的列表,要找到特定号码可能需要检查数百万次。然而,如果数据已排序,则可以使用对数时间(例如二分查找)快速定位信息,可能只需十几次查找。因此,排序和搜索是计算机科学中被深入研究的领域,甚至有一本名为《排序与搜索》的专著专门探讨这些技术。
核心概念:交换(Swap)
冒泡排序的核心操作是交换两个顺序错误的元素。我们将使用一个 swap 函数来实现这一点。该函数利用指针参数来模拟“按引用调用”,从而能够实际交换两个变量的值。
代码示例:交换函数
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
打印数组
在排序过程中,我们需要观察数组的变化。因此,我们编写一个函数来打印数组。该函数接收一个数组指针、数组元素数量以及一个标题字符串作为参数。
代码示例:打印数组函数
void printArray(int arr[], int n, char *str) {
printf("%s", str);
for (int i = 0; i < n; i++) {
printf("\t%d", arr[i]);
}
printf("\n");
}
冒泡排序算法详解
上一节我们介绍了交换和打印数组的基础操作,本节中我们来看看冒泡排序算法本身的具体实现。
冒泡排序使用双重循环。外层循环控制排序的“轮次”,每一轮会将当前未排序部分的最大元素“冒泡”到正确位置(数组右侧)。内层循环则负责在每一轮中进行相邻元素的比较和交换。
代码示例:冒泡排序函数
void bubbleSort(int arr[], int n) {
int i, j;
int go; // 用于控制程序暂停的变量
for (i = 0; i < n; i++) {
// 打印每一轮开始前的数组状态
printArray(arr, n, "Inside bubble: ");
printf("Enter an integer to continue: ");
scanf("%d", &go); // 暂停,等待用户输入以继续
// 内层循环:进行相邻元素比较和交换
for (j = n-1; j > i; j--) {
if (arr[j] < arr[j-1]) { // 如果顺序错误
swap(&arr[j], &arr[j-1]); // 交换它们
}
}
}
}
算法执行流程与效率分析
让我们通过一个具体例子来观察冒泡排序的执行过程。假设初始数组为 [78, 67, 92, 83, 88]。
以下是排序过程中数组状态的变化步骤:
-
第一轮(i=0):内层循环从右向左扫描。
- 比较88和83:顺序正确,不交换。
- 比较83和92:顺序错误,交换。数组变为
[78, 67, 83, 92, 88]。 - 比较83和67:顺序正确,不交换。
- 比较67和78:顺序错误,交换。数组变为
[67, 78, 83, 92, 88]。 - 本轮结束后,最小值67被“冒泡”到最左端。
-
后续轮次:每一轮都会将剩余未排序部分的最小元素移动到其正确位置。
- 第二轮将78移动到位置1。
- 第三轮将83移动到位置2。
- 第四轮将88移动到位置3。
- 第五轮后,92自然在位置4。
最终排序结果为 [67, 78, 83, 88, 92]。
冒泡排序的缺点:即使数组在中间某轮已经完全有序,算法仍然会继续执行完所有的外层循环,因为它无法提前“知道”数组已排序。这导致了不必要的比较。一种改进方法是设置一个标志位(flag),如果某一轮内没有发生任何交换,则提前终止算法,因为这表明数组已经有序。
主函数示例
最后,我们来看一个完整的主函数示例,它演示了如何调用上述函数。
代码示例:主函数
int main() {
int grades[] = {78, 67, 92, 83, 88};
int n = sizeof(grades) / sizeof(grades[0]);
printf("Original grades:\n");
printArray(grades, n, "");
bubbleSort(grades, n);
printf("\nSorted grades:\n");
printArray(grades, n, "");
return 0;
}

总结
本节课中我们一起学习了冒泡排序算法。我们了解了排序在数据处理中的重要性,掌握了通过 swap 函数交换元素、使用 printArray 函数观察数组状态的方法,并深入剖析了冒泡排序的双重循环逻辑及其执行过程。虽然冒泡排序易于理解和实现,但其效率对于大型数据集来说较低。建议你尝试用随机数生成更大数组来测试其性能,并思考如何通过添加标志位来优化它。理解基础排序算法是学习更高效、更复杂算法(如快速排序、归并排序)的重要基石。
051:归并排序概述

概述
在本节课中,我们将要学习一种名为“归并排序”的高效排序算法。我们将了解其背后的核心思想——分治策略,并详细解析其合并两个已排序序列的基本操作。
排序的重要性
排序是计算机科学中一个非常重要的主题。在之前的课程中,我们学习了一种非常简单的排序算法——冒泡排序。冒泡排序虽然易于理解,但效率不高,其时间复杂度为 O(n²)。对于小规模数据,计算机速度很快,冒泡排序可以胜任。但对于大规模数据,我们需要更高效的算法。
归并排序的核心思想
上一节我们介绍了冒泡排序的局限性,本节中我们来看看一种高效的排序算法:归并排序。归并排序背后的思想是计算机科学、算法和编程中一个非常重要的概念:分治策略。
归并排序的基本原理是:假设我们有两个已经排好序的序列(或“堆”),我们希望将它们合并成一个更大的、仍然有序的序列。
让我们通过一个具体例子来理解这个过程,这个例子也展示了通用算法的思路。
假设有序序列A包含元素 [3, 5, 7],有序序列B包含元素 [1, 2, 4]。请记住,这些序列在合并前已经是排好序的,合并后我们将得到一个更大的有序序列。
合并过程如下:
- 我们总是比较两个序列中当前最小的元素(即各自序列的首元素)。
- 结果序列中的下一个最小元素必然来自这两个候选元素之一。
- 选择较小的那个元素放入结果序列,并从其原序列中移除(即移动索引)。
以下是合并步骤的详细过程:
- 初始状态:候选元素是
1(来自B) 和3(来自A)。1更小,将其放入结果序列C。C =[1]。 - 下一步:候选元素是
2(来自B) 和3(来自A)。2更小,将其放入C。C =[1, 2]。 - 下一步:候选元素是
4(来自B) 和3(来自A)。3更小,将其放入C。C =[1, 2, 3]。 - 下一步:候选元素是
4(来自B) 和5(来自A)。4更小,将其放入C。C =[1, 2, 3, 4]。 - 此时,序列B已空。进入“清理”阶段:将序列A剩余的所有元素(
[5, 7])按顺序直接放入结果序列C。最终 C =[1, 2, 3, 4, 5, 7]。
这就是归并排序的核心思想:取小的有序序列,合并它们;取更大的有序序列,继续合并;不断重复,直到所有元素合并成一个完整的有序序列。
算法效率分析
可以证明(例如在《计算机程序设计艺术》第三卷“排序与查找”或其他算法书籍、维基百科页面中),归并排序的时间复杂度是 O(n log n),其中 n 是要排序的元素数量。这比冒泡排序的 O(n²) 要高效得多。
合并算法的核心实现
理解了基本思想后,我们来看看合并两个有序序列的核心算法实现。
在这个算法中,我们有:
- 有序序列A,大小为
m - 有序序列B,大小为
n - 结果序列C,用于存放合并后的结果
为了高效合并,我们使用三个索引来跟踪在各个序列中的当前位置:
i:序列A的当前索引j:序列B的当前索引k:结果序列C的当前索引
以下是合并过程的伪代码描述:
while (i < m && j < n) {
if (a[i] <= b[j]) {
c[k] = a[i];
i++;
} else {
c[k] = b[j];
j++;
}
k++;
}
这个 while 循环会持续比较 a[i] 和 b[j],将较小的元素放入 c[k],并相应地移动索引。循环条件是 i 和 j 都未到达各自序列的末尾(i < m && j < n)。
循环结束后,可能有一个序列还有剩余元素(因为两个序列长度可能不同)。这些剩余元素本身已经有序,只需将它们按顺序复制到结果序列C中即可。
以下是处理剩余元素的代码:
// 如果序列A还有剩余元素
while (i < m) {
c[k] = a[i];
i++;
k++;
}
// 如果序列B还有剩余元素
while (j < n) {
c[k] = b[j];
j++;
k++;
}
从合并到完整排序
上面介绍的是归并排序的“合并”核心。完整的归并排序算法会递归或迭代地运用这个合并操作:
- 将待排序的整个序列视为许多长度为1的微型有序序列(单个元素自然有序)。
- 将这些微型序列两两合并,形成长度为2的有序序列。
- 再将长度为2的有序序列两两合并,形成长度为4的有序序列。
- 重复此过程,直到最终合并成一个包含所有元素的有序序列。
总结


本节课中我们一起学习了归并排序算法。我们首先了解了其基于分治策略的核心思想:通过反复合并小的有序序列来构建大的有序序列。我们详细剖析了合并两个有序序列的核心算法,并使用索引和循环实现了它。最后,我们了解到归并排序具有 O(n log n) 的高效时间复杂度,远优于冒泡排序的 O(n²)。如果你不需要深入理解复杂的计算机科学原理,使用冒泡排序处理小数据是可行的,但请记住,归并排序等更高效的算法适用于处理更大规模的数据。在接下来的课程中,我们将实际编写完整的归并排序代码。
052:合并代码示例
在本节课中,我们将学习归并排序的核心算法——合并(Merge)操作的具体代码实现。我们将分析一个简化版本的合并函数,并通过一个示例程序来验证其功能。理解这个基础操作是掌握完整归并排序算法的关键。

上一节我们介绍了归并排序的基本思想,本节中我们来看看其核心合并操作的具体代码实现。
合并函数代码分析
首先,让我们单独查看合并函数的代码。在这个简化版本中,我们假设两个待合并的数组 A 和 B 大小相同,而合并后的数组 C 的大小是它们的两倍。这比在白板上演示的允许不同大小的版本更简单一些。后续在将其嵌入完整程序时,我们还会进一步假设待排序数组的长度是2的幂次方,这便于理解和实现。
以下是合并函数的核心逻辑:
void merge(int A[], int B[], int C[], int size) {
int i = 0; // 数组A的索引
int j = 0; // 数组B的索引
int k = 0; // 合并数组C的索引
while (i < size && j < size) {
if (A[i] < B[j]) {
C[k] = A[i];
i++;
} else {
C[k] = B[j];
j++;
}
k++;
}
// 清理剩余元素
while (i < size) {
C[k] = A[i];
i++;
k++;
}
while (j < size) {
C[k] = B[j];
j++;
k++;
}
}
代码解析如下:
- 索引初始化:
i、j、k分别用于追踪数组A、B和C的当前位置。 - 主合并循环:
while (i < size && j < size)循环持续比较A[i]和B[j]。这里使用了逻辑与运算符&&的短路求值特性:如果第一个条件(如i < size)为假,循环会立即停止,无需检查第二个条件。 - 元素比较与放置:在循环内部,比较
A[i]和B[j],将较小的元素放入C[k],然后递增对应数组和合并数组的索引。 - 清理阶段:当其中一个数组的所有元素都合并完毕后,循环结束。剩下的
while循环负责将另一个数组中剩余的所有元素按顺序复制到数组C的末尾。
示例程序与运行结果
为了验证合并函数,我们使用一个简单的测试程序。程序中定义了两个已排序的小数组,然后调用 merge 函数将它们合并。
以下是测试程序的关键部分:
#define SIZE 5
int main() {
int gradesA[SIZE] = {58, 69, 72, 81, 88}; // 已排序数组A
int gradesB[SIZE] = {67, 82, 83, 88, 89}; // 已排序数组B
int mergedGrades[2 * SIZE]; // 合并后的数组C
merge(gradesA, gradesB, mergedGrades, SIZE);
// ... 打印数组A、B和合并后的数组C ...
return 0;
}
程序运行后,输出结果如下:
数组A: 58 69 72 81 88
数组B: 67 82 83 88 89
合并后数组: 58 67 69 72 81 82 83 88 88 89
运行过程符合归并逻辑:首先比较58和67,58更小,被放入结果数组;接着比较67和69,67被放入;依此类推,直到所有元素合并完毕,最终得到一个完全有序的数组。
本节课中我们一起学习了归并排序中合并操作的具体代码实现。我们分析了一个简化版本的 merge 函数,它通过比较两个已排序数组的元素,将它们合并成一个新的有序数组。我们还通过一个示例程序验证了该函数的正确性。理解这个基础步骤对于接下来学习完整的、能够处理无序数组的归并排序算法至关重要。


在下一节中,我们将探讨如何将这个合并函数嵌入到一个更大的程序框架中,使其能够从一个完全无序的数组开始,递归地进行分割与合并,最终输出一个完全排序的数组。
053:合并排序代码示例-2

概述
在本节课中,我们将学习合并排序算法的完整实现。我们将分析一个专门为处理元素数量为2的幂次方的数组而设计的代码示例,并理解其内部工作原理。
代码结构与全局函数
首先,我们来看一下程序的基本结构。这里有两个熟悉的函数:print_array 用于打印数组,以及 merge 函数,它是执行排序的核心内部例程。
void print_array(int length, int arr[]) {
for (int i = 0; i < length; i++) {
printf("%d\t", arr[i]);
}
printf("\n");
}
void merge(int a[], int b[], int c[], int m, int n) {
int i = 0, j = 0, k = 0;
while (i < m && j < n) {
if (a[i] < b[j]) {
c[k++] = a[i++];
} else {
c[k++] = b[j++];
}
}
while (i < m) {
c[k++] = a[i++];
}
while (j < n) {
c[k++] = b[j++];
}
}
上一节我们介绍了合并两个已排序数组的基本操作,本节中我们来看看如何利用这个操作对整个数组进行排序。
主排序函数:merge_sort
merge_sort 函数是整个算法的驱动部分。它被设计为只处理元素数量是2的幂次方的数组,这简化了代码逻辑。你可以思考如何修改它以处理任意大小的数据集。
以下是该函数的关键部分解析:
void merge_sort(int how_many, int key[], int temp[]) {
int i, j, k, mid;
int from, to, N;
for (k = 1; k < how_many; k *= 2) {
for (from = 0; from < how_many; from = to) {
mid = from + k;
to = mid + k;
N = to - from;
for (i = from, j = mid; i < mid && j < to; ) {
if (key[i] < key[j]) {
temp[from++] = key[i++];
} else {
temp[from++] = key[j++];
}
}
while (i < mid) {
temp[from++] = key[i++];
}
while (j < to) {
temp[from++] = key[j++];
}
}
// 将临时数组temp中的结果复制回原数组key
for (i = 0; i < how_many; i++) {
key[i] = temp[i];
}
}
}
外层循环:按2的幂次方增长
变量 k 控制着当前正在合并的子数组大小。它从1开始,每次迭代乘以2(k *= 2),代表先合并长度为1的数组(即单个元素),然后是长度为2、4、8的数组,直到覆盖整个数组。
公式表示:k = 1, 2, 4, 8, ... , while k < how_many
内层循环:遍历并合并子数组
对于每个 k,内层循环遍历整个数组,每次处理两个相邻的、长度为 k 的子数组进行合并。
from:当前处理的起始索引。mid:第一个子数组的结束索引,也是第二个子数组的起始索引 (mid = from + k)。to:第二个子数组的结束索引 (to = from + 2*k)。- 合并过程与基础的
merge函数逻辑一致,但这里直接操作原数组key和临时数组temp。
关于“键(Key)”的概念
在排序上下文中,我们常将数组元素称为“键”。这是因为在实际应用中(如数据库),我们通常根据某个特定字段(如员工号、学号)进行排序,这个字段就是“键”。排序后,可以高效地根据键值查找其他关联信息。
算法执行过程演示
现在,让我们通过一个具体例子来观察算法的执行步骤。我们定义一个包含8个(2的幂次方)无序成绩的数组。
int main() {
const int SIZE = 8;
int grades[SIZE] = {99, 82, 74, 85, 92, 67, 76, 49};
int temp[SIZE];
printf("原始数组:\n");
print_array(SIZE, grades);
merge_sort(SIZE, grades, temp);
printf("排序后数组:\n");
print_array(SIZE, grades);
return 0;
}
排序步骤分解
以下是算法运行的逻辑过程:
-
第一轮 (k=1):合并相邻的单元素对。
- 比较并排序 (99, 82) -> (82, 99)
- 比较并排序 (74, 85) -> (74, 85)
- 比较并排序 (92, 67) -> (67, 92)
- 比较并排序 (76, 49) -> (49, 76)
- 结果:数组变为多个有序的长度为2的子数组:
[82, 99, 74, 85, 67, 92, 49, 76]
-
第二轮 (k=2):合并相邻的长度为2的子数组。
- 合并
[82, 99]和[74, 85]->[74, 82, 85, 99] - 合并
[67, 92]和[49, 76]->[49, 67, 76, 92] - 结果:数组变为两个有序的长度为4的子数组:
[74, 82, 85, 99, 49, 67, 76, 92]
- 合并
-
第三轮 (k=4):合并最后两个长度为4的子数组。
- 合并
[74, 82, 85, 99]和[49, 67, 76, 92]->[49, 67, 74, 76, 82, 85, 92, 99] - 结果:整个数组完全有序。
- 合并
提示:为了更直观地理解每一轮排序后数组的变化,你可以在
merge_sort函数的内层循环结束后(即更新key数组后)插入print_array语句来打印中间状态。
算法特性与总结
本节课中我们一起学习了合并排序的一个具体实现。
- 策略:这是一种“分治”策略。它将大问题(排序整个数组)递归地分解为小问题(排序子数组),解决小问题后再将结果合并。
- 时间复杂度:合并排序的时间复杂度非常高效,为 O(N log N)。这意味着即使数据量很大,其性能表现也优于像冒泡排序这样的 O(N²) 算法。
- 本实现的限制:当前代码要求数组长度必须是2的幂次方。作为一个扩展练习,你可以尝试修改代码,使其能够处理任意长度的数组,这通常涉及在合并时处理剩余的不完整子数组。


通过将数组不断对半分割直至最小单元,再有序合并,合并排序提供了一种稳定且高效的排序方案。理解其合并过程是掌握该算法的关键。

浙公网安备 33010602011771号