密西根大学-CIS150-C---编程笔记-全-

密西根大学 CIS150 C++ 编程笔记(全)

001:导论 🚀

在本节课中,我们将学习课程的基本信息、所需工具、学术诚信政策以及如何设置开发环境。我们将从课程大纲开始,逐步介绍C++编程的入门知识。

课程概述 📋

本课程是C++编程的入门课程。我们将学习编程基础、C++语法、问题解决技巧以及如何使用现代开发工具。课程包括讲座、实验、个人项目和期末考试。

课程大纲详解

教学团队与联系方式

实验室部分由Dr. Saed负责,时间为每周一16:00-17:45。主讲教师(我)不是教授,这个头衔需要满足特定条件才能使用。

联系方式:

  • 邮箱:请直接发送邮件,不要通过Canvas发送
  • 电话/短信:可以联系,但有时可能无法立即回复
  • 办公室:位于CIIS大楼,上课前后可拜访
  • 虚拟会议:可通过预约安排Zoom会议

课程材料与工具

在线教材:
我们使用ZyBooks在线教材。这是一个基于订阅的交互式学习平台,你可以在浏览器中直接编写和运行代码。每章完成后可以保存为PDF。

推荐参考书:
《Problem Solving with Abstraction and Design using C++》可作为补充阅读材料。

开发工具:

  1. GitHub - 代码托管平台
  2. Git - 版本控制系统(由Linus Torvalds创建)
  3. 编译器
    • Windows:Visual Studio 2022社区版
    • Mac:Xcode
    • Linux:需要额外配置

评分体系

课程成绩采用加权类别计算:

  • ZyBooks阅读与练习:10%
  • 每周实验:10%
  • 个人项目(4个):30%
  • 期中考试:10%
  • 期末项目:40%

重要说明: 期末项目必须完成才能通过课程。期中考试为开卷考试,允许使用书籍、笔记和网络资源。

课程时间表

以下是课程的大致安排:

  • 第1-2周:C++简介、GitHub、Visual Studio入门
  • 第3-4周:基础语法和概念
  • 第5-8周:核心编程概念(循环、函数、数组等)
  • 第9-14周:高级主题和项目开发
  • 第15周:复习和期末项目展示

期中考试计划在10月22日进行。期末项目将在课程最后一个月完成,并在期末考试时间段进行展示。

学术诚信政策 ⚖️

学术诚信是本课程的核心价值观。违反诚信政策将导致课程不及格。

允许的合作

鼓励以下形式的合作:

  • 讨论课程概念
  • 组建学习小组准备考试
  • 讨论项目规范和要求
  • 帮助理解编译器错误(但不提供具体代码)

核心原则: 可以用英语讨论问题,但不能共享代码。

生成式AI使用指南

虽然生成式AI是强大的工具,但在入门阶段需要谨慎使用。

允许的用途:

  • 学习课程概念
  • 生成示例代码供学习参考
  • 理解编译器错误信息
  • 头脑风暴测试用例
  • 生成额外的练习题

禁止的用途:

  • 生成项目代码
  • 校对或修正你的代码错误
  • 在考试期间使用
  • 将生成内容作为自己的作业提交

基本规则: 将AI视为学习伙伴,而不是代写工具。如果你不理解AI生成的代码,那么使用它就没有意义。

开发环境设置 🛠️

上一节我们介绍了课程的基本要求,本节中我们来看看如何设置开发环境。

GitHub设置

GitHub是代码托管平台,我们将使用它来管理所有代码作业。

创建仓库步骤:

  1. 访问GitHub网站并创建账户
  2. 点击"New repository"创建新仓库
  3. 设置仓库名称为"CIS150-Fall2025"
  4. 选择"Public"或"Private"(课程作业建议使用Private)
  5. 勾选"Add a README file"
  6. 添加.gitignore文件(选择C++模板)
  7. 添加许可证(可选,MIT许可证是常用选择)

Git与GitHub Desktop

Git是分布式版本控制系统,GitHub Desktop是其图形界面工具。

安装与配置:

  1. 下载并安装GitHub Desktop
  2. 克隆你的仓库到本地计算机
  3. 仓库通常保存在"Documents/GitHub"文件夹中

基本工作流程:

# 修改代码后
git add .          # 添加更改
git commit -m "描述" # 提交更改
git push           # 推送到GitHub

Visual Studio设置

对于Windows用户,Visual Studio 2022社区版是最佳选择。

安装步骤:

  1. 运行Visual Studio安装程序
  2. 选择"Desktop development with C++"工作负载
  3. 完成安装

创建第一个项目:

  1. 打开Visual Studio
  2. 选择"Create a new project"
  3. 选择"C++" -> "Console App"或"Empty Project"
  4. 将项目保存到Git仓库文件夹中
  5. 添加新的.cpp源文件

第一个C++程序

让我们编写经典的"Hello World"程序:

#include <iostream>
using namespace std;

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/40ee3330a125bd525437ea6b86b825ec_12.png)

int main() {
    cout << "Hello, World!" << endl;
    return 0;
}

代码解释:

  • #include <iostream>:包含输入输出流库
  • using namespace std;:使用标准命名空间
  • int main():程序的主函数
  • cout:标准输出流
  • endl:换行符
  • return 0;:程序正常结束

学习建议与资源 📚

时间投入建议

作为4学分的课程,预计每周需要投入约12小时:

  • 课堂时间:4小时
  • 课外学习:8小时(阅读、练习、项目)

如果学习时间明显超过这个范围,请与教师沟通。

可用资源

以下是可用的学习资源:

校内资源:

  1. 教师办公时间
  2. 实验室时间
  3. 工程成功中心(辅导服务)
  4. 食品储藏室(满足基本需求)

在线资源:

  1. ZyBooks交互式教材
  2. GitHub代码仓库
  3. 课程录像(YouTube)
  4. 密歇根大学的安装指南

成功策略

以下是成功完成课程的建议:

  1. 保持进度:每周完成阅读和练习
  2. 及时求助:遇到问题不要犹豫,及时寻求帮助
  3. 实践为主:编程需要大量练习
  4. 利用资源:充分利用所有可用资源
  5. 组建学习小组:与同学合作学习

总结与下一步 🎯

本节课中我们一起学习了课程的基本框架、工具设置和学术政策。我们了解了:

  1. 课程结构和评分体系
  2. 必需的开发工具(GitHub、Git、Visual Studio)
  3. 学术诚信政策和AI使用指南
  4. 如何设置开发环境并编写第一个C++程序
  5. 可用的学习资源和成功策略

下一步行动:

  1. 注册ZyBooks账户并完成第1章阅读
  2. 设置GitHub账户和开发环境
  3. 完成"Hello World"程序并推送到GitHub
  4. 准备第2章的学习内容

记住,编程是一项需要实践和耐心的技能。遇到困难时,不要放弃 - 寻求帮助,继续尝试。我们将在下一讲中深入探讨C++的基础语法和概念。

祝大家学习顺利! 💻✨

002:变量 🧮

在本节课中,我们将学习C++编程中的核心概念——变量。我们将了解如何声明和使用变量来存储不同类型的数据,如整数和浮点数,并学习如何通过算术运算和用户输入来操作这些数据。


课程安排与工具使用 📅

上一节我们介绍了课程的基本情况,本节中我们来看看本周的具体安排和我们将要使用的工具。

关于Zybooks作业,第一章的所有内容(包括阅读、挑战和实验)的截止日期是9月8日。第二章的截止日期是9月15日。从下周开始,我们将进入第三章,并遵循每周一截止的节奏。

以下是本课程将使用的一些资源和工具:

  • 课程代码仓库:我会将所有课堂代码示例放在这个公开的GitHub仓库中。
  • Discord服务器:这是一个可选的交流渠道,比传统的课程论坛更便捷。
  • 个人网站:上面有我的课程信息和简介。

对于编程实践,我们使用Visual Studio。在创建新项目时,选择“控制台应用”模板,它会自动生成包含 main 函数的基本代码框架,这比从空项目开始更方便。

使用GitHub Desktop可以方便地管理代码版本。当你将新文件添加到仓库文件夹时,它会自动检测到更改(绿色代表新增)。


代码格式与注释原则 ✍️

在开始编写具体代码之前,我们需要了解一些关于代码书写风格和注释的基本原则。

以自动生成的“Hello World”程序为例。以 // 开头的行是注释,编译器会忽略它们。我个人不强制要求代码注释,因为:

  1. 代码本身应该尽可能清晰、自解释。
  2. 注释容易过时,如果修改了代码但忘了更新注释,反而会造成误导。

代码的格式是为了让人阅读。编译器会忽略多余的空格和换行,但良好的格式(如合理的缩进、适当的换行)能让代码更易读、易维护。例如,将代码全部挤在一行虽然能运行,但极其难以阅读。

输出文本时,可以使用 \nstd::endl 来换行。std::endl 能确保在不同操作系统上都能正确换行,使用起来更方便。


变量的声明与使用 🔢

现在,让我们进入核心内容:变量。变量是程序中用于存储数据的基本单元,其值可以改变。

要存储一个整数(没有小数点的数字),我们使用 int 关键字。其基本语法是:

int variableName = value;

例如:

int number = 42;

这行代码告诉C++:在内存中分配一块空间来存储一个整数,并将这块空间命名为 number,然后把值 42 存进去。之后在代码中使用 number 时,程序就会去查找它存储的值。

我们可以对变量进行算术运算:

cout << number + 10; // 输出 52

也可以创建多个变量并进行运算:

int anotherNumber = 10;
int result = number + anotherNumber;
cout << result; // 输出 52

数据类型:整数与浮点数 📊

计算机使用比特(bit)和字节(byte)来存储数据。一个 int 类型通常占用4字节(32位)内存,可以存储大约正负21亿范围内的整数。

如果算术运算的结果超出了整数能存储的范围,就会发生“溢出”,导致结果错误。例如,最大的整数加10会变成一个负数。

整数除法有一个重要特点:整数除以整数的结果仍然是整数,小数部分会被直接舍弃(截断)。例如,7 / 2 的结果是 3,而不是 3.5

要存储带小数点的数字,我们需要使用 double(双精度浮点数)类型。

double actualNumber = 4.2;

double 类型占用8字节内存,可以存储很大或很小的数字,并提供约15位十进制精度。然而,由于浮点数在计算机中的存储方式,进行某些算术运算时可能会出现微小的精度误差。

还有一种 float(单精度浮点数)类型,占用4字节,精度较低,使用时需要在数字后加 f 后缀(如 3.14f)。


算术运算与求模运算符 ➗

C++支持基本的算术运算符:加(+)、减(-)、乘(*)、除(/)。

求模运算符(%)用于计算整数除法后的余数。例如:

int remainder = 7 % 2; // remainder 的值为 1

因为 7 / 2 等于 31。这个运算符在需要判断整除性或循环计数时非常有用。

运算符遵循标准的数学运算顺序(先乘除,后加减),可以使用括号 () 来改变运算顺序。


常量与用户输入 ⌨️

有时,我们希望在程序中使用一个固定不变的值。这时可以使用常量。常量在声明后其值不可更改。按照惯例,常量名通常使用全大写字母和下划线。

const int WEEKS_PER_YEAR = 50;
const double TAX_RATE = 0.15;

使用常量而不是直接写数字(“魔数”)能让代码更易读、易维护。

要让程序与用户交互,我们需要获取用户输入。使用 cin(标准输入流)可以从控制台读取用户输入的数据。

int firstNumber;
cout << "Enter your first number: ";
cin >> firstNumber; // 程序在此等待用户输入

这样,firstNumber 变量中就存储了用户输入的值,程序就可以根据不同的输入进行动态计算了。


实战练习:薪资计算器 💰

让我们综合运用所学知识,编写一个简单的薪资计算器。这个程序会询问用户的时薪、每周工作小时数、月租金及其他月度账单,然后计算其税后年薪及支付所有账单后的剩余金额。

程序的核心逻辑如下:

  1. 声明变量(如 hourlyWage, hoursPerWeek, monthlyRent 等)。
  2. 使用 cin 获取用户为这些变量输入的值。
  3. 定义常量(如 WEEKS_PER_YEAR, TAX_RATE)。
  4. 进行一系列算术计算:
    • 年薪 = 时薪 × 每周小时数 × 每年周数
    • 税后年薪 = 年薪 × (1 - 税率)
    • 年度总支出 = (月租金 + 其他月度账单) × 12
    • 最终剩余 = 税后年薪 - 年度总支出
  5. 使用 cout 输出结果。

通过这个练习,你可以看到变量、常量、输入和算术运算如何组合成一个有实际功能的程序。


程序调试简介 🐞

程序出错是常事。错误主要分为两类:

  1. 逻辑错误:程序能运行,但产生了错误的结果。这是因为我们告诉计算机执行的逻辑有问题。
  2. 运行时错误:程序在运行过程中崩溃,例如除以零。

调试是查找和修复这些错误的过程。Visual Studio 提供了强大的调试工具:

  • 设置断点:在代码行号左侧点击,会出现一个红点。当程序运行到这一行时会暂停。
  • 单步执行:在调试模式下,可以一次执行一行代码(按F10或点击“逐过程”)。
  • 查看变量:在程序暂停时,“局部变量”窗口会显示所有变量当前的值。

通过单步执行和观察变量值的变化,你可以精确地定位逻辑错误发生的位置。养成编写代码时同步进行测试和调试的习惯至关重要。


扩展知识:字符与数学函数 🔤

最后,我们简要了解两个扩展概念。

