威斯康星-CS368c-系统编程笔记-全-
威斯康星 CS368c 系统编程笔记(全)
001:第1讲 - 课程概述
概述
在本节课中,我们将学习CS368C++课程的整体介绍,包括课程目标、教学方式、评分标准以及如何成功完成这门课程。我们还将编写并运行第一个C++程序“Hello World”,并将其与Java版本进行比较,以直观地展示两种语言的基本差异。
课程介绍
大家好,这里是威斯康星大学麦迪逊分校2020年秋季学期面向Java程序员的CS368C++课程。
如果你是从其他地方观看此视频,欢迎加入我们。对于选课的同学,请注意我们的Canvas网站上提供了大量资源,我今天将介绍其中一部分。我们将从课程介绍开始。
今日大纲
今天的课程大纲如下:首先是总体课程概述,然后浏览教学大纲,讨论如何通过这门课程。接着,我们将查看一个示例程序“Hello World”——这是大多数人学习编程时编写的第一个程序。最后,我们将这个程序与Java版本进行比较。
这是本周课程内容的第一部分。
关于讲师
我的名字是Mike Douchher,请叫我Mike。如果你打开了这些幻灯片的PowerPoint版本,可以点击链接给我发邮件。
我最初是一名化学家,成为计算机科学家大约只有10年时间。我在南卡罗来纳大学获得了化学学位。之后,我在海军研究实验室担任了三年的独立承包商,这是一个支持海军和海军陆战队的军事实验室。合同结束后,我在本尼迪克特学院教了八年化学。后来,出于对计算机科学的好奇,我开始在线学习课程,积累了丰富的学生经验。当我学到足够的知识进入研究生项目后,我来到麦迪逊大学获得了硕士学位,并在S Art软件公司担任了三年的软件工程师。期间,我还在晚上教授C++和Matlab课程。最近,我接受了麦迪逊大学的全职教职工作。
课程详情
如果你访问Canvas网站并打开教学大纲,最重要的信息将是课程安排表,那里会发布所有内容的链接。
我们将使用Piazza进行课程交流,这是一个供学生提问和获取答案的平台。你还需要激活你的CS账户以访问CSL机器。
所有视频讲座将发布在YouTube上,链接会放在课程安排表中。作业将通过Canvas发布,并附有说明视频。
关于办公时间,我们将使用预约队列系统。在办公时间开始前,你可以通过填写调查问卷来预约,我会按顺序与学生见面。
教学大纲要点
这门课程是关于C++的,特别是针对已经了解Java的人。我们将涵盖一系列主题。课程没有指定教材,所有阅读材料将在线提供。
课程评分基于几次作业,没有考试。你需要获得70%或以上的总分才能通过。迟交政策相对宽松:在我们开始评分之前提交都不算迟交,一周内迟交可获得最高50%的分数。
严禁作弊。我们将使用名为Moss的软件相似性检测工具来识别抄袭行为,任何作弊行为都将被严肃处理。
课程目标与受众
本课程专为至少上过一门编程课(最好是Java或类似C的语言)的学生设计。课程内容将与CS200(可能还有CS300)中学习的结构相对应,并涵盖C++特有的功能。
根据注册信息,班级中有不同年级的学生。本课程主要针对大二水平的学生,即已经上过一门编程课的同学。请高级学生在评价课程时,考虑课程是否有效地达到了针对初学者的教学目标。
如何通过课程
这是一门“通过/不通过”的课程,没有字母等级。我们将有大约7次作业。要通过课程,你必须在所有作业中获得70%或以上的总分,并且必须尝试完成所有作业。
开发环境与工具
我将提供关于如何使用CSL机器、命令行和Vim文本编辑器进行编译的支持。虽然你可以使用任何喜欢的IDE(如Eclipse CDT、Visual Studio、Qt等),但本课程将重点讲解在命令行中使用Vim和G++编译器。掌握这些工具对于提高编程效率非常有帮助。
上一节我们介绍了课程的基本信息和规则,本节中我们来看看如何开始编写第一个C++程序。
第一个C++程序:Hello World
现在,我将跳转到CSL机器上,演示如何编写一个“Hello World”程序,并逐行讲解其含义以及与Java版本的比较。
以下是编写和运行C++ “Hello World”程序的步骤:

- 使用Vim创建一个新文件,例如
hw.cpp。 - 输入以下代码:
#include <iostream> using namespace std; int main(int argc, char* argv[]) { cout << "Hello, world!" << endl; return 0; } - 保存并退出Vim。
- 在终端中使用
g++编译器进行编译:g++ hw.cpp -o hw - 运行编译后的程序:
./hw
程序将输出 Hello, world!。
与Java的Hello World比较
让我们将C++版本与Java的“Hello World”进行逐行比较。
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
以下是核心概念的对比分析:
- 主函数:两者都有一个
main函数作为程序入口。在C++中,main位于全局作用域,且每个程序只能有一个。其返回类型是int,返回0表示程序成功执行。在Java中,main必须包含在一个类中。 - 命令行参数:C++中使用
int argc(参数计数)和char* argv[](参数字符串数组)。Java中使用String[] args。C++的char* argv[]是一个指向字符指针的指针,代表字符串数组。 - 包含头文件:C++使用
#include <iostream>预处理指令将输入输出库的内容包含进来。尖括号< >表示系统库文件。这类似于Java中的import语句,但机制不同。 - 命名空间:
using namespace std;类似于Java的import,它允许我们直接使用std命名空间中的名称(如cout),而无需前缀。 - 输出语句:C++中使用
cout << "Hello, world!" << endl;进行控制台输出。cout代表控制台输出流,<<是流插入运算符,endl表示换行并刷新缓冲区。这相当于Java中的System.out.println("Hello, world!");。
C++与Java的语法共性
许多在Java中使用的语法结构在C++中同样有效,这为Java程序员提供了便利。以下是一些例子:
- 控制结构:
for循环、if语句、while循环的语法基本相同。 - 变量声明:基本数据类型(如
int,double,bool)的声明方式类似。 - 布尔表达式:C++中的布尔表达式可以计算为数值(0表示false,非0表示true),这提供了额外的灵活性。

为了演示用户输入和一些通用语法,让我们看一个简单的“磅转换为千克”的程序示例:

#include <iostream>
using namespace std;
int main() {
cout << "Mike, how much do you weigh? ";
double pounds;
cin >> pounds; // 从键盘获取输入
double kilograms = pounds * 0.454;
cout << "You weigh " << kilograms << " kilograms." << endl;
bool greatShape = (pounds < 1000); // 布尔表达式赋值
if (greatShape) {
cout << "Wow, you're in great shape!" << endl;
}
// for循环示例
for (int i = 0; i < 3; i++) {
cout << "Have a great day!" << endl;
}
// while循环示例
int j = 0;
while (j < 3) {
cout << "Loop with while: " << j << endl;
j++;
}
return 0;
}
这个程序演示了:
- 使用
cin进行用户输入。 - 基本的算术运算和变量赋值。
- 布尔表达式的使用和
if条件判断。 for循环和while循环,其语法与Java一致。
总结
本节课中我们一起学习了CS368C++课程的概述。我们了解了课程的目标、评分方式以及重要的学术诚信规定。我们动手编写并运行了第一个C++程序“Hello World”,并通过与Java版本的详细对比,理解了C++在程序结构、输入输出和基本语法上的特点。我们还看到,许多Java的控制流和变量操作语法在C++中可以直接使用,这为学习过渡提供了便利。

请记得填写Canvas上的调查问卷,并关注即将发布的第一份作业。如果有任何问题,请在Piazza上提问。祝大家学习愉快!
002:文件I/O与字符串 📚
在本节课中,我们将学习C++中的输入/输出(I/O)操作和字符串处理。我们将从课程安排和作业提醒开始,然后深入探讨C++与Java的历史背景和设计哲学差异,最后通过实际代码示例学习如何使用流(streams)进行I/O操作以及如何操作字符串。
课程安排与作业提醒 📅
上一节我们介绍了课程的基本信息,本节中我们来看看本周的具体安排和作业。
- 办公时间:根据调查结果,我的办公时间定在周一到周五的下午2点。助教R.T. Kane Rodriguez的办公时间是周一至周四的早上8点和下午4:30。请通过Canvas课程页面上的“票务队列”链接预约。
- Piazza:请尚未加入的同学尽快注册。大约90%的同学已经加入,这是一个很好的交流平台。
- 作业1:第一部分(Homework 1A)将于明天(周四)晚上截止。第二部分(Homework 1B)将于明天发布,并于9月17日(下周四)截止。Homework 1B旨在评估你是否适合本课程,涉及基本的编程技能,如循环和字符串处理。
C++与Java:历史与设计哲学对比 ⚖️
在深入学习具体语法之前,了解C++和Java的设计目标差异至关重要。这有助于理解为什么C++的某些特性看起来更复杂或更底层。
C++脱胎于C语言,其核心目标是性能和对硬件的直接控制。它被设计为C语言的面向对象扩展,旨在生成与C语言一样高效的机器码。因此,C++将许多控制权(和相应的责任)交给了程序员,例如手动内存管理、缺乏数组边界检查等。这带来了极高的运行效率,但也增加了编写正确代码的难度。
Java的设计哲学则侧重于安全和易用性。它从C++中选取了最常用、最不易出错的特性,并加入了自动垃圾回收、严格的运行时检查等机制,以防止程序员犯常见错误。其代价是牺牲了一些底层控制能力和绝对的性能峰值。
以下是两种语言的一些关键区别:
- 目标:C++追求极致的运行速度;Java追求代码的安全性与可移植性。
- 编译与运行:C++编译为本地机器码;Java编译为字节码,在虚拟机(JVM)上运行。
- 内存管理:C++需要手动管理(
new/delete);Java有自动垃圾回收。 - 错误检查:C++在编译和运行时检查较少;Java在编译和运行时都有严格的检查。
- 指针与引用:C++支持指针和引用;Java只有引用(且不支持指针运算)。
- 多重继承:C++支持;Java通过接口实现类似功能。
- 运算符重载:C++支持;Java不支持。
理解了这些根本差异,我们就能更好地接受C++中那些看似“繁琐”或“危险”的特性,它们正是其高性能的源泉。
变量命名规范 📝
在开始编码前,我们先快速回顾一下C++的变量命名规则,这对于写出清晰可读的代码非常重要。
变量名可以由字母、数字和下划线(_)组成,且不能以数字开头。长度没有限制。虽然编译器没有强制要求,但遵循一致的命名约定是良好的编程习惯。例如:
- 变量名使用小写字母,单词间用下划线分隔(如
student_name)。 - 函数名使用“驼峰式”(如
getStudentName)或“帕斯卡式”(每个单词首字母大写)。 - 类名使用大写字母开头(如
StudentRecord)。
选择有意义的变量名至关重要。好的变量名可以让代码自文档化,让他人(包括未来的你)更容易理解。避免使用像 tt、ttt 这样含义模糊的缩写。
流(Streams):C++的I/O工具箱 🛠️
现在,让我们进入今天的核心内容:如何使用C++进行输入和输出。C++使用流的概念来处理I/O操作。你可以将流想象成一个数据的管道或序列,数据从源头(如键盘、文件)流向目的地(如屏幕、文件)。