字符(char 类型用于存储单个字符,如字母或符号。在计算机内部,字符实际上是以数字(根据ASCII或Unicode编码)存储的。字符常量用单引号括起。

char letter = ‘A’;
cout << letter + 5; // 可能会输出 ‘F’ 或其对应的数字值

C++标准库中的 `` 头文件提供了许多常用的数学函数,例如:

  • sqrt(x):计算平方根
  • pow(x, y):计算x的y次幂
  • abs(x):计算绝对值(对于整数)
    使用这些函数可以方便地进行复杂计算。


本节课中我们一起学习了C++中变量的声明与使用、不同的数据类型(int, double, char)、基本的算术运算、如何获取用户输入以及使用常量。我们还通过一个薪资计算器的例子进行了实战练习,并介绍了调试程序的基本方法。掌握这些基础是后续学习更复杂C++编程概念的基石。

003:分支结构(第一部分) 🧭

在本节课中,我们将学习C++中的分支结构。分支结构允许程序根据不同的条件执行不同的代码块,这是让程序变得“智能”和交互性的关键。我们将从基础的 ifelse 语句开始,逐步深入到更复杂的逻辑判断。


概述

上一周我们学习了变量和基本运算,但程序只能按固定顺序执行,这显得有些单调。本节我们将引入分支结构,它能让程序根据用户输入或计算结果做出决策,执行不同的代码路径。我们将重点学习布尔表达式、if-else 语句以及 switch 语句。


布尔表达式与关系运算符

在C++中,布尔表达式是分支判断的基础。它只能产生两个结果:true(真)或 false(假)。这就像电路中的开关,只有“开”或“关”两种状态。

我们使用关系运算符来构建布尔表达式,它们用于比较两个值。

以下是核心的关系运算符:

  • 大于>
  • 小于<
  • 大于等于>=
  • 小于等于<=
  • 等于== (注意:这是两个等号,用于比较;单个等号 = 是赋值运算符)
  • 不等于!=

示例公式

moneyInPocket > 10 // 检查口袋里的钱是否大于10
choice == 1         // 检查选择是否等于1


if-else 语句

if-else 语句是最基本的分支结构。它允许程序在条件为真时执行一段代码,在条件为假时执行另一段代码。

基本语法

if (条件) {
    // 如果条件为 true,则执行这里的代码
} else {
    // 如果条件为 false,则执行这里的代码
}

代码示例:根据预算决定午餐

double moneyInPocket = 12.0;
if (moneyInPocket > 10) {
    cout << "We can get lunch at Picasso." << endl;
} else {
    cout << "We get ramen noodles." << endl;
}
// 输出:We can get lunch at Picasso.

为了让程序更灵活,我们通常会让用户输入变量值:

cout << "How much money do you have in your pocket? ";
cin >> moneyInPocket;
// ... 后续的 if-else 判断

使用 else if 处理多个条件

当存在两个以上的可能情况时,可以使用 else if 来链接多个条件。这些条件是互斥的,即只会执行第一个满足条件的代码块。

代码示例:更精细的预算分级

if (moneyInPocket > 15) {
    cout << "You can get the whole combo at Picasso." << endl;
} else if (moneyInPocket > 10) {
    cout << "You can get lunch at Picasso." << endl;
} else {
    cout << "We get ramen noodles." << endl;
}

重要提示else if 链的顺序至关重要。条件应该从最严格到最宽松排列,否则后面的条件可能永远无法执行。


switch 语句

switch 语句是另一种多分支选择结构,特别适用于基于一个变量与多个常量值进行比较的情况。

基本语法

switch (表达式) {
    case 常量值1:
        // 代码块1
        break;
    case 常量值2:
        // 代码块2
        break;
    ...
    default:
        // 默认代码块(可选)
}

代码示例:饮料选择菜单

int choice;
cout << "Enter your choice (1-4): ";
cin >> choice;

switch (choice) {
    case 1:
        cout << "You got a Coke." << endl;
        break;
    case 2:
        cout << "You got a Coke Zero." << endl;
        break;
    case 3:
        cout << "You got a Mountain Dew." << endl;
        break;
    case 4:
        cout << "You got a Sprite." << endl;
        break;
    default:
        cout << "Invalid choice, no pop for you." << endl;
}

关键点

  • switch 的表达式结果必须是整型或字符型(int, char 等),不能是字符串(string
  • 每个 case 块末尾的 break; 语句至关重要,它用于退出整个 switch 结构。如果省略 break,程序会继续执行下一个 case 的代码,这称为“穿透”(fall-through),有时可利用此特性(如下例),但通常需要避免。

穿透示例:同时处理大小写字母

char input;
cin >> input;
switch (input) {
    case 'y':
    case 'Y':
        cout << "You said yes!" << endl;
        break;
    // ... 其他 case
}

逻辑运算符

有时我们需要组合多个条件。这时就需要用到逻辑运算符

以下是核心的逻辑运算符:

  • 逻辑与 (AND)&& (当且仅当两边条件都为真时,整个表达式为真)
  • 逻辑或 (OR)|| (只要一边条件为真,整个表达式就为真)
  • 逻辑非 (NOT)! (反转布尔值,真变假,假变真)

真值表示例

  • true && true 结果为 true
  • true && false 结果为 false
  • true || false 结果为 true
  • false || false 结果为 false
  • !true 结果为 false

代码示例:组合条件判断

if (moneyInPocket > 10 && choice == 1) {
    cout << "You can afford a Coke at Picasso." << endl;
}

if (input == 'y' || input == 'Y') {
    cout << "Proceed with purchase." << endl;
}

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/9e7f2d9c3bb1b18eedf189643cc1c69c_54.png)

if (!isHungry) { // 相当于 if (isHungry == false)
    cout << "Skip lunch." << endl;
}


运算符优先级

当表达式中包含算术、关系和逻辑运算符时,C++会按照特定的优先级顺序进行计算。

以下是简化的优先级顺序(从高到低):

  1. 括号 ():优先级最高,用于明确指定计算顺序。
  2. 算术运算符:如 +, -, *, /, %
  3. 关系运算符:如 <, >, <=, >=
  4. 相等性运算符:如 ==, !=
  5. 逻辑与 &&
  6. 逻辑或 ||

最佳实践:当表达式变得复杂,尤其是混合使用 &&|| 时,强烈建议使用括号来明确指定运算顺序,这能让代码更清晰,避免逻辑错误。

示例

// 使用括号使意图更清晰
if ( (score > 90) || (age < 18 && parentApproval) ) {
    cout << "Eligible for discount." << endl;
}


项目零介绍:派对规划程序 🎉

现在,我们将运用所学的分支知识来完成第一个项目。这是一个派对规划程序,它会根据客人数量和选择的食物来计算需要购买的数量和总成本。

以下是项目要求:

  1. 询问用户有多少客人(记得包括主人自己)。
  2. 提供三种食物选项供用户选择(例如:蛋糕、披萨、汽水)。
  3. 根据选择,询问对应单件物品的价格(如一个蛋糕的价格、一个披萨的价格、一瓶2升汽水的价格)。
  4. 程序需要计算:
    • 需要购买多少件物品(例如,5个人,每人吃1/4个蛋糕,则需要 ceil(5 * 0.25) = 2个蛋糕)。
    • 这些物品的总成本。
  5. 将结果输出给用户。

项目提交

  • 你需要使用GitHub Classroom创建项目仓库。
  • README.md 文件中添加程序运行的截图。
  • 根据提供的评分标准进行自我评估。


总结

本节课我们一起学习了C++中实现决策的核心工具——分支结构。

  • 我们掌握了如何使用 ifelse ifelse 语句来根据条件执行不同的代码路径。
  • 我们了解了 switch 语句,它适用于基于单个表达式与多个常量值匹配的场景,并注意了 break 关键字的作用。
  • 我们学习了构建条件的基石:布尔表达式关系运算符>, <, == 等)和逻辑运算符&&, ||, !)。
  • 我们讨论了运算符优先级的重要性,并强调使用括号来确保复杂表达式的正确求值。

掌握这些概念后,你的程序将不再只是简单地顺序执行,而是能够根据输入做出智能反应。接下来,请运用这些知识开始你的第一个项目——派对规划程序。

004:分支结构(第二部分) 🧭

在本节课中,我们将继续学习C++中的分支结构,重点探讨如何利用if-else语句处理数值范围、使用逻辑运算符组合条件,以及一些关于字符串操作和浮点数比较的实用技巧。上一节我们介绍了基本的ifswitch语句,本节中我们来看看如何更灵活地运用它们。

概述

本节课我们将学习如何将百分比分数转换为字母等级、计算关税成本、检查字符串内容以及安全地比较浮点数。这些练习将帮助我们巩固对分支结构和逻辑运算的理解。


将分数转换为字母等级 📊

一个常见的应用是将百分制分数转换为字母等级(A, B, C等)。我们可以通过一系列if-else if语句来实现,每个语句检查一个分数范围。

以下是实现此功能的步骤:

  1. 声明一个double类型的变量来存储百分比分数。
  2. 提示用户输入分数。
  3. 使用if-else if链检查分数落入哪个等级范围。
  4. 输出对应的字母等级。

核心代码示例:

double percentage;
cout << "Enter your grade percentage: ";
cin >> percentage;

if (percentage >= 94) {
    cout << "You have an A." << endl;
}
else if (percentage >= 90) { // 隐含条件:percentage < 94
    cout << "You have an A-." << endl;
}
// ... 继续为 B+, B, B-, C+, C, C-, D+, D 添加条件
else {
    cout << "You fail." << endl; // 默认情况
}

关键点: 通过按顺序(从高分到低分)检查条件,我们为每个else if语句创建了隐式范围。例如,只有当分数不满足>=94时,程序才会检查>=90,这等同于检查分数 < 94 && 分数 >= 90


计算关税成本 💰

假设我们需要根据订单金额和原产国计算关税。这涉及到多个条件的组合判断。

以下是实现此功能的步骤:

  1. 定义常量(如免税订单上限TARIFF_ORDER_MINIMUM)和变量(订单金额、原产国、关税税率)。
  2. 获取用户输入的订单金额和原产国。
  3. 使用if-else if语句判断适用的关税规则(例如,小额订单免税、本国产品免税、特定国家税率)。
  4. 计算并输出关税成本。

核心代码示例:

const double TARIFF_ORDER_MINIMUM = 800.0;
const double TARIFF_RATE_INDIA = 0.25;
const double TARIFF_RATE_BRAZIL = 0.50;

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/aca4d2d45dff76f8d9b0ad6480cab151_18.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/aca4d2d45dff76f8d9b0ad6480cab151_20.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/aca4d2d45dff76f8d9b0ad6480cab151_22.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/aca4d2d45dff76f8d9b0ad6480cab151_24.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/aca4d2d45dff76f8d9b0ad6480cab151_25.png)

double orderCost, tariffCost = 0.0;
string countryOfOrigin;

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/aca4d2d45dff76f8d9b0ad6480cab151_27.png)

// ... 获取用户输入

if (orderCost <= TARIFF_ORDER_MINIMUM) {
    tariffCost = 0.0;
}
else if (countryOfOrigin == "USA") {
    tariffCost = 0.0;
}
else if (countryOfOrigin == "India") {
    tariffCost = orderCost * TARIFF_RATE_INDIA;
}
else if (countryOfOrigin == "Brazil") {
    tariffCost = orderCost * TARIFF_RATE_BRAZIL;
}
// ... 可以添加更多国家的判断

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/aca4d2d45dff76f8d9b0ad6480cab151_29.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/aca4d2d45dff76f8d9b0ad6480cab151_31.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/aca4d2d45dff76f8d9b0ad6480cab151_33.png)

cout << "Your tariff cost is $" << tariffCost << endl;

关键点: 使用常量便于集中管理可能变化的数值(如税率)。逻辑运算符||(或)可用于检查无效输入,例如if (percentage < 0 || percentage > 100)


条件(三元)运算符 ⚡

条件运算符 (? :) 提供了一种简洁的内联方式来表达简单的if-else赋值逻辑。其语法为:条件 ? 表达式1 : 表达式2。如果条件为真,整个表达式取表达式1的值,否则取表达式2的值。

核心公式:

变量 = (条件) ? 值1 : 值2;

代码示例对比:

// 使用 if-else
if (orderCost <= TARIFF_ORDER_MINIMUM) {
    tariffCost = 0.0;
} else {
    tariffCost = orderCost * someRate;
}

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/aca4d2d45dff76f8d9b0ad6480cab151_46.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/aca4d2d45dff76f8d9b0ad6480cab151_48.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/aca4d2d45dff76f8d9b0ad6480cab151_50.png)

// 使用条件运算符实现相同功能
tariffCost = (orderCost <= TARIFF_ORDER_MINIMUM) ? 0.0 : (orderCost * someRate);

注意: 虽然条件运算符代码更紧凑,但嵌套或复杂的条件会降低可读性。对于初学者,清晰的if-else语句通常是更好的选择。务必始终使用花括号{}包裹if/else下的代码块,以避免因遗漏导致的逻辑错误。


字符串操作 🔤

string类提供了多种方法来检查和操作文本。

查找子字符串

使用.find()方法可以查找字符或子串在字符串中的位置。如果未找到,它会返回一个特殊的常量string::npos

核心代码示例:

string email = "user@example.com";
size_t atPosition = email.find('@'); // 查找'@'符号的位置

if (atPosition == string::npos) {
    cout << "Invalid email: missing '@'" << endl;
} else {
    string username = email.substr(0, atPosition); // 从开始到'@'之前
    string domain = email.substr(atPosition + 1); // 从'@'之后到结尾
    cout << "Username: " << username << ", Domain: " << domain << endl;
}

替换字符串内容

使用.replace()方法可以替换字符串中的一部分。

核心代码示例:

string message = "apple";
// 将位置1开始的2个字符("pp")替换为"bb"
message.replace(1, 2, "bb");
cout << message << endl; // 输出: abble

注意: 在没有循环的情况下进行全局查找和替换(如将所有‘a’替换为‘b’)比较繁琐,我们将在学习循环后更高效地处理。


浮点数比较的注意事项 ⚖️

由于浮点数在计算机中以二进制近似存储,直接使用==比较两个计算出的浮点数可能因微小的舍入误差而失败。

安全做法: 不检查是否“完全相等”,而是检查两者的差值是否小于一个极小的容差值(如0.000001)。

核心公式:

if (fabs(a - b) < EPSILON) {
    // 认为 a 和 b 相等
}

代码示例:

#include <cmath> // 用于 fabs
const double EPSILON = 0.000001;
double result = 1.0 / 3.0;
double expected = 0.3333333333333333;

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/aca4d2d45dff76f8d9b0ad6480cab151_83.png)

// 不安全的比较
// if (result == expected) ... 

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/aca4d2d45dff76f8d9b0ad6480cab151_85.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/aca4d2d45dff76f8d9b0ad6480cab151_87.png)

// 安全的比较
if (fabs(result - expected) < EPSILON) {
    cout << "The result is approximately one third." << endl;
}

关键点: fabs()函数用于计算浮点数的绝对值。EPSILON的值根据你所需的精度而定。


短路求值 🔌

C++在进行逻辑与(&&)和逻辑或(||)运算时采用“短路求值”。这意味着:

  • 对于表达式1 && 表达式2,如果表达式1false,则整个表达式已确定为false表达式2不会被执行
  • 对于表达式1 || 表达式2,如果表达式1true,则整个表达式已确定为true表达式2不会被执行

这可以提高效率,并在某些情况下避免错误(例如,当表达式2依赖于表达式1为真时)。

代码示例:

int x = 0;
if ( (x != 0) && (10 / x > 2) ) { // 如果 x 为 0,第一部分为 false,第二部分不会执行,避免了除以零的错误。
    // ...
}


总结

本节课中我们一起学习了:

  • 使用if-else if链处理数值范围,例如将分数转换为等级。
  • 结合逻辑运算符常量来构建更复杂的条件判断,如关税计算。
  • 认识了简洁但需谨慎使用的条件(三元)运算符 (? :)。
  • 进行了基本的字符串操作,包括查找(.find())和替换(.replace())。
  • 理解了直接比较浮点数的风险,并学会了通过判断差值是否小于容错值来进行安全比较。
  • 了解了逻辑运算中的短路求值特性及其作用。

这些概念是构建更复杂程序逻辑的基础。请务必通过教材练习和实验来巩固理解。如果你在完成作业时遇到困难,请及时向讲师、助教或辅导中心寻求帮助。

005:循环结构 part1 🔄

在本节课中,我们将要学习C++编程中一个非常强大的概念——循环。循环允许我们重复执行一段代码,直到满足特定条件为止。我们将从基础的while循环开始,然后介绍do-while循环和for循环,并通过实际例子来理解它们的工作原理和应用场景。


概述

循环是编程中用于重复执行代码块的结构。本节课我们将重点学习三种循环:while循环、do-while循环和for循环。理解循环对于编写高效和简洁的代码至关重要。


While循环

上一节我们介绍了条件语句,本节中我们来看看while循环。while循环会在条件为真时重复执行代码块。它是一种“先测试”循环,意味着在执行循环体之前会先检查条件。

基本语法:

while (condition) {
    // 要重复执行的代码
}

例如,我们可以创建一个猜数字游戏,让用户反复猜测,直到猜中为止。

int luckyNumber = rand() % 100 + 1; // 生成1到100的随机数
int guess;
cout << "猜一个1到100的数字: ";
cin >> guess;

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/81de3a9c20c9b4c2c00b2e85dcd977a1_25.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/81de3a9c20c9b4c2c00b2e85dcd977a1_27.png)

while (guess != luckyNumber) {
    if (guess < luckyNumber) {
        cout << "太低了。" << endl;
    } else {
        cout << "太高了。" << endl;
    }
    cout << "再猜一次: ";
    cin >> guess;
}
cout << "恭喜你,猜对了!" << endl;

在这个例子中,只要guess不等于luckyNumber,循环就会继续执行。每次循环都会提示用户输入新的猜测,并根据猜测给出反馈。


输入验证循环

while循环非常适合用于输入验证。我们可以要求用户输入有效的数据,直到他们提供正确的输入为止。

以下是验证用户输入是否为“Y”或“N”的例子:

char playAgain;
cout << "你想再玩一次吗?(Y/N): ";
cin >> playAgain;

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/81de3a9c20c9b4c2c00b2e85dcd977a1_48.png)

while (playAgain != 'Y' && playAgain != 'N') {
    cout << "无效输入,请输入Y或N: ";
    cin >> playAgain;
}

这个循环会持续运行,直到用户输入“Y”或“N”为止。这样可以确保程序只处理有效的输入。


Do-While循环

do-while循环是另一种循环结构,它与while循环类似,但有一个关键区别:do-while循环至少会执行一次循环体,然后再检查条件。

基本语法:

do {
    // 要重复执行的代码
} while (condition);

例如,我们可以使用do-while循环来实现同样的输入验证功能:

char more;
do {
    cout << "你想再玩一次吗?(Y/N): ";
    cin >> more;
} while (more != 'Y' && more != 'N');

do-while循环适用于那些至少需要执行一次的情况,比如菜单选择或初始输入。


For循环

for循环通常用于已知循环次数的情况。它将初始化、条件检查和更新操作集中在一行中,使代码更加简洁。

基本语法:

for (initialization; condition; update) {
    // 要重复执行的代码
}

例如,打印数字0到9:

for (int i = 0; i < 10; i++) {
    cout << i << endl;
}

for循环的等效while循环如下:

int i = 0;
while (i < 10) {
    cout << i << endl;
    i++;
}

for循环的优势在于它将循环的控制逻辑集中在一处,使代码更易读和维护。


嵌套循环

循环可以嵌套在其他循环中,这允许我们处理更复杂的问题,比如打印图形。

例如,打印一个矩形:

int rows, columns;
cout << "请输入行数: ";
cin >> rows;
cout << "请输入列数: ";
cin >> columns;

for (int row = 1; row <= rows; row++) {
    for (int col = 1; col <= columns; col++) {
        cout << "*";
    }
    cout << endl;
}

在这个例子中,外层循环控制行数,内层循环控制每行打印的星号数量。通过嵌套循环,我们可以轻松地打印出各种形状。


总结

本节课中我们一起学习了C++中的循环结构。我们介绍了while循环、do-while循环和for循环,并通过实际例子演示了它们的用法。循环是编程中不可或缺的工具,能够帮助我们编写更加高效和简洁的代码。在下一节课中,我们将继续探讨循环的高级用法和其他相关概念。

006:循环结构(第二部分)

在本节课中,我们将继续深入学习C++中的循环结构。我们将探讨breakcontinue关键字、变量的作用域、枚举类型,并通过一个掷骰子的统计示例来巩固循环的应用。


循环控制:breakcontinue

上一节我们介绍了for循环和while循环的基本用法。本节中,我们来看看两个可以改变循环流程的关键字:breakcontinue

break关键字用于立即终止整个循环。无论循环条件是否满足,一旦执行到break,循环就会停止。

while (true) {
    // 这是一个无限循环
    if (number == INT_MAX) {
        break; // 当number达到最大值时,跳出循环
    }
    number++;
}

continue关键字用于跳过当前循环迭代中剩余的代码,直接开始下一次迭代。

for (int i = 0; i < 100; i++) {
    if (i % 2 == 0) {
        continue; // 如果i是偶数,跳过本次循环的剩余部分
    }
    cout << i << endl; // 只打印奇数
}

虽然breakcontinue提供了便利,但它们有时会使代码逻辑变得不那么直观。通常,我们可以通过调整循环条件或使用if-else语句来达到相同的目的。


变量的作用域

变量的作用域决定了它在程序中的哪些部分可以被访问。一个基本原则是:变量在其被声明的代码块(由花括号{}界定)内有效。

以下是关于作用域的几个要点:

  • 在循环内部声明的变量(例如for循环中的计数器i),其作用域仅限于该循环内部。循环结束后,该变量将无法被访问。
  • 如果需要在循环结束后仍然使用某个变量的值,必须在循环外部声明该变量。
  • 应避免在不同的作用域内使用相同的变量名,这可能导致混淆和错误。

int number; // 在循环外部声明,作用域更广
for (int i = 0; i < 100; i++) { // i的作用域仅限于这个for循环
    number = i * 2;
}
// 这里可以访问number,但不能访问i
cout << number << endl;

调试器是理解作用域和跟踪变量值的绝佳工具。


枚举类型

枚举(enum)是一种用户定义的数据类型,它允许我们为整数值分配有意义的名称,从而使代码更易读、更安全。

定义一个枚举类型:

enum LightState { LS_RED, LS_GREEN, LS_YELLOW, LS_DONE };

使用枚举类型:

LightState currentLight = LS_RED;
while (currentLight == LS_RED) {
    cout << "Waiting at the red light." << endl;
    // ... 模拟等待 ...
    currentLight = LS_GREEN; // 切换状态
}
cout << "Green means go!" << endl;

使用枚举的好处在于,编译器会确保变量只能被赋予枚举中定义的值,这比直接使用整数常量(如0代表红色)更加安全可靠。


实践示例:掷骰子统计

现在,让我们运用循环来模拟掷骰子,并统计各点数出现的次数。这个例子展示了如何通过大量重复实验来观察随机事件的分布。

程序的核心步骤如下:

  1. 询问用户想要模拟掷骰子的次数。
  2. 初始化六个计数器,分别对应点数1到6。
  3. 使用循环执行指定次数的“掷骰子”操作。每次使用随机数生成器模拟掷出一个1到6的点数。
  4. 根据生成的随机数,增加对应点数的计数器。
  5. 循环结束后,输出每个点数出现的次数。
#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/8a6c773aaca9051b333cb31051acb3c1_61.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/8a6c773aaca9051b333cb31051acb3c1_63.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/8a6c773aaca9051b333cb31051acb3c1_65.png)

int main() {
    srand(time(0)); // 设置随机数种子

    int count1 = 0, count2 = 0, count3 = 0, count4 = 0, count5 = 0, count6 = 0;
    int numRolls;

    cout << "How many times do you want to roll? ";
    cin >> numRolls;

    for (int roll = 0; roll < numRolls; roll++) {
        int rollValue = (rand() % 6) + 1; // 生成1-6的随机数

        if (rollValue == 1) count1++;
        else if (rollValue == 2) count2++;
        else if (rollValue == 3) count3++;
        else if (rollValue == 4) count4++;
        else if (rollValue == 5) count5++;
        else count6++; // 一定是6
    }

    cout << "Count of 1s: " << count1 << endl;
    cout << "Count of 2s: " << count2 << endl;
    cout << "Count of 3s: " << count3 << endl;
    cout << "Count of 4s: " << count4 << endl;
    cout << "Count of 5s: " << count5 << endl;
    cout << "Count of 6s: " << count6 << endl;

    return 0;
}

随着掷骰子次数的增加(例如从10次增加到10万次),各个点数出现的次数会越来越接近,这验证了随机事件的均匀分布特性。目前,我们使用了多个独立的变量来存储计数,在后续学习了数组之后,我们可以用更简洁高效的方式重写这个程序。


本节课中我们一起学习了循环控制语句breakcontinue,理解了变量作用域的重要性,认识了能使代码更清晰的枚举类型,并通过一个有趣的统计实例巩固了循环的运用。循环是编程中实现重复操作的基础工具,掌握它们对后续的学习至关重要。

007:函数(第一部分)📚

在本节课中,我们将要学习C++中一个非常重要的概念:函数。函数本身并不能让我们做任何新的事情,但它能帮助我们编写更整洁、更高效的代码。一个很好的经验法则是:如果相同的代码出现了两次,我们就应该考虑使用函数来避免重复。本章内容将分为两周学习,作业提交截止日期是10月13日。


什么是函数?🤔

我们其实一直在使用函数。例如,int main() 本身就是一个函数。此外,像 ceil(3.5) 这样的数学函数也是函数。函数可以接收输入(参数),执行一些操作,并可能返回一个结果。

在C++中,我们通常将自定义的函数写在 main 函数的上方,以便于查找。一个函数的基本结构包括:

  • 返回类型:函数返回值的类型(如 int, double, void)。
  • 函数名:函数的标识符。
  • 参数列表:函数接收的输入,放在括号 () 内。
  • 函数体:函数要执行的代码,放在花括号 {} 内。
  • return 语句:用于返回结果(void 函数可以没有)。

下面是一个简单的函数示例,它总是返回整数10:

int someNumber() {
    return 10;
}

我们可以在 main 函数中调用它:cout << someNumber();

使用调试器的“步入”功能,可以进入函数内部观察其执行过程,理解代码是如何跳转和返回的。


创建更实用的函数⚙️

上一节我们介绍了一个简单的函数,本节中我们来看看如何创建更实用、可定制的函数。

生成随机数的函数

我们可以创建一个函数,让它返回一个指定范围内的随机数。这个函数需要接收一个参数,即随机数的最大值。

int getRandomNumber(int maxNumber) {
    return (rand() % maxNumber) + 1;
}

调用方式:getRandomNumber(100) 会返回一个1到100之间的随机数。

不返回值的函数

有些函数只执行操作,不返回任何值。这时我们使用 void 作为返回类型。

例如,一个打印指定大小和字符的三角形的函数:

void printTriangle(int size, char character) {
    for (int row = 1; row <= size; row++) {
        for (int col = 1; col <= row; col++) {
            cout << character;
        }
        cout << endl;
    }
}

调用方式:printTriangle(10, '*');

函数的一个巨大优势是 功能分解。它允许我们将一个大问题分解成许多小问题,逐个解决。每个函数只负责一个明确的小任务,这使得代码更易于设计、理解和维护。


参数传递:传值与传引用🔗

在上一节创建的函数中,我们只是使用了参数的值。本节中我们来看看如何通过函数修改外部变量的值,这涉及到两种参数传递方式。

传值

默认情况下,C++是传值调用。这意味着函数内部得到的是参数值的一个副本。在函数内部修改这个参数,不会影响函数外部的原始变量。

void tryToChange(int num) {
    num = num + 10; // 只修改了副本
}
int main() {
    int value = 5;
    tryToChange(value);
    cout << value; // 输出仍然是 5
}

传引用

如果我们希望函数能够修改外部变量的值,就需要使用传引用调用。在参数类型后加上 & 符号即可。

void actuallyChange(int &num) { // 注意 & 符号
    num = num + 10; // 修改了原始变量
}
int main() {
    int value = 5;
    actuallyChange(value);
    cout << value; // 输出是 15
}

传引用在需要函数返回多个结果时非常有用,因为一个函数只能通过 return 返回一个值。例如,求解一元二次方程的两个根:

string findIntercepts(double a, double b, double c, double &root1, double &root2) {
    double determinant = b * b - 4 * a * c;
    if (determinant < 0) {
        return "No intercepts found";
    }
    root1 = (-b + sqrt(determinant)) / (2 * a);
    root2 = (-b - sqrt(determinant)) / (2 * a);
    return "Intercepts found";
}


测试函数:断言(assert)🧪

我们常常会写错代码,自己却难以发现。让计算机自动检查代码的正确性是一个好习惯。C++标准库中的 assert 宏可以用于简单的单元测试。

以下是使用 assert 测试 findIntercepts 函数的示例:

#include <cassert>
int main() {
    double r1, r2;
    findIntercepts(2, 3, -2, r1, r2);
    assert(r1 == 0.5);   // 检查第一个根是否为0.5
    assert(r2 == -2.0);  // 检查第二个根是否为-2.0
    cout << "All tests passed!" << endl;
}

如果断言失败(条件为假),程序会崩溃并报错。虽然 assert 在教学中比较简单,但在实际项目中,有更强大的测试框架(如Visual Studio的单元测试项目、Google Test等)可以集成到持续集成/持续部署流程中,实现自动化测试。


构建可重用的工具函数🧰

前面我们学习了函数的基本用法和测试。本节中我们来看看如何将常见的代码模式抽象成可重用的工具函数,这能极大减少代码重复。

获取范围内整数的函数

输入验证是我们经常需要写的代码。我们可以将其封装成一个函数:

int getUserInputInRange(int lower, int upper) {
    int value;
    cout << "Enter a value between " << lower << " and " << upper << ": ";
    cin >> value;
    while (value < lower || value > upper) {
        cout << "Invalid, try again: ";
        cin >> value;
    }
    return value;
}

这样,无论何时需要获取某个范围内的整数,只需调用此函数即可,例如 int size = getUserInputInRange(1, 100);

获取有效字符输入的函数

同样,我们可以编写一个函数来确保用户输入的是我们允许的字符之一:

char getCharFromUser(string prompt, string validChars) {
    char input;
    bool isValid = false;
    while (!isValid) {
        cout << prompt;
        cin >> input;
        // 将输入转换为小写以便比较(可选)
        input = tolower(input);
        // 检查输入是否在有效字符列表中
        for (char c : validChars) {
            if (input == c) {
                isValid = true;
                break; // 找到匹配,跳出循环
            }
        }
    }
    return input;
}

调用方式:char choice = getCharFromUser("Play again? (y/n): ", "yn");


常见错误与总结📝

在本节课中,我们一起学习了C++函数的基础知识。让我们回顾一下重点并看看常见的错误。

常见错误

以下是编写函数时需要注意的几个常见错误:

  • 忘记返回语句:非 void 函数的所有执行路径都必须有 return 语句。
  • 参数类型不匹配:调用函数时传递的参数类型必须与函数声明一致。
  • 忽略了传值与传引用的区别:需要修改外部变量时,忘记使用 &
  • 复制粘贴错误:复制函数代码进行修改时,忘记更改函数名或返回的变量名。

总结

本节课我们主要学习了:

  1. 函数的目的:组织代码、避免重复、实现功能分解,使程序更清晰、更易维护。
  2. 函数的定义与调用:包括返回类型、函数名、参数列表和函数体。
  3. 参数传递:理解了传值(操作副本)和传引用(操作原始变量,使用 &)的关键区别。
  4. 函数测试:使用 assert 进行简单的自动化测试,并了解了工业级测试的基本概念。
  5. 构建实用函数:将输入验证等常见模式抽象成可重用的工具函数。

函数是构建复杂程序的基石。通过将代码组织成一个个具有明确功能的函数,我们可以像搭积木一样构建出强大的软件。在接下来的项目和课程中,我们将大量使用函数。

008:函数(第二部分)🎯

在本节课中,我们将继续深入学习C++函数,探讨一些高级特性,如默认参数、函数重载、作用域以及如何将函数组织到独立的文件中。这些知识将帮助你编写更灵活、更易维护的代码。


函数的作用与重要性

上一节我们介绍了函数的基础概念。本节中,我们来看看函数更深层次的价值。

函数不仅能减少代码重复,更重要的是为代码块赋予有意义的名称,这极大地提升了代码的可读性和可维护性。软件的生命周期很长,清晰的代码结构对于长期的维护和更新至关重要。

变量的作用域

在深入新特性之前,我们需要理解变量的作用域。变量的作用域决定了它在代码中的可见性和生命周期。

  • 局部变量:在函数或代码块(如循环)内部声明的变量。它们只在该函数或代码块内有效。
  • 全局变量:在所有函数之外声明的变量。理论上,程序中的任何函数都可以访问和修改它。

示例:局部变量与全局变量

int globalNumber = 10; // 全局变量

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/57c268b08bd56c88e936f4888a44dcda_8.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/57c268b08bd56c88e936f4888a44dcda_9.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/57c268b08bd56c88e936f4888a44dcda_11.png)

void addToNumber() {
    int localNumber = 5; // 局部变量,仅在此函数内有效
    globalNumber += localNumber; // 可以访问和修改全局变量
}

int main() {
    addToNumber();
    cout << globalNumber; // 输出 15
    // cout << localNumber; // 错误!localNumber 在此处不可见
    return 0;
}

注意:虽然可以使用全局变量,但过度使用会导致代码难以理解和维护。通常,更好的做法是通过参数传递和返回值在函数间交换数据。

默认参数值

有时,我们希望函数的某些参数在调用者不提供时,能自动使用一个预设值。这可以通过默认参数来实现。

默认参数必须在参数列表的末尾依次定义。因为C++是根据参数的位置来匹配的。

示例:带默认参数的函数

// 假设下限默认为1,上限必须提供
int getInputWithinRange(int upperBound, int lowerBound = 1) {
    int userInput;
    // ... 验证输入是否在 [lowerBound, upperBound] 范围内的逻辑 ...
    return userInput;
}

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/57c268b08bd56c88e936f4888a44dcda_29.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/57c268b08bd56c88e936f4888a44dcda_31.png)

int main() {
    int num1 = getInputWithinRange(10); // 相当于 getInputWithinRange(10, 1)
    int num2 = getInputWithinRange(100, 50); // 提供下限50
    return 0;
}

函数重载

C++允许我们定义多个同名函数,只要它们的参数列表(参数的类型、数量或顺序)不同。这被称为函数重载。编译器会根据调用时提供的实参来决定使用哪个版本。

示例:函数重载

// 版本1:接受整数参数
string getDate(int day, int month = 10, int year = 2025) {
    return to_string(year) + "-" + to_string(month) + "-" + to_string(day);
}

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/57c268b08bd56c88e936f4888a44dcda_37.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/57c268b08bd56c88e936f4888a44dcda_38.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/57c268b08bd56c88e936f4888a44dcda_40.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/57c268b08bd56c88e936f4888a44dcda_42.png)

// 版本2:接受字符串参数(参数类型不同,构成重载)
string getDate(string day, string month = "10", string year = "2025") {
    return year + "-" + month + "-" + day;
}

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/57c268b08bd56c88e936f4888a44dcda_44.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/57c268b08bd56c88e936f4888a44dcda_46.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/57c268b08bd56c88e936f4888a44dcda_48.png)

int main() {
    cout << getDate(15); // 调用版本1,输出 "2025-10-15"
    cout << getDate("15", "12"); // 调用版本2,输出 "2025-12-15"
    return 0;
}

重要:仅靠返回值类型不同不足以构成函数重载,因为编译器无法仅通过返回值确定调用哪个函数。

参数验证

在函数内部对传入的参数进行验证是一个好习惯。这可以确保后续的计算或操作基于有效的数据。

示例:简单的日期验证