要使用C++的I/O功能,需要包含相应的头文件:
#include <iostream>:用于标准输入输出(cin,cout)。#include <fstream>:用于文件操作。#include <iomanip>:用于格式化输出(如设置精度、宽度)。
为了简化代码,我们通常还会加上 using namespace std;,这样就不必在每个流对象前写 std:: 前缀。
标准输入与输出
- 输入流
cin:连接到键盘。使用流提取运算符>>从cin读取数据到变量。int x; cin >> x; // 从键盘读取一个整数到变量x - 输出流
cout:连接到屏幕。使用流插入运算符<<将数据发送到cout。cout << "You entered: " << x << endl;
运算符链:<< 和 >> 运算符可以连续使用,因为它们每次操作后都返回流对象本身。
int x, y;
cin >> x >> y; // 连续读取两个整数
cout << "x=" << x << ", y=" << y << endl; // 连续输出
注意:cout 的输出是缓冲的,意味着数据可能不会立即显示在屏幕上。使用 endl(换行并刷新缓冲区)或 flush(仅刷新缓冲区)可以强制立即输出。这在调试时非常有用。
字符串操作 📄
C++标准库提供了 string 类,使得字符串处理比C风格的字符数组方便得多。需要包含头文件 #include <string>。
以下是字符串的一些基本操作:
- 声明与初始化:
string first_name = "Indigo"; string last_name = "Montoya"; - 拼接:使用
+运算符或append()方法。string full_name = first_name + " " + last_name; first_name.append(" Mike"); // 将" Mike"附加到first_name末尾 - 获取长度:使用
length()或size()方法。int name_length = full_name.length(); - 访问与修改字符:使用下标运算符
[](从0开始)。char first_char = full_name[0]; // 获取第一个字符 full_name[0] = 'E'; // 将第一个字符改为'E' - 添加字符:使用
push_back()方法。full_name.push_back('!'); // 在字符串末尾添加'!' - 插入字符串:使用
insert()方法。// 在位置6(第7个字符前)插入字符串 full_name.insert(6, " the Great"); - 读取整行:使用
getline()函数读取包含空格的整行输入。string user_input; getline(cin, user_input); // 读取一整行,包括空格
总结 🎯
本节课中我们一起学习了:
- C++与Java的核心差异:理解了C++追求性能与控制、Java追求安全与易用的设计哲学。
- C++的I/O系统:学会了使用
cin和cout流对象以及<<、>>运算符进行基本的输入输出,并了解了运算符链和缓冲区刷新的概念。 - 字符串处理:掌握了C++
string类的基本操作,包括声明、拼接、获取长度、访问字符以及使用getline()读取整行输入。

这些知识是进行后续C++编程的基础,特别是对于完成即将到来的作业至关重要。请务必动手实践示例代码,加深理解。
003:迭代、条件句与IO操作 🚀

在本节课中,我们将学习C++编程的基础知识,重点探讨变量声明、布尔逻辑、循环控制以及输入输出格式化操作。这些内容是理解C++与Java差异并编写高效代码的关键。
变量声明与初始化
上一节我们介绍了课程概述,本节中我们来看看C++中变量声明的基础。与Java不同,C++中的变量在声明时不会自动初始化。
int x; // 声明变量x,其值未定义
cout << "No default initialization. X is equal to " << x << endl;
上述代码中,变量x的值是未定义的,可能是0,也可能是内存中的任意值。为了安全,我们应始终初始化变量。
以下是初始化变量的几种方式:
- 声明后赋值:
int x; x = 5; - 声明时初始化:
int y = 4; - 同一行声明多个变量:
int a, b, c;或int d = 1, e = 2, f, g = 6;
布尔值与条件语句
理解了变量声明后,我们来看看C++如何处理布尔逻辑。C++将布尔值true和false内部存储为整数1和0。
bool b = true;
cout << "Boolean statements. True is equal to " << true << " and false is equal to " << false << endl;
在条件语句(如if、while、for)中,C++会将表达式简化为布尔值。任何非零值被视为true,零值被视为false。
以下是C++中可被视为布尔条件的示例:
- 布尔变量:
if (b) - 布尔字面量:
if (true),if (false) - 整数:
if (1)(真),if (0)(假),if (42)(真),if (-1)(真) - 字符:
if ('a')(真,因为‘a’的ASCII码97非零) - 关系运算符:
if (3 < 4),if (3 == 4),if (3 != 4) - 逻辑运算符:
if (true && true),if (true || false),if (!true)

注意:在条件中使用赋值运算符(=)是允许的,但容易造成混淆,通常是不良实践。赋值表达式会返回所赋的值,该值随后被用作条件。
if ((x = 5)) { // 将5赋值给x,然后判断5(非零,为真)
cout << "true" << endl;
}

逻辑运算符&&(与)和||(或)支持短路求值。对于&&,如果左侧为假,则右侧不再计算;对于||,如果左侧为真,则右侧不再计算。
循环控制
掌握了条件判断,接下来我们深入探讨C++中的循环结构。for循环非常灵活,其三个部分(初始化、条件、递增)都可以包含多个语句或留空。
for (int i = 0, square = 0; i < 10; i++, square = i * i) {
cout << setw(3) << i << setw(6) << square << endl;
}
以下是循环控制的关键字:
break:立即终止整个循环,跳出循环体。continue:跳过当前循环迭代的剩余代码,直接进入下一次循环的条件判断。goto:跳转到程序中指定的标签处。虽然可用,但应谨慎使用,以免破坏代码结构。
C++11引入了基于范围的for循环,非常适合遍历容器(如字符串、向量)。
string quote = "It's hardware that makes a machine fast. "
"It's software that makes a fast machine slow.";
// 遍历副本,不修改原字符串
for (char c : quote) {
cout << c << endl;
}
// 使用引用遍历并修改原字符串
for (char &c : quote) {
if (c == 'I') c = 'X';
}
// 使用auto关键字自动推断类型
for (auto c : quote) {
cout << c;
}
输入输出格式化(I/O Manipulators)
最后,我们学习如何精确控制输出的格式。I/O操纵器是发送给cout的特殊指令,用于修改数字或文本的显示方式。
以下是一些常用的I/O操纵器:
showpoint/noshowpoint:显示或隐藏浮点数末尾的零和小数点。showpos/noshowpos:为非负数显示+号。hex/dec/oct:以十六进制、十进制或八进制格式输出整数。showbase/noshowbase:显示(如0x表示十六进制)或隐藏数字进制的前缀。fixed/scientific:以定点计数法或科学计数法显示浮点数。setprecision(n):设置浮点数输出的小数位数(在fixed模式下)或总有效数字位数(在默认模式下)。setw(n):设置下一个输出字段的宽度。left/right:在字段内左对齐或右对齐文本。setfill(ch):用指定字符ch填充字段的空白部分。boolalpha/noboolalpha:将布尔值true/false输出为字符串形式,而非1/0。
cout << showpos << 1.0 << endl; // 输出: +1
cout << hex << showbase << 4567 << endl; // 输出: 0x11d7
cout << fixed << setprecision(2) << 123.456789 << endl; // 输出: 123.46
cout << setw(8) << setfill('*') << "ABCD" << endl; // 输出: ****ABCD
cout << boolalpha << true << endl; // 输出: true
重要提示:大多数操纵器(如showpos, hex)的状态是持久的,一旦设置,会影响后续所有相关输出,直到被显式更改(如使用noshowpos, dec)。




本节课中我们一起学习了C++编程的核心基础:变量声明的规则、布尔逻辑的求值方式、各种循环结构及其控制语句,以及强大的I/O格式化工具。理解这些概念是编写正确、高效且输出美观的C++程序的第一步。
004:基本类型、枚举、开关语句、函数基础 🧱
在本节课中,我们将要学习C++中的基本数据类型、枚举类型、switch语句以及函数的基础知识。这些是构建C++程序的核心组件,理解它们对于从Java过渡到C++至关重要。
概述
上一节我们介绍了C++的基本结构和输入输出。本节中,我们来看看C++中更基础的元素:原始数据类型、如何定义和使用枚举、控制流中的switch语句,以及如何声明和定义函数。
基本数据类型
C++提供了多种基本数据类型,它们的大小通常是平台相关的,这与Java中固定大小的类型不同。
整数类型
C++的整数类型包括 int、short、long 和 long long。它们可以通过 signed 或 unsigned 修饰符来改变表示范围。
short:至少2字节。int:通常4字节,但大小系统相关。long:至少和int一样大。long long:至少8字节,提供更大的整数范围。
使用 unsigned 修饰符可以表示非负数,从而扩大正数范围。例如,unsigned int 的范围是 0 到约 40 亿。
获取类型大小

可以使用 sizeof 操作符来获取特定类型在您系统上的字节大小。它返回一个 size_t 类型的值。
#include <iostream>
int main() {
std::cout << "Size of int: " << sizeof(int) << " bytes\n";
std::cout << "Size of long long: " << sizeof(long long) << " bytes\n";
return 0;
}
字符类型
字符类型用于存储单个字符,在内部以数字形式存储(如ASCII码)。
char:默认1字节,可表示ASCII字符。wchar_t:宽字符,用于表示更大字符集(如Unicode)。char16_t/char32_t:用于明确表示UTF-16和UTF-32编码的字符。
浮点类型
浮点类型用于表示小数。
float:通常4字节,提供约6-7位十进制精度。double:通常8字节,提供约15位十进制精度。long double:扩展精度浮点数,大小和精度因系统而异。
字面量
字面量是直接在代码中硬编码的值。
整数字面量
可以以不同进制表示整数:
- 十进制:
42 - 八进制(以0开头):
052(注意:09是无效的,因为八进制没有数字9) - 十六进制(以0x开头):
0x2A - 二进制(C++14起,以0b开头):
0b101010
可以使用后缀指定类型:
U或u:无符号 (unsigned),例如42UL或l:长整型 (long),建议使用大写L,因为小写l容易与数字1混淆,例如42LLL或ll:长长整型 (long long),例如42LL
浮点数字面量
默认是 double 类型。可以使用后缀:
F或f:单精度 (float),例如3.14fL或l:扩展精度 (long double),例如3.14L
类型转换与运算
隐式类型转换
C++会在许多情况下自动进行类型转换,例如在赋值或算术运算中。
bool b = 5; // 非零值转换为 true,b 存储为 1
int i = 3.14; // 浮点数转换为整数,小数部分被截断,i 为 3
double d = i; // 整数转换为浮点数,d 为 3.0,但丢失了原来的 .14
unsigned int u = -1; // 负数转换为无符号数,u 变为一个很大的正数
整数除法
当两个整数相除时,C++执行整数除法,结果会被截断为整数。
int result = 23 / 4; // 结果是 5,不是 5.75
int remainder = 23 % 4; // 取余运算,结果是 3
要让除法产生浮点数结果,至少需要一个操作数是浮点类型。
double result = 23.0 / 4; // 结果是 5.75
// 或者使用类型转换
double result2 = static_cast<double>(23) / 4; // 结果也是 5.75
显式类型转换
C++提供了几种显式类型转换的方法:
- C风格转换:
(目标类型)表达式,例如(unsigned int) x - 函数风格转换:
目标类型(表达式),例如unsigned int(x) static_cast:static_cast<目标类型>(表达式),这是推荐使用的方式,更安全且易于在代码中搜索。
int x = 42;
unsigned int u1 = (unsigned int)x; // C风格
unsigned int u2 = unsigned int(x); // 函数风格
unsigned int u3 = static_cast<unsigned int>(x); // 推荐:static_cast
static_cast 在编译时进行类型检查,适用于大多数明确的类型转换场景。
枚举类型