bool isValidDate(int day, int month, int year) {
    if (month < 1 || month > 12) return false;
    if (day < 1) return false;

    // 检查每月天数(简化版,未考虑闰年)
    int daysInMonth;
    if (month == 2) {
        daysInMonth = 28;
    } else if (month == 4 || month == 6 || month == 9 || month == 11) {
        daysInMonth = 30;
    } else {
        daysInMonth = 31;
    }

    if (day > daysInMonth) return false;
    // 可以添加年份验证(如范围)
    return true;
}

分离代码文件:头文件(.h)

当项目变大时,将函数声明和定义分离到不同的文件中是一种良好的组织方式。通常,我们将函数声明放在头文件(.h 或 .hpp)中,将函数定义放在源文件(.cpp)中。

  1. 创建头文件 (例如 myFunctions.h):
    // myFunctions.h
    #pragma once // 防止头文件被重复包含(Visual Studio)
    // 或者使用传统的防护宏:
    // #ifndef MY_FUNCTIONS_H
    // #define MY_FUNCTIONS_H
    
    #include <string>
    using namespace std;
    
    // 函数声明
    string getDate(int day, int month = 10, int year = 2025);
    bool isValidDate(int day, int month, int year);
    
    // #endif // MY_FUNCTIONS_H
    

  1. 创建对应的源文件 (例如 myFunctions.cpp):

    // myFunctions.cpp
    #include "myFunctions.h"
    #include <iostream>
    using namespace std;
    
    // 函数定义
    string getDate(int day, int month, int year) {
        // ... 实现代码 ...
    }
    
    bool isValidDate(int day, int month, int year) {
        // ... 实现代码 ...
    }
    
  2. 在主程序中使用

    // main.cpp
    #include <iostream>
    #include "myFunctions.h" // 包含自定义头文件
    
    int main() {
        if (isValidDate(31, 12, 2024)) {
            cout << getDate(31, 12, 2024);
        }
        return 0;
    }
    

这样做的好处是代码结构清晰,易于管理,并且在大型项目中能显著提高编译效率。


本节课中我们一起学习了C++函数的进阶主题。我们探讨了变量作用域的概念,学会了如何使用默认参数让函数调用更简洁,了解了通过函数重载使一个函数名能应对多种情况,并强调了参数验证的重要性。最后,我们介绍了如何通过头文件(.h)和源文件(.cpp)来组织代码,这是构建大型、可维护C++项目的基础。掌握这些概念,将使你的编程能力更上一层楼。

009:数组与向量(第一部分)📚

在本节课中,我们将学习C++中两种用于存储多个数据项的重要数据结构:数组向量。我们将了解它们的基本概念、创建方法、访问元素的方式以及各自的优缺点。


概述 📋

数组和向量都允许我们在一个变量名下存储多个相同类型的值。数组是C++中的基础数据结构,大小固定。向量是C++标准库提供的一种更灵活、更强大的容器,可以动态调整大小。理解两者的区别对于编写高效、健壮的代码至关重要。

数组:固定大小的集合 🔢

数组让我们能够在一个变量中存储多个值,而不是为每个值声明单独的变量。

数组的声明与初始化

要声明一个数组,需要指定其类型、名称和大小(必须是一个常量)。数组的大小在编译时必须已知。

int numbers[5]; // 声明一个可以存储5个整数的数组

数组使用方括号 [] 和索引来访问元素。重要:C++数组的索引从0开始

numbers[0] = 42;  // 第一个元素(索引0)
numbers[1] = 77;  // 第二个元素(索引1)
numbers[2] = 100; // 第三个元素(索引2)
// ... 以此类推

访问数组元素

我们可以使用索引来读取或修改数组中的值。

int value = numbers[2]; // 读取索引2处的值
numbers[2] = value * 2; // 修改索引2处的值

数组的局限性

数组的一个主要限制是其大小必须是编译时常量。你不能在运行时根据用户输入来决定数组大小。

int size;
cin >> size;
int dynamicArray[size]; // 错误!size不是常量

此外,将数组传递给函数时,函数无法自动获知数组的大小,必须额外传递一个大小参数。

int sumArray(int arr[], int size) {
    int sum = 0;
    for (int i = 0; i < size; i++) {
        sum += arr[i];
    }
    return sum;
}

如果尝试访问数组边界之外的元素(例如 numbers[5]),C++不会阻止,但会导致读取垃圾数据或程序崩溃,这是数组不安全的一面。


向量:动态大小的集合 🚀

向量是C++标准模板库(STL)的一部分,它提供了数组的所有功能,并且更加灵活和安全。

向量的声明与初始化

要使用向量,需要包含 <vector> 头文件。声明向量时,使用模板语法指定其存储的数据类型。

#include <vector>
std::vector<int> myNumbers; // 声明一个空的整数向量
std::vector<int> presizedNumbers(5); // 声明一个初始大小为5的整数向量

访问向量元素

向量提供了两种访问元素的主要方法:使用 at() 成员函数或使用数组风格的 [] 运算符。推荐使用 at(),因为它会进行边界检查

presizedNumbers.at(0) = 42; // 使用 at() 设置值
int val = presizedNumbers.at(1); // 使用 at() 获取值

// 也可以使用 [],但 at() 更安全
presizedNumbers[2] = 100;

如果尝试使用无效索引(如 at(5)),向量会抛出 std::out_of_range 异常,使程序崩溃,这比数组返回垃圾值更安全,因为它能立即暴露错误。

向量的优势

  1. 动态大小:你可以使用 push_back() 函数在向量末尾添加新元素。
    myNumbers.push_back(10); // 向向量添加数字10
    
  2. 知晓自身大小:向量有一个 size() 成员函数,可以返回当前包含的元素数量。
    int count = myNumbers.size();
    
  3. 更安全的函数传递:将向量传递给函数时,函数可以通过 size() 获知其大小,无需额外参数。
    int sumVector(const std::vector<int>& vec) {
        int sum = 0;
        for (int i = 0; i < vec.size(); i++) {
            sum += vec.at(i);
        }
        return sum;
    }
    

遍历向量

除了使用索引的 for 循环,向量还支持“基于范围的for循环”,这使得遍历更加简洁。

int sum = 0;
for (int num : myNumbers) { // 对于myNumbers中的每个元素num
    sum += num; // 读取其值
}
// 注意:此循环中的`num`是副本,修改它不会影响向量中的原始值。

初始化向量

如果事先知道所有值,可以在声明时直接初始化向量。

std::vector<int> quickNumbers = {1, 2, 3, 4, 5};

数组与向量的比较 ⚖️

上一节我们介绍了数组和向量的基本用法,本节我们来总结一下它们的核心区别。

以下是两者的关键对比:

  • 大小
    • 数组:固定大小,编译时必须确定。
    • 向量:动态大小,可以在运行时增长或缩小。
  • 安全性
    • 数组:不进行边界检查,越界访问可能导致不可预测的行为(读取垃圾值)。
    • 向量:at() 函数进行边界检查,越界访问会抛出异常,更安全。
  • 传递给函数
    • 数组:需要额外传递大小参数。
    • 向量:自带 size() 函数,无需额外参数。
  • 性能
    • 数组:极其轻量,访问速度最快。
    • 向量:由于额外的功能(如动态内存管理),速度略慢,但对于绝大多数应用来说差异可忽略不计。
  • 易用性
    • 数组:语法简单,但功能有限,需要更多手动管理。
    • 向量:功能丰富,更易于使用,是现代的C++首选。

总结 🎯

本节课中我们一起学习了C++中存储数据集合的两种基本工具。

我们首先学习了数组,它是一种固定大小、高效但不够灵活的数据结构,需要谨慎处理大小和边界问题。接着,我们深入探讨了向量,它是标准库提供的动态数组,能够自动管理内存、知晓自身大小并提供更安全的访问方式,极大地提高了编程的便利性和代码的健壮性。

对于初学者和大多数应用场景,建议优先使用向量,因为它更安全、更易用。数组则在需要极致性能或与旧代码交互等特定场景下发挥作用。

在接下来的课程中,我们将继续探索向量的更多功能(如插入、删除元素)以及更复杂的数据结构,如二维数组和向量。

010:数组与向量 Part 2

在本节课中,我们将继续学习数组与向量的应用,并完成一个完整的“猜单词”游戏项目。我们将学习如何将程序分解为多个函数,以及如何使用向量来管理数据。


项目二:猜单词游戏实现

上一节我们介绍了项目的基本框架,本节中我们来看看如何实现具体的函数。

获取隐藏单词

首先,我们需要一个函数来从用户那里获取一个至少包含4个字符的隐藏单词。

以下是 getWord 函数的实现:

string getWord() {
    string word;
    cout << "Enter a word at least four characters long: ";
    cin >> word;
    while (word.size() < 4) {
        cout << "Enter a word at least four characters long: ";
        cin >> word;
    }
    return word;
}

检查单词是否被猜出

接下来,我们需要一个函数来判断玩家是否已经猜出了隐藏单词。该函数接收隐藏单词和已猜字母作为参数。

以下是 hasWordBeenGuessed 函数的实现:

bool hasWordBeenGuessed(string hiddenWord, string lettersGuessed) {
    for (char letter : hiddenWord) {
        bool matchFound = false;
        for (char guess : lettersGuessed) {
            if (letter == guess) {
                matchFound = true;
                break; // 找到匹配即可跳出内层循环
            }
        }
        if (!matchFound) {
            return false; // 只要有一个字母没被猜中,就返回false
        }
    }
    return true; // 所有字母都被猜中
}

判断玩家是否被“吊死”

这个函数根据错误的猜测次数来判断游戏是否结束。

以下是 hasPersonBeenHanged 函数的实现:

bool hasPersonBeenHanged(int numGuesses) {
    return numGuesses >= 6; // 错误次数达到或超过6次则游戏结束
}

显示当前单词状态

这个函数用于在每一轮显示当前猜词进度,已猜出的字母显示出来,未猜出的显示为下划线。

以下是 displayHiddenWord 函数的实现:

void displayHiddenWord(string hiddenWord, string lettersGuessed) {
    for (char letter : hiddenWord) {
        bool matchFound = false;
        for (char guess : lettersGuessed) {
            if (letter == guess) {
                matchFound = true;
                break;
            }
        }
        if (matchFound) {
            cout << letter << " ";
        } else {
            cout << "_ ";
        }
    }
    cout << endl;
}

获取玩家猜测的字母

这个函数负责获取玩家输入的一个字母,并确保该字母之前没有被猜过。

以下是 getLetterGuessed 函数的实现:

char getLetterGuessed(string& lettersGuessed) {
    char guess;
    bool alreadyGuessed;
    do {
        alreadyGuessed = false;
        cout << "Guess a letter: ";
        cin >> guess;
        for (char letter : lettersGuessed) {
            if (letter == guess) {
                alreadyGuessed = true;
                cout << "You already guessed that letter." << endl;
                break;
            }
        }
    } while (alreadyGuessed);
    lettersGuessed += guess; // 将新猜的字母添加到已猜字母列表中
    return guess;
}

检查字母是否在单词中

这个函数检查玩家猜测的单个字母是否存在于隐藏单词中。

以下是 isLetterInWord 函数的实现:

bool isLetterInWord(char guess, string hiddenWord) {
    for (char letter : hiddenWord) {
        if (letter == guess) {
            return true;
        }
    }
    return false;
}

绘制绞刑架

根据错误次数,分步骤绘制绞刑架的图形。

以下是 printGallows 函数的实现:

void printGallows(int incorrectGuesses) {
    if (incorrectGuesses == 0) {
        cout << "  ______" << endl;
        cout << "  |    |" << endl;
        cout << "       |" << endl;
        cout << "       |" << endl;
        cout << "       |" << endl;
        cout << "       |" << endl;
        cout << "========" << endl;
    } else if (incorrectGuesses == 1) {
        // 绘制头部
        cout << "  ______" << endl;
        cout << "  |    |" << endl;
        cout << "  O    |" << endl;
        cout << "       |" << endl;
        cout << "       |" << endl;
        cout << "       |" << endl;
        cout << "========" << endl;
    } else if (incorrectGuesses == 2) {
        // 绘制身体
        cout << "  ______" << endl;
        cout << "  |    |" << endl;
        cout << "  O    |" << endl;
        cout << "  |    |" << endl;
        cout << "       |" << endl;
        cout << "       |" << endl;
        cout << "========" << endl;
    } else if (incorrectGuesses == 3) {
        // 绘制一只手臂
        cout << "  ______" << endl;
        cout << "  |    |" << endl;
        cout << "  O    |" << endl;
        cout << " /|    |" << endl;
        cout << "       |" << endl;
        cout << "       |" << endl;
        cout << "========" << endl;
    } else if (incorrectGuesses == 4) {
        // 绘制两只手臂
        cout << "  ______" << endl;
        cout << "  |    |" << endl;
        cout << "  O    |" << endl;
        cout << " /|\\   |" << endl;
        cout << "       |" << endl;
        cout << "       |" << endl;
        cout << "========" << endl;
    } else if (incorrectGuesses == 5) {
        // 绘制一条腿
        cout << "  ______" << endl;
        cout << "  |    |" << endl;
        cout << "  O    |" << endl;
        cout << " /|\\   |" << endl;
        cout << " /     |" << endl;
        cout << "       |" << endl;
        cout << "========" << endl;
    } else if (incorrectGuesses >= 6) {
        // 绘制两条腿,游戏结束
        cout << "  ______" << endl;
        cout << "  |    |" << endl;
        cout << "  O    |" << endl;
        cout << " /|\\   |" << endl;
        cout << " / \\   |" << endl;
        cout << "       |" << endl;
        cout << "========" << endl;
    }
}


项目三预告:票务管理系统

在下一个项目中,我们将构建一个控制台票务管理系统。这个程序将综合运用文件操作和向量。

以下是该项目的三个主要功能:

  1. 添加演出:用户输入演出名称和总票数。程序会创建一个以演出名称命名的文件,并写入指定数量的空票(票号、空白姓名、确认码为0)。
  2. 售票:用户输入演出名称。程序读取对应文件的所有票务信息到三个平行的向量中(分别存储票号、姓名、确认码)。然后提示用户可购买的最高票号,并询问其想购买的票号。检查该票是否可用(确认码为0)。如果可用,则询问购票者姓名,生成一个随机的6位确认码,更新向量并重写整个文件。
  3. 生成报告:用户输入演出名称。程序读取文件并显示所有票务信息(票号、姓名、确认码),供检票员核对。

这个项目将重点练习文件的读取、写入、检查是否存在,以及使用向量来管理动态数量的数据。我们将使用平行向量的概念,即相同索引下的元素属于同一条记录。


平行向量的概念与应用

在票务管理项目中,我们需要管理三种信息:票号、姓名和确认码。由于我们尚未学习结构体或类,一个实用的方法是使用三个独立的向量,但确保它们在相同索引位置的数据是关联的。

以下是一个简单的示例,演示如何使用平行向量来管理球员姓名和球衣号码:

#include <iostream>
#include <vector>
#include <string>
using namespace std;

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/1cbd3c47fe55f0035380d791f7cc47f5_44.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/1cbd3c47fe55f0035380d791f7cc47f5_46.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/1cbd3c47fe55f0035380d791f7cc47f5_48.png)

int main() {
    vector<string> playerNames;
    vector<int> jerseyNumbers;
    string inputName;
    int inputNumber;

    cout << "Enter player name (or 'quit' to stop): ";
    getline(cin, inputName);

    while (inputName != "quit") {
        playerNames.push_back(inputName);

        cout << "Enter jersey number for " << inputName << ": ";
        cin >> inputNumber;
        jerseyNumbers.push_back(inputNumber);

        // 清除输入缓冲区中的换行符
        cin.ignore();

        cout << "Enter player name (or 'quit' to stop): ";
        getline(cin, inputName);
    }

    // 查找示例
    int searchNumber;
    cout << "Enter a jersey number to look up: ";
    cin >> searchNumber;

    bool found = false;
    for (size_t i = 0; i < jerseyNumbers.size(); ++i) {
        if (jerseyNumbers[i] == searchNumber) {
            cout << "Number " << searchNumber << " belongs to " << playerNames[i] << endl;
            found = true;
            break;
        }
    }
    if (!found) {
        cout << "Could not find jersey number " << searchNumber << endl;
    }

    return 0;
}

在这个例子中,playerNames[i]jerseyNumbers[i] 总是指向同一个球员的信息。这就是平行向量的核心思想。


总结

本节课中我们一起学习了如何将一个完整的猜单词游戏分解为多个函数来实现,包括获取输入、检查状态、显示界面和绘制图形。我们还预告了下一个项目——票务管理系统,并介绍了平行向量的概念,即使用多个索引对齐的向量来管理相关联的不同类型数据。掌握这些技能对于构建更复杂、结构更清晰的程序至关重要。

011:数组与向量 part3 🎯

在本节课中,我们将继续学习向量(vector)的高级操作,包括如何查找最大值、计算平均值,以及实现两种基础的排序算法:选择排序和插入排序。最后,我们将通过一个井字棋游戏的例子,学习如何使用向量的向量(二维向量)来构建一个简单的游戏棋盘。


查找最大值与计算平均值 🔍

上一节我们介绍了向量的基本操作。本节中,我们来看看如何从向量中提取有用的信息,例如最大值和平均值。

查找最大值

要找到一个整数向量中的最大值,我们可以遍历整个向量,并不断更新当前找到的最大值。

核心思路

  1. 假设第一个元素是最大值。
  2. 遍历后续的每个元素。
  3. 如果当前元素比已知的最大值更大,则更新最大值。

代码实现

int getMax(const vector<int>& numbers) {
    int largest = numbers[0]; // 假设第一个元素最大
    for (int i = 1; i < numbers.size(); ++i) {
        if (numbers[i] > largest) {
            largest = numbers[i]; // 发现更大的值,更新最大值
        }
    }
    return largest;
}

计算平均值

计算平均值需要将所有元素相加,然后除以元素的总数。需要注意的是,为了避免整数除法丢失精度,总和应使用double类型。

代码实现

double getAverage(const vector<int>& numbers) {
    double total = 0.0; // 使用double类型存储总和
    for (int value : numbers) {
        total += value; // 累加所有值
    }
    return total / numbers.size(); // 返回平均值
}

排序算法:选择排序与插入排序 📊

理解了如何获取数据后,我们来看看如何组织数据。排序是编程中的常见任务,我们将实现两种基础的排序算法。

选择排序

选择排序的思路是:重复从未排序的部分中找到最大(或最小)的元素,并将其放到已排序部分的末尾。

算法步骤

  1. 从最后一个位置开始,作为待放置最大元素的位置。
  2. 在未排序的部分(从第一个元素到当前待放置位置)中找到最大元素的索引。
  3. 将该最大元素与待放置位置的元素进行交换。
  4. 将待放置位置向前移动一位,重复步骤2-3,直到所有元素排序完成。

代码实现

void selectionSort(vector<int>& numbers) {
    for (int indexToPutBiggest = numbers.size() - 1; indexToPutBiggest > 0; --indexToPutBiggest) {
        int indexOfBiggest = 0; // 假设第一个元素最大
        // 在 0 到 indexToPutBiggest 的范围内寻找最大元素的索引
        for (int indexToCompare = 1; indexToCompare <= indexToPutBiggest; ++indexToCompare) {
            if (numbers[indexToCompare] > numbers[indexOfBiggest]) {
                indexOfBiggest = indexToCompare;
            }
        }
        // 将找到的最大元素交换到目标位置
        int temp = numbers[indexToPutBiggest];
        numbers[indexToPutBiggest] = numbers[indexOfBiggest];
        numbers[indexOfBiggest] = temp;
    }
}

插入排序

插入排序通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

算法步骤

  1. 假设第一个元素自身已经是有序的。
  2. 取下一个元素,在已经排序的元素序列中从后向前扫描。
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置。
  4. 重复步骤3,直到找到已排序的元素小于或等于新元素的位置。
  5. 将新元素插入到该位置后。
  6. 重复步骤2~5,直到所有元素排序完成。

代码实现

void insertionSort(vector<int>& numbers) {
    for (int indexToSort = 1; indexToSort < numbers.size(); ++indexToSort) {
        for (int indexToSwap = indexToSort; indexToSwap > 0; --indexToSwap) {
            if (numbers[indexToSwap] < numbers[indexToSwap - 1]) {
                // 交换元素
                int temp = numbers[indexToSwap];
                numbers[indexToSwap] = numbers[indexToSwap - 1];
                numbers[indexToSwap - 1] = temp;
            } else {
                break; // 如果不需要交换,则中断内层循环
            }
        }
    }
}

性能对比:选择排序无论数据是否有序,都需要进行大量比较。而插入排序在数据已经基本有序的情况下会快很多,但在数据完全逆序时,性能与选择排序类似。


应用:使用二维向量构建井字棋游戏 🎮

掌握了向量的基本操作和排序后,我们可以进行更复杂的应用。下面我们将使用“向量的向量”(即二维向量)来模拟一个井字棋游戏的棋盘。

初始化棋盘

棋盘可以看作是一个3x3的网格,我们用二维向量vector<vector<char>>来表示,每个位置初始为空格。

代码示例

vector<vector<char>> board = {
    {' ', ' ', ' '},
    {' ', ' ', ' '},
    {' ', ' ', ' '}
};

要访问特定位置,例如第1行第2列(索引从0开始),可以使用board[1][2]

游戏主循环

游戏的主逻辑是一个循环,直到游戏结束(分出胜负或平局)为止。

伪代码框架

char currentPlayer = 'X';
while (!isGameOver(board)) {
    printBoard(board);
    markBoard(board, currentPlayer);
    // 切换玩家
    currentPlayer = (currentPlayer == 'X') ? 'O' : 'X';
}
printBoard(board); // 打印最终棋盘

核心功能函数

以下是实现游戏所需的一些关键函数:

1. 打印棋盘

void printBoard(const vector<vector<char>>& board) {
    for (int i = 0; i < 3; ++i) {
        cout << board[i][0] << "|" << board[i][1] << "|" << board[i][2] << endl;
        if (i < 2) {
            cout << "-----" << endl;
        }
    }
}

2. 判断游戏是否结束
游戏结束的条件是:有一方获胜,或者棋盘已满(平局)。

bool isGameOver(const vector<vector<char>>& board) {
    return hasWinner(board) || isTied(board);
}

3. 判断是否平局
如果棋盘上没有任何空格,则为平局。

bool isTied(const vector<vector<char>>& board) {
    for (const auto& row : board) {
        for (char spot : row) {
            if (spot == ' ') {
                return false; // 发现空格,游戏未平局
            }
        }
    }
    return true; // 没有空格,游戏平局
}

4. 判断是否有获胜者
获胜的条件是:任意一行、一列或一条对角线上的三个字符相同且不为空格。

bool hasWinner(const vector<vector<char>>& board) {
    // 检查行
    for (const auto& row : board) {
        if (row[0] != ' ' && row[0] == row[1] && row[0] == row[2]) {
            return true;
        }
    }
    // 检查列
    for (int col = 0; col < 3; ++col) {
        if (board[0][col] != ' ' && board[0][col] == board[1][col] && board[0][col] == board[2][col]) {
            return true;
        }
    }
    // 检查对角线
    if (board[1][1] != ' ') {
        if ((board[0][0] == board[1][1] && board[0][0] == board[2][2]) ||
            (board[0][2] == board[1][1] && board[0][2] == board[2][0])) {
            return true;
        }
    }
    return false;
}

5. 玩家落子
此函数需要修改棋盘,因此参数必须为引用。它需要处理玩家的输入,并确保输入有效(位置在范围内且该位置为空)。

void markBoard(vector<vector<char>>& board, char currentPlayer) {
    int row, col;
    cout << "Player " << currentPlayer << "'s turn. Enter row and column (0-2): ";
    cin >> row >> col;

    // 验证输入有效性
    while (row < 0 || row > 2 || col < 0 || col > 2 || board[row][col] != ' ') {
        cout << "Invalid move. Try again: ";
        cin >> row >> col;
    }
    board[row][col] = currentPlayer; // 在棋盘上标记
}


总结 📝

本节课中我们一起学习了向量的更多实用操作。我们掌握了如何从向量中查找最大值和计算平均值。接着,我们深入探讨了两种基础的排序算法——选择排序和插入排序,并理解了它们的工作原理和代码实现。最后,我们通过构建一个完整的井字棋游戏,综合运用了二维向量的概念,实践了函数分解、游戏逻辑设计和用户交互处理。这些知识为处理更复杂的数据结构和算法问题奠定了坚实的基础。

012:流(第一部分)📂

在本节课中,我们将学习C++中的流(Streams),特别是如何从文件读取数据以及向文件写入数据。这将使我们的程序能够保存信息,并在程序的不同运行之间保持数据。


概述

到目前为止,我们的程序只在运行时处理信息,程序结束后信息就会丢失。通过使用流,我们可以从文件读取数据,也可以将数据写入文件,从而实现数据的持久化存储。本章节将介绍基本的文件输入输出操作。

流的基本概念

我们已经使用过流,例如 cincout,它们是标准的输入和输出流。要使用文件流,我们需要包含 <fstream> 库。

#include <fstream>

输出格式化

在输出数据时,我们可以使用一些操作符来格式化输出,例如设置精度、宽度或填充字符。这些操作符属于 <iomanip> 库。

#include <iomanip>
cout << setprecision(2) << fixed << 3.14159;

以下是常用的格式化操作符:

  • setprecision(n): 设置浮点数输出的精度(位数)。
  • fixed: 使用定点表示法输出浮点数。
  • scientific: 使用科学计数法输出浮点数。
  • setw(n): 设置下一个输出字段的最小宽度。
  • setfill(c): 设置用于填充宽度的字符。

读取输入

从用户获取输入时,cin 会在遇到空格时停止读取。为了读取包含空格的整行文本,我们可以使用 getline 函数。

string fullName;
cout << "Enter your full name: ";
getline(cin, fullName);
cout << "Your name is: " << fullName;

字符串流

有时我们需要从字符串中提取数据,或者将数据格式化为字符串。这时可以使用字符串流 <sstream>

  • istringstream: 用于从字符串读取数据。
  • ostringstream: 用于将数据格式化为字符串。

#include <sstream>
string data = "10 20 30";
istringstream iss(data);
int a, b, c;
iss >> a >> b >> c; // a=10, b=20, c=30

文件输入

要从文件读取数据,我们使用 ifstream(输入文件流)。基本步骤是:打开文件、检查是否成功打开、读取数据、关闭文件。

ifstream inputFile;
inputFile.open("data.txt");

if (!inputFile.is_open()) {
    cout << "Failed to open file." << endl;
    return 1; // 非零返回值表示错误
}

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/2c41e5cacb23fe0abcd3d6b0bf88f28e_73.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/2c41e5cacb23fe0abcd3d6b0bf88f28e_75.png)

string line;
while (getline(inputFile, line)) {
    cout << line << endl;
}

inputFile.close();

文件输出

要向文件写入数据,我们使用 ofstream(输出文件流)。注意,以默认方式打开文件进行写入会覆盖文件的原有内容。

ofstream outputFile;
outputFile.open("output.txt");

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/2c41e5cacb23fe0abcd3d6b0bf88f28e_77.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/2c41e5cacb23fe0abcd3d6b0bf88f28e_79.png)

if (!outputFile.is_open()) {
    cout << "Failed to open file for writing." << endl;
    return 1;
}

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/2c41e5cacb23fe0abcd3d6b0bf88f28e_81.png)

outputFile << "Hello, File!" << endl;
outputFile << 42 << endl;

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/2c41e5cacb23fe0abcd3d6b0bf88f28e_83.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/2c41e5cacb23fe0abcd3d6b0bf88f28e_85.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/2c41e5cacb23fe0abcd3d6b0bf88f28e_87.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/2c41e5cacb23fe0abcd3d6b0bf88f28e_88.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/2c41e5cacb23fe0abcd3d6b0bf88f28e_90.png)

outputFile.close();

综合示例:简易库存管理系统

上一节我们介绍了如何读写文件,本节我们来看一个综合应用:创建一个可以读取商品列表、进行销售并更新库存文件的程序。

假设我们有一个 items.txt 文件,格式如下(每件商品占三行:名称、价格、库存数量):

Coffee
5.00
10
Coke
2.99
20
Tea
3.99
22

以下程序演示了如何读取该文件,处理用户购买,并写回更新后的库存文件。

#include <iostream>
#include <fstream>
#include <string>
using namespace std;

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/2c41e5cacb23fe0abcd3d6b0bf88f28e_100.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/2c41e5cacb23fe0abcd3d6b0bf88f28e_102.png)

int main() {
    string item1Name, item2Name, item3Name;
    double item1Price, item2Price, item3Price;
    int item1Qty, item2Qty, item3Qty;

    // 1. 读取文件
    ifstream inFile("items.txt");
    if (!inFile.is_open()) {
        cout << "Could not open items file. Entering manual mode." << endl;
        // ... 手动输入商品信息的代码 ...
    } else {
        getline(inFile, item1Name);
        inFile >> item1Price >> item1Qty;
        inFile.ignore(); // 忽略换行符
        getline(inFile, item2Name);
        inFile >> item2Price >> item2Qty;
        inFile.ignore();
        getline(inFile, item3Name);
        inFile >> item3Price >> item3Qty;
        inFile.close();
    }

    // 2. 显示商品并处理购买
    cout << "We sell: " << item1Name << ", " << item2Name << ", " << item3Name << endl;
    string choice;
    cout << "What do you want to buy? ";
    cin >> choice;

    // ... 确定所选商品并检查库存的代码 ...

    // 3. 更新库存并写回文件
    ofstream outFile("items.txt");
    if (outFile.is_open()) {
        outFile << item1Name << endl << item1Price << endl << item1Qty << endl;
        outFile << item2Name << endl << item2Price << endl << item2Qty << endl;
        outFile << item3Name << endl << item3Price << endl << item3Qty << endl;
        outFile.close();
        cout << "Inventory updated." << endl;
    }
    return 0;
}

文件路径

打开文件时,可以指定相对路径或绝对路径。

  • 相对路径:相对于程序运行的当前目录。例如 "data.txt""folder/data.txt"。更具灵活性。
  • 绝对路径:完整的系统路径。例如 "C:\\Users\\name\\data.txt"。通常不推荐,因为可移植性差。

总结

本节课我们一起学习了C++中流的核心概念,重点是如何进行文件输入输出操作。我们掌握了使用 ifstreamofstream 来读写文件,了解了基本的文件操作流程(打开、检查、读写、关闭),并通过一个简易库存管理系统的例子实践了这些知识。这使得我们的程序能够持久化存储数据,功能变得更加强大。在接下来的课程中,我们将学习更复杂的数据结构来更好地管理此类信息。

013:流(第二部分)📂

在本节课中,我们将学习C++中关于流(Streams)的更多知识,特别是如何与文件进行交互。我们将了解输入/输出流、字符串流,并最终学习如何从文件中读取数据以及向文件中写入数据。


输入与输出流回顾 🔄

上一节我们介绍了流的基本概念。本节中,我们来看看我们一直在使用的两种标准流:cin(标准输入流)和 cout(标准输出流)。

  • cout 是输出流。我们使用插入运算符 << 将数据插入到输出流中,数据随后会显示在控制台。
    cout << "Hello, World!";
    
  • cin 是输入流。我们使用提取运算符 >> 从输入流中提取用户输入的数据。
    int number;
    cin >> number;
    


处理整行输入 📝

当我们需要获取包含空格的整行文本(例如全名)时,使用 cin >> 会遇到问题,因为它会在遇到空格时停止提取。

以下是处理整行输入的方法:

string fullName;
getline(cin, fullName); // 获取整行输入,包括空格,直到遇到回车键

注意:混合使用 cin >>getline() 可能导致意外行为,因为 cin >> 会留下换行符在输入流中。一个简单的解决方法是使用一个额外的 getline() 来“消耗”这个换行符。


字符串流:解析字符串数据 🧵

字符串流允许我们将一个字符串当作流来处理,从而可以方便地从中提取格式化的数据。这在处理从文件读取的整行数据时非常有用。

以下是使用输入字符串流 (istringstream) 的步骤:

  1. 包含头文件 <sstream>
  2. 创建一个 istringstream 对象,并用一个字符串初始化它。
  3. 像使用 cin 一样,使用 >> 运算符从这个流中提取数据。

#include <sstream>
#include <string>
using namespace std;

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/d2cadc6e5c62aff90d8f828238d53841_61.png)

string vehicleInfo = "2011 Ford Fiesta";
istringstream iss(vehicleInfo); // 创建字符串输入流

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/d2cadc6e5c62aff90d8f828238d53841_63.png)

string year, make, model;
iss >> year >> make >> model; // 从字符串流中提取数据

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/d2cadc6e5c62aff90d8f828238d53841_65.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/d2cadc6e5c62aff90d8f828238d53841_67.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/d2cadc6e5c62aff90d8f828238d53841_69.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/d2cadc6e5c62aff90d8f828238d53841_71.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/d2cadc6e5c62aff90d8f828238d53841_73.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/d2cadc6e5c62aff90d8f828238d53841_75.png)

cout << "Year: " << year << ", Make: " << make << ", Model: " << model << endl;


输出字符串流:构建字符串 🛠️

输出字符串流 (ostringstream) 允许我们像使用 cout 一样构建一个字符串,而不是直接输出到控制台。这对于格式化要写入文件或进行其他处理的字符串非常有用。