枚举(enum)允许我们创建一种新的数据类型,其值被限制为一组命名的整数常量。
定义和使用枚举

// 定义一个名为 Color 的枚举类型
enum Color { RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET };


int main() {
Color myColor = RED; // 声明一个 Color 类型的变量并赋值
// myColor = 5; // 错误:不能直接用整数赋值
// myColor = PURPLE; // 错误:PURPLE 不在枚举列表中
std::cout << "myColor value: " << myColor << std::endl; // 输出 0 (RED)
return 0;
}
默认情况下,枚举值从0开始依次递增(RED=0, ORANGE=1, ...)。也可以显式指定值:
enum Transportation { CAR = 0, PLANE = 13, BOAT = 3, BIKE }; // BIKE 会从 BOAT 递增,值为 4
枚举常量(如 RED)在定义后即进入作用域,不能在其他地方重定义。
Switch 语句
switch 语句提供了一种基于整数或枚举表达式值进行多路分支的清晰方式。它通常与枚举类型一起使用。
基本语法
switch (表达式) { // 表达式必须能求值为整型或枚举类型
case 常量表达式1:
// 语句...
break; // 跳出 switch
case 常量表达式2:
// 语句...
// 注意:如果没有 break,会“贯穿”到下一个 case
break;
// ... 更多 case
default:
// 如果所有 case 都不匹配,则执行此处的语句
break;
}
示例:与枚举结合
Transportation t = PLANE;
switch (t) {
case CAR:
std::cout << "You selected car.\n";
break;
case PLANE:
std::cout << "You selected plane.\n";
// 这里故意没有 break,会贯穿到下一个 case
case BOAT:
std::cout << "You might have selected plane or boat (no break after plane).\n";
break;
default:
std::cout << "You selected something else.\n";
break;
}
// 当 t 为 PLANE 时,输出:
// You selected plane.
// You might have selected plane or boat (no break after plane).
重要:case 标签后的 break 语句用于防止“贯穿”执行下一个 case。如果省略 break,程序会继续执行后续 case 中的语句,直到遇到 break 或 switch 结束。
函数基础
在C++中,函数可以定义在全局作用域,而不必像Java那样必须属于某个类。
函数声明与定义
- 函数声明(原型):告诉编译器函数的存在、返回值类型、名称和参数类型。参数名可选,但建议加上以提高可读性。
- 函数定义:提供函数的具体实现,包括函数体。
// 函数声明(通常在头文件或文件顶部)
int toInches(int feet, int inches = 0); // inches 有默认值 0
int main() {
int heightInInches = toInches(6, 1); // 调用函数
std::cout << "Height: " << heightInInches << " inches\n";
return 0;
}
// 函数定义
int toInches(int feet, int inches) {
return feet * 12 + inches;
}
关键点:
- 函数需要在使用前被声明(或定义)。
- 声明中的默认参数值(如
inches = 0)应放在函数声明中,而不是定义中(除非声明和定义是同一个)。 - 默认参数必须从参数列表的最右边开始连续设置。
参数传递与作用域
在上面的例子中,参数 feet 和 inches 通过值传递。这意味着调用函数时,实参(如 6 和 1)的值被复制到形参(函数内部的 feet 和 inches)中。函数内部对形参的修改不会影响外部的实参。
函数内部定义的变量(包括形参)具有局部作用域,它们只在函数执行期间存在,函数返回后即被销毁。
总结

本节课我们一起学习了C++编程的几项基础内容。我们了解了C++丰富且平台相关的基本数据类型,学会了如何使用枚举来创建有意义的常量集合,掌握了用switch语句处理多分支选择,并初步认识了如何声明和定义全局函数。这些知识是编写结构化C++程序的基石。下一节,我们将探讨数组和指针,它们是C++中管理内存和数据的强大工具。
005: 数组与指针 🧠
在本节课中,我们将要学习C++中的数组和指针。数组是存储相同类型数据的集合,而指针则是存储内存地址的变量。理解这两者及其关系是掌握C++系统级编程的关键。我们将从基础概念开始,逐步深入到它们的使用方法和注意事项。

数组基础
上一节我们介绍了基本数据类型,本节中我们来看看如何创建和使用数组。
数组的声明与初始化
以下是声明和初始化数组的几种方式:
- 指定大小声明:
int data[100];声明一个包含100个整数的数组。 - 自动推断大小:
char message[] = "Hello";编译器会根据字符串长度自动确定数组大小。 - 列表初始化:
int limits[] = {10, 12, 14, 17, 20};编译器会计算元素数量并分配空间。
数组的重要特性
以下是关于数组的几个核心特性:
- 语法差异:在C++中,方括号
[]放在变量名之后,而不是像Java那样放在类型之后。 - 非对象:数组不是对象,没有关联的方法,本质上只是一块连续的内存区域。
- 无大小信息:数组自身不存储其大小,程序员必须自己记住元素数量。
- 无边界检查:访问数组边界之外的元素是合法的(但危险),编译器不会报错。
数组访问示例
我们可以通过索引来访问和修改数组元素。索引从0开始。
int limits[] = {10, 12, 14, 17, 20};
cout << limits[0] << endl; // 输出 10
limits[4] = 25; // 修改最后一个元素
// 危险:访问越界,行为未定义
cout << limits[-1] << endl;
cout << limits[10] << endl;
数组的局限性
数组有一些重要的限制需要注意:
- 不能直接复制或赋值:不能用一个数组直接初始化另一个数组,也不能将一个数组赋值给另一个数组。
int a[5] = {1,2,3,4,5}; int b[5] = a; // 错误:必须使用大括号初始化器 int c[5]; c = a; // 错误:无效的数组赋值 - 关系运算符比较的是地址:对数组使用
==,<等运算符,比较的是它们在内存中的起始地址,而非内容。int arr1[5] = {1,2,3,4,5}; int arr2[5] = {1,2,3,4,5}; cout << (arr1 == arr2) << endl; // 输出 0 (false),因为地址不同
指针入门 🎯
上一节我们了解了数组,本节中我们来看看指针。指针是C++中强大但也容易令人困惑的特性,它存储的是另一个变量的内存地址。
指针的声明
声明指针变量需要在类型或变量名旁加上星号 *。
int* x1; // 方式一:星号靠近类型(常见)
int *x2; // 方式二:星号靠近变量名
int *x6, x7; // x6是指针,x7是普通整数。星号不作用于逗号后的变量。
指针的初始化
创建指针后,最好立即将其初始化为 nullptr(C++11推荐)或 NULL 或 0,表示它不指向任何有效内存。
int* ptr1 = nullptr;
int* ptr2 = NULL;
int* ptr3 = 0;
// 判断指针是否有效
if (ptr1) {
// ptr1 非空,可以安全使用
}
为什么使用指针?
以下是使用指针的几个主要原因:
- 固定大小:指针的大小是固定的(例如,64位系统上是8字节),便于在栈上分配。
- 动态内存管理:通过
new运算符在堆上分配内存,指针用于访问这块内存。 - 实现多态:通过基类指针指向派生类对象,实现运行时多态。
- 操作数组:数组名在很多情况下可以退化为指向其首元素的指针。
指针的基本操作
让我们通过一个例子来理解指针的赋值和解引用。
int x = 3;
int* x1 = &x; // & 是取地址运算符,将x的地址赋给指针x1
cout << “x = ” << x << endl; // 输出:x = 3
cout << “x1 = ” << x1 << endl; // 输出:x1 = 0x7ffe... (一个内存地址)
cout << “*x1 = ” << *x1 << endl; // * 是解引用运算符,输出:*x1 = 3
x = 4; // 直接修改变量x
cout << “*x1 = ” << *x1 << endl; // 输出:*x1 = 4,因为x1指向x
*x1 = 5; // 通过指针间接修改变量x
cout << “x = ” << x << endl; // 输出:x = 5
int y = 11;
x1 = &y; // 指针可以重新指向另一个变量
cout << “*x1 = ” << *x1 << endl; // 输出:*x1 = 11
指针与动态内存
使用 new 在堆上分配内存,并使用 delete 释放。
// 分配一个字符串
string* p = new string(“Hello World”);
cout << *p << endl; // 解引用以获取字符串内容
delete p; // 释放内存
// 分配一个整数数组
int* px = new int[6]; // 分配6个整数的空间
px[0] = 5; // 使用数组下标语法访问
*(px + 1) = 4; // 使用指针算术访问
delete[] px; // 释放数组内存,注意使用 delete[]

数组与指针的紧密联系 🔗
数组名在大多数表达式中会退化为指向其第一个元素的指针。这使得指针和数组的用法可以互通。
int arr[] = {0, 1, 2, 3, 4, 5};
int* pArr = arr; // arr 退化为 &arr[0]
cout << arr[2] << endl; // 输出 2
cout << pArr[2] << endl; // 输出 2,指针可以像数组一样使用下标
cout << *(arr + 3) << endl; // 输出 3,数组名可以像指针一样进行算术运算
// 指针算术:移动指针
pArr = pArr + 1; // 现在 pArr 指向 arr[1]
cout << *pArr << endl; // 输出 1
cout << pArr[0] << endl; // 输出 1,pArr[0] 现在等价于原来的 arr[1]

关键区别:数组名是一个常量指针,其值(指向的地址)不能改变。而普通指针变量可以被重新赋值。
指针的注意事项与陷阱 ⚠️
理解了基本操作后,我们来看看一些常见的陷阱。

1. 运算符优先级
解引用运算符 * 的优先级非常低。*p++ 意味着 *(p++),即先移动指针,再解引用移动前的地址。如果想增加指针指向的值,需要使用 (*p)++。
int arr[] = {0, 1, 2};
int* p = arr;
(*p)++; // 将 arr[0] 从 0 增加到 1
// *p++; // 这会将指针p移动到arr[1],可能不是你想要的效果
2. 双重释放错误
如果两个指针指向同一块堆内存,对其中一个使用 delete 后,该内存已被释放。再对另一个指针使用 delete 会导致未定义行为(通常程序崩溃)。
int* x = new int(5);
int* y = x; // y 和 x 指向同一地址
delete x; // 正确:释放内存
// delete y; // 危险!双重释放错误,y现在是一个“悬空指针”
x = nullptr; // 良好实践:delete后置为空指针
y = nullptr;
3. 指向指针的指针
指针可以指向另一个指针。一个常见的应用是处理命令行参数。

int main(int argc, char** argv) {
// argc: 参数计数,包括程序名本身
// argv: 指针数组,每个元素是一个C风格字符串(char*)
for (int i = 0; i < argc; ++i) {
cout << “Argument ” << i << “: ” << argv[i] << endl;
}
return 0;
}
引用:指针的“安全马甲” 📝
引用是另一个变量的别名。它使用 & 符号声明,但此处的 & 是引用声明符,而非取地址符。
int x = 4;
int& y = x; // y 是 x 的引用(别名)
y = 3; // 通过引用修改 x 的值
cout << x << endl; // 输出 3
// int& z; // 错误!引用必须在声明时初始化。
// y = &someOtherVar; // 错误!引用一旦绑定,不能更改其指向。

引用 vs 指针:
- 必须初始化:引用在创建时必须绑定到一个变量。
- 不能为空:不存在“空引用”。
- 不能重绑定:引用在其生命周期内始终指向初始化的变量。
- 无需解引用:使用引用就像使用原始变量一样。
引用通常用于函数参数传递,以避免拷贝大型对象,我们将在下一讲中详细讨论。

本节课中我们一起学习了C++中数组和指针的核心概念。数组是固定大小的同类型元素集合,而指针则是存储地址的变量,它们之间有着紧密的联系。我们还探讨了指针算术、动态内存管理以及常见的陷阱。最后,我们介绍了引用作为指针的一种更安全、更直观的替代品。理解这些概念是进行高效C++编程和内存管理的基础。下一讲,我们将学习如何将这些知识应用于函数参数传递。
006:C++ for Java Programmers: Lecture 6: 内存模型、多维数组与参数传递
在本节课中,我们将深入探讨C++的内存模型,理解指针和数组在内存中的布局,并学习如何向函数传递各种类型的参数,包括数组和多维数组。掌握这些概念对于编写高效、正确的C++程序至关重要。
内存模型与编译过程
上一节我们回顾了指针和引用的基本用法。本节中,我们来看看代码在计算机内部是如何被组织和执行的。
当我们编译代码时,编译器会经历多个阶段:
- 预处理器:处理
#include等指令,展开头文件,移除注释。 - 编译器:将代码转换为汇编语言。
- 汇编器:将汇编代码转换为机器码(0和1)。
编译器会维护一个符号表,用于记录变量名与其内存地址的对应关系。编译完成后,机器码中只使用内存地址,变量名不再存在。

程序运行时,操作系统会为它分配一个虚拟内存空间。这个空间主要分为以下几个区域:
- 代码区:存放编译后的机器指令。
- 数据区:存放全局变量和静态局部变量。
- 栈:存放函数的局部变量。函数调用时,其局部变量被压入栈;函数返回时,这些变量被弹出。栈从高地址向低地址增长。
- 堆:用于动态内存分配(如使用
new或malloc)。堆从低地址向高地址增长。程序员需要手动管理堆内存的分配和释放。
指针、引用与内存模型

让我们通过代码示例,将变量、指针和引用映射到内存模型中。


int x = 3;
int y = 5;
int* p = &x; // p 是一个指向 x 的指针

在内存中,x、y和p作为局部变量,会被分配在栈上。假设它们的地址如下(为简化,使用短地址表示):
x存储在地址0xF130,值为3。y存储在地址0xF134,值为5。p存储在地址0xF138,其存储的值是x的地址0xF130。


因此,指针 p 本身有一个存储位置(0xF138),其内容指向另一个内存位置(0xF130)。通过解引用操作 *p,我们可以访问或修改 x 的值。

引用则可以看作是一个变量的别名。它必须在创建时初始化,且之后不能更改其绑定的对象。

int& r = x; // r 是 x 的引用(别名)
在编译器内部,引用可能通过指针机制实现,也可能直接在符号表中处理为别名。对于程序员而言,r 和 x 访问的是同一块内存。



数组与内存模型

数组在内存中是连续存储的。声明一个数组时,数组名本质上是一个指向数组首元素的常量指针。

int a[5] = {0, 1, 2, 3, 4};
int* pa = a; // pa 是一个指向数组 a 的指针

在内存中,数组 a 的五个元素 0, 1, 2, 3, 4 会连续存放。数组名 a 的值就是第一个元素 a[0] 的地址。下标操作 a[i] 在内部被转换为 *(a + i),即“从首地址向后移动 i 个元素大小的距离,然后取该地址的值”。
动态分配的数组则存储在堆上:

int* da = new int[3] {5, 6, 7}; // da 指向堆上的数组
// ... 使用 da
delete[] da; // 必须手动释放内存

这里,指针变量 da 本身在栈上,但它存储的值是堆上数组的起始地址。
字符数组(C风格字符串)

C风格字符串是以空字符 \0 结尾的字符数组。许多字符串处理函数都依赖这个终止符来判断字符串的结束。

char c1[] = {'C', 'S', '3', '6', '8'}; // 错误:缺少 \0,打印可能导致越界
char c2[] = {'C', 'S', '3', '6', '8', '\0'}; // 正确
char c3[] = "CS368"; // 正确:编译器自动添加 \0
忘记空终止符是常见的错误来源。在现代C++中,建议使用 std::string 类型来避免这些问题。
多维数组
多维数组在内存中也是线性连续存储的。例如,一个 3行 x 4列 的二维数组:
int md[3][4] = {
{11, 12, 13, 14},
{21, 22, 23, 24},
{31, 32, 33, 34}
};
在内存中的布局顺序是:11, 12, 13, 14, 21, 22, 23, 24, 31, 32, 33, 34。md[1] 表示第二行的首地址,其类型是 int*。

可以使用嵌套的范围for循环来遍历多维数组(C++11):

for (auto& row : md) { // row 是对一个“包含4个int的数组”的引用
for (int& elem : row) {
cout << elem << ' ';
}
cout << endl;
}


参数传递
向函数传递参数主要有以下几种方式:
1. 按值传递
这是默认方式。函数获得实参的一个副本,对副本的修改不影响原始数据。
void dontChangeX(int x) {
x = 5; // 只修改了局部副本
}
int main() {
int x = 3;
dontChangeX(x); // x 仍然是 3
}

2. 按引用传递
函数参数是实参的引用(别名)。对形参的修改直接影响实参。常用于避免复制大型对象,或需要函数修改外部变量时。


void callByReference(int& x) {
x = 5; // 直接修改了 main 中的 x
}
int main() {
int x = 3;
callByReference(x); // x 变为 5
}
如果函数不会修改参数,应使用 const 引用,如 void func(const BigObject& obj)。
3. 按指针传递
与按引用传递类似,但语法上需要显式使用地址操作符 & 和解引用操作符 *。这种方式明确地向调用者暗示参数可能被修改。
void callByPointer(int* xPtr) {
*xPtr = 7; // 解引用并修改
}
int main() {
int x = 3;
callByPointer(&x); // 传递 x 的地址,x 变为 7
}
4. 传递数组
当数组作为函数参数时,实际上传递的是指向其首元素的指针。因此,函数内部无法直接获知数组的大小,通常需要额外传递一个大小参数。
void printArray(int arr[], int size) { // `int arr[]` 等价于 `int* arr`
for (int i = 0; i < size; ++i) {
cout << arr[i] << ' ';
}
}
由于传递的是指针,函数内对数组元素的修改会影响原始数组。
5. 传递多维数组
传递多维数组时,必须指定除第一维之外的所有维度大小。
void printMatrix(int matrix[][4], int rows) { // 必须指定列数 4
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < 4; ++j) {
cout << matrix[i][j] << ' ';
}
cout << endl;
}
}
// 也可以写成:void printMatrix(int (*matrix)[4], int rows)
返回指针或引用
函数可以返回指针或引用,但必须格外小心,绝不能返回指向局部变量(位于栈上)的指针或引用,因为函数返回后局部变量就被销毁了。
// 错误示例:返回局部变量的引用
int& badFunction() {
int localVar = 42;
return localVar; // 危险!localVar 即将被销毁
}
// 正确示例:返回动态分配内存的指针(调用者负责 delete)
int* createArray(int size) {
int* arr = new int[size];
// ... 初始化 arr
return arr; // 返回堆上内存的地址
}
// 调用者必须:int* myArr = createArray(10); ... delete[] myArr;
// 正确示例:返回传入参数的引用
const int& findMax(const std::vector<int>& vec) {
int maxIndex = 0;
// ... 查找最大值的索引
return vec[maxIndex]; // 返回 vec 中某个元素的引用,安全
}
总结

本节课中我们一起学习了C++程序的内存布局,理解了栈、堆、代码区、数据区的概念。我们深入探讨了指针和数组在内存中的表示,以及它们之间的紧密联系。我们还学习了如何向函数传递各种类型的参数,包括基本类型、数组、多维数组,并比较了按值传递、按引用传递和按指针传递的区别与适用场景。最后,我们了解了在函数中返回指针或引用时需要避免的陷阱。掌握这些底层知识,将帮助你写出更高效、更健壮的C++代码。
007:C++ for Java Programmers: Lecture 7: Vector 🧮
在本节课中,我们将要学习C++标准模板库中的容器,并重点介绍vector。vector是一个动态数组,类似于Java中的ArrayList,它极大地简化了数组操作。我们将学习如何创建、初始化、访问和修改vector,并了解其基本操作和特性。
容器与算法库概述
上一节我们介绍了课程安排,本节中我们来看看C++标准模板库的两个核心组成部分:容器和算法。
- 容器:是预定义的数据结构,用于存储和管理数据集合。它们像容器一样持有对象,这些对象可以是基本类型、字符串或自定义类。C++中的容器是模板化的,这意味着在创建时需要指定存储的数据类型,例如
vector<int>。 - 算法库:包含约50-60个通用函数,用于对数据范围(如
vector的一部分或全部)执行操作,例如排序(sort)。这些算法通过迭代器(类似于指针)来访问和操作容器中的数据。
容器主要分为两类:
- 序列容器:数据按顺序存储在相邻的内存位置,可通过索引快速访问。
vector就属于此类。 - 关联容器:通过键来访问数据,例如
map和set。
Vector 简介