#include <sstream>
using namespace std;

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/d2cadc6e5c62aff90d8f828238d53841_91.png)

ostringstream oss; // 创建字符串输出流
oss << "The result is: " << 42 << " and " << 3.14;

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/d2cadc6e5c62aff90d8f828238d53841_93.png)

string finalString = oss.str(); // 获取构建好的字符串
cout << finalString << endl; // 输出:The result is: 42 and 3.14


文件操作:读取文件 📖

现在,我们进入核心部分:文件操作。使用文件可以让我们处理持久化数据,而不仅仅是在程序运行时存在于内存中的数据。

处理文件通常遵循三个步骤:打开文件读取/写入文件关闭文件

以下是读取文件的基本步骤:

  1. 包含头文件 <fstream>
  2. 创建一个 ifstream(输入文件流)对象。
  3. 使用 .open() 方法打开文件。
  4. 检查文件是否成功打开。
  5. 使用 >>getline() 从文件流中读取数据。
  6. 使用 .close() 方法关闭文件。
#include <fstream>
#include <iostream>
using namespace std;

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/d2cadc6e5c62aff90d8f828238d53841_113.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/d2cadc6e5c62aff90d8f828238d53841_115.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/d2cadc6e5c62aff90d8f828238d53841_117.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/d2cadc6e5c62aff90d8f828238d53841_119.png)

int main() {
    ifstream inputFile; // 创建输入文件流对象
    inputFile.open("data.txt"); // 打开名为"data.txt"的文件

    // 检查文件是否成功打开
    if (!inputFile.is_open()) {
        cout << "Could not open file data.txt" << endl;
        return 1; // 返回非零值表示错误
    }

    int value;
    // 从文件中读取一个整数
    inputFile >> value;
    cout << "Read value: " << value << endl;

    inputFile.close(); // 关闭文件
    return 0;
}

重要概念:文件路径

  • 相对路径:如 "data.txt"。程序会在其运行目录中寻找该文件。在集成开发环境(如Visual Studio)中运行时,这通常是项目目录。这是最常用和可移植的方式。
  • 绝对路径:如 "C:\\Users\\Name\\data.txt"(注意Windows中需要使用双反斜杠\\)。指定了文件的完整位置,但程序移植到其他计算机时可能失效。


读取文件中的所有数据 🔄

我们通常需要读取文件中的多个数据,直到文件末尾。这可以通过循环来实现。

#include <fstream>
#include <iostream>
using namespace std;

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/d2cadc6e5c62aff90d8f828238d53841_142.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/d2cadc6e5c62aff90d8f828238d53841_143.png)

int main() {
    ifstream inputFile("numbers.txt"); // 创建并打开文件(构造函数方式)

    if (!inputFile) { // 更简洁的打开检查方式
        cerr << "Error opening file." << endl;
        return 1;
    }

    int number;
    int total = 0;

    // 当可以成功从文件中提取一个整数时,继续循环
    while (inputFile >> number) {
        total += number;
        cout << "Read: " << number << endl;
    }

    // 循环结束后,检查是否正常到达文件末尾
    if (inputFile.eof()) {
        cout << "End of file reached successfully." << endl;
    } else {
        cout << "Error reading file before reaching the end." << endl;
    }

    cout << "Total sum: " << total << endl;

    inputFile.close();
    return 0;
}


总结 📚

本节课中我们一起学习了C++流的高级应用:

  1. 回顾了标准输入/输出流 (cin/cout)。
  2. 学习了如何使用 getline() 获取整行输入,并注意了与 cin >> 混用时的陷阱。
  3. 掌握了字符串流 (istringstreamostringstream) 的用法,用于解析字符串和构建字符串。
  4. 重点学习了文件输入操作,包括如何打开文件、检查状态、读取数据(单个或全部)以及正确关闭文件。
  5. 理解了相对路径和绝对路径的区别。

文件操作是程序与外部世界交互的关键,它使得数据处理变得自动化且高效。在接下来的课程中,我们将学习如何向文件中写入数据。

014:流操作(三)📂

在本节课中,我们将继续学习C++中的文件流操作。我们将探讨如何使用分隔符在单行中存储和读取多个数据项,如何以追加模式打开文件,以及如何使用向量来灵活地处理数量不定的数据。课程最后,我们会回顾期中考试的相关安排。


文件读写与分隔符

上一节我们学习了如何按行读写文件。本节中我们来看看如何将多个数据项存储在一行中,并使用特定的字符(分隔符)将它们分开。

这种方法在数据格式紧凑时非常有用。例如,我们可以将商店名和购物清单中的所有物品写在同一行,用“|”字符分隔。

以下是写入文件的代码示例:

#include <fstream>
#include <string>
using namespace std;

int main() {
    ofstream outFile;
    outFile.open("list.txt");
    outFile << "Kroger|Milk|Lettuce|Eggs" << endl;
    outFile.close();
    return 0;
}

这段代码创建了一个名为list.txt的文件,并将字符串Kroger|Milk|Lettuce|Eggs写入其中。


从文件中读取并解析数据

写入数据后,我们需要能够将其读回程序。这涉及到读取整行字符串,然后根据分隔符将其拆分成独立的部分。

以下是读取并解析文件的步骤:

  1. 打开文件进行读取。
  2. 使用getline函数读取一整行。
  3. 使用find函数在字符串中定位分隔符的位置。
  4. 使用substr函数根据分隔符的位置提取子字符串。

以下是实现该过程的代码示例:

#include <fstream>
#include <string>
#include <iostream>
using namespace std;

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/c4172b75d9daff1b3143444ea32988f3_21.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/c4172b75d9daff1b3143444ea32988f3_22.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/c4172b75d9daff1b3143444ea32988f3_24.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/c4172b75d9daff1b3143444ea32988f3_26.png)

int main() {
    ifstream inFile;
    inFile.open("list.txt");
    string line;
    getline(inFile, line); // 读取整行

    char delimiter = '|';
    size_t index = 0;
    size_t nextIndex = line.find(delimiter);

    // 提取商店名(第一个分隔符之前的部分)
    string storeName = line.substr(0, nextIndex);
    cout << "Store: " << storeName << endl;

    // 循环提取所有物品
    while (nextIndex != string::npos) {
        index = nextIndex + 1; // 移动到分隔符之后
        nextIndex = line.find(delimiter, index); // 查找下一个分隔符

        // 提取一个物品
        string item = line.substr(index, nextIndex - index);
        if (!item.empty()) {
            cout << "Item: " << item << endl;
        }
    }

    inFile.close();
    return 0;
}

这段代码会输出商店名和所有以“|”分隔的物品。


使用向量存储动态数量的数据

上面的例子假设我们知道有多少个物品。为了更灵活地处理任意数量的数据项,我们可以使用vector容器。

向量可以动态地存储多个同类型的数据项,我们无需在编写代码时确定其具体数量。

以下是使用向量改进后的读取代码:

#include <fstream>
#include <string>
#include <vector>
#include <iostream>
using namespace std;

int main() {
    ifstream inFile;
    inFile.open("list.txt");
    string line;
    getline(inFile, line);

    char delimiter = '|';
    vector<string> items;
    size_t index = 0;
    size_t nextIndex = line.find(delimiter);

    // 第一个部分是商店名
    string storeName = line.substr(0, nextIndex);

    // 循环提取所有物品并存入向量
    while (nextIndex != string::npos) {
        index = nextIndex + 1;
        nextIndex = line.find(delimiter, index);
        string item = line.substr(index, nextIndex - index);
        if (!item.empty()) {
            items.push_back(item); // 将物品添加到向量末尾
        }
    }

    cout << "List for " << storeName << ":" << endl;
    for (const string& item : items) {
        cout << "- " << item << endl;
    }

    inFile.close();
    return 0;
}

现在,无论一行中有多少个物品,代码都能正确读取并存储它们。


以追加模式打开文件

默认情况下,用ofstream打开文件会覆盖原有内容。如果我们想在不删除旧数据的情况下添加新内容,可以使用追加模式。

以下是使用追加模式打开文件的代码:

ofstream outFile;
outFile.open("list.txt", ios::app); // ios::app 表示追加模式
outFile << "Gamestop|Battlefield|GTA|Ghost of Tsushima" << endl;
outFile.close();

运行此代码后,新的购物清单会被添加到list.txt文件的末尾,而不会清除“Kroger”的那条记录。


调试技巧

在编写文件解析代码时,很容易遇到索引错误或逻辑问题。使用调试器是解决这类问题的最佳方法。

以下是简单的调试步骤:

  1. 在可能出错的代码行设置断点。
  2. 逐行执行程序。
  3. 观察变量(如indexnextIndexline)的值是否符合预期。
  4. 根据观察结果调整代码逻辑。

期中考试安排与复习

本节课的最后,我们来看看期中考试的安排。考试将于指定日期在Canvas平台以线上测验形式进行。

以下是考试的基本信息:

  • 形式:开卷、开笔记、可查阅资料。
  • 内容:包含选择题、简答题和编程题。
  • 要求:所有提交的答案必须是个人独立完成,引用外部资源需注明出处。

为了帮助大家复习,上一学期的期中试卷已作为练习题提供。建议先尝试独立完成,再看讲解。


课程总结

本节课中我们一起学习了C++文件流操作的高级技巧。我们掌握了如何使用分隔符在一行内组织数据、如何读取并解析这种格式的数据、如何使用vector来灵活处理数据集合,以及如何以追加模式向文件添加内容。此外,我们还介绍了使用调试器排查代码问题的方法,并了解了期中考试的相关信息。掌握这些技能将帮助你更有效地在程序中处理持久化数据。

015:指针与动态内存管理

在本节课中,我们将学习C++中指针的核心概念,包括动态内存的分配与释放、指针与对象的关系,以及继承和多态性的初步介绍。理解这些概念对于编写高效、灵活的C++程序至关重要。

动态内存分配与释放

上一节我们介绍了使用代码模拟现实场景的优势。本节中我们来看看如何通过动态内存管理来增强程序的灵活性。

当我们在程序中使用 new 关键字时,是在向操作系统请求在上分配内存。堆是一个比栈大得多的内存池,栈的空间在程序启动时就已预先分配好。

核心公式:

int* ptr = new int; // 在堆上分配一个整数的内存

使用 new 分配内存后,我们必须在使用完毕后手动释放它,否则会导致内存泄漏。释放内存使用 delete 关键字。

核心公式:

delete ptr; // 释放单个指针指向的内存
delete[] arrayPtr; // 释放动态数组的内存

对于动态数组,必须使用 delete[] 而非 delete。如果忘记释放内存,程序占用的内存会持续增长。

指针与对象

指针不仅可以指向基本数据类型,也可以指向类的对象。这使得我们可以在堆上创建对象。

当我们使用指针访问对象的成员函数时,语法与普通对象不同。需要使用箭头操作符 -> 而非点操作符 .

核心代码示例:

Chair* ericsChair = new Chair(); // 在堆上创建一个Chair对象
ericsChair->setColor("blue"); // 使用箭头操作符调用成员函数

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/e6d154982b8a44dcdbf0ef77b8fd2046_26.png)

Chair jebsChair; // 在栈上创建一个Chair对象
jebsChair.setColor("black"); // 使用点操作符调用成员函数

虽然目前看来在堆上创建单个对象优势不明显,但随着程序复杂度增加,堆内存的管理能力将变得非常重要。

继承简介

继承是面向对象编程的核心原则之一,它允许我们创建一个新类(子类)来继承现有类(父类)的属性和行为。这就像生物学中的遗传,子类继承了父类的特性,同时可以添加自己独有的特性。

例如,我们可以有一个基础的 Chair 类。如果我们想要一个带马达的椅子,无需重写整个 Chair 类,只需创建一个继承自 ChairMotorizedChair 类,并添加驱动功能即可。

核心代码示例:

class MotorizedChair : public Chair { // MotorizedChair 继承自 Chair
public:
    void drive() {
        cout << "vroom" << endl;
    }
};

class FloatingChair : public Chair { // FloatingChair 继承自 Chair
public:
    void drive() {
        cout << "splash" << endl;
    }
};

多态性与虚函数

继承的一个强大特性是多态性。它允许我们使用父类的指针或引用来操作子类的对象,并在运行时决定调用哪个版本的函数。

为了实现多态性,我们需要在父类中将函数声明为 virtual(虚函数),并在子类中使用 override 关键字来重写该函数。

核心代码示例:

class Chair {
public:
    virtual void drive() { // 声明为虚函数
        // 普通椅子不能驱动,什么都不做
    }
};

void driveChair(Chair* aChair) {
    aChair->drive(); // 根据实际对象类型调用正确的 drive 函数
}

当我们调用 driveChair 并传入一个 MotorizedChair 指针时,程序会在运行时识别出它实际上是 MotorizedChair 类型,从而调用 MotorizedChair::drive() 输出 “vroom”,而不是父类中什么都不做的版本。这就是多态性的魔力。

如果没有指针,这种运行时行为解析将无法实现。多态性允许我们编写更通用、更灵活的代码,例如,一个函数只需要知道对象“可以驱动”,而不必关心它具体是哪种椅子。

总结与后续

本节课中我们一起学习了C++指针的几个关键方面:

  1. 使用 newdelete 进行动态内存的手动管理。
  2. 指针与对象结合使用时,需使用 -> 操作符。
  3. 继承的基本概念,允许子类扩展父类的功能。
  4. 多态性的初步介绍,通过虚函数和重写,实现运行时函数调用的动态绑定。

指针是C++强大但也容易出错的特性。现代C++推荐使用智能指针(如 unique_ptr, shared_ptr)来自动管理内存,减少内存泄漏的风险。此外,关于拷贝构造函数、赋值运算符和析构函数的“三法则”,我们将在后续课程中深入探讨。

掌握指针和动态内存是深入理解C++的重要一步,它们为构建复杂、高效的程序奠定了基础。

016:面向对象编程 part1 🧱

在本节课中,我们将要学习面向对象编程的基础概念。我们将了解什么是对象和类,如何定义它们,以及如何通过封装和抽象来组织代码,使其更易于理解和维护。


什么是对象和类? 🤔

上一节我们介绍了课程概述,本节中我们来看看对象和类的核心概念。

对象是类的实例。这听起来像是一个有趣的区分。例如,“椅子”这个词。当你听到“椅子”时,你想到的是椅子的定义(一个供人坐的东西,有颜色、腿、可能带轮子),还是一个特定的椅子(比如你正坐着的那个灰色的、有四个轮子的办公椅)?类的定义就像是蓝图,它描述了某类事物共有的属性和行为。而对象则是根据这个蓝图创建出来的具体实例。

在代码中,我们通过定义类来创建这种蓝图。一个类通常包含两个主要部分:私有(private)部分和公有(public)部分。

公式/代码描述:

class Chair {
private:
    // 私有属性(数据成员)
    std::string color;
    int heightInCm;
    int numberOfWheels;
    bool hasArms;
    int minHeightCm;
    int maxHeightCm;

public:
    // 公有函数(成员函数)
    std::string getColor();
    void setColor(std::string newColor);
    int getHeightInCm();
    void setHeightInCm(int newHeight);
    // ... 其他 get 和 set 函数
};

私有部分包含了类的属性(也称为数据成员),这些细节对外部代码是隐藏的。公有部分则包含了类的方法(也称为成员函数),这些构成了外部代码与类交互的接口,我们称之为应用程序编程接口(API)。这种将实现细节隐藏起来,只暴露必要接口的思想,就叫做封装抽象


创建和使用对象 🛋️

上一节我们定义了Chair类,本节中我们来看看如何创建和使用它的对象。

要创建一个类的对象(也称为实例化),我们使用类名,后跟对象名。然后,我们可以通过点运算符(.)来访问该对象的公有成员函数。

公式/代码描述:

int main() {
    // 创建 Chair 类的对象
    Chair ericsChair;
    Chair bobsChair;

    // 使用 setter 函数设置属性
    ericsChair.setHeightInCm(30);
    ericsChair.setColor("gray and blue");
    ericsChair.setNumberOfWheels(4);
    ericsChair.setHasArms(false);

    bobsChair.setHeightInCm(40);
    bobsChair.setColor("black");
    bobsChair.setNumberOfWheels(5);
    bobsChair.setHasArms(true);
    bobsChair.setMinHeightCm(30);
    bobsChair.setMaxHeightCm(50);

    // 使用 getter 函数获取属性
    std::cout << "Eric's chair color: " << ericsChair.getColor() << std::endl;
    std::cout << "Bob's chair has arms: " << std::boolalpha << bobsChair.getHasArms() << std::endl;

    return 0;
}

每个对象在内存中都有自己独立的空间,存储其属性的值。ericsChairbobsChair是两个不同的对象,它们的属性值互不影响。


构造函数与访问控制 🔐

上一节我们创建了对象并设置了属性,本节中我们来看看如何更安全、更便捷地初始化对象。

有时,我们不希望对象的某些属性在创建后被修改。例如,一张彩票的号码一旦选定就不应更改。为了实现这一点,我们可以使用构造函数来初始化这些属性,并且不提供修改它们的公有函数(setter)。

构造函数是一种特殊的成员函数,它在创建对象时自动调用,用于初始化对象。构造函数没有返回类型,其名称与类名相同。

公式/代码描述:

class LottoTicket {
private:
    int number1;
    int number2;
    int number3;

public:
    // 构造函数:在创建对象时设置号码
    LottoTicket(int num1, int num2, int num3) {
        this->number1 = num1;
        this->number2 = num2;
        this->number3 = num3;
    }

    // 只有 getter,没有 setter,号码不可更改
    int getNumber1() { return number1; }
    int getNumber2() { return number2; }
    int getNumber3() { return number3; }

    // 检查是否中奖的函数
    bool isWinner(LottoTicket winningTicket) {
        return (number1 == winningTicket.getNumber1() &&
                number2 == winningTicket.getNumber2() &&
                number3 == winningTicket.getNumber3());
    }
};

使用示例:

LottoTicket winningTicket(5, 7, 1); // 创建时即确定中奖号码
LottoTicket myTicket(3, 3, 7); // 创建我的彩票

if (myTicket.isWinner(winningTicket)) {
    std::cout << "You win!" << std::endl;
}

通过构造函数,我们确保了LottoTicket对象的号码在生命周期内保持不变。this指针用于在成员函数内部指向调用该函数的当前对象,当参数名与成员变量名冲突时特别有用。


在类中添加业务逻辑 🧠

上一节我们使用构造函数控制了对象的初始化,本节中我们来看看如何在类的方法中加入更多逻辑。

类的强大之处在于,它不仅能存储数据,还能包含操作这些数据的逻辑。我们可以在 setter 函数中加入验证逻辑,以确保对象的属性始终处于有效状态。

例如,对于可调节高度的椅子,在设置当前高度时,我们应该确保这个高度在最小和最大高度范围内。

公式/代码描述:

void Chair::setHeightInCm(int newHeight) {
    // 验证逻辑:新高度必须在最小和最大高度之间
    if (newHeight >= minHeightCm && newHeight <= maxHeightCm) {
        heightInCm = newHeight;
    }
    // 如果不符合条件,则静默忽略或未来可以用异常处理
}

同样,我们可以为numberOfWheels的 setter 添加检查,防止设置为负数。

void Chair::setNumberOfWheels(int wheels) {
    if (wheels >= 0) {
        numberOfWheels = wheels;
    }
}

将这样的规则封装在类内部,是保证对象数据完整性和一致性的关键。类的设计应反映现实世界的约束。


分离文件与单元测试简介 📁

上一节我们在类中实现了业务逻辑,本节中我们简要了解一下如何组织代码和验证其正确性。

对于较大的项目,通常将类的声明(在头文件 .h 中)和定义(在源文件 .cpp 中)分开。这有助于管理代码并提高编译效率。

公式/代码描述:
Chair.h (头文件 - 声明)

#ifndef CHAIR_H // 防止头文件被重复包含
#define CHAIR_H
#include <string>

class Chair {
private:
    std::string color;
    int heightInCm;
    // ... 其他属性
public:
    // 函数声明
    std::string getColor();
    void setColor(std::string newColor);
    // ...
};
#endif

Chair.cpp (源文件 - 定义)

#include "Chair.h"
// 实现各个成员函数
std::string Chair::getColor() {
    return color;
}
void Chair::setColor(std::string newColor) {
    color = newColor;
}
// ...

关于测试:编写测试代码来验证你的类是否按预期工作至关重要。这被称为单元测试。虽然课程中展示的方法可能不够优雅,但核心理念是创建对象,调用其方法,并检查结果是否符合预期。确保代码“做它该做的事”是开发者的重要责任。


总结 🎯

本节课中我们一起学习了面向对象编程的基础。

我们了解了是定义事物属性和行为的蓝图,而对象是根据这个蓝图创建的具体实例。我们探讨了通过私有(private)和公有(public) 修饰符来实现封装,将数据隐藏起来,只通过公有方法(API)与外界交互,这体现了抽象的思想。

我们学习了如何创建对象,如何使用构造函数来初始化对象,并控制对属性的访问。我们还看到了如何在成员函数(如 setter)中加入业务逻辑来维护对象的有效状态。最后,我们简要了解了将代码分离到头文件和源文件的好处,以及单元测试的重要性。

掌握这些概念是构建更复杂、更易维护的 C++ 程序的基础。在接下来的课程中,我们将继续深入探索面向对象编程的其他强大特性。

017:面向对象编程 part2

在本节课中,我们将要学习面向对象编程的剩余部分,包括如何将类分离到不同的文件中、如何进行单元测试、函数重载、运算符重载以及静态成员。最后,我们还将初步介绍指针的概念。

分离文件与单元测试

上一节我们介绍了类和对象的基本概念。本节中我们来看看如何将代码组织得更好,并确保其正确性。

将不同类的代码放在同一个文件中会显得混乱。更好的做法是将每个类放在独立的文件中。在C++中,通常使用头文件(.h)来声明类,使用源文件(.cpp)来实现类。为了简化,我们可以将所有内容都放在头文件中。

以下是如何创建一个头文件:

// lotto_ticket.h
#pragma once
#include <vector>
using namespace std;

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/2354747665c9aab38f92ad4c45f244f5_10.png)

class LottoTicket {
private:
    int number1;
    int number2;
    int number3;
public:
    LottoTicket(int n1, int n2, int n3);
    LottoTicket(vector<int> numbers);
    bool isWinner(const LottoTicket& winningTicket) const;
};

创建头文件后,在主程序中需要包含它:

#include "lotto_ticket.h"

仅仅编写代码还不够,我们需要验证代码是否按预期工作。手动检查输出容易出错。更好的方法是编写代码来自动测试我们的代码,这就是单元测试。

以下是设置单元测试的步骤:

  1. 在解决方案中添加一个新的“本机单元测试项目”。
  2. 在测试项目中,包含需要测试的类的头文件。由于文件在不同目录,可能需要指定路径,例如 #include "../Week11_Classes/lotto_ticket.h"
  3. 编写测试方法。一个常见的模式是AAA模式:准备(Arrange)、执行(Act)、断言(Assert)。

以下是一个测试示例:

TEST_METHOD(WinningTicketTest)
{
    // Arrange: 设置测试所需变量
    LottoTicket ticket(1, 2, 3);
    LottoTicket winningTicket(1, 2, 3);

    // Act: 调用被测试的代码
    bool result = ticket.isWinner(winningTicket);

    // Assert: 验证结果是否符合预期
    Assert::IsTrue(result);
}

编写测试后,可以通过“测试资源管理器”运行所有测试。通过的测试会显示绿色对勾,失败的则显示红色叉号。单元测试有助于在修改代码后快速确认原有功能未被破坏,是软件开发中的重要实践,常与持续集成和持续部署结合使用。

函数与运算符重载

在C++中,我们可以为同一个函数名定义多个版本,只要它们的参数列表不同,这称为函数重载。构造函数也可以重载。

例如,LottoTicket类可以有两个构造函数:

LottoTicket(int n1, int n2, int n3);
LottoTicket(vector<int> numbers);

这样,创建对象时既可以直接传入三个整数,也可以传入一个整数向量。

运算符重载允许我们为自定义类型(类)定义运算符的行为,例如 +, -, == 等。这需要为运算符定义特殊的成员函数。

以下是可重载的运算符列表:

  • 算术运算符:+, -, *, /, %
  • 关系运算符:==, !=, <, >, <=, >=
  • 赋值运算符:=
  • 下标运算符:[]
  • 函数调用运算符:()

静态成员

到目前为止,类的每个对象都拥有自己的一套数据成员副本,这些称为实例成员。有时,我们希望所有对象共享同一个数据成员,这时可以使用静态成员。

静态成员属于类本身,而不是类的某个特定对象。它在内存中只存在一份,被所有对象共享。静态成员变量需要在类外进行定义和初始化。

例如,可以为LottoTicket类添加一个静态常量来表示彩票价格:

// 在类定义中声明
class LottoTicket {
public:
    static const double TICKET_PRICE;
    // ... 其他成员
};

// 在类外定义和初始化
const double LottoTicket::TICKET_PRICE = 1.0;

静态成员函数则不能访问类的非静态成员,因为它没有this指针。

指针初步

指针是C++中一个强大但容易出错的概念。指针是一个变量,其存储的值是另一个变量的内存地址。

要声明一个指针,在类型后使用*符号:

int* numberPtr; // 声明一个指向整数的指针

&运算符用于获取变量的地址:

int number = 42;
numberPtr = &number; // numberPtr 指向 number 的内存地址

*运算符用于解引用指针,即访问指针所指向地址中存储的值:

*numberPtr = 77; // 将 number 的值改为 77
cout << *numberPtr; // 输出 77
cout << numberPtr; // 输出 number 的内存地址(如0x7fff5fbff8ac)

指针的一个常见用途是动态内存分配。这允许我们在程序运行时请求内存,例如创建大小在运行时才确定的数组。

使用new运算符在堆上分配内存:

int size;
cin >> size;
int* dynamicArray = new int[size]; // 动态分配一个大小为 size 的整数数组

for (int i = 0; i < size; ++i) {
    cin >> dynamicArray[i]; // 像普通数组一样使用
}

// 使用完毕后,必须用 delete[] 释放内存,防止内存泄漏
delete[] dynamicArray;

忘记释放动态分配的内存会导致内存泄漏,即程序持续占用不再使用的内存。

最终项目介绍

本节课的最后,我们介绍了本课程的最终项目:开发一个简化版的“大富翁”游戏。

项目基本要求如下:

  • 创建一个10x10的棋盘(共40个格子)。
  • 支持2到8名玩家,初始资金为$1500。
  • 玩家掷两个六面骰子前进。
  • 包含“起点”、“监狱”等特殊格子(功能可简化)。
  • 玩家可以购买地产,当其他玩家踏上时需支付租金。
  • 程序需支持开始新游戏和加载保存的游戏。
  • 必须使用类来组织代码(如Player, Property类)。

项目提交要求:

  • 将代码托管在GitHub仓库。
  • 至少需要有10次有效提交,分布在10个不同的日期,每次提交都应包含可运行代码的截图。
  • 可以选择与一名伙伴合作完成。

在课程最后,需要进行项目演示,展示游戏的基本功能,并能解释代码的关键部分。

总结

本节课中我们一起学习了面向对象编程的更多高级主题。我们了解了如何通过分离文件来更好地组织代码结构,并掌握了编写单元测试来自动验证代码正确性的方法。我们还探讨了函数重载和运算符重载,它们能增加代码的灵活性。静态成员让我们可以在类的所有实例间共享数据。最后,我们初步接触了指针的概念,包括其声明、使用以及动态内存分配,这是后续深入学习C++的重要基础。通过这些知识,你将能够编写更结构化、更健壮且更易于维护的C++程序。

018:派对策划程序 🎉

在本节课中,我们将回顾项目0的解决方案,并学习如何构建一个简单的派对策划程序。我们将涵盖用户输入、条件判断、循环验证以及基本的数学计算。


项目概述

项目0的目标是创建一个程序,帮助用户计算派对所需物品的数量和总成本。程序需要询问来宾人数、服务类型(蛋糕、披萨或汽水),并根据人均消耗量计算需要购买多少份完整物品,同时将结果向上取整。

核心步骤解析

1. 询问来宾人数

首先,程序需要询问有多少位客人会来参加派对。

int numberOfGuests;
cout << "How many guests are coming to the party? ";
cin >> numberOfGuests;
// 记得加上主人自己
int totalPeople = numberOfGuests + 1;

2. 选择服务类型并验证

接下来,询问用户将提供什么食物或饮料。为了确保输入有效,我们使用一个验证循环。

string choice;
cout << "Are you serving cake, pizza, or pop? ";
cin >> choice;

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/04785d3c771cf9f526d2a80f5095a48f_35.png)

// 验证循环:确保输入是“cake”、“pizza”或“pop”之一
while (choice != "cake" && choice != "pizza" && choice != "pop") {
    cout << "Invalid choice. Please enter cake, pizza, or pop: ";
    cin >> choice;
}

3. 确定人均消耗量

根据用户的选择,设定每个人消耗的完整物品比例。

double amountPerPerson = 0.0;
if (choice == "cake") {
    amountPerPerson = 0.25; // 每人消耗1/4个蛋糕
} else if (choice == "pizza") {
    amountPerPerson = 0.33333; // 每人消耗1/3个披萨
} else { // 只能是 pop
    amountPerPerson = 0.375; // 每人消耗3/8瓶汽水 (0.75升 / 2升)
}

4. 询问单价并计算所需数量

询问单个物品的价格,并计算需要购买的总数量。注意,我们需要使用 ceil 函数将结果向上取整,因为不能购买部分物品。

double pricePerItem;
cout << "How much does it cost to buy one " << choice << "? ";
cin >> pricePerItem;

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/04785d3c771cf9f526d2a80f5095a48f_52.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/04785d3c771cf9f526d2a80f5095a48f_54.png)

// 计算所需物品数量并向上取整
double itemsNeeded = totalPeople * amountPerPerson;
itemsNeeded = ceil(itemsNeeded); // 使用ceil函数向上取整
// 引用来源:cplusplus.com/reference/cmath/ceil/

5. 输出结果

最后,向用户展示需要购买多少物品以及总成本。

cout << "To serve " << totalPeople << " people " << choice
     << " at your party, you need to buy " << itemsNeeded
     << " " << choice << "(s)." << endl;
cout << "Your party will cost about $" << (pricePerItem * itemsNeeded) << "." << endl;

测试程序

为了确保程序正确运行,我们需要进行测试。以下是一些测试用例:

  1. 蛋糕:4位客人,蛋糕单价$2。总人数为5,需要 ceil(5 * 0.25) = ceil(1.25) = 2 个蛋糕。总成本应为 $4。
  2. 披萨:3位客人,披萨单价$6.75。总人数为4,需要 ceil(4 * 1/3) = ceil(1.333...) = 2 个披萨。总成本应为 $13.50。
  3. 汽水:2位客人,汽水单价$2.99。总人数为3,需要 ceil(3 * 3/8) = ceil(1.125) = 2 瓶汽水。总成本应为 $5.98。

运行程序并验证输出是否符合这些预期结果。


项目总结

在本节课中,我们一起学习了如何构建项目0的派对策划程序。我们实践了以下核心概念:

  • 使用 cincout 进行基本的输入输出。
  • 使用 if-else 语句进行条件判断。
  • 使用 while 循环进行输入验证。
  • 使用 ceil() 函数进行数学上的向上取整。
  • 将问题分解为清晰的步骤并组合成完整的程序。

这个项目是后续更复杂编程任务的良好开端。请确保在提交前运行你的程序,并按照要求完成自评。

019:项目2 - 月球着陆器与刽子手游戏 🚀

在本节课中,我们将回顾项目1(月球着陆器游戏)的解决方案,并介绍项目2(刽子手游戏)的具体要求。我们将重点学习如何运用函数进行功能分解,将复杂问题拆解为更小、更易管理的部分。

回顾项目1:月球着陆器游戏

上一节我们完成了函数章节的学习。本节中,我们来看看如何将函数应用到实际项目中。项目1的目标是创建一个简单的文本游戏,玩家需要调整飞船的X轴和Y轴倾斜度,并使用推进器安全降落在月球表面。

核心游戏逻辑与功能分解

解决此类问题的关键在于功能分解:将一个大问题分解为一系列小步骤。对于月球着陆器游戏,我们可以识别出以下主要步骤:

  • 生成随机倾斜度:需要为X轴和Y轴生成一个介于-10到10之间的随机初始值。
  • 显示与更新状态:需要向玩家显示当前的倾斜度和距离,并根据玩家选择更新这些值。
  • 处理玩家输入:需要提供一个菜单让玩家选择操作,并验证输入的合法性。
  • 实现自毁序列:当选择自毁时,游戏应进入特殊状态,只允许输入取消代码。
  • 判断游戏结果:根据最终状态(倾斜度是否为0)判断是成功着陆还是坠毁。
  • 游戏循环:允许玩家在单局游戏结束后选择重新开始。

以下是部分核心步骤的代码框架示例:

// 示例:获取随机数的函数框架
int getRandomNumber() {
    // TODO: 返回一个-10到10之间的随机数
    return 0; // 占位符
}