上一节我们了解了容器的分类,本节中我们聚焦于序列容器vector。
vector是一个动态数组,其大小可以在运行时增长或缩小,无需手动管理内存。数据存储在连续的内存块中,因此通过索引访问速度很快。vector不自动对元素排序,也允许存在重复值。
创建一个vector需要包含头文件:
#include <vector>
初始化 Vector
以下是创建和初始化vector的几种常见方法。
列表初始化
如果已知所有初始值,可以使用列表初始化。
std::vector<std::string> colors = {"red", "green", "blue"};
// 或省略等号(C++11及以上)
std::vector<std::string> colors2 {"red", "green", "blue"};
注意:使用圆括号()进行列表初始化是常见错误,编译器会将其解释为函数声明。
创建空Vector
要创建一个空的vector,只需声明其类型和变量名。
std::vector<int> numbers; // 正确
std::vector<int> numbers2(); // 错误!这声明了一个函数
带参数的构造
可以指定vector的初始大小和所有元素的默认值。
std::vector<int> numbers3(10, -2); // 创建大小为10,每个元素都是-2的vector
std::vector<int> numbers4(4); // 创建大小为4,每个元素默认为0的vector
重要区别:vector<int> v(10, -2); 创建10个值为-2的元素。vector<int> v{10, -2}; 则创建两个元素:10和-2。
拷贝构造与赋值
可以从另一个vector复制内容。
std::vector<int> v1 {1, 2, 3, 4, 5};
std::vector<int> v2(v1); // 拷贝构造函数
std::vector<int> v3 = v1; // 另一种拷贝形式(本质上也是拷贝构造)
std::vector<int> v4;
v4 = v1; // 赋值操作
这些操作创建的是元素的独立副本(深拷贝)。
访问与修改元素
上一节我们学习了如何创建vector,本节来看看如何访问和修改其中的数据。
使用下标运算符
像数组一样使用[]通过索引访问元素。
colors[0] = "orange"; // 修改第一个元素
std::cout << colors[0]; // 访问第一个元素
注意:[]不进行边界检查,访问越界会导致未定义行为。
使用 at() 函数
at()函数提供带边界检查的访问,越界时会抛出std::out_of_range异常。
std::cout << colors.at(0); // 安全访问
// colors.at(100); // 会抛出异常
使用 front() 和 back() 函数
快速访问首尾元素。
std::cout << colors.front(); // 第一个元素
std::cout << colors.back(); // 最后一个元素
front()和back()返回的是引用,因此可以用于修改元素:
colors.front() = "cyan"; // 修改第一个元素
获取大小
使用size()成员函数获取vector中元素的数量。
int count = colors.size();
遍历 Vector
以下是遍历vector元素的两种常用方法。
基于范围的for循环 (C++11)
这是最简单、最推荐的遍历方式。
for (std::string color : colors) {
std::cout << color << " ";
}
若要修改元素,需使用引用:
for (auto& color : colors) { // auto 自动推导类型,& 表示引用
color = "dark_" + color; // 可以修改原元素
}
使用下标的标准for循环
当需要索引时可以使用。
for (size_t i = 0; i < colors.size(); ++i) {
std::cout << colors[i] << " ";
}
添加与删除元素
vector是动态的,可以方便地添加或删除元素。
添加元素
使用push_back()在vector末尾添加新元素。
colors.push_back("black");
删除元素
pop_back():删除最后一个元素(不返回该元素)。colors.pop_back();clear():清空所有元素。colors.clear();erase():删除指定位置或范围的元素(需要迭代器,见下文)。// 删除第三个元素(迭代器版本) colors.erase(colors.begin() + 2);
迭代器简介
上一节我们使用了基于范围的for循环,其底层依赖于迭代器。本节简要介绍迭代器的概念。
迭代器是指向容器内元素的类指针对象。对于vector,迭代器行为非常接近原生指针。
获取迭代器
begin():返回指向第一个元素的迭代器。end():返回指向最后一个元素之后位置的迭代器(尾后迭代器)。
使用迭代器遍历
std::vector<int> vec {4, 3, 6, 2, 7, 1, 5};
for (auto it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " "; // 解引用迭代器以获取值
}
这里auto自动推导出it的类型为std::vector<int>::iterator。
迭代器的应用
许多STL算法和vector自身的成员函数(如sort, insert)接受迭代器作为参数来指定操作范围。
#include <algorithm>
std::sort(vec.begin(), vec.end()); // 排序整个vector
// 将另一个vector的部分内容插入到指定位置
std::vector<int> vec2 {10, 20};
vec.insert(vec.begin() + 2, vec2.begin(), vec2.end());
insert和erase等函数使用迭代器来精确定位。
其他实用技巧与注意事项
Vector 的 Vector(多维数组)
可以创建vector的vector来模拟矩阵。
std::vector<std::vector<int>> matrix; // 注意 >> 间的空格
重要:std::vector<std::vector<int>>中两个>之间必须有空格,否则编译器会将其解析为流提取运算符>>。
关系运算符
vector支持==, !=, <, <=, >, >=等比较操作。比较是逐元素进行的,类似于字典序比较。
std::vector<int> a {1, 2, 3};
std::vector<int> b {1, 2, 3};
std::vector<int> c {1, 2, 4};

bool eq1 = (a == b); // true
bool eq2 = (a == c); // false
bool lt = (a < c); // true (比较到3<4)
容量管理
resize(n):改变vector的大小。reserve(n):预分配内存空间,避免多次动态扩容,以提高性能。capacity():返回当前已分配的内存空间能容纳的元素数。empty():检查vector是否为空。
总结与资源
本节课中我们一起学习了C++标准模板库中的vector容器。我们涵盖了如何初始化、访问、修改、遍历vector,以及如何添加和删除元素。我们还简要介绍了迭代器的概念及其在算法和容器操作中的应用。

vector是C++中最常用、最通用的序列容器。要深入了解其所有成员函数(如emplace_back, shrink_to_fit, data等),建议查阅官方文档,例如 cppreference.com。在接下来的课程中,我们将继续探索其他容器类型和更强大的STL算法。
008:关联容器 🗂️
在本节课中,我们将要学习C++标准模板库(STL)中的关联容器,特别是set和map。关联容器允许我们根据键(key)来高效地存储和访问数据,这与我们之前学习的序列容器(如vector)有很大不同。我们将从set开始深入探讨,然后了解其变体,最后学习功能更强大的map。
关联容器概述
上一节我们介绍了序列容器,它们适用于需要按顺序索引元素的情况。然而,并非所有数据都适合用数字索引来组织。
本节中我们来看看关联容器。关联容器根据键来存储数据,这使得检查成员资格(例如,某个物品是否在库存中)或建立映射关系(例如,将商品名称映射到库存数量)变得非常高效。C++提供了几种关联容器,本节课我们将重点学习set和map。
深入理解 Set 🧮
set是STL的一部分,它是一个表示无序(就插入顺序而言)元素集合的容器,类似于数学中的集合,不允许重复元素。它的核心功能是支持高效的插入、删除和查找(检查成员资格)。


在内部,set通常实现为平衡二叉搜索树(例如红黑树),以实现这些操作的效率。这意味着存入set的元素必须能够进行比较排序,因此类型需要定义小于运算符(<)。
创建与初始化 Set
要使用set,首先需要包含头文件。创建set时,必须通过模板参数指定其存储元素的类型。
#include <iostream>
#include <set>
#include <vector>
using namespace std;
int main() {
// 1. 创建一个空的整数set
set<int> s1;
// 2. 使用初始化列表创建并初始化一个字符串set
set<string> avengers {"Thor", "Hulk", "Captain America", "Iron Man", "Black Widow"};
// 打印set中的元素(将按字母顺序输出)
for (auto hero : avengers) {
cout << hero << " ";
}
cout << endl;
// 输出: Black Widow Captain America Hulk Iron Man Thor
return 0;
}
向 Set 中插入元素
可以使用insert成员函数向set中添加新元素。insert操作会维护集合的唯一性。
avengers.insert("Vision");
avengers.insert("Spider-Man"); // 插入后,遍历时会按字母顺序出现
// 也可以使用迭代器范围进行插入
vector<string> more_heroes {"Wasp", "Ant-Man", "Hawkeye", "Scarlet Witch"};
avengers.insert(more_heroes.begin(), more_heroes.end());
insert操作会返回一个pair<iterator, bool>,其中first是指向被插入元素(或已存在元素)的迭代器,second是一个布尔值,表示插入是否成功(即元素是否为新加入的)。
auto result = avengers.insert("Black Panther");
cout << "Inserted " << *(result.first) << "? " << boolalpha << result.second << endl;
// 输出: Inserted Black Panther? true
result = avengers.insert("Black Panther"); // 再次尝试插入相同元素
cout << "Inserted again? " << result.second << endl;
// 输出: Inserted again? false
查找与遍历元素
在set中查找元素使用find函数,它返回一个指向该元素的迭代器,如果未找到则返回end()迭代器。
// 查找元素
auto it_cap = avengers.find("Captain America");
auto it_spidey = avengers.find("Spider-Man");
// 使用迭代器遍历一个范围(从Captain America到Spider-Man之前)
for (auto it = it_cap; it != it_spidey; ++it) {
cout << *it << " ";
}
cout << endl;
对于有序容器(如set),还可以使用lower_bound和upper_bound进行范围查询。
lower_bound(key): 返回指向第一个不小于key的元素的迭代器。upper_bound(key): 返回指向第一个大于key的元素的迭代器。
// 打印所有首字母在 ‘C‘ 和 ‘S‘ (不包括S) 之间的英雄
auto low = avengers.lower_bound("C"); // 第一个 >= "C" 的元素,如 "Captain America"
auto up = avengers.upper_bound("S"); // 第一个 > "S" 的元素,如 "Scarlet Witch"
for (auto it = low; it != up; ++it) {
cout << *it << " ";
}
删除元素与容器信息
使用erase函数可以删除元素,可以按键值删除,也可以按迭代器范围删除。
// 按键值删除,返回删除的元素数量(0或1)
size_t num_erased = avengers.erase("Captain America");
cout << "Erased " << num_erased << " element." << endl;
// 按迭代器范围删除 [low, up)
avengers.erase(low, up);

// 其他通用操作
cout << "Size: " << avengers.size() << endl;
cout << "Empty? " << avengers.empty() << endl;
avengers.clear(); // 清空所有元素
cout << "Size after clear: " << avengers.size() << endl;
Set 的变体 🔄

set有几个重要的变体,它们修改了set的某些特性以适应不同需求。
无序集合 (unordered_set)
unordered_set同样存储唯一元素,但其内部使用哈希表实现,因此不保证元素的任何顺序。它的优点是查找、插入和删除的平均时间复杂度接近O(1),但代价是失去了基于顺序的范围查询能力(如lower_bound)。使用它要求元素类型必须有可用的哈希函数。
#include <unordered_set>
unordered_set<string> u_avengers(avengers.begin(), avengers.end());
for (const auto& hero : u_avengers) {
cout << hero << " "; // 输出顺序不确定,且可能随容器扩容而改变
}
多重集合 (multiset)
multiset允许存储重复的元素。其接口与set类似,但insert总是成功,且count(key)和erase(key)的行为会针对重复元素进行调整。
#include <set> // multiset也在<set>头文件中
multiset<string> m_avengers;
m_avengers.insert("Hawkeye");
m_avengers.insert("Hawkeye"); // 允许重复

cout << "Count of Hawkeye: " << m_avengers.count("Hawkeye") << endl; // 输出: 2
// 删除所有"Hawkeye"
m_avengers.erase("Hawkeye");

对应的还有unordered_multiset,它结合了哈希表和允许重复的特性。
深入理解 Map 🗺️
map是关联容器的核心,它存储的是键值对(key-value pairs)。每个键在map中是唯一的,并关联一个值。你可以将其理解为一种将键映射到值的字典。
map的模板参数有两个:map<KeyType, ValueType>。内部通常也实现为平衡二叉搜索树,因此键类型必须支持小于比较。
键值对 (pair)
map中存储的元素类型是std::pair<const KeyType, ValueType>。pair是一个模板类,用于将两个值组合成一个单元。
#include <utility> // 包含pair,但通常由其他容器头文件间接引入
// 创建pair的几种方式
pair<string, int> p1("C++", 3); // 构造函数
auto p2 = make_pair("Data Programming", 220); // 使用make_pair辅助函数,自动推导类型
pair<string, int> p3 = {"Systems Programming", 368};
// 访问pair的成员
cout << p1.first << ": " << p1.second << endl; // 输出: C++: 3
创建与使用 Map
以下是map的基本操作:
#include <iostream>
#include <map>
using namespace std;
int main() {
// 1. 创建一个空的map,将字符串映射到整数
map<string, int> course_to_number;
// 2. 使用初始化列表创建map
map<string, int> numbers {
{"one", 1},
make_pair("two", 2), // 也可以混用make_pair
pair<string, int>("three", 3)
};
// 3. 插入元素:使用下标运算符(如果键不存在则插入,存在则修改值)
course_to_number["C++"] = 368;
course_to_number["Data Programming"] = 220;
// 使用insert插入(如果键已存在,则不会修改值)
auto ret = course_to_number.insert(make_pair("C++", 999));
// ret.second 为 false,因为"C++"已存在,值保持为368
// 4. 访问元素:使用下标运算符(注意:如果键不存在,会插入一个具有默认值的键值对!)
int val = course_to_number["C++"]; // val = 368
// int val2 = course_to_number["Java"]; // 危险!会插入 {"Java", 0}
// 安全的查找:使用find或count
auto it = course_to_number.find("Java");
if (it != course_to_number.end()) {
cout << "Found: " << it->first << " -> " << it->second << endl;
} else {
cout << "Key not found." << endl;
}
// 或者使用count(对于map,结果为0或1)
if (course_to_number.count("C++")) {
cout << "C++ exists in the map." << endl;
}
// 5. 删除元素
course_to_number.erase("Data Programming");
// 6. 遍历map
// 方法1:基于范围的for循环(推荐)
for (const auto& entry : course_to_number) {
cout << entry.first << ": " << entry.second << endl;
}
// 方法2:使用迭代器
for (auto it = course_to_number.begin(); it != course_to_number.end(); ++it) {
// it->first 是键, it->second 是值
cout << it->first << " -> " << it->second << endl;
}
return 0;
}
Map 的变体
与set类似,map也有对应的变体:
multimap: 允许重复的键。不能使用下标运算符[]进行访问,因为同一个键可能对应多个值。需要使用equal_range(key)、lower_bound(key)/upper_bound(key)或find来获取特定键对应的所有值。unordered_map: 基于哈希表实现,提供平均O(1)的查找性能,但不保证元素顺序。
总结 📚
本节课中我们一起学习了C++中强大的关联容器。

我们首先深入探讨了set,它是一个存储唯一元素的有序集合,支持高效的成员检查、插入和删除。我们学习了其创建、初始化、插入(insert)、查找(find、lower_bound、upper_bound)、删除(erase)以及遍历操作。

接着,我们了解了set的变体:允许重复元素的multiset和基于哈希表实现、提供更快访问但无序的unordered_set及其多重版本。

然后,我们将重点转向map,它存储键值对,是将一个键唯一映射到一个值的理想数据结构。我们学习了键值对pair的用法,以及map的插入(使用[]或insert)、访问(使用[]或安全的find/count)、删除和遍历。最后,我们简要提及了允许重复键的multimap和基于哈希的unordered_map。

理解这些关联容器及其适用场景,对于在C++中高效地组织和管理数据至关重要。在接下来的实践中,请尝试使用它们来解决具体问题,例如统计词频或管理库存。
009:类与面向对象编程入门 🏗️
在本节课中,我们将学习C++中类的基础知识,这是面向对象编程的核心。我们将了解如何定义类、创建对象、管理数据成员的访问权限,以及编写成员函数。通过本课,你将能够创建自己的自定义数据类型。

类的定义与对象创建
在C++中,使用 class 关键字来定义一个类。类是一种自定义的数据结构,它封装了数据(成员变量)和操作这些数据的方法(成员函数)。
代码示例:定义一个简单的 Point 类
class Point {
public:
int x;
int y;
};
定义好类之后,我们可以像使用内置类型一样创建该类的对象(也称为实例)。
代码示例:创建 Point 对象并访问其成员
Point p; // 创建一个Point对象
p.x = 3; // 使用点运算符访问公共成员变量
p.y = 6;
上一节我们介绍了类的基本定义,本节中我们来看看如何控制类成员的访问权限。
访问控制:Public 与 Private
C++使用访问说明符来控制类成员的可见性。这有助于封装数据,保护其不被意外修改。
public:在public部分声明的成员,可以在类的外部被直接访问。private:在private部分声明的成员,只能在类的成员函数内部访问。
默认情况下,类(class)的成员是 private 的,而结构体(struct)的成员是 public 的。结构体在C++中基本与类相同,通常用于表示纯数据容器。
代码示例:使用 Public 和 Private
class Point {
private:
int x; // 私有成员,外部无法直接访问
int y;
public:
// 公共成员函数,可以访问私有成员
void setX(int newX) { x = newX; }
int getX() const { return x; } // const 表示此函数不修改对象
};
成员函数
成员函数是定义在类内部的函数,用于操作类的数据成员。它们可以直接访问类的所有成员(包括 private 成员)。
代码示例:为 Point 类添加打印功能
class Point {
public:
int x;
int y;
void printPoint() {
std::cout << "(" << x << ", " << y << ")" << std::endl;
}
};
每个类的实例都拥有自己独立的一套数据成员,但它们共享相同的成员函数代码。
类的嵌套与组合
一个类的数据成员可以是另一个类的对象。这允许我们构建更复杂的数据结构。
代码示例:使用 Point 类构建 Box 类
class Box {
public:
Point minCorner;
Point maxCorner;
void printBox() {
minCorner.printPoint();
maxCorner.printPoint();
}
};
通过点运算符可以链式访问嵌套对象的成员:myBox.minCorner.x = 10;
成员函数的声明与定义分离
为了提高代码的可读性和可维护性,通常将成员函数的声明放在类定义内部,而将具体的函数定义(实现)放在类的外部。这时需要使用作用域解析运算符 :: 来指明该函数属于哪个类。
代码示例:在类外定义成员函数
// 在类内部声明
class Box {
public:
std::pair<int, int> edgeLengths() const; // 声明
};
// 在类外部定义
std::pair<int, int> Box::edgeLengths() const {
return {maxCorner.x - minCorner.x, maxCorner.y - minCorner.y};
}
在类内部直接定义的短小函数,编译器可能会将其视为内联函数,这可以避免函数调用的开销。
Getter 与 Setter(访问器与修改器)
为了保持数据的封装性,最佳实践是将数据成员设为 private,然后提供公共的成员函数来读取(Getter)和修改(Setter)它们。这允许你在修改数据时加入验证逻辑。
以下是实现 Getter 和 Setter 的方法:
- Getter:通常以
get开头,返回数据成员的值。对于不修改对象的 Getter,应使用const关键字修饰。 - Setter:通常以
set开头,接受一个参数来设置数据成员的值。
代码示例:为私有成员提供 Getter 和 Setter
class PrivatePoint {
private:
int x;
int y;
public:
// Setter
void setX(int xIn) { x = xIn; }
void setY(int yIn) { y = yIn; }
// Const Getter
int getX() const { return x; }
int getY() const { return y; }
};
const 成员函数的重要性:当你在一个承诺不修改对象的上下文(例如,函数接受 const 对象引用)中调用成员函数时,只能调用被标记为 const 的成员函数。这确保了数据的完整性。
构造函数
构造函数是一种特殊的成员函数,在创建类的新对象时自动调用。它的名称与类名相同,没有返回类型。
代码示例:定义构造函数
class Point {
public:
int x;
int y;
// 默认构造函数
Point() {
std::cout << "默认构造函数被调用" << std::endl;
}
// 带参数的构造函数
Point(int xIn, int yIn) {
x = xIn;
y = yIn;
}
};
C++会为类提供一个什么都不做的默认构造函数。但一旦你定义了任何构造函数,编译器就不再提供默认版本。你可以通过参数默认值来创建灵活的构造函数。
初始化列表
在构造函数中,更高效的方式是使用初始化列表来初始化成员变量,而不是在构造函数体内进行赋值。这对于类类型的成员尤其重要,因为它直接调用拷贝构造函数,而不是先调用默认构造函数再赋值。
代码示例:使用初始化列表的构造函数
class Point {
public:
int x;
int y;
// 使用初始化列表
Point(int xIn, int yIn = 77) : x(xIn), y(yIn) {
// 构造函数体可以为空,或用于其他初始化逻辑
}
};
公式/概念:初始化列表语法
ClassName(parameters) : member1(value1), member2(value2), ... {
// 构造函数体
}
this 指针
在类的成员函数内部,this 是一个指向当前对象实例的指针。当函数参数名与成员变量名相同时,可以使用 this-> 来明确指代成员变量。
代码示例:使用 this 指针
class Point {
public:
int x;
Point(int x) {
this->x = x; // 使用this区分参数x和成员变量x
}
};

本节课中我们一起学习了C++中类的基础知识,包括如何定义类、控制访问权限、编写成员函数、使用构造函数初始化对象,以及通过 Getter/Setter 实现封装。理解这些概念是掌握C++面向对象编程的第一步。下一讲我们将深入探讨构造函数的更多细节,以及析构函数、拷贝构造函数和赋值运算符。
010:终端与CSL机器入门指南 🖥️
在本节课中,我们将学习如何使用终端,并连接到威斯康星大学的CSL(计算机系统实验室)Linux机器来完成编程作业。课程内容涵盖终端的基本概念、常用命令、文件操作以及如何在本地计算机和远程服务器之间传输文件。
概述
终端是程序员与计算机系统交互的重要工具。本节将介绍终端的历史、基本操作,并演示如何连接到远程的CSL Linux服务器进行开发工作。
终端简介
很久以前,人们并不拥有个人计算机。部门会购买昂贵的大型机供所有人使用。为了共享资源,他们会购买“哑终端”——这些只是带有屏幕和键盘的文本界面盒子,用于连接到大型机。
如今,我们不再使用那种终端和大型机。我们拥有更强大的本地个人计算机。因此,我们使用的是“终端模拟器”。
为何使用终端模拟器
使用终端模拟器可能比使用鼠标操作更快。它在浏览文件系统时非常高效。例如,通过键入目录名称来切换路径,通常比在文件列表中寻找并点击鼠标要快得多。
以下是两条建议:
- 寻找优秀的工具,并投入时间精通它们。这项投资非常重要,能避免你因使用笨拙或不熟悉的工具而付出额外努力。
- 掌握能让你在任何地方工作的工具。这一点尤为重要。例如,当需要在家工作(如全球疫情期间),或者想在更舒适的地方(如海滩)工作时。
Shell 的作用
当我们在计算机上运行终端模拟器(如Windows的PowerShell或Mac的Terminal)时,该程序会运行另一个名为“Shell”的程序。
Shell非常简单,主要做两件事:
- 允许用户在系统的文件夹(目录)中移动并查看所有文件。
- 允许用户运行程序。
它的工作流程是:询问“我应该做什么?”,用户输入一个命令;然后再次询问“现在我应该做什么?”,用户输入另一个命令。
常见的 Shell 类型
在终端模拟器中,我们可以运行多种不同的Shell。以下是一些例子:
- CMD:Windows的默认Shell。
- PowerShell:我将使用的Shell。Windows版本的CMD通常不接受很多Linux风格的命令,而PowerShell则接受这些命令,方便熟悉Linux/Mac的用户。
- Bash (Bourne Again Shell):这是你登录CSL机器时默认看到的Shell。它由Stephen Bourne在1979年开发,后经改进成为Bash。
连接到 CSL 机器
现在,我将演示如何连接到CSL机器。这是我们完成作业和提交作业的环境。
由于我使用的是Windows计算机,需要下载一个SSH客户端。我将使用 PuTTY。你可以在描述中找到链接,或者直接访问 putty.org。
如果你使用Mac或Linux机器,可以直接使用终端,因为它内置了SSH客户端。
Windows 用户步骤 (使用 PuTTY)
- 下载PuTTY安装程序(32位或64位)。如果不确定,选择32位版本。
- 运行安装程序并完成安装。
- 打开PuTTY。
- 在“主机名”字段中输入你的CSL用户名,格式为:
你的用户名@bestlinux.cs.wisc.edu。 - 点击“打开”进行连接。
- 输入你的CSL登录密码。
Mac/Linux 用户步骤
- 打开终端(Terminal)。
- 使用
ssh命令连接,格式为:ssh 你的用户名@bestlinux.cs.wisc.edu。 - 输入你的CSL登录密码。