// 示例:获取玩家选择的函数框架
string getChoice() {
    // TODO: 显示菜单并获取、验证玩家输入
    return “”; // 占位符
}

// 示例:打印当前游戏状态的函数
void printDetails(int xTilt, int yTilt, int distance) {
    cout << “X轴倾斜度: “ << xTilt << endl;
    cout << “Y轴倾斜度: “ << yTilt << endl;
    cout << “距离月面: “ << distance << endl;
}

项目1实现要点

在实现过程中,有几点需要注意:

  1. 使用常量增强可读性:用有意义的常量名称(如 X_TILT_POSITIVE)代替魔法数字(如 1),使代码更易理解。
  2. 合理规划函数范围:有些操作(如简单的加减法)可能不需要单独写成函数,而有些复杂流程(如自毁序列)则适合封装。
  3. 处理自毁状态:自毁激活后,需要通过一个布尔标志(如 selfDestructActive)改变主循环的行为,暂时禁用其他命令,只处理取消代码的输入。
  4. 随机数生成:使用 srand(time(0)) 初始化随机种子,并通过模运算和偏移来得到特定范围的随机数。

通过将问题分解并逐个实现这些小函数,最终在 main 函数中组合它们,就能构建出完整的游戏。这种方法使得代码结构清晰,易于调试和维护。

介绍项目2:刽子手游戏

接下来,我们将开始项目2。本项目的主要目的是练习编写和使用函数。我们将构建一个经典的刽子手猜字游戏。

项目2概述与要求

在这个项目中,我将提供一个 main 函数的框架。你的任务是按照严格的规范,完成一系列指定的函数。当所有函数都正确实现后,提供的 main 函数应该能正常运行整个游戏。

以下是需要你实现的函数列表及其简要说明:

  • getWord():提示用户输入一个单词,并确保该单词至少包含4个字符。该函数返回这个隐藏单词。
  • displayHiddenWord(string hiddenWord, string lettersGuessed):接收隐藏单词和已猜字母作为参数。在一行中打印出隐藏单词的当前状态:已猜出的字母显示其本身,未猜出的字母显示为下划线 _
  • getLetterGuess(string lettersGuessed):接收已猜字母字符串,提示用户输入一个字母猜测。此函数需验证:1) 输入是单个字母;2) 该字母之前没有被猜过。返回验证通过的猜测字母。
  • isLetterGuessedInWord(char guess, string hiddenWord):接收猜测字母和隐藏单词,检查该字母是否存在于隐藏单词中。存在则返回 true,否则返回 false
  • hasWordBeenGuessed(string hiddenWord, string lettersGuessed):接收隐藏单词和所有已猜字母,检查是否所有隐藏单词中的字母都已被猜出。是则返回 true,否则返回 false
  • printGallows(int incorrectGuesses):接收错误猜测的次数,根据次数打印出对应阶段的绞刑架和“小人”图案。这是一个较长的函数,因为需要处理从0次到6次错误的不同情况。
  • hasPersonBeenHanged(int incorrectGuesses):接收错误猜测的次数,如果次数达到或超过6次(即小人被完全画出),则返回 true,表示游戏失败。

提供的main函数框架

你的代码将围绕以下主循环构建:

// 假设所有函数已正确实现
int main() {
    string lettersGuessed = “”;
    string hiddenWord = getWord();
    int incorrectGuesses = 0;

    while (!hasWordBeenGuessed(hiddenWord, lettersGuessed) && !hasPersonBeenHanged(incorrectGuesses)) {
        displayHiddenWord(hiddenWord, lettersGuessed);
        char guess = getLetterGuess(lettersGuessed);
        // 将新猜的字母加入已猜字母列表
        lettersGuessed += guess;

        if (!isLetterGuessedInWord(guess, hiddenWord)) {
            incorrectGuesses++;
        }
        printGallows(incorrectGuesses);
    }

    // 游戏结束,判断输赢
    // ...
    return 0;
}

项目2评分要点与建议

本项目共20分,每个指定函数(除 hasPersonBeenHanged 为2分外)各值3分。评分将严格依据函数是否按描述正确实现。

在实现过程中,请注意:

  • 字符串处理:你将需要运用循环来遍历字符串,检查字符是否存在。
  • 输入验证:在 getWordgetLetterGuess 函数中,确保对用户输入进行充分的验证。
  • 函数协作:注意函数之间的数据传递。例如,lettersGuessed 字符串需要在 main 函数中维护,并传递给多个函数使用。
  • printGallows 函数:这是最繁琐的部分,建议使用 if-else ifswitch 语句,根据 incorrectGuesses 的值打印不同的ASCII艺术图案。

总结

本节课中我们一起回顾了如何通过功能分解的方法,使用函数构建了项目1(月球着陆器游戏)。接着,我们详细介绍了项目2(刽子手游戏)的要求,你需要按照给定的函数规范完成实现,以练习编写具有特定输入、输出和功能的模块化代码。

记住,从大问题中识别出独立的小任务,并为每个任务编写函数,是结构化编程的核心技能。开始项目2时,请逐一实现每个函数并进行测试,确保它们能正确集成到提供的 main 函数框架中。

020:期中复习 📚

在本节课中,我们将一起回顾期中考试可能涉及的核心概念,并通过分析几个典型的编程题目来巩固所学知识。课程内容涵盖变量、运算符、循环、函数以及文件操作等基础主题。


概述

本节课是期中复习课。我们将回顾考试中可能出现的选择题和编程题类型,并逐一解析关键知识点。复习的重点包括:变量命名规则、运算符、循环结构、函数定义与调用,以及文件输入输出操作。

上一节我们完成了项目一的评分,本节中我们来看看期中考试的复习要点。


选择题要点回顾

以下是考试中可能出现的一些判断题及其解析,帮助我们厘清基本概念。

  1. 变量可以命名为任何内容。

    • 答案: 错误。
    • 解析: 变量名不能是C++的保留关键字(如 int, return, if)。
  2. 取模运算符(%)给出整数除法后的小数余数。

    • 答案: 错误。
    • 解析: 取模运算符给出的是整数除法的余数。
  3. 除非提供唯一的种子值,否则 rand() 函数每次运行程序都会生成相同的随机数序列。

    • 答案: 正确。
    • 解析: 这就是我们通常使用 srand(time(0)) 来获取当前时间作为随机种子的原因。
  4. if-else if-else 链总是可以用 switch 语句替换。

    • 答案: 错误。
    • 解析: 反过来才是正确的:switch 语句总是可以用 if-else if-else 链替换,但反之则不一定,因为 switch 通常用于离散值(如整数、字符)的等值比较。
  5. for 循环和 while 循环完全可以互换。

    • 答案: 正确。
    • 解析: 从技术上讲,for 循环的三个部分(初始化、条件、增量)都可以省略或调整,以实现 while 循环的功能。但通常 for 循环更适合已知循环次数的场景。
  6. 在循环中使用 breakcontinue 通常是有效的,但它们通常会使程序更难以理解,应尽量避免。

    • 答案: 正确。
    • 解析: 虽然有时很有用,但过度使用会降低代码的可读性。
  7. 使用函数的两个原因。

    • 答案示例: 提高代码可读性;实现代码复用。
  8. 默认情况下,参数是以值传递的方式传递给函数的。

    • 答案: 正确。
    • 解析: 除非使用引用符号 &,否则函数接收的是参数的副本。
  9. 函数只能返回单个值或对象。

    • 答案: 正确。
    • 解析: return 关键字确实只能返回一个值。但我们可以通过返回数组(一个包含多个值的对象)或使用引用参数来“返回”多个值。
  10. 如果参数有默认值,调用函数时必须始终为其提供实参。

    • 答案: 错误。
    • 解析: 正是因为参数有默认值,调用时才可以省略对应的实参。

  1. 使用文件流时必须做的三件事。

    • 答案: 打开文件;读取或写入数据;关闭文件。
  2. 默认情况下,以写入模式打开文件会清除文件的原有内容。

    • 答案: 正确。
    • 解析: 如果需要追加内容,必须指定为追加模式(如 ios::app)。

  1. 读写文件时必须使用文件的绝对路径。
    • 答案: 错误。
    • 解析: 默认使用相对路径即可,也可以根据需要选择使用绝对路径。

编程题解析

在回顾了选择题之后,我们通过几个编程题来实践如何应用这些概念。

题目一:输入验证与计数

要求: 让用户输入10个0到100之间的分数。对每个有效输入,统计其中大于80的分数个数,并在最后输出这个计数。

思路分析: 这是一个典型的循环输入加条件验证和计数的题目。我们需要一个循环来获取10次输入,每次输入后进行范围验证,并对符合条件的值进行计数。

以下是可能的实现代码:

#include <iostream>
using namespace std;

int main() {
    int score;
    int countGreaterThan80 = 0;

    for (int i = 0; i < 10; i++) {
        cout << "请输入分数 (0-100): ";
        cin >> score;

        // 输入验证循环
        while (score < 0 || score > 100) {
            cout << "无效分数,请重新输入 (0-100): ";
            cin >> score;
        }

        // 统计大于80的分数
        if (score > 80) {
            countGreaterThan80++;
        }
    }

    cout << "大于80的分数个数为: " << countGreaterThan80 << endl;
    return 0;
}

代码说明:

  • 使用 for 循环控制输入10次。
  • 内层 while 循环用于验证输入是否在有效范围内。
  • 使用 if 语句判断并计数。

题目二:3x+1函数

要求: 编写一个函数 stepsToOne,接受一个整数,计算它通过以下规则变为1所需的步数:若数为奇数,乘以3再加1;若数为偶数,除以2。

思路分析: 这是一个数学趣味问题,核心是使用 while 循环反复应用规则,直到数值变为1,同时用一个计数器记录步数。

以下是函数实现的示例:

#include <iostream>
using namespace std;

int stepsToOne(int value) {
    int steps = 0;
    while (value != 1) {
        if (value % 2 == 1) { // 奇数
            value = value * 3 + 1;
        } else { // 偶数
            value = value / 2;
        }
        steps++; // 步数加1
    }
    return steps;
}

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/6e03c1a39bd1e35c7d4413b41a133aa6_21.png)

![](https://github.com/OpenDocCN/cs-notes-zh/raw/master/docs/umich-cis150-cpp-prog/img/6e03c1a39bd1e35c7d4413b41a133aa6_23.png)

int main() {
    int num;
    cout << "请输入一个整数: ";
    cin >> num;
    cout << "从 " << num << " 到 1 需要 " << stepsToOne(num) << " 步。" << endl;
    return 0;
}

代码说明:

  • 函数 stepsToOne 接收一个整数参数 value
  • while (value != 1) 是循环条件。
  • value % 2 == 1 用于判断奇偶性。
  • 每执行一次规则,steps 计数器增加。

题目三:统计文件中字母出现次数

要求: 编写函数 countLetterInFile,接受两个参数:一个字符(字母)和一个字符串(文件名)。函数尝试打开该文件,并统计该字母在文件文本中出现的次数。

思路分析: 这道题综合了函数、文件I/O和字符串处理。需要使用 ifstream 打开文件,逐行读取内容,然后遍历每一行的每个字符进行比对。

以下是函数实现的示例:

#include <iostream>
#include <fstream>
#include <string>
using namespace std;

int countLetterInFile(char letter, string fileName) {
    ifstream inputFile;
    inputFile.open(fileName);

    if (!inputFile.is_open()) {
        // 如果文件打开失败,可以返回-1表示错误,这里简单返回0
        return 0;
    }

    int count = 0;
    string line;

    // 使用 getline 逐行读取,可以正确统计空格
    while (getline(inputFile, line)) {
        for (char ch : line) { // 遍历该行的每个字符
            if (ch == letter) {
                count++;
            }
        }
    }

    inputFile.close();
    return count;
}

int main() {
    char myLetter = 'a';
    string myFile = "sample.txt";
    int result = countLetterInFile(myLetter, myFile);
    cout << "字母 '" << myLetter << "' 在文件 " << myFile << " 中出现了 " << result << " 次。" << endl;
    return 0;
}

代码说明:

  • 使用 ifstreamopen() 打开文件。
  • 使用 is_open() 检查文件是否成功打开。
  • 使用 getline(inputFile, line) 循环读取每一行。
  • 使用范围 for 循环 for (char ch : line) 遍历字符串中的每个字符。
  • 使用 close() 关闭文件流(良好的编程习惯)。

考试注意事项

在解析完题目后,这里有一些重要的考试提示:

  • 形式: 开卷考试,可以查阅笔记和资料。
  • 时间: 题目设计时长约10-20分钟每题,总时长充裕。
  • 工具: 强烈建议在 Visual Studio 等集成开发环境中编写和测试代码,确保无误后,再将代码复制粘贴到考试答题区。不要在答题框中直接编写代码。
  • 诚信: 遵守学术诚信政策,独立完成考试。

总结

本节课中我们一起学习了期中考试的复习要点。我们回顾了变量、运算符、循环、函数和文件操作的核心概念,并通过三个编程题目实践了输入验证、循环计算和文件处理。请大家利用这些知识认真准备考试,如果在复习中遇到任何问题,请及时提出。

祝大家考试顺利!我们下节课再见。

021:期末复习 🎓

在本节课中,我们将回顾课程后期涵盖的几个核心主题,包括用例图与活动图、递归函数以及面向对象编程的要点。这些概念是软件设计和开发的重要基础。


用例图与活动图 📊

上一节我们介绍了函数分解等编程思想。本节中我们来看看如何通过图表来规划和沟通软件设计。

用例图描述了不同用户(称为“参与者”)与系统交互的不同方式。例如,在选课系统中,学生注册课程,而教师录入成绩,这是两种不同的用例。

活动图则展示了软件使用的具体步骤和流程。它类似于流程图,可以清晰地描绘出决策点(如“是/否”分支)和不同部门或模块之间的协作(称为“泳道”)。

以下是绘制这些图表的要点:

  • 图表是沟通复杂想法的有效工具,比大段文字更直观。
  • 可以使用Microsoft Visio(实验室已安装)、Microsoft Word的形状工具或一些免费的在线工具(如Lucidchart、diagrams.net)来绘制。
  • 在活动图中,菱形代表决策点,矩形代表活动,箭头指示流程方向。


递归函数 🔄

接下来,我们探讨一个独特的编程概念:递归。简单来说,递归就是函数调用自身。

一个递归函数必须包含一个基线条件,用于结束递归调用,否则函数将无限循环并最终导致程序崩溃。

倒计时示例

以下是一个简单的递归函数,用于实现倒计时:

void countdown(int number) {
    if (number <= 0) { // 基线条件
        std::cout << "Blast off!";
    } else {
        std::cout << number << " ";
        countdown(number - 1); // 递归调用
    }
}

调用 countdown(5) 会输出:5 4 3 2 1 Blast off!。在调试时,你可以在调用堆栈中看到一系列 countdown 函数的调用,每个调用都有其独立的 number 参数值。

斐波那契数列示例

斐波那契数列是一个经典的递归示例,其定义是:F(n) = F(n-1) + F(n-2),且 F(1)=1, F(2)=1

int fibonacci(int n) {
    if (n <= 2) { // 基线条件
        return 1;
    }
    return fibonacci(n - 1) + fibonacci(n - 2); // 递归调用
}

注意:这种实现方式效率非常低,因为会进行大量重复计算(例如计算 fibonacci(5) 时会重复计算 fibonacci(3) 多次)。在实际开发中,通常会使用循环或“记忆化”技术来优化。这个例子主要用于理解递归的基本原理。


面向对象编程与期末项目提示 🏗️

最后,我们简要回顾面向对象编程(OOP)并给出期末项目建议。

指针、继承和多态(通过 virtualoverride 关键字实现)是OOP的强大功能,允许我们设计出更灵活、更易维护的代码结构。这些将是下学期的重点。

对于你们的“大富翁”期末项目,请记住以下实践建议:

  • 采用渐进式开发:写一点代码,就测试一点功能,确保其正常工作,然后再添加新功能。
  • 善用图表进行规划:在编码前,用活动图梳理游戏流程(如玩家回合、购买房产、存盘/读盘等),这能帮助理清逻辑,避免后期大量返工。
  • 项目演示准备:最终演示时,请准备好可运行的游戏,展示几个关键操作(如移动、交易、存盘读盘)。演示时间约5分钟。

本节课中我们一起回顾了软件设计图表(用例图、活动图)的用途、递归函数的基本原理与实现,以及面向对象编程的核心思想和期末项目开发的最佳实践。这些知识将为你们后续的编程学习打下坚实的基础。祝大家在期末项目和考试中取得好成绩!

posted @ 2026-03-29 09:41  布客飞龙I  阅读(3)  评论(0)    收藏  举报