连接成功后,你将看到命令行提示符,例如 doher@royal13:~$。其中 doher 是用户名,royal13 是计算机名,~ 表示当前在用户的主目录,$ 是提示符结束标志。

基本终端命令
成功登录后,让我们学习一些基本的终端命令来导航和操作文件。
清理屏幕与查看当前位置
屏幕可能有很多信息。使用 clear 命令可以清屏。
要查看当前所在目录的完整路径,使用 pwd (Print Working Directory) 命令。
pwd
在CSL机器上,你的主目录路径可能类似于 /u/d/o/doher,这是系统按用户名首字母组织用户的方式。
列出文件与切换目录
要查看当前目录下的文件和文件夹,使用 ls (List) 命令。
ls
要切换目录,使用 cd (Change Directory) 命令。
cd 目录名:进入指定目录。cd ..:返回上一级目录。cd ~或cd:直接返回用户主目录。
小技巧:输入目录或文件名时,可以按 Tab 键自动补全,提高效率。
创建目录与文件
要创建一个新目录,使用 mkdir (Make Directory) 命令。
mkdir demo
要创建一个简单的文本文件,可以使用 echo 命令配合输出重定向 >。
echo "Hello" > demo.txt
这条命令将“Hello”写入(或覆盖)到 demo.txt 文件中。如果要将内容追加到文件末尾,使用 >>。
echo "World" >> demo.txt
查看、复制、移动与重命名文件
要查看文件内容,使用 cat (Concatenate) 命令。
cat demo.txt
要复制文件,使用 cp (Copy) 命令。
cp demo.txt ../demo_copy.txt # 复制到上一级目录并重命名
要移动或重命名文件,使用 mv (Move) 命令。在同一个目录下使用 mv 就是重命名。
mv demo.txt howdy.txt # 重命名
mv howdy.txt .. # 移动到上一级目录
获取命令帮助
如果不熟悉某个命令的用法,可以使用 man (Manual) 命令查看手册。
man pwd
在手册页面中,可以使用方向键、Page Up/Page Down 浏览,按 q 键退出。
提高效率的技巧

- 上箭头键:快速调出之前执行过的命令。
history命令:列出所有最近使用过的命令。Ctrl + R:进入反向搜索模式,输入关键词可以查找历史命令。
文件传输


完成作业后,你需要将文件在本地计算机和CSL机器之间相互传输。

从 Windows 上传文件到 CSL (使用 PSCP)
PuTTY 提供了 pscp 命令用于安全拷贝。在 Windows 的 PowerShell 或命令提示符中使用。
pscp compilerTest.cpp doher@bestlinux.cs.wisc.edu:/u/d/o/doher/private/teaching/cs368/homework1/
compilerTest.cpp:本地要上传的文件。doher@...:你的CSL登录信息。:/path/to/destination/:CSL机器上的目标路径。
从 CSL 下载文件到 Windows (使用 PSCP)
pscp doher@bestlinux.cs.wisc.edu:/u/d/o/doher/private/teaching/cs368/homework1/compilerTest.cpp .\CT.cpp
- 第一部分指定了远程服务器上的文件路径。
.\CT.cpp是下载到本地后保存的文件名和路径(.代表当前目录)。
Mac/Linux 用户文件传输

Mac 和 Linux 用户使用内置的 scp (Secure Copy) 命令,语法与 pscp 类似,但命令名不同。
上传:
scp compilerTest.cpp doher@bestlinux.cs.wisc.edu:/path/to/destination/
下载:
scp doher@bestlinux.cs.wisc.edu:/path/to/source/file.cpp ./
编译与运行 C++ 程序
连接到CSL并准备好源代码后,你需要编译和运行它。CSL机器通常使用 g++ 编译器。
以下是一个编译示例:
g++ -Wall -o compTest compilerTest.cpp
-Wall:启用所有警告,有助于发现代码问题。-o compTest:指定生成的可执行文件名为compTest。compilerTest.cpp:要编译的源代码文件。
如果编译成功(没有错误和警告),不会有输出。使用 ls 命令可以看到新生成的绿色可执行文件 compTest。
运行程序:
./compTest
程序会提示你输入学号,然后进行计算并输出一个结果代码。你需要将这个代码填写到作业文件的指定位置。
使用文本编辑器 (Vim)

在CSL机器上编辑文件,你需要使用命令行文本编辑器,如 Vim 或 Emacs。本节简要提及,会有单独视频详细介绍 Vim。
打开文件进行编辑:
vim compilerTest.cpp
在 Vim 中,你需要进入插入模式(按 i)才能输入文字。编辑完成后,按 Esc 退出插入模式,然后输入 :wq 保存并退出。
总结

本节课我们一起学习了终端和CSL机器的基本使用。我们了解了终端的历史与作用,掌握了如何通过 PuTTY (Windows) 或 SSH (Mac/Linux) 连接到远程的 CSL Linux 服务器。我们学习了一系列基本的终端命令,包括导航目录 (cd, pwd, ls)、操作文件 (mkdir, cp, mv, cat)、以及获取帮助 (man)。我们还演示了如何在本地和远程服务器之间传输文件 (pscp/scp),并完成了在CSL上编译和运行一个简单C++程序的完整流程 (g++, ./program)。掌握这些技能是进行后续系统级编程作业的基础。
011:Vim 基础入门 🚀

在本节课中,我们将学习 Vim 文本编辑器的基本使用方法。Vim 是一个基于模式的编辑器,掌握其核心模式与操作能极大提升文本编辑效率。我们将从进入 Vim、切换模式、基础导航开始,逐步介绍编辑、搜索、复制粘贴以及保存退出等关键操作。
Vim 教程:1.1:Vim 的三种核心模式
Vim 的核心在于其不同的操作模式。上一节我们概述了课程内容,本节中我们来看看 Vim 的三种基本模式。
Vim 主要包含三种模式:
- 命令模式:也称为导航模式。在此模式下,键盘输入被视为命令,用于移动光标、删除、复制等操作。
- 插入模式:在此模式下,键盘输入会作为文本内容插入到文件中,就像普通的文本编辑器一样。
- 可视模式:在此模式下,可以选择文本块进行操作。它又分为字符可视模式、行可视模式和块可视模式。
要进入插入模式,请在命令模式下按下 i 键。
要退出插入模式并返回命令模式,请按下 Esc 键。
Vim 教程:1.2:基础导航
在命令模式下,你可以高效地移动光标而无需使用鼠标。以下是基础的导航键位。
将手指放在键盘的“Home Row”上,使用 H、J、K、L 这四个键进行移动:
H:向左移动光标。J:向下移动光标。K:向上移动光标。L:向右移动光标。
你可以在这些命令前加上数字,以实现多单位移动。例如:
5j:向下移动5行。10l:向右移动10个字符。
Vim 教程:1.3:进入插入模式的不同方式
除了使用 i 键在光标前进入插入模式,Vim 还提供了其他几种方式,以适应不同的编辑场景。
以下是进入插入模式的几种方法:
i:在光标当前位置之前进入插入模式。a:在光标当前位置之后进入插入模式。o:在当前行的下方新建一行,并进入插入模式。O:在当前行的上方新建一行,并进入插入模式。
Vim 教程:1.4:高级导航技巧
掌握了基础移动后,我们来看看如何更快地在文件内和行内跳转。
文件内跳转
gg:跳转到文件第一行。G:跳转到文件最后一行。5G:跳转到第5行。
行内跳转
0:跳转到当前行的行首。$:跳转到当前行的行尾。
按单词跳转
w:跳转到下一个单词的词首。b:跳转到上一个单词的词首。e:跳转到当前或下一个单词的词尾。
同样,可以结合数字使用,例如 3w 表示向后移动3个单词。
Vim 教程:1.5:搜索文本
在 Vim 中搜索文本非常高效。上一节我们学习了如何跳转,本节中我们来看看如何查找特定内容。
- 在命令模式下,按下
/键。 - 输入要搜索的词汇,例如
mode。 - 按下
Enter键。所有匹配项会被高亮,光标跳至第一个匹配处。 - 使用
n键跳转到下一个匹配项。 - 使用
N键跳转到上一个匹配项。
若光标已在一个单词上,可以使用 # 键向上搜索该单词,使用 * 键向下搜索该单词。
要清除搜索高亮,可以搜索一个不存在的字符串,例如 /asdfghjkl。
Vim 教程:1.6:编辑与修改文本
现在我们已经知道如何移动和查找,接下来学习如何编辑文本。
删除字符
x:删除光标下的字符。
删除更多内容
d 命令结合移动命令可以删除文本。
dw:删除一个单词(从光标处到词尾)。d$或D:删除从光标到行尾的内容。dd:删除整行。d5j:删除光标及向下的5行。
替换字符
r:然后输入一个新字符,将替换光标下的字符。例如r8会将当前字符替换为8。
撤销与重做
u:撤销上一次操作。Ctrl + r:重做被撤销的操作。
Vim 教程:1.7:复制、剪切与粘贴
Vim 中的复制操作称为“yank”,剪切操作即“delete”,因为它们会将内容存入寄存器。
复制
yw:复制一个单词。yy或Y:复制整行。
剪切
d 命令在删除的同时也执行了剪切。例如 dd 剪切整行。
粘贴
p:在光标后粘贴内容。P:在光标前粘贴内容。
Vim 教程:1.8:可视模式操作
可视模式允许你选择文本块,然后对其进行操作。这是进行批量编辑的强大工具。
有三种进入可视模式的方式:
v:进入字符可视模式,按字符选择文本。V:进入行可视模式,按整行选择文本。Ctrl + v:进入块可视模式,按矩形块选择文本,非常适合对齐的代码列。
进入可视模式并选择文本后,你可以使用 y 复制、d 删除或 x 剪切所选内容。
在块可视模式下,选择一列后,按 Shift + i 进入插入模式,输入文本后按 Esc,该文本会被插入到所选块的每一行前,非常适用于同时为多行代码添加注释或类型声明。
Vim 教程:1.9:保存与退出
编辑完成后,你需要保存文件并退出 Vim。这是最关键的操作之一。
所有保存和退出操作都需要在命令模式下输入(以冒号 : 开头):
:w:保存文件。:q:退出 Vim。如果文件有未保存的修改,Vim 会阻止退出。:q!:强制退出,不保存任何修改。:wq或:x:保存文件并退出。
Vim 教程:1.10:总结与配置建议
本节课中我们一起学习了 Vim 编辑器的核心概念和基本操作。
我们涵盖了:
- Vim 的三种核心模式:命令模式、插入模式和可视模式。
- 使用
h、j、k、l及w、b、gg、G等进行高效导航。 - 使用
i、a、o、O进入插入模式进行编辑。 - 使用
/和n进行文本搜索。 - 使用
d、x、r进行删除与替换,以及u和Ctrl + r进行撤销重做。 - 使用
y、d、p进行复制、剪切和粘贴。 - 利用
v、V、Ctrl + v进入可视模式进行块操作。 - 使用
:w、:q、:wq保存和退出。
个性化配置:Vim 可以通过 ~/.vimrc 配置文件进行高度定制,例如开启语法高亮、显示行号、设置缩进等。建议初学者在熟悉基本操作后,逐步探索和配置自己的 .vimrc 文件。

熟练掌握 Vim 需要练习,但一旦习惯,其键盘驱动的编辑方式将带来极高的效率。请选择适合你的工具并坚持练习。
012:文件读写实践 📄

在本节课中,我们将学习如何在C++中进行文件的写入和读取操作。我们将通过一个简单的示例程序,逐步演示如何创建文件、向文件写入数据,以及如何从文件中读取数据。
概述
文件输入输出是编程中常见的任务,它允许程序将数据持久化存储到磁盘,或从磁盘读取数据。C++标准库提供了fstream库来简化文件操作。本节将介绍使用ofstream写入文件和ifstream读取文件的基本方法。

写入文件

上一节我们介绍了文件操作的基本概念,本节中我们来看看如何具体实现文件的写入。

首先,进行文件写入需要包含<fstream>头文件,并使用std::ofstream类。以下步骤详细说明了写入文件的过程:

- 包含必要的头文件:首先需要包含
<fstream>库。#include <fstream>

-
创建输出文件流对象:声明一个
ofstream对象,并指定要打开的文件名。std::ofstream outFile("example.txt"); -
检查文件是否成功打开:在写入之前,应检查文件流是否成功打开。
if (outFile.is_open()) { // 文件打开成功,可以写入 } else { // 文件打开失败,处理错误 }

-
向文件写入数据:使用插入运算符
<<将数据写入文件,就像使用cout向屏幕输出一样。outFile << "这是要写入文件的第一行文本。\n"; outFile << "这是第二行。"; -
关闭文件:完成写入操作后,应关闭文件流以释放资源。
outFile.close();

核心代码示例:以下是一个完整的写入文件的函数:
void writeToFile(const std::string& filename) {
std::ofstream outFile(filename);
if (outFile.is_open()) {
outFile << "第一行内容\n";
outFile << "第二行内容\n";
outFile << "第三行内容\n";
outFile.close();
std::cout << "文件写入成功。\n";
} else {
std::cout << "无法打开文件进行写入。\n";
}
}
运行此函数后,会在程序同级目录下创建一个名为example.txt的文件,并包含三行文本。

读取文件
在学会了如何写入文件之后,接下来我们学习如何从文件中读取数据。

读取文件需要使用std::ifstream类。基本流程与写入类似,但使用的是输入操作。以下是读取文件的步骤:
-
创建输入文件流对象:声明一个
ifstream对象。std::ifstream inFile("example.txt"); -
检查文件是否成功打开。
if (!inFile.is_open()) { std::cout << "无法打开文件进行读取。\n"; return; } -
从文件中读取数据:常用的方法是使用
std::getline函数逐行读取。std::string line; while (std::getline(inFile, line)) { std::cout << line << std::endl; }在这个
while循环中,getline会一直读取,直到文件末尾。每次读取一行内容到line字符串中,然后将其打印到控制台。 -
关闭文件。
inFile.close();
核心代码示例:以下是一个完整的读取文件的函数:
void readFromFile(const std::string& filename) {
std::ifstream inFile(filename);
std::string line;
if (inFile.is_open()) {
while (std::getline(inFile, line)) {
std::cout << line << std::endl;
}
inFile.close();
} else {
std::cout << "无法打开文件: " << filename << std::endl;
}
}
调用此函数读取之前创建的example.txt文件,会将文件中的三行内容依次打印在控制台上。
完整程序演示

为了更清晰地展示整个流程,我们可以将写入和读取操作放在main函数中依次执行。
#include <iostream>
#include <fstream>
#include <string>
int main() {
const std::string filename = "example.txt";
// 1. 写入文件
std::ofstream outFile(filename);
if (outFile.is_open()) {
outFile << "这是新写入的第一行。\n";
outFile << "这是新写入的第二行。\n";
outFile.close();
std::cout << "--- 文件写入完成 ---\n";
}
// 2. 读取文件
std::cout << "\n--- 开始读取文件内容 ---\n";
std::ifstream inFile(filename);
std::string line;
if (inFile.is_open()) {
while (std::getline(inFile, line)) {
std::cout << line << std::endl;
}
inFile.close();
}
std::cout << "--- 文件读取结束 ---\n";
return 0;
}
运行此程序,会先创建(或覆盖)example.txt文件并写入两行文本,随后立即读取该文件并将内容打印到控制台。
总结

本节课中我们一起学习了C++文件输入输出的基础操作。我们掌握了:
- 使用
std::ofstream和<<操作符向文件写入数据。 - 使用
std::ifstream和std::getline函数从文件逐行读取数据。 - 在操作文件前后,使用
is_open()检查状态以及使用close()关闭文件流的重要性。

这些是处理文本文件最基本且最常用的方法,是后续学习更复杂文件操作(如二进制文件、随机存取等)的坚实基础。
013:运行测试指南 🧪
在本教程中,我们将学习如何为作业4下载资源、上传至CSL机器,以及如何运行测试脚本以获取程序开发过程中的反馈。
概述 📋
上一节我们介绍了作业的基本要求。本节中,我们来看看如何获取测试资源并运行自动化测试。这个过程能帮助你在开发过程中验证代码的正确性。
下载资源 💾
首先,需要从Canvas下载作业4所需的所有资源文件。
以下是需要下载的文件列表:
- 模板文件
- 包含待分析客户元数据的文件
- 格式化摘要文件(即程序应生成的最终输出格式)
- 测试脚本
- 输入文件及其对应的解决方案文件

下载完成后,所有文件将保存在本地机器的下载文件夹中。
上传至CSL机器 ⬆️
现在,我将把这些资源上传到CSL机器。
首先,在本地创建一个专用文件夹(例如 homework3),并将所有下载的文件移入其中。接着,通过SSH连接到CSL机器,并切换到目标目录。最后,使用文件传输工具(如 scp)将整个文件夹上传。
操作完成后,可以在CSL机器的相应目录下看到所有已上传的资源文件。
运行测试脚本 ▶️
成功上传文件后,就可以运行测试脚本了。
在运行脚本之前,需要先为其添加可执行权限。使用以下命令:
chmod +x run_tests.sh
权限设置完成后,即可执行测试脚本:
./run_tests.sh
脚本运行后,会输出一系列测试结果。
理解测试输出 📊
测试脚本会按顺序检查作业的各个部分。
最初的测试仅验证程序能否成功编译。随着你逐步实现更多功能,后续测试会检查更具体的内容。
例如,测试可能依次验证:
- 数据是否正确读入
- 客户姓名格式是否正确
- 是否为每位客户计算了成本和百分比
测试输出会明确指示每个环节是通过还是失败,帮助你定位需要修改的代码部分。
总结 🎯

本节课中,我们一起学习了为C++作业配置测试环境的完整流程:从Canvas下载资源文件,将其上传到CSL机器,为测试脚本添加执行权限并运行它,最后解读测试输出以指导后续开发。定期运行测试是确保代码符合要求的高效方法。
014:向量操作与测试驱动开发 📚
在本教程中,我们将学习如何完成一个C++编程作业。该作业的核心是操作一个数字向量(std::vector<int>),并实践测试驱动开发(TDD)的工作流程。我们将编写多个函数来实现格式化输出、插入、删除、计算统计信息以及排序和去重等功能。
作业概述与流程 🔄
上一节我们介绍了本课程的目标,本节中我们来看看完成本次作业的具体步骤。
整个作业流程可以分为以下几个关键步骤:
- 编译程序:首先,你需要编译源代码文件以生成可执行文件。
- 运行测试:作业中已包含测试代码。编译后,直接运行生成的可执行文件即可执行所有测试。
- 调试与修改:当测试失败时,测试框架会提供清晰的错误信息,指出哪个函数测试失败以及期望的结果是什么。你需要根据这些信息修改你的代码。
- 迭代:修改代码后,重新编译并运行测试,直到所有测试通过。
以下是完成作业的详细步骤列表:
- 编译:使用C++编译器(如g++)编译包含作业代码的源文件。
- 测试:运行编译后的可执行程序,自动执行所有预设的单元测试。
- 调试:阅读测试失败时输出的描述性错误信息,定位问题。
- 修改:根据错误信息,修正对应函数的实现逻辑。
- 验证:保存修改,重新编译并运行测试,确认问题已解决。
核心任务:向量操作函数 ✨
上一节我们了解了作业的工作流程,本节中我们来看看需要实现的具体函数。你的主要任务是实现一系列操作整数向量的函数。
需要实现的函数包括:
-
格式化输出:
formatNumbers- 功能:接收一个整数向量,生成一个格式化的字符串,显示每个元素及其在向量中的索引。
- 示例:向量
[10, 20, 30]可能被格式化为“[0:10, 1:20, 2:30]”。
-
插入与删除:
insertNumberAtIndex:在向量的指定索引处插入一个数字。需要处理索引越界等边界情况。deleteNumberAtIndex:删除向量中指定索引处的数字。同样需要处理边界情况。

-
统计计算:
computeSum:计算向量中所有数字的总和。- 公式:
sum = numbers[0] + numbers[1] + ... + numbers[n-1]
- 公式:
computeAverage:计算向量中所有数字的平均值。- 公式:
average = sum(numbers) / count(numbers)
- 公式:
-
排序与去重:
sortAscending:将向量中的数字按默认升序排列。sortDescending:将向量中的数字按降序排列。sortAscendingAndRemoveDuplicates:将向量按升序排序,并移除所有重复的值。removeAllInstances:从向量中移除所有与指定数字相等的元素。
每个函数在代码中都附有详细的描述,说明了其预期的行为、参数和返回值,请仔细阅读。
代码结构与开发实践 💻
上一节我们列出了所有需要实现的函数,本节中我们来看看代码的组织形式和推荐的开发方法。
作业的源代码文件结构如下:
- 测试函数:文件底部包含了大量的测试用例(例如
testInsertNumberAtIndexEmptyVector)。不建议修改这部分代码。 - 待实现函数:文件中部是等待你填充代码的函数框架(如
formatNumbers,insertNumberAtIndex等)。 - 主函数:
main函数负责调用所有测试方法。
开发过程中,你将遵循测试驱动开发模式:
- 初始运行测试时,许多测试会因函数未实现而失败。
- 例如,测试
insertNumberAtIndex时,失败信息会明确告知你,对于空向量的插入操作,实际返回结果与期望结果(如“[]”)不符。 - 你根据此信息去实现
insertNumberAtIndex函数,处理空向量的情况。 - 实现后,重新编译并运行测试,观察该测试是否通过,并继续解决下一个失败测试。
这就是完成作业五的整个过程,它结合了具体的向量操作编程任务和测试驱动开发的实践方法。
总结 📝

本节课中,我们一起学习了如何完成一个C++编程作业。我们详细介绍了作业的完整流程:从编译程序、运行测试,到根据测试反馈进行调试和代码修改。我们明确了需要实现的核心功能,包括对std::vector<int>的格式化、增删、统计计算、排序及去重操作。最后,我们分析了代码结构,并实践了测试驱动开发的基本迭代步骤:运行测试、查看失败信息、修改代码、重新验证,直至所有功能符合预期。

浙公网安备 33010602011771号