Open WangShuXian6 opened 1 week ago
从基础到更高级的概念。 这样,在课程结束时,你将能够自信地编写 C++ 代码。
本课程涵盖了你需要了解的所有 C++ 知识,你不需要在各种随机教程之间来回跳跃。
我是一名软件工程师,拥有超过二十年的经验,并且通过这个频道和我的在线学校 Marsh.com,已经教会了数百万的人如何编程。
一定要订阅我的频道,因为我会不断上传新的教学视频。
这门课程是我完整 C++ 系列的第一部分。每一部分大约有三到四个小时长,所以你可以轻松地在一两天内完成。
你正在观看的这一部分,我们将探索 C++ 的基础知识。
在这一部分中,你将学习编程的基本概念,包括:
在整个课程中,我将给你提供大量的练习,帮助你培养问题解决能力,并提高编写代码的自信心。
事实上,许多练习题都是常见的面试题。
我们将探索中级概念,例如:
在最后一部分,我们将讨论高级概念,例如:
通过这整个系列的学习,最终你将对 C++ 有一个扎实的理解,并准备好将其应用到实际生活中。
如果你想使用 Unreal Engine(一个流行的游戏引擎)来开发游戏,你将拥有必要的 C++ 技能来构建游戏。你只需要学习 Unreal Engine 的相关知识。
所以我希望你能坚持下去,掌握 C++ 这门语言,它是目前最快和最高效的编程语言之一。
C++ 是世界上最流行的编程语言之一,它是构建性能关键型应用程序的首选语言。你可以用 C++ 来开发:
目前最新的版本是 C++ 20,下一版本将在明年发布。
认为 C++ 已经不再相关,因为出现了像 Java 或 C# 这样的新语言,但这是不正确的。
C++ 仍然是最快、最高效的编程语言之一,所以,如果你想开发一个需要快速且高效利用内存的应用程序,C++ 是一个非常好的选择。
C++ 也是计算机科学或软件工程专业学生经常学习的第一门语言,因为它影响了许多编程语言,如:
根据 Indeed.com 的数据,美国 C++ 程序员的平均年薪超过 17 万美元。
这些功能在几乎每个应用程序中都是必需的。因此,我们不需要每次都从头开始编写这些功能,而是可以重用标准库中一些现成的 C++ 代码,快速构建应用程序。
但标准库非常庞大,所以我们只能触及它的表面。如果你想深入了解,有专门的书籍可以学习这个话题。
但实际上,你不需要掌握 C++ 的所有内容就能编写出有意义的程序。
就像你不需要了解电视提供的每一个功能才能使用和享受它一样。
随着学习的进展,我将向你展示如何在学习 C++ 的同时编写一些非常酷的程序。
你会发现 C++ 实际上并不那么难。
你将能够自信地编写 C++ 代码。
我们使用集成开发环境(IDE),它基本上是一个包含代码编辑器、构建工具和调试工具的应用程序。
其中一些是免费的,其他的则是商业软件。不过,排名前几的 IDE 包括:
Microsoft Visual Studio(Windows 专用)
Xcode(Mac 专用)
CLion(跨平台)
你可以使用一些免费的替代工具。市面上有很多不同的 IDE 可供选择来创建 C++ 程序。
但你并不需要使用它来跟随课程进度,你可以使用任何你喜欢的工具,因为我们在这里的重点是 C++ 语言本身,而不是工具。
我建议你下载 CLion 的免费版本,这样你可以轻松跟随课程学习。之后,你可以选择购买许可证,或者使用其他免费的替代工具。
如果你是 Mac 用户,请确保下载正确的 DMG 文件,因为 Mac 上有两种不同的构建版本:
根据你 Mac 的处理器类型,确保下载适合你设备的 DMG,因为性能差异非常明显。
请安装并准备好,在下一课中,我们将一起创建我们的第一个 C++ 程序。
首次打开 CLion 时,你会看到一个弹出框,要求激活许可证。
暂时选择“开始试用”。
你需要登录你的 JetBrains 账户。
现在,回到 CLion,开始试用。
在页面上,点击新建项目。
在顶部,你可以指定项目的位置。
Users / myname / CLionProjects
。给项目命名为hello_world(不带空格,全部小写)。
接下来,你可以指定 C++ 的语言标准。
点击创建项目。
main.cpp
的文件,里面已经有一些代码。
main.cpp
文件。CMakeLists.txt
,我们暂时不需要它,可以关闭。main
,它是程序的入口点,就像电视的电源按钮一样。main
之前使用了大写字母 M
,这会改变程序的含义。main
函数我们在 main
函数前面需要指定它的返回值类型。
main
函数应该返回类型为 int
的值,int
是整型的缩写,表示一个整数(例如 1、2、3 等)。当你运行程序时,操作系统(如 Windows 或 macOS)将执行这个函数。该函数返回的值会告诉操作系统程序是否正常终止。
C++ 中,空格通常不重要,因此无论你用一个空格还是十个空格都没关系,但为了代码的可读性和格式化,我们建议使用一个空格。
在 main
函数的花括号 {}
内部编写代码。
main
函数时被执行。代码格式化:在代码的花括号位置,有两种流派:
没有对错,只要保持一致即可。在本课程中,我将左花括号放在函数定义的同一行。
我们要编写代码将“Hello, World!”打印到屏幕上。为此,我们将使用 C++ 标准库。
标准库提供了我们在大多数应用中需要的功能。我们需要的功能是输出信息到屏幕。
#include
,后跟尖括号 <>
,并指定文件名 iostream
(代表输入输出流)。标准库就像超市有不同的区域一样,每个文件也有不同的功能。在课程中,你会学到标准库中的其他文件。
std::cout
输出回到 main
函数后,我们输入 std::
,它代表标准库。
std::cout
是用于输出字符到屏幕的功能。有些人认为这是“控制台输出”,但实际上这是不对的。使用 std::cout
对象,我们可以在屏幕上输出一个或多个字符。
std::cout
后面,输入两个左尖括号 <<
,然后在双引号内输入要打印的文本:"Hello, World!"
。代码结束时,我们使用分号 ;
来结束语句,就像我们在写句子时使用句号一样。
return 0;
来返回一个整数值 0,表示程序正常结束。iostream
。main
函数,它是程序的入口点。main
函数返回一个整数类型的值(例如 0)。在下一课中,我将向你展示如何编译并运行这个程序。
exe
),但这个文件只能在 Windows 上运行。回到我们的代码,要运行程序,我们点击工具栏上的播放图标。
Control + R
,我强烈推荐使用快捷键,因为它能让你事半功倍。点击运行后,在屏幕底部会出现一个小窗口,这是我们程序的控制台或终端窗口。
Hello, World!
的消息被成功输出。由于我们使用的是控制台应用程序,控制台应用程序相对更容易创建,特别是对于刚开始学习编程的人来说。
最小化这个窗口,故意在代码中制造一个小问题。
现在,重新运行程序。
如果你是刚开始学习编程,遇到这些错误是完全正常的,可能是拼写错误或漏掉了分号等等。不要因此气馁。
修复错误后,我们可以继续进行下一步,学习更多内容。
在下一课中,我们将继续深入学习。
接下来,我想改变编辑器的颜色,并展示如何进行设置,因为很多人问我视频中使用的主题是什么。
打开SeaLion,点击首选项(Preferences)菜单。
在这里,你可以看到已经安装的主题。
在这个页面中,我将根据下载次数对主题进行排序,这样可以查看最受欢迎的主题。
选择好主题后,点击安装。
main.cpp
文件的第五行第八列发生的,错误信息是:“不能给 pi 赋值,因为它是常量。”_
来分隔它们。//
开头的文本是 注释。+
是加法运算符,而 X 和 Y 被称为操作数(operands)。std::cout
来输出结果,看看我们得到什么。X = X + 5;
++
)和自减运算符(--
)。X = X + 1;
X++;
--
),但是没有与乘法或除法运算符等效的自增自减运算符。++X
)和后置(X++
)。让我们看看它们的区别。++X
。X++
),首先将 X 的当前值赋给变量,然后再自增。++X
),则先将 X 自增,再将其值赋给变量。std::cout
输出std
命名空间并使用 std::cout
,这是一个表示标准输出流的对象。std::cout
,我们可以将一串字符写入标准输出,即控制台窗口。std::cout
来输出 X。std::cout
和分号,然后将所有内容连接在一起。std::endl
换行std::endl
,它表示行尾并会自动换行。std::cout
和分号。std::
命名空间std::
。using
指令来引用 std
命名空间。using
指令using namespace std;
,这样就不需要在每次使用 std::
时重复书写它。double
类型(即使没有小数值)。double
类型。double sales = 95000;
。double state_tax_rate = 0.04;
来表示州税率,并将其应用到计算中。county_tax_rate
来表示县税率。std::cout
表示标准输出流(我们之前已经讨论过)。在 I/O 流 中,我们还有另一个对象叫做 std::cin
,它代表标准输入流。std::cin
,我们可以从控制台读取数据。std::cin
读取输入std::cout
打印一个标签到屏幕上,提示用户输入一个值。std::cin
来读取这个值,并将其存储到一个变量中。std::cin
和流提取运算符(>>
)来读取这个值,并将其存储到 value 变量中。>>
运算符叫做 流提取运算符,它是 流插入运算符(<<
)的反向操作。std::cout
是将一串字符输出到控制台。double
类型std::cout
,我们也可以将多个输入语句链接在一起。std::cin
语句,通过链式调用 流提取运算符 来读取第二个值。std::cout
打印标签 "Enter temperature in Fahrenheit"。std::cin
读取用户输入的华氏温度并存入 Fahrenheit 变量。double
类型是因为计算可能会得到浮动的小数值。std::cout
打印出转换后的摄氏温度。#include
指令来再次引入一个名为 <cmath>
的文件。<cmath>
文件声明了一些有用的数学函数。<cmath>
库中声明的所有函数。ceil
,它会将一个值向上取整。floor
,它会将一个值向下取整。double
,并返回一个 double
类型的值。floor
函数main
函数中,使用 floor
函数时,我们写 floor()
,然后在括号中提供一个输入值。double
类型的变量 result
,并将 floor()
函数的返回值赋值给它。floor
函数的调用,它会接受一个值并返回一个新值。result
。pow
函数pow
函数就是其中之一。pow
函数要求两个参数,我们需要传递两个值,用逗号分隔。pow
pow()
时,我们可以看到 x
和 y
分别是此函数的参数。std::cout
打印提示标签 "Enter radius"。double
类型的变量 radius
来存储输入的半径值。std::cin
读取用户输入的半径值,并将其存储到 radius
变量中。area
来存储计算结果。pow
函数pow(radius, 2)
计算 radius 的平方,然后将其与 π 相乘,得到圆的面积。std::cout
输出计算得到的面积。//
,之后我们在这两个斜杠后面写的内容将会被视为注释。/*
表示,结束由 */
表示。现在你已经了解了 C++ 中的一些基本内建类型,接下来我们将看看几种不同的变量声明与初始化方式。
double
类型变量我们从声明一个名为 price
的 double
类型变量开始,并将其初始化为 99.99。到这里没有什么新内容。
float
类型变量那么,如何声明一个 float
类型的变量呢?我们可以声明一个名为 interest_rate
的 float
类型变量,并将其初始化为 3.6。这里有一点需要注意:在数值后面加上 f
字母,这是 float
类型的标志。这个非常重要,因为如果你不加上 f
,编译器会默认将这个数值当作 double
类型来处理,然后尝试将 double
存储到 float
变量中,这可能会导致数据丢失。所以,处理 float
类型的值时,始终在数值后加上 f
,无论是大写还是小写 f
都可以,不影响结果。
long
类型变量接下来,声明一个 long
类型的变量,我们可以声明一个名为 file_size
的变量,并将其初始化为 90,000。类似于 float
类型,我们应该在数值后加上 l
后缀,因为如果没有加这个后缀,编译器会默认将这个值当作整数来处理。因此,为了强制编译器将其视为 long
类型,我们需要加上 l
后缀,可以使用大写或小写字母 l
,但是因为小写 l
和数字 1 很容易混淆,最好的做法是使用大写的 L
。
char
类型变量现在,声明一个存储字符的变量,我们声明一个名为 letter
的 char
类型变量,并使用单引号表示字符,比如 'a'
。
boolean
类型变量最后,声明一个 boolean
类型的变量,我们将其命名为 is_valid
,并将其值设置为 true
或 false
。boolean
类型的变量只接受这两个值。
auto
关键字对于这些类型的变量,我们还可以使用 auto
关键字,让编译器自动推断变量的类型。例如,如果将 bool
改为 auto
,然后将鼠标悬停在 is_valid
上,编译器就能识别 is_valid
是 bool
类型。同样的,如果将 char
改为 auto
,然后查看变量的类型,编译器就会显示它是 char
类型。
对于 long
类型的变量,如果将 long
改为 auto
,编译器会根据你添加的 l
后缀,知道它是 long
类型;如果没有加 l
后缀,file_size
会被当作 int
类型来处理。同样,对于 float
类型,如果使用 auto
,编译器会知道 interest_rate
是 float
类型,而如果没有加 f
后缀,它就会被当作 double
类型处理。
使用 auto
的好处是可以让代码更简洁一致,你不必手动指定类型。虽然你可以不使用 auto
,但在处理更复杂类型时,auto
关键字特别有用,未来我们会讨论到这一点。
在现代 C++ 中,还有一种初始化变量的方式,称为“大括号初始化”。假设我们声明一个名为 number
的整数,并将其初始化为 12。这时我们会看到一个警告,代码仍然可以编译并执行。输出的结果是 1,因为小数部分被去掉了。
另一种初始化变量的方式是使用大括号而非赋值操作符。将数值放在大括号里。现在,编译器会报告一个错误,因为我们没有正确初始化变量。因此,大括号初始化能够帮助我们避免这种错误。并且,如果不提供值,变量会被默认初始化为零。因此,如果运行程序,number
的值就是 0。
但是,如果你移除大括号初始化并运行程序,你会看到一个随机值,这通常称为“垃圾值”。每次运行程序时,这个值都可能不同,导致程序行为不可预测。因此,我们应该确保要么使用赋值操作符为变量初始化一个正确的值,要么使用空的大括号初始化。
在数学和编程中,我们有不同的数字系统,它们有各自的用途。
在日常生活中,我们使用的是十进制系统(即基数为 10),其中的数字从 0 到 9。计算机并不理解这些数字,它们只理解 0 和 1,这就是为什么我们有了二进制系统(基数为 2)。所以,在二进制系统中,数字只能是 0 或 1。
我们可以将任何数字表示为二进制。例如,十进制中的 55 在二进制中是 110111
,这是一个很长的数字。因此,为了简化表示,我们使用十六进制系统(基数为 16)。
十六进制系统的数字可以包含从 0 到 9 的数字以及从 a 到 f 的字母。正如我们所见,十六进制数字比二进制数字更加简洁。通常在编程中,我们使用十六进制数字来表示颜色。
你可能听说过 RGB(红绿蓝)颜色模型,使用六位数字的十六进制数来表示颜色。在十六进制中,红色、绿色和蓝色的值分别可以从 00 到 ff。通过这种方式,我们可以表示任何颜色,这在编程中非常有用,因为我们无需处理非常大的十进制或二进制数字。
接下来,我们来看如何在 C++ 中表示这些数字。假设我要声明一个名为 code_number
的整数并将其初始化为 55。如果我们想用二进制表示这个数字,可以在数字前加上 0b
前缀,然后输入二进制数。例如:0b110111
。
当我们打印该数字时,输出将是 55,正如预期的那样。
如果要用十六进制表示同样的数字,则在数字前加上 0x
前缀,然后输入十六进制数。在这个例子中,55 的十六进制表示为 0x37
。同样,输出的结果会是 55。
在编程中,我们大多数时候使用的是十进制数字。我会说 99% 的时间我们都使用十进制数字,但根据应用程序的类型,在某些情况下,使用二进制或十六进制来表示数字可能更合适。
无论我们使用哪种表示方式,数字都可以是正数或负数。如果是正数,我们不需要显式写出正号,默认就是正数。然而,对于负数,我们必须显式地写出负号。
unsigned
在 C++ 中,有一个特殊的关键字 unsigned
。如果将 unsigned
应用于一个数字类型,该类型就不能接受负值。表面上看,这似乎是一个好功能,但它实际上可能会导致一些难以发现的编程问题。
例如,如果你在程序中声明一个 unsigned
类型的变量并打印它,可能会得到一个非常大的正数。另一个例子是,如果你将该数字初始化为零,并在程序的其他地方递减它,当你打印它时,可能不会得到预期的负数,而是得到一个非常大的正数。这是因为 unsigned
类型不能表示负数,超出范围的值会变成一个非常大的正数。
因此,我的建议是避免使用 unsigned
关键字。尽管 C++ 提供了这个特性,但并不意味着你应该使用它。正如在课程开始时我所说的,构建有用和实质性的程序时,你不需要学习 C++ 中的所有特性。
所以,尽量避免使用 unsigned
关键字。
在处理数字时,有一个概念是必须理解的,那就是 缩小转换(narrowing)。它发生在使用较大类型初始化较小类型的变量时。
举个例子,我们声明一个整数 number
,并将其设置为 1000000(百万)。为了让代码更具可读性,我们可以使用单引号分隔这些数字,这样看起来更加清晰。
接下来,我们声明一个 short
类型的变量 another
,并将 number
的值赋给它。这时我们会看到一个警告:“从 int
到 short
的缩小转换(narrowing conversion),short
类型的大小是由实现决定的”。这段警告信息可能有点复杂,但其实它是在告诉我们,由于我们将一个整数值赋给了一个 short
类型变量,这种转换被视为缩小转换。因为 short
类型只能存储较小的数字,它不能完全容纳 1000000 这样的较大数字。
由于进行缩小转换,我们的数字会被“缩小”。例如,假设我们将 1000000 赋给 short
类型的变量 another
,运行程序后,我们发现 another
的值变成了 16000,这就是缩小转换的结果。此时,原本存储 1000000 的 int
类型数字被截断,超出 short
类型范围的部分丢失了。
如果我们使用大括号初始化(brace initialization),则可以避免这种情况。在这种情况下,编译器会阻止代码编译并报告错误。大括号初始化的另一个好处是,它能有效地防止这种缩小转换带来的问题。
那么,如果我们进行反向操作呢?假设我们将一个 short
类型的数字赋给 int
类型的变量。此时,我们会看到一个警告,原因是这个 short
类型的数字太大,无法完全适配 short
类型。就像我之前提到的,short
类型可以存储的数字范围是从 -32,768 到 32,767。数字如果超出了这个范围,就无法存储在 short
类型变量中。
例如,如果我们将 short
类型的数字 100 赋给 int
类型的变量,它不会有问题,因为 int
类型能够存储比 short
更大的数字。
实际上,将一个较小的数字存储到较大的类型(如将 short
存储到 int
)时不会有数据丢失,因为 short
类型仅占用 2 字节,而 int
类型占用 4 字节。存储一个较小的数字在更大的内存空间中是完全安全的,额外的内存空间将被填充为零。因此,这种操作通常不会导致数据丢失。
int
)赋给较小范围的变量(如 short
),这种转换可能导致数据丢失。在 C++ 中,生成随机数是一个非常有用的功能,尤其在涉及到游戏中掷骰子、发牌或其他随机元素时非常常见。今天我们将学习如何在 C++ 中生成随机数。
C++ 提供了一个函数 rand()
来生成随机整数,它定义在标准库的 cstdlib
文件中。为了使用这个功能,我们需要在文件的顶部包含这个库。
#include <cstdlib>
然后,我们调用 rand()
函数来获取一个随机整数。你可以将生成的随机数存储在变量中,并将其打印到屏幕上。运行程序时,每次执行你都会得到一个不同的随机数。例如:
int randomValue = rand();
std::cout << randomValue << std::endl;
每次运行程序时,你会得到不同的随机数。注意,这些数其实并不是完全随机的,它们是基于某种数学公式生成的,所以每次运行时它们的生成模式是相同的。
为了让每次运行程序时产生不同的随机数,我们需要为随机数生成器提供一个不同的种子。C++ 提供了一个名为 srand()
的函数,用于设置随机数种子。
srand(5);
此时,每次运行程序时,我们都将得到相同的随机数,因为我们给 srand()
提供了一个固定的种子值 5。如果你希望每次运行时得到不同的随机数,可以使用当前时间作为种子。时间是不断变化的,因此每次运行程序时,时间都会不同,从而生成不同的随机数。
首先,你需要在代码的顶部引入时间相关的库:
#include <ctime>
然后,你可以使用 time(NULL)
函数获取当前的时间,它返回从 1970 年 1 月 1 日以来经过的秒数。我们可以将这个值作为种子来初始化随机数生成器。
srand(time(NULL));
这样,每次运行程序时,随机数生成器就会使用不同的种子,从而生成不同的随机数。
生成的随机数通常很大,如果你想指定一个上限,比如将随机数限制在 0 到 9 之间,可以使用取余运算符 %
。通过指定一个上限,你可以确保结果总是落在你设定的范围内。
int randomValue = rand() % 10; // 0 到 9 之间的随机数
每次运行时,你会得到一个介于 0 和 9 之间的随机数。如果你需要一个不同的范围,只需修改模数,例如:
int randomValue = rand() % 6 + 1; // 1 到 6 之间的随机数
这样,每次运行时,程序都会输出 1 到 6 之间的随机数。
在 C++11 中,标准库提供了一种更强大且更复杂的方式来生成随机数。虽然这种方法提供了更好的随机性,但对于初学者来说,还是先掌握传统的 rand()
方法较为合适。在未来,你会学到如何使用 C++11 的 random
库。
你可以将生成随机数的代码进行简化。比如,time(NULL)
直接作为 srand()
的参数,而不需要单独定义变量:
srand(time(NULL));
这种方法使得代码更加简洁,避免了定义不必要的变量。
现在,我们来做一个练习:编写一个程序来模拟掷骰子。每次运行程序时,我们应该得到两个介于 1 到 6 之间的随机数,模拟两个骰子的点数。
首先,我们需要包含必要的头文件:
#include <iostream>
#include <cstdlib>
#include <ctime>
然后,我们需要为随机数生成器设置种子,通常使用当前时间作为种子:
srand(time(NULL));
接下来,我们用公式生成两个 1 到 6 之间的随机数:
int die1 = rand() % 6 + 1;
int die2 = rand() % 6 + 1;
最后,我们将掷骰子的结果打印到屏幕上:
std::cout << "Die 1: " << die1 << ", Die 2: " << die2 << std::endl;
运行程序后,你会得到两个随机的骰子点数。例如:
Die 1: 3, Die 2: 5
为避免“魔法数字”带来的困扰,代码中的 1 和 6 可以使用常量来代替,使代码更加可读且易于维护。我们可以定义两个常量:
const int MIN_VALUE = 1;
const int MAX_VALUE = 6;
然后,修改随机数生成的代码:
int die1 = rand() % MAX_VALUE + MIN_VALUE;
int die2 = rand() % MAX_VALUE + MIN_VALUE;
这样,当你查看代码时,就能清楚地知道 1
和 6
分别代表了骰子的最小和最大值。
#include <iostream>
#include <cstdlib>
#include <ctime>
int main() {
// 设置随机数种子
srand(time(NULL));
// 定义常量
const int MIN_VALUE = 1;
const int MAX_VALUE = 6;
// 生成两个 1 到 6 之间的随机数
int die1 = rand() % MAX_VALUE + MIN_VALUE;
int die2 = rand() % MAX_VALUE + MIN_VALUE;
// 输出结果
std::cout << "Die 1: " << die1 << ", Die 2: " << die2 << std::endl;
return 0;
}
每次运行这个程序时,你都会看到两个不同的随机骰子结果。例如:
Die 1: 4, Die 2: 2
通过上述方法,我们学会了如何在 C++ 中生成随机数、限制其范围,并模拟了一个掷骰子的程序。掌握这些基本的随机数操作对于编写游戏和其他随机应用程序非常重要。
我们已经学习了如何使用 cout
打印数据,现在让我们来看看如何控制日期的格式。首先,我们使用 cout
打印一些标签,例如“spraying”后面跟着“nice”。然后,添加一个 endl
换行符,在第二行打印“summer”后面跟着“hot”。如果你运行这个程序,你会发现这些标签紧挨在一起。
但是,如果你想将这些标签整理成列该怎么办呢?在之前的学习中,我们使用过一种叫做 流操控符(stream manipulators)的方法。流操控符是我们用来操作或修改流(例如 cout
)的一些函数。
io manipulator
库首先,在文件顶部包含另一个标准库文件:io manip
,它是 io manipulator
的简写。这个文件中包含了许多用于操控流的函数。例如,setw
是其中的一个函数,它是 set width
(设置宽度)的缩写。我们可以调用这个函数,指定要保留的字符宽度,用于格式化后续打印的数据。
setw
来设置列宽例如,我们想为第一个标签“spraying”预留 10 个字符空间。我们通过调用 setw
并传递 10 来实现。当调用 setw
后,输出将会预留 10 个字符用于打印数据。我们来运行一下程序,看看效果。
cout << setw(10) << "spraying";
输出中,“spraying”将会被右对齐,并占据 10 个字符的宽度。
接着,我们为第二个标签“nice”也预留 10 个字符宽度,方法是再次调用 setw(10)
。运行后,我们会看到第一行输出变成了两个宽度为 10 的列,分别是“spraying”和“nice”。
如果我们希望这两个标签左对齐,而不是默认的右对齐,可以使用另一个流操控符:left
。只要我们在需要的地方使用它,后续的输出都会左对齐。
cout << left << setw(10) << "spraying";
cout << left << setw(10) << "nice";
此时,“spraying”和“nice”将会左对齐显示。
这里有一个重要的概念:流操控符是粘性的(sticky)。这意味着一旦我们应用了某个操控符,它将持续生效,直到我们更改它。例如,left
一旦被应用,所有后续输出都会左对齐,直到我们调用 right
或 setw
来更改它。
我们可以进一步整理代码,使其格式更美观。通过分开设置每一列的对齐方式,我们可以控制打印格式。例如:
cout << left << setw(10) << "summer";
cout << left << setw(10) << "hot";
这样,我们就可以打印出一个整齐的表格,其中的标签左对齐。
接下来,我们来看看如何格式化浮点数。我们先删除之前的代码,并打印一个浮点数,如 1.234567
。你会发现,输出的数字只显示四位小数。
cout << 1.234567;
在你的计算机上,依赖于编译器的不同,你可能会看到该数字以科学计数法显示。如果我们想强制数字以固定点格式输出,可以使用流操控符 fixed
。使用 fixed
后,数字将会按常规的浮动小数格式显示。
cout << fixed << 1.234567;
这时,输出的数字将显示为 6 位小数(默认情况下)。
如果你希望自己控制小数点后显示的位数,可以使用 setprecision
流操控符。假设你希望只显示两位小数,可以这样写:
cout << fixed << setprecision(2) << 1.234567;
这样,输出就会显示为 1.23
。
如果你希望显示更多的小数位数,可以传入更大的数字。例如,使用 setprecision(10)
可以让数字显示更多的小数位。
同样,fixed
和 setprecision
也是粘性的。这意味着一旦应用了这些操控符,后续的浮动数字将会使用这些设置,直到你更改它们。例如:
cout << fixed << setprecision(2) << 1.234567;
cout << fixed << setprecision(2) << 2.345678;
这两个数字将都会按照 setprecision(2)
的设置,显示为两位小数。
现在,我给你一个练习,要求你编写一个程序,打印一个包含两行两列的表格。注意,文本标签应该是左对齐的,而数字应该是右对齐的。请暂停视频,完成这个练习,然后回来看我的解答。
首先,我们使用 cout
打印表头。这里我们有“课程”和“学生”这两列。我们为第一列分配 15 个字符宽度,为第二列分配 10 个字符宽度。接着,使用 endl
进入下一行。
cout << setw(15) << left << "Course" << setw(10) << left << "Students" << endl;
这时,输出的表头将是左对齐的。接下来,我们打印数据。例如:
cout << setw(15) << left << "C++" << setw(10) << right << 100 << endl;
这里,我们给“C++”分配 15 个字符宽度,并给学生人数 100 分配 10 个字符宽度。通过 right
操控符,学生人数将会右对齐。
继续为下一行“JavaScript”添加数据:
cout << setw(15) << left << "JavaScript" << setw(10) << right << 50 << endl;
此时,我们的输出表格已经完成,文本左对齐,数字右对齐。
之前我们提到过,数据类型的大小与系统有关。通常情况下,可以假设 short
类型是 2 个字节,而 int
类型是 4 个字节。现在,我们来做一些有趣的实验,探究一下在我们机器上每个数据类型的大小。我将展示一个例子,并希望你自己完成其余部分。
sizeof
调查数据类型大小在代码中,我们有一个叫做 sizeof
的函数。通过调用该函数,并给它传递一个数据类型(例如 int
),它将返回在当前机器上存储该数据类型所需的字节数。我们将结果打印到控制台,看看会得到什么。
cout << sizeof(int) << endl;
假设你在机器上运行这段代码,输出的值应该是 4,这表示在这台机器上,一个 int
占用 4 个字节。
numeric_limits
接下来,我们要探究一下在 4 字节内存中,我们可以存储的 int
类型的数值范围。我们将使用一个名为 numeric_limits
的类来帮助我们进行此项工作。稍后我们会详细讨论类的内容,目前你可以将类理解为我们程序的构建块。
numeric_limits
是一个模板类,它可以告诉我们特定数据类型的极限。为了获取 int
类型的最大值,我们可以使用 numeric_limits<int>::max()
。
#include <iostream>
#include <limits> // 引入 numeric_limits 头文件
int main() {
std::cout << "最大值: " << std::numeric_limits<int>::max() << std::endl;
}
输出结果应该是:2147483647
,即 int
类型可以存储的最大值。
接下来,我们来测试一下如果我们存储一个超出 int
类型范围的数字会发生什么。我们可以创建一个 int
类型的变量,设置其值为 int
类型能存储的最大值,然后将其加 1,再打印到终端。
int number = std::numeric_limits<int>::max();
number++;
std::cout << number << std::endl;
你会发现,输出的值变成了 -2147483648
,这是因为 int
类型的最大值加 1 会导致溢出,即它会回绕到 int
类型能够存储的最小值。这就像是我们有一个水桶,水桶装满了水,如果你继续往里倒水,水就会溢出来。
int number = std::numeric_limits<int>::max();
number = number + 1; // 溢出
std::cout << "溢出的结果: " << number << std::endl;
输出:-2147483648
(这是 int
的最小值),因为超出了范围。
除了溢出,我们还有一个与之相对的概念,叫做 下溢。如果我们将 number
设置为 int
类型能够存储的最小值,然后将其减 1,那么 number
就会发生下溢,回绕到 int
类型能够存储的最大值。
int number = std::numeric_limits<int>::min();
number--; // 下溢
std::cout << "下溢的结果: " << number << std::endl;
输出:2147483647
(这是 int
的最大值),因为值太小,导致回绕。
作为练习,我希望你能够利用本节课学到的内容,查找你机器上其他数据类型的大小和极限。你可以使用类似 sizeof
和 numeric_limits
的方法来获取 short
、long
、float
等数据类型的信息。
例如:
std::cout << "short 的大小: " << sizeof(short) << std::endl;
std::cout << "long 的大小: " << sizeof(long) << std::endl;
std::cout << "float 的最大值: " << std::numeric_limits<float>::max() << std::endl;
通过这些方法,你可以探索出更多关于不同数据类型在你机器上的表现,理解它们的大小和溢出行为。
在 C++ 以及几乎所有其他编程语言中,我们都有一种称为 布尔类型 的数据类型,用于表示 true
和 false
值,这对于编写条件判断非常有用。比如,我们可以声明一个布尔变量 is_new_user
,并将它设置为 true
或 false
。这些是 C++ 中的关键字。
bool is_new_user = true;
在代码中,我们打印 is_new_user
变量的值并运行程序,可能会看到如下的输出结果:
cout << is_new_user << endl;
虽然在代码中我们使用 true
和 false
,但在计算机内部,布尔值通常是用 1 和 0 来表示的。也就是说:
true
对应的值是 1false
对应的值是 0因此,上面的代码实际上输出的就是数字 1
或 0
,具体取决于 is_new_user
的值。
// 输出 1,因为布尔值 true 被表示为 1
cout << is_new_user << endl;
如果我们用 0
或 1
来代替 true
或 false
,程序仍然可以正常工作:
bool is_new_user = 1; // 或者 bool is_new_user = 0;
cout << is_new_user << endl;
然而,这种方式虽然可行,但并不是最佳实践。因为它可能导致代码的可读性差,尤其是当变量名或程序逻辑变得复杂时。因此,虽然使用 1
或 0
不会报错,但它并不推荐作为布尔值的表示方式。
为了更清晰地输出布尔值的 true
或 false
,C++ 提供了一个流操作符(stream manipulator),叫做 boolalpha
。它的作用是在打印布尔值时,将 true
和 false
显示为文字而不是数字。
在打印布尔变量之前,我们可以使用 boolalpha
来启用这个显示方式:
cout << std::boolalpha << is_new_user << endl; // 输出 true
这样,当我们打印布尔值时,它将显示为 true
或 false
,而不是 1
或 0
。
boolalpha
是一个粘性操作符boolalpha
是一个粘性操作符(sticky manipulator),意味着一旦我们应用它,之后打印的所有布尔值都会以 true
或 false
形式显示,直到我们显式关闭它为止。为了关闭 boolalpha
,我们可以使用 noboolalpha
操作符。
cout << std::noboolalpha << is_new_user << endl; // 输出 0 或 1
在使用 noboolalpha
后,布尔值将重新以数字形式(0 或 1)输出。
#include <iostream>
using namespace std;
int main() {
bool is_new_user = false;
// 默认输出数字 0
cout << is_new_user << endl;
// 使用 boolalpha 打印 true 或 false
cout << boolalpha << is_new_user << endl;
// 关闭 boolalpha,恢复为数字形式输出
cout << noboolalpha << is_new_user << endl;
return 0;
}
输出结果:
0
false
0
true
和 false
表示,但在内部,它们通常表示为 1 和 0。boolalpha
流操作符来将 true
和 false
显示为文字。boolalpha
是粘性操作符,意味着一旦启用,它会影响之后所有布尔值的输出,直到我们使用 noboolalpha
来关闭它。在后面的决策部分(Decision Making)中,我们将更深入地使用布尔类型进行条件判断,帮助你更好地理解其实际应用。
char
)和字符串类型(string
)char
类型的使用在 C++ 中,char
类型用于存储单个字符。我们可以声明一个 char
变量并将其初始化为一个字符。例如:
char ch = 'a';
字符在计算机内部是通过数字表示的,因为计算机只理解数字,特别是二进制数字。因此,每个字符都有一个对应的数字表示,这种表示通常遵循 ASCII(美国标准信息交换码)编码标准。
例如,字母 'a'
的 ASCII 值是 97,字母 'b'
的 ASCII 值是 98。
cout << ch << endl; // 输出 'a'
cout << int(ch) << endl; // 输出 97
上面的代码中,使用 int(ch)
可以输出字符 'a'
对应的数字 97。如果将字符变量 ch
改为 'b'
,则其对应的数字是 98。
ch = 'b';
cout << ch << endl; // 输出 'b'
cout << int(ch) << endl; // 输出 98
尽管直接使用数字 98 来表示字符 'b'
是可行的,但这种写法并不推荐,因为这样写会让代码难以理解。为了提高代码的可读性,应该使用字符字面量而不是它们的数字表示。
string
类型的使用C++ 中的 string
类型用于存储字符的序列(即字符串)。我们可以通过双引号 ""
来定义字符串。例如:
string name = "Marsh Hammad";
cout << name << endl; // 输出 "Marsh Hammad"
string
类型允许存储一系列字符,可以用来表示名字、地址等信息。
我们可以通过 cin
来从用户输入读取字符串。例如:
string name;
cout << "Enter your name: ";
cin >> name;
cout << "Hi " << name << endl;
但是,如果用户输入的字符串中包含空格,例如 "Marsh Hammad"
, 那么 cin
只会读取第一个单词 "Marsh"
,后面的部分会被忽略。
getline
读取包含空格的字符串为了读取包含空格的字符串,我们需要使用 getline
函数,它可以读取一整行文本,包括空格。为了使用 getline
,我们需要在代码中包含 <iostream>
以及 std
命名空间。
#include <iostream>
#include <string>
using namespace std;
int main() {
string name;
cout << "Enter your name: ";
getline(cin, name); // 使用 getline 读取整个字符串,包括空格
cout << "Hi " << name << endl;
return 0;
}
通过这种方式,我们可以正确地读取包含空格的字符串。例如,当用户输入 "Marsh Hammad"
, 程序将输出:
Hi Marsh Hammad
接下来是一个练习,要求我们编写一个程序,读取用户的地址信息,并按特定格式输出。
我们需要声明几个 string
类型的变量,分别用于存储街道(street
)、城市(city
)、州(state
)和邮政编码(zip
)。然后,我们使用 getline
读取这些信息,并将它们按照特定格式输出。
解决方法:
#include <iostream>
#include <string>
using namespace std;
int main() {
string street, city, state, zip;
// 读取街道信息
cout << "Enter street: ";
getline(cin, street); // 使用 getline 读取街道地址
cout << endl;
// 读取城市信息
cout << "Enter city: ";
getline(cin, city); // 读取城市名称
cout << endl;
// 读取州信息
cout << "Enter state: ";
getline(cin, state); // 读取州名称
cout << endl;
// 读取邮政编码
cout << "Enter zip code: ";
getline(cin, zip); // 读取邮政编码
cout << endl;
// 输出格式化的地址
cout << "Address: " << endl;
cout << street << endl;
cout << city << ", " << state << " " << zip << endl;
return 0;
}
输入示例:
Enter street: 123 Main St
Enter city: Los Angeles
Enter state: California
Enter zip code: 90001
输出结果:
Address:
123 Main St
Los Angeles, California 90001
这个程序首先要求用户输入街道、城市、州和邮政编码,然后按指定格式输出地址。每次读取一个数据后,通过 getline
函数确保能够正确地读取包含空格的字符串。
char
类型用于存储单个字符,每个字符在计算机中都有一个数字表示,通常使用 ASCII 码。string
类型用于存储字符的序列,可以表示名字、地址等。cin
来读取单个单词的输入,而使用 getline
读取包含空格的整行字符串。在后续课程中,我们将进一步探索字符串的高级操作和字符处理技术。
在 C++ 中,数组是用来存储多个元素的集合。数组允许我们存储一组数据,这些数据可以是相同类型的数字、日期、字符串等。
例如,如果我们要存储一个整数列表,我们可以声明一个 int
类型的变量。如果我们要存储一个整数数组(即多个整数),则需要在变量名后面加上方括号 []
,并在方括号内指定要存储的元素数量。
例如:
int numbers[5];
这行代码表示我们声明了一个 numbers
数组,并指定它可以存储 5 个整数。数组中的元素会被连续地存储在内存中,我们可以通过索引来访问数组中的任何元素。
在声明数组时,我们可以直接为数组提供初始值,例如:
int numbers[5] = {10, 20, 30, 40, 50};
这行代码初始化了一个包含 5 个整数的数组。每个数组元素对应一个初始值,依次为 10、20、30、40 和 50。
如果我们不指定初始值,数组中的元素会默认初始化为零。
int numbers[5]; // 默认初始化为 0
数组中的每个元素都有一个索引,索引从 0 开始。例如,numbers[0]
是数组中的第一个元素,numbers[4]
是数组中的最后一个元素。
我们可以通过索引访问数组中的任意元素:
numbers[0] = 10; // 将第一个元素赋值为 10
numbers[1] = 20; // 将第二个元素赋值为 20
数组在内存中是连续存储的。如果我们打印出数组的地址(例如使用 %p
格式化输出),我们可以看到数组的内存地址。这个地址通常是一个十六进制数,类似于 0x7fffd2eae670
,表示数组在内存中的位置。
cout << &numbers << endl; // 打印数组的内存地址
C++ 允许我们访问数组中未定义的索引(即越界访问),但这样做会导致程序行为不可预测。比如:
int numbers[5];
cout << numbers[5] << endl; // 越界访问,未定义的行为
对于一些现代语言(如 C# 或 Java),越界访问会抛出错误或异常,但 C++ 并不会阻止这种访问。因此,程序员需要非常小心,避免越界访问。
当我们在声明数组时,直接给出初始化值,C++ 编译器会自动推导出数组的大小:
int numbers[] = {10, 20, 30, 40, 50}; // 编译器自动推导出数组大小为 5
在这种情况下,我们不需要明确指定数组的大小。编译器会根据初始值的个数来确定数组的大小。
当我们访问一个数组中不存在的元素时,会得到一个警告。例如,如果我们声明了一个只包含 2 个元素的数组,但尝试访问第三个元素:
int numbers[2] = {10, 20};
cout << numbers[2] << endl; // 数组越界访问
这将导致程序输出一个垃圾值(通常是随机的数字),因为我们访问了数组的非法位置。现代语言通常会避免这种情况,防止程序继续运行。
我们来看一个简单的练习,要求我们存储三个名字,并打印出第一个名字。
解决方法:
#include <iostream>
#include <string>
using namespace std;
int main() {
string names[3]; // 声明一个字符串数组来存储三个人的名字
// 输入三个名字
cout << "Enter the first name: ";
cin >> names[0];
cout << "Enter the second name: ";
cin >> names[1];
cout << "Enter the third name: ";
cin >> names[2];
// 输出第一个名字
cout << "The first name is: " << names[0] << endl;
return 0;
}
输入示例:
Enter the first name: John
Enter the second name: Bob
Enter the third name: Mary
输出结果:
The first name is: John
这个程序的关键点在于:
names
的数组,用于存储三个名字。cin
逐个读取名字并将其存储在数组的相应位置。在编写更多代码时,我们可能会遇到需要将一种数据类型转换为另一种数据类型的情况,这就是所谓的 类型转换(Casting)。类型转换有时是隐式的,编译器会自动进行转换,但有时我们也需要显式地告诉编译器如何进行转换,特别是在可能丢失数据的情况下。
让我们来看一个简单的例子。假设我们声明了一个 int
类型的变量 x
,并将其赋值为 1,然后声明一个 double
类型的变量 y
,并将其赋值为 2.2。
接着,我们声明另一个 double
类型的变量 z
,并将其赋值为 x + y
,其中 x
是整数,y
是浮点数。
int x = 1;
double y = 2.2;
double z = x + y;
当我们打印 z
的值时,输出将是 3.2
。这正是我们所期望的结果。
背后的机制:
x
是 int
类型,y
是 double
类型,C++ 编译器会自动将 x
从 int
类型转换为 double
类型。这种转换被称为 隐式类型转换。x
被提升为 double
类型,随后执行加法运算。由于 double
类型的精度较高,并且占用的内存空间比 int
类型大,因此从 int
转换为 double
不会导致数据丢失。现在,假设我们将 z
的类型从 double
改为 int
。这时,程序会发出警告,因为将一个 double
类型的值赋给 int
类型的变量时,可能会导致数据丢失。让我们来看一个例子:
int x = 1;
double y = 2.2;
int z = x + y; // 可能会丢失数据
背后的机制:
x
是 int
类型,y
是 double
类型,C++ 编译器首先将 x
转换为 double
,然后执行加法运算,得到一个 double
类型的结果。double
类型的结果赋给一个 int
类型的变量 z
。这时,由于 double
类型的结果可能包含小数部分(如 3.2),而 int
类型只能存储整数,因此小数部分会丢失,最终结果是 3(小数部分被截断)。当我们运行这个程序时,输出会是 3
,但是这可能不是我们想要的结果。为了防止数据丢失,我们可以显式地告诉编译器,我们知道会发生数据丢失,并强制执行类型转换。
强制类型转换是指我们明确告诉编译器如何转换数据类型。有两种主要的方式来进行强制类型转换:
C 风格的强制类型转换: 这种方式使用一个括号,括号内指定目标类型,例如:
int z = (int)(x + y); // C 风格的强制类型转换
这种方式的问题是,如果转换无法进行,程序会在运行时发生错误。因此,我们必须在运行时测试程序,以确保转换是有效的。
C++ 风格的强制类型转换:
C++ 提供了更安全的方式,即使用 static_cast
运算符。它能够在编译时捕获错误,这样我们就不必在运行时测试程序。
int z = static_cast<int>(x + y); // C++ 风格的强制类型转换
使用 static_cast
比 C 风格的转换更安全,因为如果转换无法进行,编译器会告诉我们错误。
假设我们有两个整数 x
和 y
,我们要将 x
除以 y
,并将结果存储在一个 double
类型的变量 z
中。让我们看一下代码:
int x = 5;
int y = 2;
double z = x / y; // 结果是 2,因为整数相除会丢失小数部分
如果我们打印 z
,输出将是 2
,而不是我们预期的 2.5
,因为 x
和 y
都是整数类型,整数相除会舍弃小数部分。
如何修改代码,使得结果包含小数部分?
我们可以强制转换 x
或 y
中的一个值为 double
类型,这样就能确保除法运算返回一个 double
类型的结果。修改后的代码如下:
int x = 5;
int y = 2;
double z = static_cast<double>(x) / y; // 强制转换 x 为 double
或者:
double z = x / static_cast<double>(y); // 强制转换 y 为 double
这样,输出将是 2.5
,因为我们确保至少有一个操作数是 double
类型,从而执行浮点数除法。
static_cast
。通过掌握类型转换,我们可以更灵活地处理数据类型,并避免潜在的错误和数据丢失。
欢迎回到《终极 C++ 课程》的另一章节。在这一部分中,你将学习如何编写能够做出决策的程序。我们将讨论比较运算符和逻辑运算符,用于编写条件判断和业务规则。接着,我们会讲解如何使用 if
和 switch
语句来控制程序的流程。你还将学会如何使用条件运算符来简化 if
语句。通过这一部分的学习,你将能够编写更有用的 C++ 程序。
让我们开始吧!
在这一部分,我们将讨论 比较运算符,这些运算符用于比较值。首先,假设我们声明了一个变量 X
并设置它的值。在此例中,我们知道 X
的值,但 X
也可以持有一个在程序运行时动态读取的值,这种情况下我们无法预知它的内容。
假设你想要检查 X
是否大于 5。那么我们可以使用比较运算符 >
,这就是所谓的“大于”运算符,它是许多比较运算符之一。其他常见的比较运算符包括:
>=
(大于或等于)<
(小于)<=
(小于或等于)==
(等于)!=
(不等于)当你写出类似 X > 5
的代码时,这段代码被称为 布尔表达式。在编程中,表达式 是指能够计算出一个值的代码片段;而 布尔表达式 是计算出一个布尔值的表达式。我们可以将该布尔表达式的结果存储在一个布尔变量 result
中。
接下来,我们可以将 X
与另一个变量 Y
进行比较。例如,声明 Y
并将其赋值为 5,然后检查 X
是否不等于 Y
。在这种情况下,我们将使用 !=
运算符:
int X = 10;
int Y = 5;
bool result = (X != Y);
在这里,我们会看到一个警告,提示“条件总为真”,这是因为编译器知道 X
的值为 10,Y
的值为 5,且 10 不等于 5,因此不会出错。但如果这些变量的值是通过程序动态获取的,我们就不会收到这个警告。
现在,如果你在程序中想要查看布尔值,而不是看到 1
(表示 true
)或 0
(表示 false
),你可以使用一个流操控符 boolalpha
。例如:
std::cout << std::boolalpha << result;
这样,终端中会显示 true
而不是 1
。使用 boolalpha
后,布尔值会以 true
或 false
的形式显示,增加代码的可读性。
==
有一个很常见的错误是,初学者容易将 ==
和 =
混淆。==
是检查相等性的运算符,而 =
是赋值运算符。例如,如果你写 X = Y
,那么实际上是将 Y
的值赋给了 X
,而不是比较它们的值。对于布尔表达式,使用单个等号会导致 X
的值被改变,这是非常容易引发错误的地方。
通常我们会比较相同类型的值。例如,两个整数之间的比较。然而,C++ 允许将不同类型的值进行比较,编译器会自动将较小或不精确的类型转换为更大的、更精确的类型。让我们看看一个例子,比较一个整数和一个浮动值:
int X = 10;
double Y = 5.0;
bool result = (X == Y);
运行程序时,我们会得到 false
,这是因为 10
并不等于 5.0
,但这里有一个有趣的地方,编译器会自动将 X
从 int
转换为 double
,以便执行比较。因此,当比较不同类型的值时,编译器会自动进行类型转换。
你也可以使用比较运算符来比较字符。举个例子,我们可以声明两个字符变量并比较它们:
char first = 'a';
char second = 'A';
bool result = (first == second);
此时,程序会输出 0
,表示 false
。因为在 C++ 中,字符比较是区分大小写的——小写字母 'a'
和大写字母 'A'
是不同的字符。所以,字符比较是大小写敏感的。
通过这些运算符,你可以在 C++ 中进行各种类型的数据比较,用于编写有条件的决策逻辑。
接下来我们将讨论 逻辑运算符。逻辑运算符用于组合两个或多个布尔表达式或条件。我们常见的逻辑运算符有 &&
(与运算符)、||
(或运算符)和 !
(非运算符)。
&&
)假设我们声明一个变量 age
并设置它为 20。接着,我们声明一个布尔变量 isEligible
并将其设置为 age > 18
。然后,我们将其输出到终端:
int age = 20;
bool isEligible = (age > 18);
std::cout << std::boolalpha << isEligible;
运行时输出为 true
,因为 age
大于 18。
但是,如果我们想要同时检查年龄是否小于 65,我们可以使用 与运算符 (&&
) 来添加第二个条件。具体来说,我们可以设置:
isEligible = (age > 18) && (age < 65);
与运算符要求两个条件都为真时,结果才会为真。如果其中任意一个条件为假,那么整个表达式的结果就是假。举个例子,如果我们把 age
设置为 10,那么第一个条件 age > 18
将为假,因此无论第二个条件是什么,整个表达式都会为假:
age = 10;
isEligible = (age > 18) && (age < 65);
std::cout << std::boolalpha << isEligible;
这时,输出为 false
,因为第一个条件已经是假的。
为了提高效率,C++ 编译器会从左到右开始评估表达式。如果第一个条件已经是假的,编译器就不再评估第二个条件,这称为 短路求值。
||
)与运算符的对立面是 或运算符 (||
)。如果任何一个条件为真,整个表达式的结果就会为真。例如,如果我们将 age
设置为 10,第二个条件永远为真(假设我们检查 age < 100
),那么整个表达式的结果会为真:
age = 10;
isEligible = (age > 18) || (age < 100);
std::cout << std::boolalpha << isEligible;
在这种情况下,第二个条件是 true
,所以无论第一个条件是否为真,最终结果都会是 true
。然而,编译器也会应用短路求值,首先评估第一个条件。如果第一个条件为真,它将跳过对第二个条件的评估。
!
)非运算符 (!
) 用于反转布尔值。如果布尔变量 isEligible
为真,那么应用非运算符后,它将变为假。例如:
isEligible = true;
isEligible = !isEligible; // 结果为 false
std::cout << std::boolalpha << isEligible;
运行后,输出为 false
,因为 !true
结果为 false
。
逻辑运算符不仅限于两条条件,你可以组合多个条件。以下是一个例子,假设我们想要检查一个人是否符合两个条件:年龄在 18 到 65 岁之间,且年薪大于 30,000。我们可以这样编写代码:
int age = 20;
double salary = 50000;
bool isEligible = (age > 18) && (age < 65) && (salary > 30000);
std::cout << std::boolalpha << isEligible;
在这个例子中,isEligible
将为 true
,因为年龄和薪水条件都满足。为了代码更清晰,可以使用括号将每个条件包围起来,这样我们可以清楚地看到哪些条件与年龄相关,哪些与薪水相关:
isEligible = ((age > 18) && (age < 65)) && (salary > 30000);
这种做法有助于提高代码的可读性和可维护性。
有时你可能需要将逻辑运算符混合使用。例如,如果你想检查一个人是否 符合年龄条件或薪水条件,你可以使用 或运算符 (||
):
isEligible = ((age > 18) && (age < 65)) || (salary > 30000);
这表示只要年龄在 18 到 65 岁之间,或者年薪超过 30,000,条件就成立。虽然这种逻辑在现实中可能不太符合常理,但它演示了如何使用不同的运算符来组合多个条件。
逻辑运算符(&&
, ||
, !
)使得我们能够组合多个布尔条件进行更复杂的决策和判断。它们广泛应用于需要多重条件评估的场景。使用逻辑运算符时,始终注意运算符的优先级,并尽可能地使用括号来明确各个条件的优先级,从而提高代码的可读性。
在之前的课程中,我们讨论了数学表达式时,乘法和除法运算符的优先级高于加法和减法运算符。逻辑运算符也有类似的优先级顺序,记住这一点非常重要。逻辑运算符的优先级如下:
!
)&&
)||
)就像数学运算一样,我们可以通过括号来改变运算顺序。
让我们声明三个布尔变量 a
, b
和 c
,并使用抽象的名字(如 a
, b
, c
)来避免复杂的实际场景干扰。然后声明一个布尔变量 result
,并将其设置为 b && !a
,看看 C++ 是如何评估这个表达式的:
bool a = true;
bool b = true;
bool c = false;
bool result = b && !a;
std::cout << std::boolalpha << result;
在这个例子中,非运算符 (!
) 的优先级高于 与运算符 (&&
)。首先,C++ 会先评估 !a
,因为 a
为 true
,所以 !a
会变为 false
。然后,b && false
结果为 false
,最终 result
的值是 false
。
接下来,我们更改表达式,使用 a || b && c
,并看看 C++ 是如何评估这个表达式的:
result = a || b && c;
std::cout << std::boolalpha << result;
请停下来思考一下这个表达式的执行顺序。
答案是:首先会评估 b && c
,因为 与运算符 (&&
) 的优先级高于 或运算符 (||
)。由于 b
为 true
,c
为 false
,所以 b && c
的结果为 false
。接着评估 a || false
,由于 a
为 true
,最终结果为 true
。
为了改变运算顺序,我们可以使用括号。例如,若要确保 a || b
先被评估,可以加上括号:
result = (a || b) && c;
std::cout << std::boolalpha << result;
现在,a || b
先被评估,然后再与 c
进行 &&
运算。如果 a
或 b
为 true
,就会进入 &&
操作。
假设我们需要判断一个申请人是否符合工作要求:该申请人必须是美国公民,并且必须拥有 学士学位 或者 至少两年工作经验。
我们可以使用以下变量来编写逻辑表达式:
isCitizen
:判断是否为美国公民。hasBachelorDegree
:判断是否拥有学士学位。yearsOfExperience
:申请人的工作经验年限。假设我们先给出以下变量值:
bool isCitizen = true; // 申请人是美国公民
bool hasBachelorDegree = false; // 申请人没有学士学位
int yearsOfExperience = 3; // 申请人有3年工作经验
我们想判断是否符合条件。逻辑表达式应该是:
bool isEligible = isCitizen && (hasBachelorDegree || yearsOfExperience >= 2);
std::cout << std::boolalpha << isEligible;
此时输出应该为 true
,因为虽然没有学士学位,但有足够的工作经验(3年)。
接下来,我们修改 yearsOfExperience
变量,测试不同的输入值来确保程序的正确性。
true
,因为工作经验已满足条件。false
,因为条件要求至少 2 年的工作经验。true
。false
。yearsOfExperience = 1;
isEligible = isCitizen && (hasBachelorDegree || yearsOfExperience >= 2);
std::cout << std::boolalpha << isEligible; // 输出 false,因为工作经验不足
hasBachelorDegree = true; // 设为学士学位
isEligible = isCitizen && (hasBachelorDegree || yearsOfExperience >= 2);
std::cout << std::boolalpha << isEligible; // 输出 true,因为有学士学位
isCitizen = false; // 设为非美国公民
isEligible = isCitizen && (hasBachelorDegree || yearsOfExperience >= 2);
std::cout << std::boolalpha << isEligible; // 输出 false,因为不是美国公民
if
语句用于控制程序中的逻辑流。通过使用 if
语句,我们可以根据不同的条件执行不同的代码块。接下来,我们通过一些示例来解释如何使用 if
语句。
假设我们有一个变量 temperature
,并将其设置为 70。根据 temperature
的值,我们希望打印不同的消息。例如,如果温度低于 60,我们想要打印“穿外套”。
我们可以使用如下代码:
int temperature = 70;
if (temperature < 60) {
std::cout << "Wear a coat" << std::endl;
}
此时,temperature
的值是 70,它不小于 60,所以条件为假,程序不会打印任何信息。
else
语句我们可以在 if
语句后面添加一个 else
语句,它在 if
条件不成立时执行。例如,如果温度不低于 60,我们可以打印“温暖的衣物”。
int temperature = 70;
if (temperature < 60) {
std::cout << "Wear a coat" << std::endl;
} else {
std::cout << "Wear warm clothes" << std::endl;
}
此时,由于 temperature
大于 60,条件不成立,程序将打印“Wear warm clothes”。
else if
当有多个条件时,我们可以使用 else if
来进一步检查其他条件。例如,如果温度低于 60 打印“穿外套”,如果温度低于 90 打印“温暖”,否则打印“热”。
int temperature = 70;
if (temperature < 60) {
std::cout << "Wear a coat" << std::endl;
} else if (temperature < 90) {
std::cout << "Nice weather" << std::endl;
} else {
std::cout << "It's hot" << std::endl;
}
在这个例子中,temperature
为 70,符合第二个条件,因此程序会打印“Nice weather”。
假设我们需要根据销售额来设定佣金。例如:
我们可以使用以下代码来实现这个要求:
int sales = 11000; // 示例销售额
double commission = 0.0; // 初始化佣金为0
if (sales <= 10000) {
commission = sales * 0.1; // 销售额小于等于 10,000 时,佣金为 10%
} else if (sales <= 15000) {
commission = sales * 0.15; // 销售额在 10,000 到 15,000 之间,佣金为 15%
} else {
commission = sales * 0.2; // 销售额超过 15,000 时,佣金为 20%
}
std::cout << "The commission is: " << commission << std::endl;
在此示例中,销售额为 11,000 美元,落在 10,000 到 15,000 美元之间,因此佣金为 15%,输出为:
The commission is: 1650
在编写条件语句时,有时我们可以简化代码。例如,下面的代码:
if (sales > 10000 && sales <= 15000) {
commission = sales * 0.15;
}
如果第一个条件 sales > 10000
不成立,我们就知道 sales
已经大于 10,000。所以我们可以去掉重复的部分,只保留后面的条件:
else if (sales <= 15000) {
commission = sales * 0.15;
}
这样,代码就更简洁了,避免了冗余的检查。
if
语句判断一个条件,满足条件时执行相应的代码。else if
可以处理多个条件,根据不同情况执行不同的代码块。else
来执行默认操作。通过这些示例,我们可以灵活地使用 if
语句来控制程序逻辑,处理各种不同的情况。
在某些情况下,我们需要在一个 if
语句中嵌套另一个 if
语句,这就是所谓的 嵌套 if 语句。嵌套 if
语句能够让我们处理更复杂的逻辑条件。接下来,我们通过一个示例来演示如何在 C++ 中使用嵌套的 if
语句。
假设我们需要根据学生的国籍和居住地来计算学费。规则如下:
首先,我们需要两个变量来检查学生的身份和居住地。我们用布尔变量来表示学生是否是美国公民,以及是否是加利福尼亚州的居民。
bool isCitizen = true; // 学生是否是美国公民
bool isCaliforniaResident = true; // 学生是否是加利福尼亚州的居民
int tuition = 0; // 学费初始为0
我们可以先检查学生是否是美国公民。如果是美国公民,那么进一步检查他们是否是加利福尼亚州的居民。如果他们是加利福尼亚州的居民,学费为 0;如果不是,学费为 1000。否则,如果学生不是美国公民,学费为 3000。
if (isCitizen) { // 外层 if 语句:检查是否是美国公民
if (isCaliforniaResident) { // 内层 if 语句:检查是否是加利福尼亚州的居民
tuition = 0; // 加利福尼亚州居民,学费为0
} else {
tuition = 1000; // 非加利福尼亚州居民,学费为1000
}
} else {
tuition = 3000; // 非美国公民,学费为3000
}
std::cout << "Tuition is: " << tuition << std::endl;
在这个示例中,我们有一个外层 if
语句用于检查学生是否是美国公民。如果是美国公民,程序会进入内层 if
语句,进一步检查他们是否是加利福尼亚州的居民。根据这些条件,学费会被设置为相应的值。如果学生不是美国公民,学费则被直接设置为 3000。
在有时,嵌套的 if
语句可能会变得冗长和难以阅读。我们可以通过简化表达式来避免不必要的嵌套。
例如,考虑下面的修改:
!
) 来简化判断学生是否为非加利福尼亚州居民。简化后的代码如下:
int tuition = 0; // 学费初始为0
if (isCitizen) { // 如果是美国公民
if (!isCaliforniaResident) { // 如果不是加利福尼亚州的居民
tuition = 1000; // 非加利福尼亚州居民,学费为1000
}
} else {
tuition = 3000; // 非美国公民,学费为3000
}
std::cout << "Tuition is: " << tuition << std::endl;
在这个修改版中,我们简化了条件语句。通过使用 !isCaliforniaResident
,我们减少了一个额外的 else
语句,使代码更加简洁。
if
语句中再写另一个 if
语句,以便处理复杂的条件逻辑。嵌套 if
语句在处理复杂逻辑时非常有用,但也需要注意过度嵌套可能导致代码的复杂性增加。
在 C++ 中,有一个特殊的运算符,叫做 条件运算符(?:
),它可以用来简化我们的 if
语句。这个运算符在许多源自 C++ 的编程语言中都有使用,如 C#、Java、JavaScript 等。接下来,我们通过一个示例来演示如何使用条件运算符。
首先,我们定义一个整数 sales
来表示销售额,并将其设为 11,000。然后,我们定义一个双精度浮点数 commission
来表示佣金。假设如果销售额大于 10,000,我们将佣金设置为 0.1,否则设置为 0.05。
if
语句我们可以使用传统的 if
语句来实现这一逻辑:
int sales = 11000;
double commission;
if (sales > 10000) {
commission = 0.1; // 销售额大于10,000,佣金为0.1
} else {
commission = 0.05; // 销售额小于等于10,000,佣金为0.05
}
std::cout << "Commission: " << commission << std::endl;
现在,我们可以用条件运算符来简化上面的代码。条件运算符的语法是:
condition ? value_if_true : value_if_false;
condition
是我们要判断的条件;true
,则返回 value_if_true
;false
,则返回 value_if_false
。我们可以将上述的 if
语句改为条件运算符:
int sales = 11000;
double commission = (sales > 10000) ? 0.1 : 0.05; // 使用条件运算符初始化佣金
std::cout << "Commission: " << commission << std::endl;
这段代码与前面的 if
语句功能完全相同,但更加简洁。
条件运算符 ?:
包含一个问号和一个冒号,结构如下:
condition ? value_if_true : value_if_false;
首先,判断 condition
条件是否为 true
:
true
,则返回 value_if_true
并赋值给变量;false
,则返回 value_if_false
并赋值给变量。通过这种方式,我们可以将一个 if-else
语句简化为一行代码,从而提高代码的简洁性和可读性。
接下来,我们通过一个练习来进一步掌握条件运算符的使用:
题目:编写一个程序,要求用户输入两个值,找出较大的值并打印出来。首先使用传统的 if
语句实现,然后使用条件运算符简化代码。
if
语句#include <iostream>
using namespace std;
int main() {
int first, second;
cout << "Enter two values: ";
cin >> first >> second;
int result;
if (first > second) {
result = first; // 如果first大于second,设置result为first
} else {
result = second; // 否则,设置result为second
}
cout << "The larger value is: " << result << endl;
return 0;
}
在这个例子中,我们让用户输入两个数 first
和 second
,然后用 if
语句比较两个数的大小,最后输出较大的那个数。
我们可以通过条件运算符将 if
语句简化为一行:
#include <iostream>
using namespace std;
int main() {
int first, second;
cout << "Enter two values: ";
cin >> first >> second;
// 使用条件运算符简化比较
int result = (first > second) ? first : second;
cout << "The larger value is: " << result << endl;
return 0;
}
在这段简化的代码中,我们通过条件运算符直接在一行代码中找出较大的数,并将其存储在 result
变量中。
?:
):用于简化 if-else
语句的代码结构。通过在一行代码中判断条件并选择赋值,可以提高代码的简洁性和可读性。condition ? value_if_true : value_if_false;
if-else
语句,特别是当代码非常简单时,它的可读性更好。通过掌握条件运算符,您可以减少代码的冗余,使程序更加简洁明了。
在 C++ 中,除了 if
语句之外,还有一个用于做决策的语句,那就是 switch 语句。它可以用来替代多个 if-else
语句,尤其在需要判断一个变量的多个可能值时,switch
语句可以让代码更加简洁和清晰。我们通过一个示例来演示如何使用 switch
语句。
我们首先打印一个菜单给用户,如果用户选择不同的选项,则执行不同的操作。例如:
if
语句我们首先可以用 if
语句实现这个逻辑:
#include <iostream>
using namespace std;
int main() {
int input;
cout << "Select an option:\n1. Create an account\n2. Change password\n3. Quit\n";
cin >> input;
if (input == 1) {
cout << "You selected 1: Create an account\n";
} else if (input == 2) {
cout << "You selected 2: Change password\n";
} else {
cout << "Goodbye\n";
}
return 0;
}
在这个例子中,我们首先输出一个菜单,接着根据用户的输入选择相应的操作。
switch
语句现在我们用 switch
语句来重写这个代码:
#include <iostream>
using namespace std;
int main() {
int input;
cout << "Select an option:\n1. Create an account\n2. Change password\n3. Quit\n";
cin >> input;
switch (input) {
case 1:
cout << "You selected 1: Create an account\n";
break;
case 2:
cout << "You selected 2: Change password\n";
break;
case 3:
cout << "Goodbye\n";
break;
default:
cout << "Invalid option\n";
break;
}
return 0;
}
这里的 switch
语句通过比较 input
的值与各个 case
标签对应的值。如果匹配成功,就执行该 case
下的代码。如果没有匹配到任何 case
,则会执行 default
标签下的代码。
switch
语句的工作原理switch
语句:
case
标签后面跟着一个值,表示要比较的目标值。如果 switch
中的变量和 case
后的值相等,就执行该 case
后的语句。case
后需要使用 break
语句来结束当前 case
,避免执行到下一个 case
。default
标签是可选的,如果没有任何 case
匹配时,就会执行 default
部分的代码。break
语句:
break
用来终止 switch
语句的执行。如果没有 break
,则代码会继续执行下一个 case
,即使条件不匹配(这就是所谓的“贯穿”)。switch
和 if
语句的比较switch
语句与 if
语句有一些关键区别:
switch
语句 只能用于单一变量的 等值比较,即只能判断某个变量是否等于一组可能的值。它不支持复杂的条件判断,如 greater than
(大于)或 less than
(小于)。if
语句 可以执行更复杂的条件判断,支持逻辑运算符(如 &&
, ||
)和各种条件组合。不过,在面对多种选择的情境时,switch
语句的结构更加清晰,代码也更简洁。
现在,作为练习,我们编写一个简单的计算器程序,要求用户输入两个数字和一个运算符(加、减、乘、除)。根据运算符的不同,计算并输出结果。如果用户输入了一个无效的运算符,则输出“无效运算符”的提示。
switch
语句#include <iostream>
using namespace std;
int main() {
double first, second;
char oper;
cout << "Enter two numbers: ";
cin >> first >> second;
cout << "Enter an operator (+, -, *, /): ";
cin >> oper;
switch (oper) {
case '+':
cout << "Result: " << first + second << endl;
break;
case '-':
cout << "Result: " << first - second << endl;
break;
case '*':
cout << "Result: " << first * second << endl;
break;
case '/':
if (second != 0) {
cout << "Result: " << first / second << endl;
} else {
cout << "Cannot divide by zero!" << endl;
}
break;
default:
cout << "Invalid operator" << endl;
break;
}
return 0;
}
first
和 second
,以及一个运算符 oper
,然后根据运算符进行相应的计算。switch
语句:
case
,如果是加法就执行加法操作,以此类推。default
块,输出“无效运算符”。+
,结果是 30。-
,结果是 -10。%
,输出“无效运算符”。switch
语句:用于根据某个变量的值选择执行不同的代码块。它只能用于简单的等值比较,且结构清晰、简洁。if
语句:适用于复杂的条件判断,支持各种逻辑操作和条件组合,灵活性更强。switch
语句在处理多个条件判断时非常有用,尤其是当判断的条件是一个变量的多个离散值时,可以让代码更易读和管理。
欢迎回来!在这节课中,我们将探索 C++ 中的各种循环结构。你将学习如何使用 for
循环、基于范围的 for
循环,以及 while
和 do-while
循环。同时,我们还会讨论如何使用 break
和 continue
语句来改变程序的控制流。
如果你跟着练习并完成所有的任务,你将能够编写更复杂的程序。事实上,计算机科学学生学习的大多数算法都可以通过循环和条件语句来实现。因此,掌握循环的使用是非常重要的,让我们马上开始吧!
for
循环是最常见的循环结构之一,它允许我们在某个条件下重复执行一段代码。for
循环通常用于已知次数的循环。
for
循环的基本结构:for (初始化; 条件; 更新) {
// 循环体
}
true
时,循环才会继续执行。如果条件为 false
,循环结束。#include <iostream>
using namespace std;
int main() {
for (int i = 1; i <= 10; i++) {
cout << i << " ";
}
return 0;
}
输出:
1 2 3 4 5 6 7 8 9 10
在这个示例中,i
从 1 开始,每次循环 i
加 1,直到 i
大于 10,循环结束。
基于范围的 for
循环(Range-based for
loop)是 C++11 引入的一种循环方式,用于遍历容器中的元素(如数组、vector
等)。
for (元素类型 变量名 : 容器) {
// 循环体
}
vector
、list
等。#include <iostream>
using namespace std;
int main() {
int arr[] = {10, 20, 30, 40, 50};
for (int num : arr) {
cout << num << " ";
}
return 0;
}
输出:
10 20 30 40 50
在这个示例中,基于范围的 for
循环遍历了数组中的每个元素,并打印它们。
while
循环是一种基础的循环结构,适用于当你不知道循环会执行多少次时,可以使用 while
循环,直到某个条件为 false
。
while
循环的基本结构:while (条件) {
// 循环体
}
true
时,循环才会继续执行。#include <iostream>
using namespace std;
int main() {
int i = 1;
while (i <= 10) {
cout << i << " ";
i++;
}
return 0;
}
输出:
1 2 3 4 5 6 7 8 9 10
在这个示例中,i
从 1 开始,每次循环 i
加 1,直到 i
大于 10,循环结束。
do-while
循环与 while
循环非常相似,不同之处在于 do-while
循环至少会执行一次循环体,因为条件判断是在循环体执行之后才进行的。
do-while
循环的基本结构:do {
// 循环体
} while (条件);
true
,则继续执行循环。#include <iostream>
using namespace std;
int main() {
int i = 1;
do {
cout << i << " ";
i++;
} while (i <= 10);
return 0;
}
输出:
1 2 3 4 5 6 7 8 9 10
在这个示例中,do-while
循环与 while
循环的区别在于即使 i
的初值不满足条件,循环体也会执行一次。
在循环结构中,break
和 continue
语句用于改变程序的控制流。
break
用于立即退出循环,跳出当前的循环体,不再继续执行循环。
break
退出循环#include <iostream>
using namespace std;
int main() {
for (int i = 1; i <= 10; i++) {
if (i == 5) {
break; // 当 i 等于 5 时退出循环
}
cout << i << " ";
}
return 0;
}
输出:
1 2 3 4
在这个示例中,当 i
等于 5 时,break
语句会让循环立即退出,因此 5 之后的数字不会被打印出来。
continue
用于跳过当前循环的剩余部分,立即进入下一次循环的条件判断。
continue
跳过某些数字#include <iostream>
using namespace std;
int main() {
for (int i = 1; i <= 10; i++) {
if (i == 5) {
continue; // 跳过 i 等于 5 时的输出
}
cout << i << " ";
}
return 0;
}
输出:
1 2 3 4 6 7 8 9 10
在这个示例中,当 i
等于 5 时,continue
语句会跳过当前循环的剩余部分,直接进入下一次循环,因此 5 不会被打印出来。
在本节课中,我们学习了 C++ 中的四种主要循环结构:for
循环、基于范围的 for
循环、while
循环和 do-while
循环。每种循环有其适用的场景:
for
循环:适用于已知循环次数的情况。for
循环:适用于遍历容器中的所有元素。while
循环:适用于不确定循环次数的情况,直到某个条件不满足为止。do-while
循环:类似于 while
循环,但保证至少执行一次循环体。此外,我们还介绍了 break
和 continue
语句,它们可以用来改变循环的控制流。在实际编程中,掌握这些循环结构和控制语句将使你能够编写更高效、更灵活的程序。
for
循环基础在本节中,我们将讨论 for
循环,它是用来重复执行一组指令的一种结构。首先,假设我们想打印数字从 1 到 5,这是一个常见的需求。我们可以使用 cout
打印数字 1,然后依次打印数字 2、3、4 等。为了实现这个,我们可能需要复制粘贴多次相同的语句。显然,这不是一个理想的编程方法,因为如果我们需要打印从 1 到 100 万的数字,那我们就需要写 100 万行代码来完成这一任务。
幸运的是,循环提供了更好的解决方案,能够自动重复执行一段代码。因此,我们可以用一个循环来替代重复写代码的繁琐工作。
for
循环for
循环可以帮助我们重复执行一个或多个语句,并且它的结构非常简洁。我们可以通过一个变量来控制循环的执行次数,并且每次循环时,变量的值会发生变化。接下来,我将展示如何使用 for
循环来完成这一任务。
for
循环的结构for
循环的基本语法如下:
for (初始化; 条件; 更新) {
// 循环体
}
true
时,循环才会继续执行。#include <iostream>
using namespace std;
int main() {
for (int i = 0; i < 5; i++) {
cout << i << " ";
}
return 0;
}
输出:
0 1 2 3 4
在这个示例中,i
从 0 开始,每次增加 1,直到 i
达到 5 时,循环结束。通过这两行代码,我们就能打印出从 0 到 4 的数字。
让我们逐步解释这两行代码的执行过程:
i
被初始化为 0。i < 5
这个条件被检查,第一次检查时,i
是 0,条件成立,因此进入循环体。i
的值。i++
,将 i
增加 1。如果我们希望打印从 1 到 5 的数字,可以通过初始化 i
为 1,而不是 0。同样,如果我们希望打印从 5 到 1 的数字,我们可以设置 i
从 5 开始,然后每次减少 1。
#include <iostream>
using namespace std;
int main() {
for (int i = 1; i <= 5; i++) {
cout << i << " ";
}
return 0;
}
输出:
1 2 3 4 5
#include <iostream>
using namespace std;
int main() {
for (int i = 5; i >= 1; i--) {
cout << i << " ";
}
return 0;
}
输出:
5 4 3 2 1
有时,我们可能只想打印某些特定的数字。例如,如果我们想打印 5 到 1 之间的所有奇数数字,我们可以使用模运算来判断当前数字是否为奇数。
#include <iostream>
using namespace std;
int main() {
for (int i = 5; i >= 1; i--) {
if (i % 2 != 0) { // 如果 i 是奇数
cout << i << " ";
}
}
return 0;
}
输出:
5 3 1
在这个示例中,i % 2 != 0
判断 i
是否是奇数,如果是,就打印出来。
现在,让我们做一个更有趣的练习——编写一个程序来计算用户输入数字的阶乘。阶乘是指一个数字乘以所有小于它的正整数。例如,3! = 1 * 2 * 3 = 6
,4! = 1 * 2 * 3 * 4 = 24
。
我们将提示用户输入一个正整数,然后计算并输出该数的阶乘。如果用户输入的是负数,我们将输出一个错误信息。
#include <iostream>
using namespace std;
int main() {
int number;
cout << "Enter a positive number: ";
cin >> number;
if (number < 0) {
cout << "Error: Number is not positive." << endl;
} else {
int factorial = 1; // 初始化阶乘变量为 1
for (int i = 1; i <= number; i++) {
factorial *= i; // 阶乘计算
}
cout << "Factorial of " << number << " is " << factorial << endl;
}
return 0;
}
5
输出:Factorial of 5 is 120
0
输出:Factorial of 0 is 1
-3
输出:Error: Number is not positive.
for
循环计算该数字的阶乘。1
开始,逐步将每个数字与当前阶乘结果相乘,直到数字为止。在本节课中,我们学习了如何使用 for
循环来重复执行代码,如何控制循环次数,以及如何结合条件判断来改变程序的行为。通过这些基础知识,我们能够编写更灵活的程序,自动执行重复性任务,如打印数字、计算阶乘等。
for
循环在现代 C++ 中,我们有另一种 for
循环,用于遍历一个范围或一组值。举个例子,我们可能有一个整数数组叫 numbers
,我们将其初始化为 1, 2, 3
。现在,我们希望遍历这个数组,并将每个数字打印在新的行上。
for
循环遍历数组我们可以使用之前学过的传统 for
循环来实现这个目标:
#include <iostream>
using namespace std;
int main() {
int numbers[] = {1, 2, 3};
int size = 3; // 数组的大小
for (int i = 0; i < size; i++) {
cout << numbers[i] << endl; // 打印每个数字
}
return 0;
}
在上面的例子中,使用传统的 for
循环来遍历数组。我们首先声明一个循环变量 i
,并让它从 0
开始,每次增加 1
,直到小于数组的大小 3
为止。然后,我们使用 numbers[i]
来访问每个数组元素,并打印它。
然而,这种方法有一个问题,就是我们将数组的大小 3
硬编码在代码中。如果以后我们改变数组的大小(比如添加新的元素),我们就需要回到代码中更新这个数字。
为了避免硬编码数组大小,我们可以使用 sizeof
运算符来动态计算数组的大小:
#include <iostream>
using namespace std;
int main() {
int numbers[] = {1, 2, 3};
int size = sizeof(numbers) / sizeof(numbers[0]); // 动态计算数组的大小
for (int i = 0; i < size; i++) {
cout << numbers[i] << endl;
}
return 0;
}
在这个版本中,我们使用 sizeof(numbers)
获取整个数组占用的字节数,再通过 sizeof(numbers[0])
获取每个元素的字节数,从而得到数组中元素的数量。这样,无论数组的大小如何变化,程序都能够正确计算并处理它。
for
循环在 C++11 及以后版本中,C++ 提供了一种更简洁的方式来遍历数组,即基于范围的 for
循环。这种循环方式更加简洁,不需要我们使用数组的索引。它直接遍历容器中的每个元素。
#include <iostream>
using namespace std;
int main() {
int numbers[] = {1, 2, 3};
for (int number : numbers) { // 直接遍历数组中的元素
cout << number << endl;
}
return 0;
}
在这个例子中,我们用 for (int number : numbers)
来声明循环变量 number
,它会依次保存数组 numbers
中的每个元素。每次循环,number
就会持有数组中的一个值,我们可以直接对 number
进行操作。
for
循环遍历字符串基于范围的 for
循环不仅可以用于数组,也可以用于字符串,因为字符串本质上是一个字符序列。例如,假设我们有一个字符串 name
,我们想要遍历每个字符并打印出来:
#include <iostream>
using namespace std;
int main() {
string name = "Marsh Hammad";
for (char ch : name) { // 遍历字符串中的每个字符
cout << ch << endl;
}
return 0;
}
在这个例子中,我们使用 for (char ch : name)
来遍历字符串 name
中的每个字符。每次循环,ch
就会保存字符串中的一个字符,我们将其打印出来。
现在让我们来看一个实际的例子:计算一组温度的平均值。首先,我们有一个温度数组:
#include <iostream>
using namespace std;
int main() {
int temperatures[] = {68, 70, 80, 90}; // 一组温度
int sum = 0;
int count = sizeof(temperatures) / sizeof(temperatures[0]); // 数组的大小
for (int temp : temperatures) {
sum += temp; // 累加每个温度
}
double average = static_cast<double>(sum) / count; // 计算平均值
cout << "Average temperature is " << average << endl;
return 0;
}
temperatures
,并初始化它。for
循环遍历数组中的每个温度,并将其累加到 sum
变量中。sum
除以数组中的元素数量 count
来计算平均值,并打印结果。for
循环的两种形式:传统的 for
循环和基于范围的 for
循环(range-based for
loop)都有各自的用途。传统的 for
循环适用于需要访问数组元素索引的情况,而基于范围的 for
循环则在遍历容器时更加简洁。sizeof
运算符:我们使用 sizeof
来动态计算数组的大小,避免了硬编码的缺点。sum
强制转换为 double
类型。通过这些方法,你可以更加高效和简洁地处理数组、字符串以及其他容器中的数据。
while
循环在 C++ 中,我们还有另一种类型的循环,叫做 while
循环。任何可以用 for
循环实现的事情,我们都可以用 while
循环来做,只不过代码的写法会有所不同。
for
循环打印 1 到 5首先,让我们来看一个使用 for
循环打印 1 到 5 的例子:
#include <iostream>
using namespace std;
int main() {
for (int i = 1; i <= 5; i++) {
cout << i << endl; // 打印数字
}
return 0;
}
在这个例子中,for
循环从 1
开始,直到 i
小于等于 5,每次迭代后 i
自增 1。程序将打印 1 到 5 的数字。
while
循环实现相同的功能现在,我们将这个例子改用 while
循环来实现。while
循环的写法略有不同:
#include <iostream>
using namespace std;
int main() {
int i = 1; // 在循环外部声明变量
while (i <= 5) { // 条件判断
cout << i << endl; // 打印数字
i++; // 增加变量
}
return 0;
}
在这个 while
循环的实现中,我们首先声明并初始化变量 i
,然后通过 while
循环检查条件是否为真。只要 i <= 5
条件成立,循环就会继续执行,每次循环后 i
会自增 1。
while
循环 vs for
循环我们可以看到,while
循环的代码比 for
循环稍微多了一些,但 while
循环特别适合那些我们不确定循环次数的场景。如果我们知道循环次数(例如,打印数字 1 到 5),使用 for
循环会更简洁;但如果我们不知道循环次数,或者循环次数取决于某些条件,while
循环就非常有用了。
while
循环进行用户输入验证接下来,假设我们想让程序询问用户输入一个在 1 到 5 之间的数字,如果用户输入的数字超出这个范围,则程序会提示用户重新输入。这里我们不知道要重复询问多少次,因此 while
循环非常适合这种场景。
#include <iostream>
using namespace std;
int main() {
int number = 0; // 初始化输入数字为 0
while (number < 1 || number > 5) { // 只要输入的数字不在 1 到 5 之间
cout << "Please enter a number between 1 and 5: ";
cin >> number; // 读取用户输入的数字
}
cout << "You entered a valid number: " << number << endl;
return 0;
}
在这个例子中,程序会不断询问用户输入,直到用户输入一个有效的数字(在 1 到 5 之间)。一旦用户输入有效数字,循环结束,程序终止。
我们可以进一步改进程序,在用户输入无效数字时给出错误提示:
#include <iostream>
using namespace std;
int main() {
int number = 0; // 初始化输入数字为 0
while (number < 1 || number > 5) { // 只要输入的数字不在 1 到 5 之间
if (number != 0) { // 如果不是初次输入,打印错误信息
cout << "Invalid number. ";
}
cout << "Please enter a number between 1 and 5: ";
cin >> number; // 读取用户输入的数字
}
cout << "You entered a valid number: " << number << endl;
return 0;
}
在这个版本中,程序在每次用户输入无效数字时都会输出 "Invalid number." 提示信息。这样,用户可以更清楚地知道输入的数字不符合要求。
在上面的代码中,我们有一个小问题——条件 number < 1 || number > 5
在两个地方都出现了。如果我们将来修改业务逻辑或更改范围,那么就需要在两个地方同时进行更新。为了提高代码的可维护性,我们可以将这个条件提取到一个变量中,避免重复。
接下来,我们来实现一个简单的猜数字游戏,程序会持续要求用户猜一个秘密数字,直到用户猜对为止。假设我们的秘密数字是 7。
#include <iostream>
using namespace std;
int main() {
int secret = 7; // 秘密数字
int guess = 0; // 用户猜测的数字
while (guess != secret) { // 只要猜测不等于秘密数字
cout << "Guess the secret number: ";
cin >> guess; // 读取用户输入的数字
if (guess != secret) { // 如果猜测不对
cout << "Wrong guess! Try again." << endl;
}
}
cout << "Congratulations! You guessed the secret number!" << endl;
return 0;
}
secret
(秘密数字),另一个是 guess
(用户的猜测)。while
循环,条件是用户的猜测不等于秘密数字 secret
,只要条件为真,循环就会继续。Guess the secret number: 4
Wrong guess! Try again.
Guess the secret number: 5
Wrong guess! Try again.
Guess the secret number: 6
Wrong guess! Try again.
Guess the secret number: 7
Congratulations! You guessed the secret number!
for
循环:适合已知循环次数的场景,代码简洁明了。while
循环:适合循环次数不确定的场景,尤其是当我们需要根据某些条件不断重复执行代码时,while
循环非常有用。do-while
循环在 C++ 中,除了常见的 for
循环和 while
循环,还有一种叫做 do-while
循环的循环结构。虽然它不是经常使用,但它在某些特定情况下非常有用,值得了解。
while
循环 vs do-while
循环首先,我们复习一下 while
循环的工作方式。while
循环会先判断条件,如果条件为真,则继续执行循环体。如果条件为假,循环体将完全不会执行。
while (condition) {
// 循环体
}
而 do-while
循环的工作方式稍有不同。do-while
循环无论条件是否成立,至少执行一次循环体。这意味着在执行条件判断之前,循环体中的代码已经被执行过一次。
do-while
循环的语法do-while
循环的语法结构如下:
do {
// 循环体
} while (condition);
do
关键字表示开始一个 do-while
循环。{}
花括号中。while
关键字后面跟着一个条件判断,条件判断位于圆括号内。;
结束。do-while
与 while
的区别do-while
循环与 while
循环的主要区别在于,do-while
循环 无论如何都会至少执行一次循环体,然后再评估条件。相比之下,while
循环会在执行循环体之前先检查条件,如果条件一开始就不满足,则循环体一次都不会执行。
这使得 do-while
循环适用于那种我们 至少希望执行一次代码 的场景。
do-while
循环让我们使用 do-while
循环来实现之前的例子:询问用户输入一个 1 到 5 之间的数字,直到用户输入有效数字为止。
#include <iostream>
using namespace std;
int main() {
int number = 0; // 初始化输入数字为 0
do {
cout << "Please enter a number between 1 and 5: ";
cin >> number; // 读取用户输入的数字
} while (number < 1 || number > 5); // 条件判断
cout << "You entered a valid number: " << number << endl;
return 0;
}
number
,并初始化为 0。do-while
循环中,我们要求用户输入一个数字。然后,我们检查数字是否在 1 到 5 之间。Please enter a number between 1 and 5: 0
Please enter a number between 1 and 5: 10
Please enter a number between 1 and 5: 3
You entered a valid number: 3
在上面的程序中,尽管用户第一次输入了 0 或 10 这样的无效数字,程序会一直提示用户输入,直到输入一个有效数字(例如 3)为止。
do-while
循环中,变量 number
必须在循环体外部声明,因为在 do
块内,变量 number
不能被直接访问。我们需要确保该变量的作用域覆盖整个循环体。do-while
循环至少会执行一次,所以它特别适合需要至少进行一次用户输入或执行某项操作的场景。while
循环:先判断条件,再执行循环体。如果条件一开始不成立,循环体不会被执行。do-while
循环:先执行循环体,再判断条件。无论条件如何,循环体至少会被执行一次。do-while
循环通常用于那些必须执行一次操作之后再判断是否继续的场景,例如用户输入验证、菜单选择等。
break
和 continue
语句在 C++ 中,除了常规的循环控制语句,我们还可以使用两个额外的控制语句:break
和 continue
。这些语句能帮助我们更灵活地控制循环的执行过程。
break
语句break
语句用于 提前终止循环。当循环体中的某个条件满足时,可以使用 break
来跳出循环,直接跳到循环体外的代码执行。通常,break
语句常用于在某些特定条件下提前结束循环。
continue
语句continue
语句用于 跳过当前循环中的迭代,并继续执行下一次迭代。与 break
不同,continue
不会终止整个循环,而是跳过当前的循环步骤,直接开始下一次迭代。
break
和 continue
让我们通过一个简单的例子来演示 break
和 continue
的使用。
#include <iostream>
using namespace std;
int main() {
// 使用 for 循环打印数字 1 到 5
for (int i = 1; i <= 5; ++i) {
if (i % 3 != 0) {
cout << i << endl; // 如果 i 不是 3 的倍数,就打印 i
}
}
cout << "----------" << endl;
// 使用 continue 语句跳过 3
for (int i = 1; i <= 5; ++i) {
if (i % 3 == 0) {
continue; // 如果 i 是 3 的倍数,跳过当前迭代
}
cout << i << endl;
}
cout << "----------" << endl;
// 使用 break 语句在 i 为 3 时跳出循环
for (int i = 1; i <= 5; ++i) {
if (i == 3) {
break; // 当 i 为 3 时,提前终止循环
}
cout << i << endl;
}
return 0;
}
第一部分:我们使用了一个简单的 for
循环,打印从 1 到 5 的数字。但是通过 if (i % 3 != 0)
判断,只打印不是 3 的倍数的数字。结果输出是:1、2、4、5。
第二部分:我们使用 continue
语句来跳过每一个 3 的倍数。如果 i % 3 == 0
,那么程序会跳过当前的迭代,继续下一次迭代。输出结果是:1、2、4、5。
第三部分:我们使用 break
语句,当 i
等于 3 时,终止循环。输出结果是:1、2。
1
2
4
5
----------
1
2
4
5
----------
1
2
break
语句接下来,我们将通过一个例子来演示如何使用 break
语句来改进之前的程序。在这个例子中,我们会使用一个 无限循环,并通过 break
语句在满足特定条件时跳出循环。
#include <iostream>
using namespace std;
int main() {
int number;
// 创建一个无限循环
while (true) {
cout << "Please enter a number between 1 and 5: ";
cin >> number;
// 检查输入是否在有效范围内
if (number >= 1 && number <= 5) {
cout << "Valid number entered: " << number << endl;
break; // 输入有效数字时跳出循环
} else {
cout << "Error: Enter a number between 1 and 5" << endl;
}
}
return 0;
}
while (true)
,这个循环会一直执行,直到用户输入一个有效的数字(在 1 到 5 之间)。break
语句终止循环,并打印有效的数字。Please enter a number between 1 and 5: 0
Error: Enter a number between 1 and 5
Please enter a number between 1 and 5: 10
Error: Enter a number between 1 and 5
Please enter a number between 1 and 5: 3
Valid number entered: 3
使用无限循环和 break
语句的一个主要优点是 我们只需要在代码中写一次条件。相比之前的实现方式,我们在 while
循环中处理了所有的逻辑,使得代码更简洁,避免了条件的重复判断。如果我们仅仅依赖 while
循环并在循环外部进行条件判断,我们可能会面临重复代码的问题。
break
:用于提前退出循环。continue
:用于跳过当前迭代,继续下一次循环。break
语句实现条件终止。通过这种方式,我们的程序更加清晰且易于维护。
在 C++ 中,除了基本的单一循环结构外,我们还可以使用 嵌套循环 来处理更加复杂的任务。嵌套循环指的是将一个循环结构放在另一个循环结构内,这种技术非常强大,能够帮助我们实现一些复杂的算法。
假设我们想要生成从 1 到 5 的 X 和 Y 坐标组合,具体地,我们希望生成类似于以下的坐标对:
(1,1), (1,2), (1,3), (1,4), (1,5)
(2,1), (2,2), (2,3), (2,4), (2,5)
(3,1), (3,2), (3,3), (3,4), (3,5)
(4,1), (4,2), (4,3), (4,4), (4,5)
(5,1), (5,2), (5,3), (5,4), (5,5)
为了解决这个问题,我们需要使用 嵌套循环。外部循环负责生成 X 坐标,内部循环负责生成 Y 坐标。
#include <iostream>
using namespace std;
int main() {
// 外部循环负责生成 X 坐标
for (int x = 1; x <= 5; ++x) {
// 内部循环负责生成 Y 坐标
for (int y = 1; y <= 5; ++y) {
// 打印坐标对
cout << "(" << x << "," << y << ") ";
}
cout << endl; // 换行,进入下一行
}
return 0;
}
(1,1) (1,2) (1,3) (1,4) (1,5)
(2,1) (2,2) (2,3) (2,4) (2,5)
(3,1) (3,2) (3,3) (3,4) (3,5)
(4,1) (4,2) (4,3) (4,4) (4,5)
(5,1) (5,2) (5,3) (5,4) (5,5)
在这个练习中,我们将编写一个程序来打印一个星号三角形。用户输入行数,程序根据用户的输入生成相应的三角形。
#include <iostream>
using namespace std;
int main() {
int rows;
// 让用户输入行数
cout << "Enter the number of rows: ";
cin >> rows;
// 外部循环:根据用户输入的行数打印相应的行
for (int i = 1; i <= rows; ++i) {
// 内部循环:打印当前行的星号数量
for (int j = 1; j <= i; ++j) {
cout << "*"; // 打印星号
}
cout << endl; // 换行,进入下一行
}
return 0;
}
rows
,表示三角形的行数。for (int i = 1; i <= rows; ++i)
,外部循环控制打印的行数,行数从 1 到 rows
。for (int j = 1; j <= i; ++j)
,内部循环控制每行星号的数量。每行的星号数量与当前行数 i
相等。cout << endl;
输出换行符,进入下一行。Enter the number of rows: 5
*
**
***
****
*****
我们还可以改进这个程序,处理一些特殊输入,例如输入 0 或负数,或者输入一个非常大的数字。
#include <iostream>
using namespace std;
int main() {
int rows;
// 让用户输入行数
cout << "Enter the number of rows: ";
cin >> rows;
// 输入验证:如果行数小于等于 0,给出提示
if (rows <= 0) {
cout << "Please enter a positive number greater than 0." << endl;
return 1; // 结束程序
}
// 外部循环:打印三角形
for (int i = 1; i <= rows; ++i) {
// 内部循环:打印当前行的星号数量
for (int j = 1; j <= i; ++j) {
cout << "*";
}
cout << endl;
}
return 0;
}
Enter the number of rows: 5
*
**
***
****
*****
通过这些示例,你可以看到嵌套循环在实际问题中的应用,同时也能够学会如何处理一些常见的编程问题,如用户输入的验证和程序的扩展性。
随着我们编写的程序变得越来越复杂,我们需要将代码组织成更小的、可重用的部分。这就是函数的作用所在。在本节中,你将学习如何定义和调用函数,如何为函数分配默认值参数,如何重载函数,如何通过值或引用传递参数,还会了解局部变量和全局变量的区别,以及如何在不同文件中组织函数。
函数是实现特定任务的代码块。你可以通过调用函数来执行这些任务。在 C++ 中,定义函数的一般格式如下:
// 函数的声明
返回类型 函数名(参数列表) {
// 函数体
// 执行某些操作
}
#include <iostream>
using namespace std;
// 定义一个简单的函数
void greet() {
cout << "Hello, world!" << endl;
}
int main() {
greet(); // 调用函数
return 0;
}
在这个示例中,greet
函数没有参数,也没有返回值。我们在 main
函数中调用它,它会打印一条欢迎信息。
函数可以接受参数,这些参数在调用时提供给函数,函数可以根据这些参数来执行特定的任务。你还可以为参数赋予默认值,这样在调用函数时,如果没有提供相应的值,函数就会使用默认值。
#include <iostream>
using namespace std;
// 定义一个带参数的函数
void greet(string name) {
cout << "Hello, " << name << "!" << endl;
}
int main() {
greet("Alice"); // 调用函数并传入参数
return 0;
}
#include <iostream>
using namespace std;
// 定义一个带默认值参数的函数
void greet(string name = "Guest") {
cout << "Hello, " << name << "!" << endl;
}
int main() {
greet(); // 不传递参数,使用默认值
greet("Alice"); // 传递参数
return 0;
}
在这个示例中,如果我们没有传递任何参数给 greet
函数,它会使用默认值 "Guest"
。
C++ 支持函数重载,这意味着你可以定义多个同名但参数不同的函数。当你调用一个函数时,编译器会根据传递的参数类型和数量来决定调用哪个版本的函数。
#include <iostream>
using namespace std;
// 定义重载的 greet 函数
void greet() {
cout << "Hello, world!" << endl;
}
void greet(string name) {
cout << "Hello, " << name << "!" << endl;
}
void greet(string name, int age) {
cout << "Hello, " << name << ". You are " << age << " years old." << endl;
}
int main() {
greet(); // 调用无参数版本
greet("Alice"); // 调用带一个参数版本
greet("Bob", 30); // 调用带两个参数版本
return 0;
}
在 C++ 中,你可以通过 值传递 或 引用传递 来传递参数。两者的主要区别在于:
#include <iostream>
using namespace std;
// 通过值传递参数
void modifyValue(int x) {
x = 10;
}
int main() {
int num = 5;
modifyValue(num);
cout << "Value of num: " << num << endl; // 输出 5,因为修改只发生在函数内部
return 0;
}
#include <iostream>
using namespace std;
// 通过引用传递参数
void modifyValue(int &x) {
x = 10;
}
int main() {
int num = 5;
modifyValue(num);
cout << "Value of num: " << num << endl; // 输出 10,因为引用传递修改了原始数据
return 0;
}
在 C++ 中,变量的作用域决定了它的生命周期和可访问范围。根据变量的声明位置,变量可以是 局部变量 或 全局变量:
#include <iostream>
using namespace std;
void greet() {
int count = 0; // 局部变量
count++;
cout << "Greet count: " << count << endl;
}
int main() {
greet();
greet();
return 0;
}
在上面的代码中,count
是一个局部变量,每次调用 greet
时都会重新初始化为 0
。
#include <iostream>
using namespace std;
int count = 0; // 全局变量
void greet() {
count++;
cout << "Greet count: " << count << endl;
}
int main() {
greet();
greet();
return 0;
}
在这个例子中,count
是一个全局变量,它的值在多个函数调用之间保持不变。
在大型程序中,为了提高代码的可维护性和可读性,我们常常将函数分离到不同的文件中。这样可以避免在一个文件中包含所有代码,提高代码的模块化程度。
greet.h(头文件)
#ifndef GREET_H
#define GREET_H
void greet(string name);
#endif
greet.cpp(源文件)
#include <iostream>
#include "greet.h"
using namespace std;
void greet(string name) {
cout << "Hello, " << name << "!" << endl;
}
main.cpp(主程序文件)
#include <iostream>
#include "greet.h"
using namespace std;
int main() {
greet("Alice");
return 0;
}
掌握这些函数的基本概念和技巧,会让你在编写复杂程序时更加得心应手。
在课程的早期,我们提到过电视机有几个功能,每个功能负责完成特定的任务。电视机有更换频道、控制音量、调节亮度等功能。同样地,我们的程序往往也有几十、几百甚至上千个函数,每个函数负责一个独立的任务。到目前为止,我们的程序只有一个函数,那就是 main
函数。现在,让我们来看一下如何创建其他函数来实现不同的任务。
就像我们定义 main
函数一样,创建其他函数的方法是类似的。我们首先定义函数的返回类型,返回类型可以是你已经学过的任何类型,比如 int
、double
、boolean
、string
等等。如果函数没有返回值,我们需要使用 void
关键字作为返回类型。
#include <iostream>
using namespace std;
// 定义一个函数,返回类型为 void
void greet() {
cout << "Hello, world!" << endl;
}
int main() {
greet(); // 调用 greet 函数
return 0;
}
在上面的代码中,greet
函数没有参数,并且不返回任何值。当我们在 main
函数中调用它时,它会打印 "Hello, world!"。
我们可以为函数添加一个或多个参数,用来传递值。在调用函数时,这些参数将被传递给函数,函数会根据这些参数来执行任务。
#include <iostream>
using namespace std;
// 定义一个带参数的函数
void greet(string firstname, string lastname) {
cout << "Hello, " << firstname << " " << lastname << "!" << endl;
}
int main() {
greet("John", "Doe"); // 调用函数,并传递参数
return 0;
}
在这个例子中,greet
函数接收两个参数:firstname
和 lastname
,并将它们组合打印出来。我们在 main
函数中调用时传递了实际的名字。
例如,在上面的 greet
函数中,firstname
和 lastname
是参数,而 "John"
和 "Doe"
则是实参。
有些函数需要返回一个值,而不仅仅是执行某些操作。在这种情况下,我们需要在函数中使用 return
语句来返回一个值。
#include <iostream>
using namespace std;
// 定义一个返回值为 string 的函数
string fullName(string firstname, string lastname) {
return firstname + " " + lastname; // 返回组合后的全名
}
int main() {
string name = fullName("John", "Doe"); // 调用函数并获取返回值
cout << "Full name: " << name << endl; // 打印全名
return 0;
}
在这个例子中,fullName
函数接收 firstname
和 lastname
两个参数,并返回它们的组合(全名)。我们在 main
函数中调用该函数并存储返回值。
通过将不同的任务分配给不同的函数,我们可以使程序更具可读性和可维护性。比如,可以创建一个函数来生成全名,然后另一个函数来打印问候语。
#include <iostream>
using namespace std;
// 定义 greet 函数,接收一个全名参数
void greet(string name) {
cout << "Hello, " << name << "!" << endl;
}
// 定义 fullName 函数,接收 firstname 和 lastname,返回组合后的全名
string fullName(string firstname, string lastname) {
return firstname + " " + lastname;
}
int main() {
string name = fullName("John", "Doe"); // 生成全名
greet(name); // 打印问候语
return 0;
}
在这个例子中,我们首先调用 fullName
函数生成全名,然后将这个全名传递给 greet
函数来打印问候语。
在 C++ 中,我们可以使用条件运算符(? :
)来简化一些简单的条件判断代码。条件运算符的语法是:
条件 ? 如果条件为真时执行的表达式 : 如果条件为假时执行的表达式;
#include <iostream>
using namespace std;
// 定义一个返回较大值的函数
int max(int first, int second) {
return (first > second) ? first : second; // 使用条件运算符返回较大值
}
int main() {
int result = max(10, 20); // 调用 max 函数
cout << "Max value: " << result << endl; // 打印结果
return 0;
}
在这个例子中,max
函数使用条件运算符来返回 first
和 second
中较大的值。
编写函数时,测试不同的输入和场景是很重要的。确保函数能正确处理各种边界情况,例如相同的值、不同的顺序、负数等。
max
函数#include <iostream>
using namespace std;
// 定义一个返回较大值的函数
int max(int first, int second) {
return (first > second) ? first : second; // 返回较大值
}
int main() {
cout << max(10, 20) << endl; // 打印 20
cout << max(20, 10) << endl; // 打印 20
cout << max(10, 10) << endl; // 打印 10
return 0;
}
在这个例子中,我们测试了 max
函数的不同输入,确保它在不同场景下都能正确工作。
return
语句返回函数的结果。通过这些技巧,你可以编写更模块化、可维护的代码,并有效地解决各种编程问题。
有时我们需要为函数的参数指定默认值。这样,在调用函数时,如果没有提供某个参数的值,系统会自动使用这个默认值。下面是一个关于如何为参数设置默认值的例子。
假设我们需要创建一个计算税额的函数。这个函数应该返回一个 double
类型的值,函数名为 calculateTax
,并接受两个参数:income
和 taxRate
。在函数内部,我们会将 income
与 taxRate
相乘来计算税额。
#include <iostream>
using namespace std;
// 定义计算税额的函数,税率有一个默认值
double calculateTax(double income, double taxRate = 0.2) {
return income * taxRate; // 计算税额
}
int main() {
double income = 10000;
// 不传递税率,使用默认税率0.2
double tax = calculateTax(income);
cout << "The tax amount is: " << tax << endl; // 打印税额
// 传递税率0.3,覆盖默认值
tax = calculateTax(income, 0.3);
cout << "The tax amount is: " << tax << endl; // 打印税额
return 0;
}
在这个例子中,calculateTax
函数接受两个参数:income
和 taxRate
,其中 taxRate
的默认值是 0.2
。当我们调用 calculateTax
函数时,如果没有提供税率,默认税率 0.2
会被使用;如果我们提供了税率(例如 0.3
),则默认值会被新的税率覆盖。
0.2
,因此税额为 10000 * 0.2 = 2000
。0.3
,因此税额为 10000 * 0.3 = 3000
。需要注意的是,具有默认值的参数必须出现在没有默认值的参数之后。如果在具有默认值的参数前面有没有默认值的参数,编译器会报错。
#include <iostream>
using namespace std;
// 错误:默认值参数在没有默认值的参数之前
double calculateTax(double income = 0.0, double taxRate) { // 编译错误
return income * taxRate;
}
int main() {
return 0;
}
在这个例子中,taxRate
是没有默认值的,而 income
有默认值。按照规则,必须先声明没有默认值的参数,再声明有默认值的参数。因此,这样的代码会导致编译错误。
#include <iostream>
using namespace std;
// 正确:没有默认值的参数在前,有默认值的参数在后
double calculateTax(double taxRate, double income = 0.0) {
return income * taxRate;
}
int main() {
return 0;
}
在这个例子中,taxRate
没有默认值,而 income
有默认值。这样的顺序是合法的。
通过为函数参数设置默认值,我们可以简化函数调用,使得某些参数变得可选,从而提高程序的灵活性。
有时候我们需要创建多个名称相同,但参数不同的函数,这种技术称为函数重载(Function Overloading)。重载函数允许我们定义多个同名函数,但它们的参数列表不同。
假设我们想创建一个函数来打印问候语。这个函数将接受一个参数,即人的名字,我们将其命名为 greet
。
#include <iostream>
using namespace std;
// 第一个版本的 greet 函数
void greet(string name) {
cout << "Hello " << name << endl;
}
上面的 greet
函数接收一个字符串参数 name
,并打印问候语。现在,我们希望有另一个版本的 greet
函数,它不仅接受名字,还接受一个人的称谓(例如:Mr., Mrs., Dr.)。我们可以重载这个函数,创建一个新的版本来处理这种情况。
#include <iostream>
using namespace std;
// 第二个版本的 greet 函数
void greet(string title, string name) {
cout << "Hello " << title << " " << name << endl;
}
在第二个版本中,我们添加了一个 title
参数,它表示一个人的称谓。这个函数和第一个版本的 greet
函数不同,因为它接受两个参数。
当我们调用重载函数时,编译器会根据传递的参数类型来决定调用哪个版本的函数。如果我们只传递一个名字参数,那么编译器会调用第一个版本;如果我们传递称谓和名字参数,那么编译器会调用第二个版本。
int main() {
greet("Mosh"); // 调用第一个版本,输出 "Hello Mosh"
greet("Mr.", "Mosh"); // 调用第二个版本,输出 "Hello Mr. Mosh"
return 0;
}
greet("Mosh")
调用了第一个版本的 greet
函数,因为我们只传递了一个参数,即名字。greet("Mr.", "Mosh")
调用了第二个版本的 greet
函数,因为我们传递了两个参数:称谓和名字。重载函数时,函数签名是一个非常重要的概念。函数签名由函数名称和参数的数量及类型组成。函数参数的名称并不影响函数签名,只有参数的数量和类型才决定签名是否唯一。
greet
函数的签名是:greet(string)
。greet
函数的签名是:greet(string, string)
。签名的关键点是:每个重载函数必须有唯一的签名,否则编译器无法区分这些函数,导致编译错误。
#include <iostream>
using namespace std;
// 错误:这两个函数的签名相同
void greet(string name) {
cout << "Hello " << name << endl;
}
void greet(string name) { // 错误:函数签名与前一个相同
cout << "Hi " << name << endl;
}
int main() {
greet("Mosh");
return 0;
}
在上面的例子中,两个 greet
函数的签名完全相同,都会接收一个 string
类型的参数,因此会导致编译错误。即使参数的名称不同,编译器仍然会认为它们是相同的函数。
通过函数重载,我们可以提高程序的灵活性,允许我们在不同的情况下使用相同的函数名。
在 C++ 中,理解如何传递参数是非常重要的概念。参数可以通过值传递(pass by value)或引用传递(pass by reference)来传递。下面我们来详细讲解这两种方式的区别,以及它们的实际应用。
在值传递中,函数接收的是实参的一个副本。也就是说,函数内部操作的只是副本,而不是原始变量。因此,在函数内部对参数进行修改时,原始变量的值不会受到影响。
我们定义一个函数 increasePrice
来增加商品价格。该函数接收一个 double
类型的参数,并将其增加 20%。
#include <iostream>
using namespace std;
// increasePrice 函数,通过值传递接收 price 参数
void increasePrice(double price) {
price = price * 1.2; // 将价格增加 20%
}
int main() {
double price = 100.0; // 定义价格变量并初始化为 100
increasePrice(price); // 调用 increasePrice 函数
cout << "Price after increase: " << price << endl; // 输出价格
return 0;
}
运行上述程序后,输出的价格仍然是 100。为什么呢?因为我们传递的是 price
的副本,在 increasePrice
函数内部修改的是副本的值,而不是原始变量。
为了能够看到更新后的价格,我们可以将函数的返回类型改为 double
,并返回更新后的价格值。
#include <iostream>
using namespace std;
// increasePrice 函数,返回更新后的价格
double increasePrice(double price) {
return price * 1.2; // 返回增加 20% 后的价格
}
int main() {
double price = 100.0; // 定义价格变量并初始化为 100
price = increasePrice(price); // 接收更新后的价格
cout << "Price after increase: " << price << endl; // 输出价格
return 0;
}
运行后,输出为 Price after increase: 120
,这是因为我们返回了更新后的价格并将其存储在 price
变量中。
在引用传递中,函数接收的是实参的地址,也就是说,函数内部操作的是原始变量。因此,函数内部对参数的修改会直接影响到原始变量。
我们可以通过引用传递来避免复制操作,并直接在原始变量上进行修改。我们在函数的参数类型后加上一个 &
来表示引用传递。
#include <iostream>
using namespace std;
// increasePrice 函数,通过引用传递接收 price 参数
void increasePrice(double &price) {
price = price * 1.2; // 将价格增加 20%
}
int main() {
double price = 100.0; // 定义价格变量并初始化为 100
increasePrice(price); // 直接修改原始价格
cout << "Price after increase: " << price << endl; // 输出更新后的价格
return 0;
}
运行后,输出为 Price after increase: 120
,这次价格更新是因为我们通过引用传递直接修改了原始 price
变量的值。
int
或 double
),因为它们的大小较小,使用值传递通常足够高效。#include <iostream>
using namespace std;
// greet 函数,使用引用传递处理字符串
void greet(string &name) {
cout << "Hello " << name << endl;
name = "Alice"; // 修改 name 参数
}
int main() {
string name = "Bob";
greet(name); // 传递 name 引用
cout << "Name after greet: " << name << endl; // 输出修改后的 name
return 0;
}
在这个例子中,name
参数是通过引用传递的,因此 greet
函数中的修改会影响到原始变量 name
。运行后,输出为:
Hello Bob
Name after greet: Alice
如果不希望在函数中修改传递给函数的参数,可以将参数声明为常量引用(const &
)。这样,虽然参数是通过引用传递的,但它不能在函数中被修改,从而避免了意外的修改。
#include <iostream>
using namespace std;
// greet 函数,使用常量引用传递
void greet(const string &name) {
cout << "Hello " << name << endl;
// name = "Alice"; // 编译错误:不能修改常量引用参数
}
int main() {
string name = "Bob";
greet(name); // 传递常量引用
cout << "Name after greet: " << name << endl; // 输出未被修改的 name
return 0;
}
在此例中,name
是通过常量引用传递的,因此在函数内部不能修改它,确保了函数不会意外更改传递给它的值。
了解值传递与引用传递的差异,可以帮助我们在实际编程中做出更合适的选择,优化代码的性能与可读性。
在 C++ 中,变量的作用范围(Scope)决定了它在哪些地方可被访问。我们可以将变量分为 本地变量(Local Variables) 和 全局变量(Global Variables),它们各自有不同的作用范围和使用场景。
本地变量是在某个函数或代码块内部声明的变量,它们只能在该函数或代码块中使用。换句话说,本地变量的作用范围仅限于它被声明的地方。一旦离开该作用域,本地变量就不再有效。
#include <iostream>
using namespace std;
void calculateTax(int sales) {
double taxRate = 0.2; // 本地变量 taxRate
double tax = sales * taxRate;
cout << "Tax is: " << tax << endl;
}
int main() {
int sales = 10000;
calculateTax(sales); // 调用 calculateTax 函数
return 0;
}
在这个例子中,taxRate
是在 calculateTax
函数内部声明的本地变量,它只能在这个函数内使用。如果我们尝试在 main
函数中访问它,会发生编译错误,因为 taxRate
超出了它的作用域。
全局变量是在所有函数之外声明的变量,因此它们对程序中的所有函数都是可见的。换句话说,全局变量的作用范围是整个程序。
#include <iostream>
using namespace std;
double taxRate = 0.2; // 全局变量 taxRate
void calculateTax(int sales) {
double tax = sales * taxRate; // 直接使用全局变量
cout << "Tax is: " << tax << endl;
}
int main() {
int sales = 10000;
calculateTax(sales); // 调用 calculateTax 函数
return 0;
}
在这个例子中,taxRate
是一个全局变量,它在 main
函数和 calculateTax
函数中都可以使用。因为它是在所有函数之外声明的,所以无论哪个函数都可以直接访问它。
虽然全局变量可以方便地在多个函数之间共享数据,但过度使用全局变量可能会带来一些问题。
因此,通常建议尽量避免使用全局变量,除非有特别的需求。
虽然全局变量应该尽量避免使用,但 全局常量(Global Constants) 是一种例外。常量全局变量的值在程序运行期间不会改变,因此它们不会引发意外的行为。
#include <iostream>
using namespace std;
const double TAX_RATE = 0.2; // 全局常量 TAX_RATE
void calculateTax(int sales) {
double tax = sales * TAX_RATE; // 使用全局常量
cout << "Tax is: " << tax << endl;
}
int main() {
int sales = 10000;
calculateTax(sales); // 调用 calculateTax 函数
return 0;
}
在这个例子中,TAX_RATE
是一个全局常量,它的值在程序的整个运行过程中都不会改变。因此,它不会引发意外的修改,也不会导致其他函数对其进行修改。
如果全局变量没有声明为常量,那么在程序中其他部分就可能不小心修改它的值,导致程序的行为出乎意料。
#include <iostream>
using namespace std;
double taxRate = 0.2; // 全局变量 taxRate
void calculateTax(int sales) {
double tax = sales * taxRate;
cout << "Tax is: " << tax << endl;
}
int main() {
int sales = 10000;
taxRate = 0.0; // 不小心将税率设置为 0
calculateTax(sales); // 调用 calculateTax 函数
return 0;
}
在这个例子中,我们不小心在 main
函数中将全局变量 taxRate
设置为 0,这导致 calculateTax
函数中的税费计算为 0。由于 taxRate
是一个普通的全局变量,所以它的值可以在程序的任何地方被修改,从而引发了这个错误。
尽量避免使用全局变量,尤其是在没有特别必要的情况下。如果必须使用全局变量:
总的来说,避免使用全局变量,尤其是可修改的全局变量,可以提高程序的可靠性和可维护性。
在 C++ 中,通常我们会在 main
函数之前定义所有的函数,这样编译器在 main
函数中调用这些函数时能够识别它们。但如果你希望将函数定义放在 main
函数之后,程序会出现编译错误,因为在调用函数之前,编译器并不知道该函数的存在。
main
函数之后假设我们有一个 greet
函数,接受一个名字并打印一个问候语。
#include <iostream>
using namespace std;
int main() {
greet("Mosh"); // 调用 greet 函数
return 0;
}
void greet(string name) { // 定义 greet 函数
cout << "Hello " << name << endl;
}
在这个例子中,我们在 main
函数之后定义了 greet
函数,结果编译器会报错,提示 greet
函数在 main
函数中不可见。原因是编译器在编译 main
函数时还没有看到 greet
函数的定义,所以它无法识别并调用该函数。
为了让编译器知道某个函数的存在,并且允许在函数定义之前调用它,我们可以使用 函数声明(也叫 函数原型)。函数声明告诉编译器该函数的返回类型、名字以及参数类型,但没有函数的实现细节(即没有函数体)。
#include <iostream>
using namespace std;
void greet(string name); // 函数声明
int main() {
greet("Mosh"); // 调用 greet 函数
return 0;
}
void greet(string name) { // 函数定义
cout << "Hello " << name << endl;
}
在这个例子中,我们在 main
函数之前声明了 greet
函数,告诉编译器这个函数存在,并且它接受一个 string
类型的参数,返回 void
。之后,在 main
函数中调用该函数时,编译器知道该函数的存在,因此不会报错。greet
函数的具体实现仍然位于 main
函数之后。
函数声明(函数原型):仅包含函数名、返回类型和参数类型,但没有实现函数体。它告诉编译器该函数存在,可以在代码中调用。声明通常放在文件的顶部或函数调用之前。
语法:
返回类型 函数名(参数类型1, 参数类型2, ...);
函数定义(实现):包含完整的函数实现,包括函数体。定义告诉编译器函数的实际操作以及如何执行。定义通常放在函数声明之后,或者在 main
函数之后。
语法:
返回类型 函数名(参数类型1, 参数类型2, ...) {
// 函数体
}
main
函数之前或者文件的顶部。main
函数之后。通过声明函数,编译器就能在调用该函数时知道它的存在,并且可以根据函数的声明来进行调用。
随着程序的复杂化,main
文件通常会变得越来越庞大,难以维护。因此,为了提高代码的可读性和可维护性,我们可以将代码分离到不同的文件中,每个文件专注于一个功能模块,就像我们将物品组织到不同的容器中一样。在本节课中,我们将展示如何将一个 greet
函数从 main
文件中分离出来,放到一个单独的文件中。
将函数分离到不同文件的好处有两个:
main
文件变得更小,更容易管理。让我们开始操作,创建一个新的文件来存放 greet
函数。
创建一个新目录:
utils
,表示其中存放实用功能的文件。添加两个新文件:
创建 .cpp
文件:
utils
目录下创建一个 greet.cpp
文件,存放 greet
函数的实现。#include <iostream>
using namespace std;
void greet(string name) { // greet 函数的定义
cout << "Hello " << name << endl;
}
greet.cpp
中使用了 string
和 cout
,我们需要在文件顶部引入适当的头文件。#include <iostream>
#include <string>
using namespace std;
void greet(string name) {
cout << "Hello " << name << endl;
}
utils
目录下创建一个 greet.h
文件,存放 greet
函数的声明。#ifndef GREET_H // 防止头文件重复包含
#define GREET_H
#include <string>
void greet(std::string name); // 函数声明
#endif
main.cpp
文件中,我们需要包含 greet.h
头文件,并且确保在调用 greet
函数之前引入它。#include <iostream>
#include "utils/greet.h" // 引入自定义的头文件
using namespace std;
int main() {
greet("Mosh");
return 0;
}
在头文件中,我们使用了 #ifndef
、#define
和 #endif
指令来防止该头文件被重复包含。如果没有这些指令,多个包含同一个头文件会导致编译器重复处理相同的声明,从而产生错误。
当我们将 greet.cpp
文件从主文件分离到 utils
目录中时,构建工具(如 CMake)需要更新,以便将新的源文件包含进构建过程。
CMakeLists.txt
文件。greet.cpp
文件,以确保它被包含在构建过程中:add_executable(MyProject main.cpp utils/greet.cpp)
虽然手动创建文件和添加代码可能比较繁琐,许多 IDE 提供了快捷方式来自动生成头文件和源文件。比如在 CMake 或 Visual Studio 中,我们可以右键点击项目目录,选择“新建源文件”或“新建头文件”,IDE 会自动生成必要的模板代码并添加到 CMakeLists.txt
中。
通过将代码分离到多个文件中,我们不仅提高了代码的可维护性,还使得代码更具复用性。分离的过程包括:
#ifndef
和 #define
。通过这些步骤,我们能够更好地组织代码并提高其扩展性。
我们将 greet
函数移动到一个单独的文件中,这样我们就能在其他项目中复用了。但是,复用别人的代码时可能会遇到一个问题:如果别人也写了一个 greet
函数,或者有一个同名函数,如何避免冲突呢?这时我们就需要使用命名空间。
命名空间(Namespace)就像一个容器,帮助我们将函数和类组织在一个特定的空间里,从而避免与其他代码库中的同名函数发生冲突。我们已经见过命名空间的使用,像 std
命名空间,它包含了标准库中的所有类和函数。例如,标准库中的 string
类是定义在 std
命名空间中的,如果我们自己定义了一个名为 string
的类,它将与标准库中的 string
类区分开来,因为我们定义的 string
类不会处于 std
命名空间中。
现在,让我们把 greet
函数放入一个命名空间中,以避免未来的命名冲突。
创建命名空间:
greet
函数时,我们都要将它放入一个命名空间。例如,我们可以创建一个名为 messaging
的命名空间,因为这个命名空间下的所有函数都与显示消息相关。更新函数声明和定义:
greet.h
中将 greet
函数放入 messaging
命名空间。#ifndef GREET_H
#define GREET_H
#include <string>
namespace messaging { // 创建命名空间
void greet(std::string name); // 函数声明
}
#endif
greet.cpp
中,我们也需要将函数定义放入同样的命名空间:#include <iostream>
#include <string>
using namespace std;
namespace messaging { // 使用相同的命名空间
void greet(string name) {
cout << "Hello " << name << endl;
}
}
在主文件中调用函数:
如果我们不使用命名空间,编译器将无法找到 greet
函数。为了调用 greet
函数,我们有两种方式:
方法 1:显式地指定命名空间:我们每次调用函数时都需要加上 messaging::
前缀。
#include <iostream>
#include "utils/greet.h"
using namespace std;
int main() {
messaging::greet("Mosh"); // 使用命名空间前缀
return 0;
}
using
指令来导入整个命名空间,这样我们就不需要在每次调用时都加上命名空间前缀了。#include <iostream>
#include "utils/greet.h"
using namespace std;
using namespace messaging; // 导入命名空间
int main() {
greet("Mosh"); // 现在可以直接调用
return 0;
}
使用 using namespace messaging;
之后,所有位于 messaging
命名空间中的函数和类都可以直接使用,无需每次都加上命名空间前缀。
避免命名冲突:
如果我们在不同的命名空间中定义了相同名称的函数,例如在 messaging
命名空间中和在另一个命名空间中都定义了 greet
函数,那么这时就会发生命名冲突。为了避免这种情况,我们可以使用更加精细的控制。
using
指令仅导入需要的函数。例如:using messaging::greet; // 仅导入 greet 函数
这样,只有 greet
函数可以直接使用,其他 messaging
命名空间中的函数则不会受到影响。
using
指令仅导入某些对象,例如 std::cout
或 std::cin
,而不导入整个 std
命名空间。using std::cout; // 仅导入 cout 对象
这样,在文件中,我们只需要使用 cout
,而不必每次都加上 std::
前缀。
使用命名空间:命名空间可以帮助我们避免不同模块或库之间的命名冲突,将函数和类放入不同的命名空间中。
导入命名空间:
using namespace
导入整个命名空间,可以简化代码,使得不需要每次都加上命名空间前缀。命名冲突处理:当两个命名空间中有相同名称的函数时,可以通过显式指定命名空间或仅导入特定函数来解决。
命名空间是一种组织代码的有效工具,特别是在需要复用别人代码或在大型项目中开发时,它帮助我们保持代码整洁并避免混乱。
调试是定位程序错误的一种技术,是每个程序员必须掌握的关键技能。所以请仔细阅读下面的内容。
在本节中,我们将创建一个打印奇数的函数。该函数接受一个限制值,并使用一个 for
循环遍历所有数字,判断数字是否为奇数。如果是奇数,打印该数字。我们会在主函数中调用此函数,打印出 1 到 10 之间的奇数。
接着,我们故意在程序中创建一个错误。我将改变代码中的表达式,使用 ==
(双等号),导致程序运行时输出偶数而不是奇数。这样,我们就能演示如何使用调试工具来找出错误。
void printOddNumbers(int limit) {
for (int i = 0; i < limit; i++) {
if (i % 2 == 0) // 这里的 == 是错误的,应该使用 != 来判断奇数
cout << i << endl;
}
}
int main() {
printOddNumbers(10); // 打印1到10的奇数
return 0;
}
运行此程序时,控制台会输出偶数而不是奇数。假如你写的程序不能正常工作,这时就可以开始调试,逐行执行代码,查看变量的值如何变化,进而找出问题所在。
在本节中,我们将展示如何使用 SeaLion(或其他 IDE)中的调试工具。几乎所有的开发环境都有调试工具,唯一的区别可能在于快捷键或图标。
插入断点:
printOddNumbers
函数内部插入一个断点,观察程序的执行。开始调试:
Ctrl + R
(开始程序),接着使用 F7
或 F8
来单步调试。逐步执行(Step Over 和 Step Into):
Step Over
(F8)让程序跳过当前行,直接执行到下一行。Step Into
(F7)则进入函数内部,逐行执行。这对于想要了解函数内部执行过程时非常有用。查看和修改变量:
i
和 limit
的值。运行程序并暂停: 当我们启动调试时,程序会在断点处暂停。此时我们可以查看控制台输出,发现它显示的是偶数而不是奇数。
逐行调试:
我们按 F8
(Step Over)逐行调试,发现程序开始时的 i
值是 0
。这时程序打印了 0
,这是错误的,因为我们应该打印的是奇数。
查看变量值:
我们在调试窗口中添加了一个监视表达式,用来查看 i % 2 == 0
这个条件的值。我们发现当 i
为 0 时,这个条件的值为 true
,导致程序错误地打印了偶数。
修改并修复错误:
通过调试,我们发现问题的根源是我们使用了 ==
(等于)运算符,而应该使用 !=
(不等于)。修改代码如下:
if (i % 2 != 0) // 改为判断是否为奇数
重新启动调试: 修改代码后,我们需要停止调试并重新启动,以便查看修改后的效果。通过继续逐步执行,我们可以看到程序最终正确地打印了奇数。
移除断点并结束调试: 一旦调试完成,记得移除所有的断点,并停止调试会话。这样就能确保程序回到正常的执行状态。
调试是查找和修复程序错误的有力工具。通过逐行执行代码,检查变量的值,并使用监视和断点等功能,我们可以精准地定位问题所在。在调试过程中,我们学到了如何:
Step Over
和 Step Into
来逐行调试代码。掌握调试技能能够大大提高你编程时解决问题的效率,它是每个程序员必备的核心技能。
感谢你们参与并完成了本课程的第一部分!在此,我衷心感谢你们让我担任你们的讲师。我希望你们在这一部分中学到了很多有用的知识。
接下来,我们将在第二部分的课程中探讨更为深入的中级主题,包括:
如果你喜欢这门课程,并且从中受益,请支持我,通过向他人推荐我的编程学校来帮助我继续发展。非常感谢你的支持!
这就是第一部分的全部内容。我们很快会在第二部分见面!
在这一部分,你将学习中级概念,例如:
要顺利学习本部分内容,你需要先完成第一部分的学习,掌握我在第一部分中讲解的基础概念。你应该了解基本数据类型、条件语句、循环和函数等内容。
我非常激动能成为你们这部分课程的讲师,接下来让我们马上开始吧!
在本系列的第一部分中,你已经学习了数组的基础知识。在这一部分,我们将更详细地探讨数组的使用。你将学习:
让我们开始吧!
在本课程的第一部分中,你了解了数组的基本概念。简单来说,数组用于在内存中存储一系列对象,比如一系列数字、字符串等。这里是一个例子:
我们声明了一个整数类型的数组,叫做 numbers
。建议在命名数组时使用复数形式,因为它存储的是多个对象。例如:
int numbers[5];
numbers
数组有 5 个整数的存储空间,这些整数将会被连续存储在内存中。在 C++ 中,与许多其他编程语言不同,我们需要特别注意索引的合法性。如果你使用了一个无效的索引,编译器不会抛出错误,而只是发出警告并继续编译。举个例子:
numbers[5] = 10; // 错误:索引越界
如果你打印这个无效索引,程序将输出一个随机值。这是因为数组越界访问没有被阻止,我们需要特别小心。
numbers[0] = 10;
numbers[1] = 20;
int numbers[5] = {10, 20}; // 其他元素默认为 0
在这个例子中,我们为数组提供了两个值(10 和 20),其余的元素默认为 0。
int numbers[] = {10, 20}; // 数组大小为 2
如果我们尝试直接打印数组而不是访问它的单个元素,输出的将不是数组的内容,而是一个十六进制地址值,这是该数组在内存中的地址。这被称为 指针。我们将在后面进一步讨论指针的概念。
这就是第一部分中关于数组的主要内容。
接下来,我们将讨论如何确定数组的大小。
在这一节中,我们将继续学习如何遍历数组并打印出其中的每个元素。如果你有一个包含两个元素的整数数组,并希望打印每个元素,最简单的解决方案是使用范围基的 for
循环。回顾一下:
for (int number : numbers) {
std::cout << number << " ";
}
number
会持有数组中的一个值,比如 10 或 20。我们还可以使用 auto
关键字来让编译器推断出变量的类型:
for (auto number : numbers) {
std::cout << number << " ";
}
auto
可以让代码更加简洁。for
循环?有些情况下,我们不能使用范围基 for
循环进行迭代。比如,当我们需要使用索引来访问数组的单独元素时,就需要用到普通的 for
循环。这些情况通常出现在需要复制、比较数组元素等操作时。在下一个小节中,我们将讨论这些情况的具体例子。
for
循环和动态计算数组大小接下来,我们看一下如何使用传统的 for
循环遍历数组。我们首先声明一个循环变量 i
作为索引,然后遍历数组。
for (int i = 0; i < sizeof(numbers) / sizeof(numbers[0]); i++) {
std::cout << numbers[i] << " ";
}
在这里,我们需要知道数组的大小。为了避免硬编码数组大小,假设你将来决定往数组里添加新元素,这时你需要手动更新数组大小。为了避免这种麻烦,我们可以通过动态计算数组的大小。
sizeof(numbers)
返回数组占用的字节数。在这个例子中,如果数组有 3 个元素,每个元素占 4 字节(假设是 int
类型),那么总字节数就是 12
。sizeof(numbers[0])
返回数组单个元素占用的字节数,通常是 4 字节。sizeof(numbers)
除以 sizeof(numbers[0])
就能得到数组的元素个数。std::size
简化计算在标准库中,我们还有一个更简单的方式来计算数组的大小。我们可以使用 std::size
函数来获取数组的大小,而不需要手动使用 sizeof
操作符。代码如下:
for (int i = 0; i < std::size(numbers); i++) {
std::cout << numbers[i] << " ";
}
std::size(numbers)
会返回数组的元素数量,这比手动计算要简单得多。注意: 由于 std::size
是定义在 std
命名空间中的,所以如果你没有在文件顶部引入 std
命名空间,需要使用 std::size
来访问它。
#include <iostream>
#include <array> // 需要包含此头文件
for
循环来遍历数组非常简便,尤其是当你只需要访问数组中的每个元素时。for
循环可以帮助你控制索引。sizeof
或者 std::size
,其中 std::size
是更现代且推荐的方式。这就是我们在这一部分学习的关于数组遍历的内容。
在这节课中,我们将讨论如何在 C++ 中复制数组。假设我们有一个整数数组 first
,并希望将其复制到另一个数组 second
。
如果你尝试直接通过赋值将一个数组复制到另一个数组:
int first[] = {10, 20, 30};
int second[] = first; // 这会导致编译错误
你会遇到编译错误。C++ 不允许直接将一个数组赋值给另一个数组,因为数组是固定大小的,因此不能使用赋值运算符将一个数组的值传递给另一个数组。
为了正确复制数组,我们必须逐个元素地复制每个元素。此时我们需要使用传统的 for
循环来实现。代码如下:
int first[] = {10, 20, 30};
int second[3]; // 声明一个大小为 3 的数组
for (int i = 0; i < sizeof(first) / sizeof(first[0]); i++) {
second[i] = first[i]; // 逐个复制每个元素
}
sizeof(first) / sizeof(first[0])
来动态计算数组的大小,以确保即使将来数组大小发生变化,代码依然有效。first[i]
的每个元素赋值给 second[i]
,从而完成数组的复制。为了验证我们的实现是否正确,我们可以再次遍历 second
数组并打印每个元素:
for (int number : second) {
std::cout << number << " ";
}
for
循环来遍历 second
数组,并打印出每个元素。10 20 30
,这意味着我们已经成功地将 first
数组的内容复制到了 second
数组。for
循环。sizeof
计算数组的大小,确保代码在数组大小发生变化时仍然有效。在本节中,我们将讨论如何比较两个数组。在 C++ 中,如果我们想比较两个数组,我们不能直接使用 ==
运算符,因为这只会比较数组的内存地址,而不是数组中的实际值。因此,我们需要逐个元素地比较数组的值。
假设我们有两个数组,它们的值是相同的:
int first[] = {10, 20, 30};
int second[] = {10, 20, 30};
如果我们直接使用 ==
运算符来比较这两个数组:
if (first == second) {
std::cout << "Equal";
}
编译器会给出警告,表示条件总是 false
。这是因为 first
和 second
代表的是数组的内存地址,而不是数组的值。即使它们的值相同,内存地址仍然不同。所以,编译器会指出这两者不相等。
我们可以打印这两个数组的地址来看发生了什么:
std::cout << "first: " << first << "\n";
std::cout << "second: " << second << "\n";
输出的可能类似于:
first: 0x7ffee4baf540
second: 0x7ffee4baf560
可以看到,两个数组的地址几乎相同,但最后两位(例如 F0
和 F6
)是不同的,这就是为什么编译器给出警告的原因。
为了正确地比较数组,我们必须逐个元素进行比较。可以使用 for
循环来实现这一点:
bool areEqual = true;
for (int i = 0; i < sizeof(first) / sizeof(first[0]); i++) {
if (first[i] != second[i]) {
areEqual = false;
break; // 一旦发现不同的元素,直接退出循环
}
}
在这个例子中:
areEqual
变量初始化为 true
。for
循环遍历数组中的每个元素,逐个比较 first[i]
和 second[i]
。areEqual
设置为 false
,并跳出循环。areEqual
仍然保持为 true
,表示两个数组相等。为了测试我们的实现,我们可以打印 areEqual
变量的值:
std::cout << "Arrays are " << (areEqual ? "equal" : "not equal") << "\n";
Arrays are equal
。not equal
。例如,修改 first
数组中的一个元素:
first[0] = 99;
再运行程序,输出将变为:
Arrays are not equal
==
运算符来比较数组,应该逐个元素比较。for
循环遍历数组并逐个比较每个元素,如果发现不同的元素就可以提前退出循环。areEqual
)来表示数组是否相等,并在循环结束后输出结果。在 C++ 中,传递数组到函数时可能会遇到一些特殊情况,尤其是当我们尝试使用范围基 for
循环(range-based for loop)来遍历数组时。我们将探讨这种情况以及如何解决它。
假设我们有一个整数数组,并希望将其传递给一个函数来打印数组的内容。我们定义一个函数 printNumbers
,它接收一个整数数组作为参数:
void printNumbers(int numbers[]) {
for (int number : numbers) {
std::cout << number << " ";
}
}
但是,编译时会出现以下错误:
Cannot build range expression with a function parameter numbers
这个错误表明我们不能直接在函数参数中传递一个数组,原因在于数组传递给函数时会被转换为指针。
在 C++ 中,当我们传递数组到函数时,数组会被转换为指向数组首元素的指针。例如,数组 numbers
传递给 printNumbers
后,它实际上变成了一个指针,指向数组的第一个元素。这意味着函数内部看到的 numbers
不是数组,而是一个指针,这就是导致我们无法使用范围基 for
循环的原因。
指针只是存储一个内存地址,它本身并不包含数组的大小信息。所以,无法直接在范围基 for
循环中使用 numbers
,因为我们没有数组的大小,只有指针地址。
for
循环为了绕过这个问题,我们可以改用传统的 for
循环,手动使用数组的大小。假设我们知道数组的大小(在这里是通过传递大小参数来提供),我们可以通过索引访问数组的元素:
void printNumbers(int numbers[], int size) {
for (int i = 0; i < size; i++) {
std::cout << numbers[i] << " ";
}
}
现在我们需要修改 main
函数,传递数组以及它的大小:
int numbers[] = {10, 20, 30, 40, 50};
int size = sizeof(numbers) / sizeof(numbers[0]);
printNumbers(numbers, size);
在上面的代码中,我们传递了数组 numbers
和它的大小 size
给 printNumbers
函数。这里的关键点是,当我们将数组传递给函数时,数组会被转换为指针,而指针无法提供数组的大小信息。因此,我们必须手动传递数组的大小作为函数的另一个参数。
如果不传递数组的大小,函数就无法知道数组的元素数量,这样我们就无法有效地访问数组中的每个元素。我们需要明确地提供大小,以确保函数能够正确地访问和操作数组。
for
循环来遍历数组。通过这种方式,我们可以避免编译错误,并成功地遍历和操作传递给函数的数组。
size_t
类型在 C++ 中,size_t
和 sizeof
操作符返回的值都是 size_t
类型。size_t
是一种专门用于表示对象大小的数据类型,它的主要用途是存储对象的大小,且保证足够大以容纳系统可以处理的最大对象的大小。
size_t
的定义与作用size_t
是标准库定义的一个数据类型,用来表示对象的大小。其作用是确保其大小足够大,可以表示当前系统能够处理的最大对象的大小。通常情况下,size_t
的大小要比普通的整数类型(如 int
)更大,它能够容纳更大的值。
int
和 size_t
的大小我们可以通过打印 int
和 size_t
的大小来查看它们占用的内存大小。假设我们在代码中加入如下内容:
std::cout << "Size of int: " << sizeof(int) << " bytes" << std::endl;
std::cout << "Size of size_t: " << sizeof(size_t) << " bytes" << std::endl;
在不同的机器上,int
和 size_t
的大小可能不同。在 64 位机器上,int
通常占用 4 个字节,而 size_t
占用 8 个字节,因为 size_t
的大小足够大,可以表示更大的数字。
size_t
与 long long
类型的比较size_t
在一些系统中等价于 unsigned long long
类型。具体来说,size_t
总是无符号的,这意味着它只能表示正整数和零,而不能表示负数。相比之下,long long
类型是带符号的,它可以表示负数、零和正数。
我们可以使用 numeric_limits
类来查看 long long
和 size_t
的最小值和最大值。例如:
#include <iostream>
#include <limits>
int main() {
std::cout << "Max value for long long: " << std::numeric_limits<long long>::max() << std::endl;
std::cout << "Min value for long long: " << std::numeric_limits<long long>::min() << std::endl;
std::cout << "Max value for size_t: " << std::numeric_limits<size_t>::max() << std::endl;
return 0;
}
输出将显示 long long
的最小和最大值,以及 size_t
的最大值。对于 size_t
,由于它是无符号的,因此它的最小值是零,最大值比 long long
更大(如果我们使用的是 64 位系统)。
size_t
的平台依赖性需要注意的是,size_t
的大小是平台相关的:
size_t
等同于 unsigned int
,通常占用 4 个字节。size_t
等同于 unsigned long long
,通常占用 8 个字节。因此,size_t
的大小依赖于系统架构,但它始终足够大,能够表示系统中可以处理的最大对象的大小。
size_t
是一个专门用于表示对象大小的类型,它能够存储足够大的数值。size_t
总是无符号的,通常等同于 unsigned long long
类型,在 32 位系统中等同于 unsigned int
。numeric_limits
类,可以查看 size_t
和其他类型(如 long long
)的最小值和最大值。size_t
保证大到足以容纳系统可以处理的最大对象的大小。在 C++ 中,数组解包是一种非常实用的技术,允许我们将数组中的元素快速赋值给多个变量。这种技术使得代码更加简洁和易于理解。下面,我们将通过一个示例来讲解如何使用这种技术。
假设我们有一个包含三个元素的整数数组,我们将它们视为 x
、y
和 z
坐标。首先,我们可以手动声明三个变量 x
、y
和 z
,并将数组中的值逐个赋给它们:
int values[3] = {10, 20, 30};
int x = values[0];
int y = values[1];
int z = values[2];
这是一种直接且明确的方法,但它需要写很多重复的代码,尤其当数组的大小增大时。
为了简化这种情况,C++ 提供了一个简洁的技术——结构绑定(Structured Binding)。虽然这个名字有点儿拗口,在 Python 中我们称之为解包(Unpacking)。这种方法可以在一行代码中完成多个变量的赋值。
使用这种技术,我们可以将数组直接解包到多个变量中,而无需写多行代码。具体实现方法如下:
auto [x, y, z] = values;
这里,auto
关键字会自动推断变量 x
、y
和 z
的类型,然后我们使用结构绑定的语法将 values
数组的元素解包到这三个变量中。
我们可以通过打印这些变量的值来验证解包是否成功:
std::cout << "x: " << x << ", y: " << y << ", z: " << z << std::endl;
输出将会是:
x: 10, y: 20, z: 30
这表明我们成功地将数组 values
的值解包到了 x
、y
和 z
变量中。
std::tuple
和 std::pair
)。结构绑定不仅限于数组,还可以用于解包其他类型的数据结构。例如,可以用它来解包 std::tuple
或 std::pair
。
例如,对于一个包含两个元素的 std::pair
:
std::pair<int, int> coordinates = {10, 20};
auto [x, y] = coordinates;
这样,我们就可以直接通过 x
和 y
访问 coordinates
中的值。
数组解包是一种非常实用的技术,它能够减少代码量并提高代码的可读性。通过使用结构绑定(或解包),我们可以轻松地将数组的多个值赋给多个变量,而无需手动逐个赋值。
在计算机科学中,线性搜索(Linear Search)是一种最简单的搜索算法,用于查找数组或列表中的目标值。我们将逐步解释这一算法及其实现方法。
假设我们有一个整数数组,并且我们想要查找某个特定的值。线性搜索的基本思路是:
-1
,表示该值不存在于数组中。在计算复杂度上,线性搜索的时间复杂度是 O(n),其中 n
是数组的大小。也就是说,随着数组大小的增加,线性搜索所需的比较次数也会线性增加。
我们可以使用 C++ 来实现线性搜索算法。以下是一个简单的实现代码:
#include <iostream>
// 线性搜索函数
int find(int numbers[], int size, int target) {
for (int i = 0; i < size; ++i) {
if (numbers[i] == target) {
return i; // 找到目标值,返回索引
}
}
return -1; // 未找到目标值,返回 -1
}
int main() {
int numbers[] = {5, 10, 15, 20, 25};
int size = sizeof(numbers) / sizeof(numbers[0]);
int target = 10;
int index = find(numbers, size, target);
std::cout << "目标值 " << target << " 的索引是: " << index << std::endl;
target = 30;
index = find(numbers, size, target);
std::cout << "目标值 " << target << " 的索引是: " << index << std::endl;
return 0;
}
函数定义:find
函数接受三个参数:
numbers[]
:一个整数数组,包含我们要搜索的元素。size
:数组的大小。target
:要查找的目标值。遍历数组:我们使用 for
循环遍历数组中的每个元素。如果某个元素等于目标值,立即返回该元素的索引。
返回 -1
:如果循环结束仍未找到目标值,返回 -1
。
对于上述代码,假设数组是 {5, 10, 15, 20, 25}
,我们分别查找目标值 10
和 30
:
10
,返回索引 1
。30
,返回 -1
,因为该值不存在于数组中。输出结果如下:
目标值 10 的索引是: 1
目标值 30 的索引是: -1
这是一个经典的线性搜索的实现,您可以尝试在不同的数据集上测试,理解它的行为和性能。
冒泡排序是所有排序算法中最简单的之一。它通过重复遍历数组,不断比较相邻元素,并交换它们的位置,直到数组完全有序。接下来,我们将详细讨论冒泡排序的工作原理,并展示如何在 C++ 中实现这个算法。
假设我们有一个整数数组,目标是将这个数组按升序排列。在使用冒泡排序时,我们会从左到右扫描数组:
我们需要重复多次比较,直到数组完全排序。
第一轮比较:
第二轮比较:
n-1
个元素。重复这个过程,直到整个数组完全有序。
接下来我们来看 C++ 中的冒泡排序实现。
#include <iostream>
// 定义交换函数
void swap(int numbers[], int i, int j) {
int temp = numbers[i];
numbers[i] = numbers[j];
numbers[j] = temp;
}
// 冒泡排序函数
void bubbleSort(int numbers[], int size) {
for (int pass = 0; pass < size - 1; ++pass) { // 外层循环:进行多次比较
// 每一轮的比较
for (int i = 1; i < size - pass; ++i) {
if (numbers[i - 1] > numbers[i]) {
// 如果左边的元素大于右边的元素,则交换它们
swap(numbers, i - 1, i);
}
}
}
}
int main() {
int numbers[] = {30, 20, 10};
int size = sizeof(numbers) / sizeof(numbers[0]);
// 调用冒泡排序
bubbleSort(numbers, size);
// 输出排序后的数组
std::cout << "Sorted array: ";
for (int i = 0; i < size; ++i) {
std::cout << numbers[i] << " ";
}
std::cout << std::endl;
return 0;
}
swap
函数:用于交换数组中的两个元素。我们用一个临时变量 temp
来保存一个元素的值,然后完成交换。bubbleSort
函数:外层循环控制冒泡排序的轮数,内层循环逐一比较并交换元素。假设数组为 {30, 20, 10}
,执行排序后,输出结果为:
Sorted array: 10 20 30
如果数组已经排序好(例如 {10, 20, 30}
),冒泡排序仍然能够正常工作,输出结果为:
Sorted array: 10 20 30
若数组只有两个元素或一个元素,冒泡排序也能够正确处理:
{20, 10}
排序后为 {10, 20}
。{10}
不需要排序,仍然是 {10}
。通过这个实现,您可以更好地理解冒泡排序的工作原理,并且能够在 C++ 中实现这一基本的排序算法。
我们到目前为止声明的所有数组都是一维的,也就是说它们只有一组值。但我们也可以声明多维数组。例如,我们可以使用二维数组来表示一个矩阵。这里有一个例子,假设我们要表示一个 2x3 的矩阵,即两行三列。那么我们可以声明一个名为 matrix
的二维数组,尺寸为 2 和 3。就像一维数组一样,我们也可以使用花括号来初始化它。现在,每一行都有三列,所以我们需要三个值。对于每一行,可能需要一对独立的花括号。为了让我们更清楚地看到,我将它们换行写。
现在,第一个我想写的值是 1, 1, 2 和 13。对于第二行,我将提供值 21, 22 和 23,这只是任意值。好了,这样我们就得到了一个 2x3 的矩阵,或者说一个二维数组。
现在要查看这个数组,我们需要使用两个循环。外层循环用于行,内层循环用于列。我们可以说 for (int row = 0; row < 2; row++)
。我之前告诉过你不要使用魔法数字,所以让我们提取这个数字并将它存储为一个常量 int rows
,然后可以在这里和这里使用它。同样,我们也可以对列进行相同的操作,声明一个常量 int columns = 3
,并在这里使用它。现在我们有了一个矩阵。这里是外层循环,我们还需要一个内层循环。for (int col = 0; col < columns; col++)
,在这里访问单个元素时,我们需要使用索引,所以我们可以通过 matrix[row][col]
来访问二维数组中的单个元素。
现在我们有了一个二维数组,但实际上在维度数量上并没有限制。然而,超过三维就变得不太实际了。那么,现在我们来打印这些数字到终端上。
好了,如果你想将这个数组作为参数传递给一个函数该怎么办?我们可以定义一个名为 printMatrix
的函数,它接收一个二维数组作为参数。所以 int matrix[][]
,这里也有维度。为了方便,我将行和列的数量移到主函数外部,这样它们就成为全局常量了。我们就可以再使用它们一次。现在,我们把这个矩阵移到新的函数里,并将 for 循环放进去。最后,我们调用这个函数并确保一切正常工作。我们调用 printMatrix
并传入我们的矩阵。只需要传入数组,不需要为数组的大小或每个维度的大小传递单独的参数。好了,程序运行并确保一切都能正常工作。
这就是如何声明和初始化多维数组的过程。
欢迎回来,进入终极 C++ 课程的另一个章节。在本章节中,我们将探索指针,这是 C++ 中一个强大但常常被误解的概念。我们将从讲解指针是什么以及为什么使用它们开始。接下来,我将向你展示如何声明和使用指针。一旦你掌握了指针的基本原理,我们将看到如何使用指针高效地在函数之间传递大量数据以及动态分配内存。最后,我们将讨论指针的问题,以及如何通过现代 C++ 中的智能指针来避免这些问题。
现在,让我们开始吧!
那么,什么是指针呢?指针是一个特殊的变量,它保存了内存中另一个变量的地址。这里有一个例子,假设我们有一个名为 number
的变量,存储了值 10。你知道变量只是内存地址的标签,所以这个 number
变量可能存储在某个特定的内存位置。现在,我们可以声明另一个名为 pointer
的变量,它保存 number
的地址。
那么,为什么我们要这么做呢?使用指针有几个原因。第一个原因是为了高效地在函数之间传递大对象。在课程的第一部分,我们讨论了按值或按引用传递参数。我告诉过你,如果你想在函数之间传递大量数据,直接复制数据并不高效,应该通过引用传递。因此,引用参数或引用变量是高效传递大量数据的一种方式。指针是解决这个问题的另一种方法。我们稍后会在本节中看一个例子。
第二个原因是我们使用指针进行动态内存分配,这样我们的程序可以根据输入调整内存使用。例如,在本节的后面,我将展示如何在运行时动态调整数组的大小。当数组满了之后,我们可以重新调整它的大小,以便存储更多的数据。
使用指针的第三个原因是启用多态,这是面向对象编程的能力之一。这是一个高级话题,我们将在课程的下一部分讨论它。
现在,既然你理解了指针是什么以及为什么我们要使用它们,让我们看看如何声明和使用指针。
好,我们来看一下如何创建和使用指针。首先,我将声明一个名为 number
的整数,并将其设置为 10。你知道,每个变量都有一个地址。如果你想打印 number
的地址,我们应该在它前面加上取地址运算符 &
,这样就可以获取它的地址了。现在,我们打印 number
的地址。得到的是一个十六进制的数字。请注意,你在自己的机器上看到的值会不同,所以不要被这个值分心。
接下来,我们声明一个整数指针。我们可以通过在类型前面加上星号 *
来声明指针。所以,星号 *
表示这是一个指针,指针指向的类型是整数。接下来,我们给这个指针变量命名,我们可以称它为 PR
或其他名字,然后将它初始化为 number
的地址。这样我们就声明了一个整数指针,并且这个指针只能指向整数类型的变量。如果你把 number
的类型改为其他类型,例如 double
,编译器会报错,提示不能将 int
类型的指针初始化为 double
类型的值。
所以我们需要把它改回 int
类型。就像其他变量一样,如果我们没有初始化这个指针,它将会保存垃圾值或者无效值。所以,为了避免这个问题,我们可以打印指针的值,而不是 number
的地址。第一次运行程序时,我们看到的是零,第二次运行时会看到一个不同的数字。未初始化的指针问题在于,如果我们使用它,可能会访问到不应该访问的内存区域,这时操作系统会终止程序,并提示内存访问违规。因此,最佳实践是始终初始化指针。我们可以把它初始化为另一个变量的地址,或者如果我们不确定它指向什么值,可以将其初始化为 nullptr
(现代 C++ 中的空指针)。nullptr
是一个不指向任何内存位置的特殊关键字。
使用空指针的好处在于,过去的 C++ 或 C 版本中,我们可能会用 NULL
或 0
来表示空指针,而现代 C++ 推荐使用 nullptr
,这是首选的做法。作为最佳实践,始终初始化指针。如果我们没有指定指向哪个变量,最好将它初始化为 nullptr
。在需要使用这个指针时,我们可以先检查它是否为 nullptr
,如果不是空指针,再使用它。
现在,指针已经初始化为 number
的地址。那我们可以对这个指针做什么呢?我们有一个称为间接引用(dereferencing)或解引用(indirection)的运算符。我们可以在指针前加上星号 *
,通过这个操作符,我们可以访问指针指向的内存位置上的数据。比如,如果我们打印出通过指针解引用后的值,就能看到 number
的值,10。
现在,使用这个语法,我们还可以更改目标内存位置上的值。我们可以通过解引用指针并将其设置为 20,这样我们就改变了目标内存位置上的值,所以如果我们打印 number
,就会看到 20。
让我们快速回顾一下:我们在这行代码中使用的是解引用(dereferencing)或间接引用(indirection)运算符,而在那行代码中,&
是取地址运算符。
最后一个小细节,在这节课中我将星号 *
放在数据类型后面,但我们也可以将其放在变量名之前。之所以我不喜欢这种写法,是因为它可能会与解引用运算符混淆。因此,在声明指针时,我更倾向于将星号放在数据类型后面。
练习题:
现在给你一个练习题,猜猜这段程序的输出是什么。如果我们打印出 X
和 Y
的值,最后我们会看到什么呢?请花一分钟时间,理解每一行代码在做什么。
答案:
在第 6 和第 7 行,我们声明了两个整数变量。在第 8 行,我们声明了一个整数指针,并将其初始化为指向 X
的地址。然后,通过解引用运算符,我们访问到存储 X
的内存位置,得到它的值 10,然后将其乘以 2。所以,在这一点上,如果你打印 X
,它的值应该是 20。
接着,在第 10 行,我们将指针指向 Y
的地址,所以现在这个指针指向了一个不同的变量。然后,同样地,我们通过解引用运算符,访问 Y
的内存位置,得到它的值 20,并将其乘以 3。最终,程序结束时,X
的值为 20,Y
的值为 60。
好的,现在我们来看一下指针与常量结合的三种情况。
第一种情况:数据是常量,但指针不是常量
在这种情况下,我们可以将指针指向其他的变量。换句话说,数据的值是常量,但是指针本身可以在之后指向其他地方。为了演示这个场景,我将声明一个整数 X
,并声明一个指针,将其初始化为指向 X
的地址。现在,如果我们将 X
声明为常量整数,编译器会报错,因为我们不能让一个整数指针指向常量整数。此前,我提到过,如果 X
是 double
类型,编译器也会报错,因为一个整数指针不能指向一个 double
类型的值,类型必须一致。
如果 X
是常量整数,那么我们的指针应该是一个常量整数指针。换句话说,数据是常量,但指针不是常量。此时,我们可以使用间接引用运算符(解引用运算符)来访问 X
的内存位置,但尝试修改 X
的值会导致编译错误,因为 X
是常量。因此,如果我们声明一个新变量 Y
,并将指针指向 Y
,就没有问题了,因为我们的指针仍然可以指向不同的变量。
第二种情况:指针是常量,但数据不是常量
接下来,假设我们希望声明一个常量指针。要定义一个常量指针,我们应该把 const
关键字放在星号 *
后面,这样我们的指针就变成了常量指针。一旦我们声明并初始化了常量指针,就不能再改变它指向的地址了。如果我们尝试将指针重新指向 Y
的地址,编译器会报错,因为常量指针的值(即它指向的地址)是不可修改的。
在这种情况下,最好在声明常量指针时立即初始化它。如果我们不初始化它,编译器会报错,因为以后我们不能重新设置常量指针。
第三种情况:数据和指针都是常量
最后,我们来看第三种情况,数据和指针都是常量。在这种情况下,首先,我们将 X
声明为常量整数,并将指针也声明为常量指针。这时候,我们的指针指向的内存地址和数据的值都无法修改。
这种写法看起来可能有些奇怪,但它的意思是我们有一个常量指针,它指向一个常量整数。换句话说,我们的指针是常量,并且指向的值也是常量。
通过这三种场景,我们可以更好地理解如何使用常量指针,并避免编程中的常见错误。
好,现在让我们来看一下指针的第一个应用。之前我提到过,使用指针可以高效地在函数之间传递大量数据。与其复制数据,不如传递数据的引用。课程的第一部分我们通过引用参数来解决这个问题,那是现代且推荐的方式,但我们也可以通过指针来解决这个问题。
让我们先看一个例子。我定义了一个名为 increase_price
的函数,它有一个类型为 double
的参数 price
,我们将在函数中将 price
的值乘以 1.2。
在我们的 main
函数中,我们声明一个 double
类型的变量 price
,然后调用 increase_price
函数并传递 price
,最后打印出 price
的值。你知道,当我们运行这个程序时,看到的 price
是原始值,而不是更新后的值,因为默认情况下,C++ 中的参数是按值传递的。因此,当我们调用这个函数并传递 price
时,实际传递的是 price
的副本,这个副本在函数中作为局部变量存在。
在课程的第一部分,我们用引用参数来解决了这个问题。我们只需要在参数前加上一个 &
,这就声明了一个引用参数。这样,当我们调用函数并传递 price
时,实际上是传递了变量的引用,而不是其副本。因此,在函数中对变量 price
所做的任何更改,在函数外部也是可见的。
但是,我们也可以使用指针来解决这个问题,不过会稍微复杂一些。首先,我们需要将参数声明为 double*
类型,也就是指向一个现有变量的指针。
现在,我们会遇到编译错误,因为我们不能直接将一个指针与常量值相乘。这里的关键是使用解引用(间接引用)运算符来访问存储在该内存位置的值。所以,我们需要通过指针解引用来获取该值并将其乘以 1.2。
另外,编译器会提示第二个错误,因为我们不能将一个数值直接传给指针。我们必须传递 price
的地址。因此,正确做法是将 price
的地址传递给这个函数。
总结来说,我们需要做三个修改来使用指针解决这个问题:
&
将变量的地址传递给函数。虽然使用指针来传递数据更复杂,但它可以解决相同的问题。最终,程序运行结果和使用引用参数时是一样的。
总的来说,像这种情况最好还是使用引用参数,因为它更简洁且不容易出错。但我在这里解释使用指针来传递数据,是因为你可能会遇到老旧项目中仍然使用指针来传递数据的情况。
现在,我想让你实现一个交换函数,使用指针交换两个变量的值。假设我们有两个整数指针作为参数。花几分钟理解这个问题,然后尝试自己实现。
解题思路:
我将定义一个名为 swap
的函数,接收两个整数指针参数 first
和 second
。与之前一样,我们需要一个临时变量来交换这两个值。我将声明一个名为 temp
的整数变量,并用它来暂时存储数据。
我们将把第一个指针指向的值存储在 temp
中。然后我们将第一个指针指向的值设置为第二个指针指向的值。最后,我们将第二个指针指向的值设置为 temp
的值。
void swap(int* first, int* second) {
int temp = *first; // 将 first 指向的值存储到 temp 中
*first = *second; // 将 second 指向的值赋给 first
*second = temp; // 将 temp 的值赋给 second
}
接下来,在 main
函数中,我会声明两个整数 X
和 Y
,并将它们的地址传递给 swap
函数。然后,打印 X
和 Y
的值,我们应该看到交换后的结果。
int main() {
int X = 10;
int Y = 20;
swap(&X, &Y); // 传递 X 和 Y 的地址
std::cout << "X: " << X << ", Y: " << Y << std::endl; // 输出交换后的值
return 0;
}
当我们运行这个程序时,X
的值应该变成 20,Y
的值应该变成 10。
通过这次练习,你能更好地理解如何使用指针来传递数据并在函数中修改它们的值。
让我们来讨论一下数组和指针之间的关系,以便让你完全理解这一概念。我将声明一个整数数组并初始化它为三个数字。这里的 numbers
实际上是指向数组第一个元素的指针。你知道,如果我们打印 numbers
,我们会看到一个十六进制的数字,这个数字表示的是内存地址,也就是数组第一个元素的地址。如果我们使用解引用运算符(*
),我们应该会看到数组第一个元素的值,即 10
。
我们可以声明一个整数指针,并用数组来初始化它。编译器会把这个数组当作一个整数指针来处理。因此,我们可以将这个指针初始化为数组的地址。如果我们打印 pointer
,我们会看到数组第一个元素的内存地址。如果我们使用解引用运算符,我们将看到第一个元素的值。我们还可以使用括号表示法([]
)来访问数组中的其他元素,比如第二个元素。
接下来,让我们定义一个函数,该函数接受一个整数数组作为参数。函数的定义如下:
void printNumbers(int numbers[]) {
// 打印数组中的元素
}
编译器将把 numbers
当作一个指针来处理。这也是为什么在这个函数中我们不能将 numbers
数组传递给一个求数组大小的函数,比如 size()
,因为 numbers
是一个指针,它只包含内存地址,不能传递到 size()
函数来得出数组的大小。我们无法知道数组有多少个元素。即使我们有五个元素或十个元素,size()
函数也无法知道这些信息。正因为如此,我们也不能在这里使用基于范围的 for
循环,如 for (int number : numbers)
,这会导致编译错误,因为我们不能在一个内存地址上进行迭代。
在这里需要注意的是,因为 numbers
被视为一个整数指针,所以我们的数组是按引用传递的。也就是说,传递到函数中的数组实际上是对原数组的引用。任何在函数中对数组的修改,都将在外部函数中可见。为了验证这一点,我们可以将数组的第一个元素修改为 0
,然后在外部打印数组的第一个元素:
void printNumbers(int numbers[]) {
numbers[0] = 0; // 修改数组第一个元素
}
int main() {
int numbers[] = {10, 20, 30};
printNumbers(numbers); // 调用函数修改数组
std::cout << numbers[0] << std::endl; // 打印数组第一个元素
return 0;
}
当我们运行这个程序时,输出将会是 0
,因为我们在 printNumbers
函数中修改了数组的第一个元素。
函数参数中的数组是按引用传递的,这样做是出于效率考虑。当我们将数组从一个函数传递到另一个函数时,我们不希望复制数组中的所有值。因为数组可能非常大,可能包含数千个或数百万个元素。如果每次传递数组时都要复制整个数组,那将消耗大量的时间和内存。因此,C++ 编译器总是通过指针的方式传递数组,这使得在函数之间传递数组变得高效。
让我们来讨论一下我们可以对指针执行的数学运算。我将声明一个整数数组并初始化它为三个值。接着,我将声明一个整数指针并将其初始化为零。现在,这个指针指向数组的第一个元素。该指针所持有的地址是 100
。为了避免使用复杂的十六进制数,我们将使用简单的十进制数。
当我们将指针加 1 时,其值并不会变成 101
,而是变成 104
。原因在于,指针的增量与数据类型的大小有关。在这种情况下,整数的大小是 4 字节。因此,指针加 1 时,它指向的地址将增加 4,而不是简单地加 1。可以想象数组中每个整数占用了 4 字节的内存空间,所以数组的第一个元素的地址是 100
,而第二个元素的地址是 104
。
举个例子:
int numbers[] = {10, 20, 30}; // 定义并初始化数组
int* ptr = numbers; // 定义指针并初始化为指向数组的第一个元素
ptr++; // 增加指针,指向下一个元素
当我们增加指针的值时,指针会跳到数组中的第二个元素,因此,如果我们使用解引用操作符(*
),我们将看到数组的第二个元素的值。例如:
std::cout << *ptr << std::endl; // 输出:20
类似地,我们也可以通过减小指针来使它回到数组的第一个位置。当我们减少指针时,它指向的地址将减去数据类型的大小(在本例中是 4 字节),因此指针回到原来的位置。
我们也可以使用加法和减法操作符来移动指针。比方说,如果我们想要将指针移动到数组的第二个元素,可以写出如下表达式:
*(ptr + 1); // 这等价于 numbers[1]
同样,我们可以通过解引用该地址来访问数组中的元素:
std::cout << *(ptr + 1) << std::endl; // 输出:20
上述表达式和使用 []
操作符来访问数组的元素效果相同:
std::cout << numbers[1] << std::endl; // 输出:20
这三种写法是等价的。显然,括号表示法更简单、更简洁、更易于理解。因此,推荐使用数组下标([]
)这种表示法,而不是通过指针运算来进行访问。实际上,编译器会在底层将表达式转换为类似的指针运算,但是没有必要使用那种更复杂的方式。
除了加法和减法之外,我们不能对指针进行乘法或除法操作。如果尝试将指针乘以某个值,例如:
ptr = ptr * 2; // 编译错误
这会导致编译错误,因为指针只能执行加法和减法运算,不能进行乘法和除法。这是因为乘法和除法操作在内存地址层面上并没有实际意义,指针的操作是基于它所指向的数据类型的大小进行的,不能直接通过数值的乘除来改变它指向的地址。
*(ptr + 1)
来访问数组的第二个元素。通过这些操作,我们可以更灵活地操作内存中的数据和数组,使得代码更高效且易于理解。
现在我们可以使用所有你学过的比较操作符来对指针进行比较,但是你需要记住,实际上我们比较的是指针所存储的内存地址。下面是一个例子:
假设我们声明两个整数 x
和 y
,以及两个整数指针。我们可以将第一个指针命名为 prX
,让它指向 x
,将第二个指针命名为 prY
,让它指向 y
。如果我们写出类似于 prX < prY
的表达式,那么我们实际上是在比较这两个指针所存储的地址。
如果我们想要比较实际的值(比如 10
和 20
),那么我们必须解引用这些指针。也就是说,如果你想比较指针所指向的值,就需要通过解引用来获取值进行比较:
if (*prX < *prY) {
// 比较实际的值
}
然而,并不总是需要解引用指针进行比较。有时候,我们可能只想比较地址。例如,我们可以将 prY
改为指向 x
,这时我们有两个指针指向同一个变量。我们可以写出一个布尔表达式:
if (prX == prY) {
// 如果两个指针指向相同的地址
}
这段代码的意思是,两个指针指向的是同一个内存位置。如果条件成立,我们可以输出类似“same”的消息:
std::cout << "same" << std::endl;
在进行指针操作时,特别是在使用指针之前,我们需要确保指针不是空指针。一个好的编程实践是在解引用指针之前先检查它是否为空。比如,在我们尝试解引用 prX
之前,我们应该先验证它是否为非空指针:
if (prX != nullptr) {
std::cout << *prX << std::endl;
}
这种方式确保我们在使用指针之前,它已经指向了有效的内存地址。
假设我们有一个整数数组,任务是创建一个指针指向数组的最后一个元素,并通过 while
循环反向遍历数组,打印每个元素。目标是打印数组中的元素,顺序是从后往前,比如 30
、20
、10
。
首先,我们声明一个整数数组并初始化它:
int numbers[] = {10, 20, 30};
接着,我们声明一个指针,并将它初始化为指向数组最后一个元素的地址。我们可以通过获取数组大小并减去 1 来实现这个目标,而不必硬编码数组的最后一个索引:
int* ptr = &numbers[2]; // 指向数组的最后一个元素
或者,使用更通用的写法:
int* ptr = numbers + (sizeof(numbers) / sizeof(numbers[0])) - 1; // 计算数组的最后一个元素
现在我们可以使用 while
循环来遍历数组,并且在每次循环中打印当前的值,然后将指针向前移动(指向前一个元素)。我们会一直进行循环,直到指针到达数组的第一个元素为止:
while (ptr >= numbers) {
std::cout << *ptr << std::endl;
ptr--; // 移动指针到前一个元素
}
在这个循环中,首先打印的是数组的最后一个元素 30
,然后是 20
,最后是 10
。这是一个有效的反向遍历数组的例子。
nullptr
,以避免解引用空指针。while
循环,我们可以反向遍历数组并打印每个元素。这些技巧使得指针操作更为灵活且高效,在处理内存和数组时非常有用。
当你声明一个整数数组,比如:
int numbers[10];
这个数组的大小是固定的,最多只能容纳 10 个数字。那么,如果在程序运行时,我们从用户那里获取的数据超过了 10 个数字,或者我们从文件中读取了更多的数据,怎么办呢?我们可以通过将数组的大小增加到 100 或 1000,甚至更多来解决这个问题,但这总是会有一个限制。更糟糕的是,如果用户只输入了一个数字,我们就浪费了大量的内存空间来存储这一单一值。
这时,动态内存分配就派上了用场。通过动态内存分配,我们可以在程序运行时根据实际需要调整数组的大小,而不是一开始就为可能不需要的元素分配内存空间。
动态内存分配允许我们在程序运行时根据需求分配内存。为了实现这一点,我们使用 new
运算符而不是常规的声明语法。下面是如何使用 new
运算符分配动态内存的示例:
int* numbers = new int[10]; // 动态分配一个能存储10个整数的数组
在这个例子中,new
运算符为 numbers
分配了一个大小为 10 的整数数组,并返回该数组的指针。这个指针被存储在 numbers
变量中。使用这种方式声明的变量存储在内存的堆区(heap)中,而不是栈区(stack)。堆和栈的区别如下:
new
运算符分配的内存不会自动释放,必须显式地使用 delete
运算符来释放。堆区的内存不会像栈区的内存那样自动回收。程序员需要负责释放堆区分配的内存,否则会造成内存泄漏(memory leak)。内存泄漏会导致程序占用越来越多的内存,最终导致程序崩溃。
例如:
delete[] numbers; // 释放动态分配的内存
上述代码会释放通过 new
运算符分配的内存。在这里,numbers
是一个指向整数数组的指针,因此我们使用 delete[]
来释放这个数组的内存。
除了分配数组外,new
运算符还可以用于分配单个变量。例如:
int* number = new int; // 动态分配一个整数
这会在堆区分配一个整数,并返回该整数的指针。这时,我们不需要使用方括号,因为我们只分配了一个整数,而不是一个数组。虽然在这种情况下可以使用 new
来分配内存,但通常情况下我们更倾向于使用栈区的变量,因为栈区变量的生命周期由编译器自动管理,无需担心内存释放的问题。
当我们不再使用动态分配的内存时,应该显式地释放它:
delete number; // 释放单个整数的内存
在释放内存之后,最好将指针重置为 nullptr
,以避免悬挂指针问题。悬挂指针是指向已经释放内存的指针,如果继续使用它会导致不可预期的行为。
number = nullptr; // 重置指针
numbers = nullptr; // 同样可以重置数组指针
new
运算符可以在程序运行时动态分配内存,避免了预先设定固定大小的问题。new
运算符分配内存时,程序员需要在使用完毕后通过 delete
运算符释放内存,否则会导致内存泄漏。nullptr
,避免悬挂指针的使用。这些基本概念为后续涉及更复杂的内存管理(如使用指针动态创建复杂数据结构)打下了基础。
在这部分,我们将展示如何使用指针动态调整数组的大小。这个过程涉及到创建一个初始大小的数组,并在数组满时自动调整其大小以容纳更多元素。接下来,我会带你一步步完成实现。
首先,我们声明一个大小为 5 的整数数组,并创建一个指针来存储该数组的地址。然后,我们创建一个无限循环,持续要求用户输入数字并将其存储在数组中。为了跟踪用户输入的数字数量,我们还需要一个变量 entries
来记录当前输入的数字个数,初始值为 0。
代码如下:
int numbers[5]; // 创建一个大小为 5 的数组
int* ptr = numbers; // 指针指向数组的起始地址
int entries = 0; // 记录输入的数字数量
// 无限循环,持续获取用户输入
while (true) {
int num;
cout << "Enter a number: ";
cin >> num;
// 如果用户输入无效(非数字),则退出循环
if (cin.fail()) {
break;
}
// 存储输入的数字
numbers[entries] = num;
entries++; // 增加已输入的数字数量
// 如果已输入数字达到了数组的最大容量,则需要重新调整数组大小
if (entries == 5) {
// 这里将会处理动态调整数组大小的部分
}
}
// 输出所有已输入的数字
for (int i = 0; i < entries; i++) {
cout << numbers[i] << " ";
}
当用户输入的数字数量达到了数组的最大容量(例如 5 个数字)时,我们需要调整数组的大小以容纳更多的数据。这个过程包括以下几个步骤:
delete
运算符释放原数组的内存,以避免内存泄漏。我们来看如何实现这些步骤:
if (entries == 5) {
// 创建一个大小为 10 的临时数组
int* temp = new int[10];
// 将原数组中的元素复制到临时数组中
for (int i = 0; i < entries; i++) {
temp[i] = numbers[i];
}
// 让原指针指向新的数组
delete[] numbers; // 释放原数组的内存
numbers = temp; // 更新原指针指向新的数组
}
为了使数组的大小更加灵活,我们可以引入一个 capacity
变量,而不是硬编码数组的初始大小(5)。capacity
表示当前数组的容量,而 entries
表示已使用的元素数量。每当数组满时,我们就将 capacity
扩大一倍。
int capacity = 5; // 初始容量
int* numbers = new int[capacity]; // 创建一个初始大小的数组
int entries = 0; // 记录当前输入的数字个数
while (true) {
int num;
cout << "Enter a number: ";
cin >> num;
// 如果用户输入无效(非数字),则退出循环
if (cin.fail()) {
break;
}
// 如果数组已满,则重新分配内存
if (entries == capacity) {
// 增加容量
capacity *= 2;
// 创建新的数组,并将数据复制到新数组中
int* temp = new int[capacity];
for (int i = 0; i < entries; i++) {
temp[i] = numbers[i];
}
// 释放原数组的内存
delete[] numbers;
// 更新指针指向新的数组
numbers = temp;
}
// 存储用户输入的数字
numbers[entries] = num;
entries++; // 增加已输入的数字数量
}
// 输出所有已输入的数字
for (int i = 0; i < entries; i++) {
cout << numbers[i] << " ";
}
完成程序后,不要忘记释放所有动态分配的内存。最后,确保删除指向数组的指针:
delete[] numbers; // 释放动态分配的内存
在这个例子中,我们学习了如何使用指针进行动态内存分配,并在数组满时自动调整数组大小。主要步骤如下:
new
运算符动态分配内存。delete[]
运算符释放不再需要的内存,避免内存泄漏。这种方式可以确保我们的程序能够根据需求动态调整数组大小,充分利用内存,并且避免内存浪费。
虽然我们手动实现了动态内存分配和数组扩展的功能,但 C++ 标准库提供了更方便的容器类 std::vector
,它自动处理了动态内存分配和调整大小。我们将在后续课程中讨论 std::vector
的使用。
在这部分,我们学习了当使用 new
运算符分配内存时,我们必须记得使用 delete
运算符来释放内存。如果我们忘记释放内存,程序将无法重用这些已分配的内存。随着内存的不断分配,程序将消耗越来越多的内存,最终会导致内存不足并崩溃,这就是我们所说的“内存泄漏”。内存泄漏意味着程序不断消耗内存而不释放,最终可能会导致程序崩溃。
假设我们不释放内存,会导致什么情况发生。下面是一个简单的示例:
int* ptr = new int(10); // 分配内存
// 忘记释放内存,导致内存泄漏
在这个例子中,我们使用 new
运算符分配了一个整数类型的内存,但如果没有调用 delete
来释放它,程序就会造成内存泄漏。
另一个需要注意的情况是 双重删除,即尝试释放同一块内存两次。这是一个非常危险的操作,因为它可能导致程序崩溃。来看看下面的代码:
int* ptr = new int(10); // 分配内存
delete ptr; // 第一次删除
delete ptr; // 第二次删除,程序崩溃
这里,第一次 delete
会释放内存,而第二次 delete
尝试释放同一块内存,这会导致程序崩溃,因为该内存区域已经被释放,再次释放它是无效的,并且可能会破坏程序的内存管理系统。
在现实中的 C++ 程序中,管理指针和内存非常复杂。我们可能会有数十、数百甚至数千个函数,这些函数可能会在各个地方创建指针来分配内存。在这种情况下,记住何时以及在哪里删除这些指针是非常困难的。而且,我们还需要小心,避免重复删除同一个指针。
为了解决这个问题,C++ 引入了 智能指针(Smart Pointers),它们让我们不再需要手动调用 delete
来释放内存。智能指针通过自动管理内存来减少内存泄漏和错误释放的问题。
智能指针的主要好处是,它们可以像普通的变量一样使用,不需要关心何时释放内存。智能指针会在其生命周期结束时自动释放内存,从而避免了内存泄漏。
在现代 C++ 中,主要有两种类型的智能指针:unique_ptr
和 shared_ptr
。
unique_ptr
:表示独占所有权的智能指针。每个 unique_ptr
只能有一个所有者,且该所有者在销毁时自动释放内存。当 unique_ptr
被销毁时,指向的内存将被释放,不需要手动调用 delete
。shared_ptr
:表示共享所有权的智能指针。多个 shared_ptr
可以指向同一块内存,当最后一个 shared_ptr
被销毁时,内存才会被释放。下面是如何使用 unique_ptr
和 shared_ptr
的简单示例:
unique_ptr
示例:
#include <memory> // 引入智能指针头文件
void example() {
std::unique_ptr<int> ptr(new int(10)); // 创建一个 unique_ptr
// 不需要手动释放内存,ptr 会在离开作用域时自动释放
}
shared_ptr
示例:
#include <memory> // 引入智能指针头文件
void example() {
std::shared_ptr<int> ptr1 = std::make_shared<int>(10); // 创建一个 shared_ptr
std::shared_ptr<int> ptr2 = ptr1; // ptr2 和 ptr1 共享内存
// 当所有指向该内存的 shared_ptr 都销毁时,内存会自动释放
}
在现实世界中的 C++ 编程中,手动管理内存非常复杂且容易出错。为了避免内存泄漏和其他内存管理问题,C++ 提供了 智能指针,它们可以自动管理内存,减少我们在编程时的负担。
unique_ptr
适用于独占所有权的情况;shared_ptr
适用于共享所有权的情况。在后续的课程中,我们将详细讨论如何使用这两种智能指针来管理内存。
智能指针不仅使得内存管理变得更加安全,而且它们的使用也让代码更加简洁和易于维护。
unique_ptr
是 C++ 中的一种智能指针类型,它拥有它所指向的内存块。一个 unique_ptr
只能指向一块内存,这意味着不能有两个 unique_ptr
指向同一块内存。如果需要多个指针共享内存,应该使用 shared_ptr
,这是下一个课题。现在,我们来看看如何在 C++ 中使用 unique_ptr
。
为了使用 unique_ptr
,我们需要在程序中引入一个标准库文件:
#include <memory> // 引入内存管理功能
在这个文件中,C++ 提供了一个名为 unique_ptr
的类。我们将在后续的课程中详细讨论类的概念,但现在你可以将类视为包含多个功能(函数)的“构建块”。unique_ptr
是一个模板类,可以用于管理不同类型的内存。
unique_ptr
要创建 unique_ptr
,我们需要指定它所管理的类型。接下来,我们通过 new
运算符来分配内存:
std::unique_ptr<int> x(new int); // 创建一个 unique_ptr,管理一个整数
这行代码中,unique_ptr<int>
表示我们创建了一个管理 int
类型的智能指针 x
。new int
用于在堆上分配一个整数,并将它的地址传递给 unique_ptr
。这样,x
就拥有了这块内存,并且我们不需要担心手动删除内存,因为 unique_ptr
会自动处理。
unique_ptr
与常规指针类似,我们可以通过 unique_ptr
来访问指向的内存:
*x = 10; // 通过 unique_ptr 设置值
std::cout << *x << std::endl; // 打印指针指向的值
这样,我们就可以使用 *x
来访问 unique_ptr
指向的整数,并为其赋值。此时,我们可以像普通指针一样使用 unique_ptr
,但是它会在作用域结束时自动释放内存。
make_unique
创建 unique_ptr
C++11 引入了一个更简洁的方式来创建 unique_ptr
,我们可以使用 make_unique
函数:
auto y = std::make_unique<int>(10); // 创建一个 unique_ptr,指向值为 10 的整数
在这个例子中,make_unique<int>(10)
会创建一个 unique_ptr
,并初始化为值 10。auto
关键字让编译器自动推导出类型,避免了重复声明类型的麻烦。
unique_ptr
unique_ptr
不仅可以用于管理单个对象,也可以用于管理数组。我们可以像下面这样创建一个指向整数数组的 unique_ptr
:
std::unique_ptr<int[]> numbers = std::make_unique<int[]>(10); // 创建一个大小为 10 的整数数组
这个 unique_ptr
会管理一个有 10 个整数元素的动态数组。我们可以通过数组下标来访问和修改数组中的元素:
numbers[0] = 5; // 给数组的第一个元素赋值
std::cout << numbers[0] << std::endl; // 输出第一个元素的值
unique_ptr
的特点unique_ptr
只能有一个所有者。当 unique_ptr
超出作用域时,它会自动释放指向的内存。unique_ptr
不能进行指针算术操作(例如递增或递减指针),这是它的一项限制。unique_ptr
不需要手动调用 delete
来释放内存。它会在生命周期结束时自动处理内存释放。通过 unique_ptr
,我们可以更安全地管理内存,避免手动删除内存带来的错误和复杂性。C++ 的智能指针 unique_ptr
会自动管理内存,确保我们不必担心内存泄漏或重复删除内存。
下一个课题将会讨论 shared_ptr
,它允许多个指针共享对同一块内存的所有权,并在所有者都不再使用该内存时自动释放它。
在 C++ 中,shared_ptr
是另一种智能指针,它允许多个指针共享对同一块内存的所有权。这与 unique_ptr
不同,后者保证一个内存位置只能有一个指针拥有。在一些场景中,我们需要多个指针共享内存,此时 shared_ptr
就派上了用场。
与 unique_ptr
类似,要使用 shared_ptr
,我们首先需要引入 <memory>
文件:
#include <memory> // 引入内存管理功能
在这个文件中,C++ 提供了一个名为 shared_ptr
的类,也同样是一个模板类,可以用于管理不同类型的内存。
shared_ptr
与 unique_ptr
类似,我们可以用 shared_ptr
创建一个指针并初始化它。这里有两种方法:
方法一:直接使用 shared_ptr
类
std::shared_ptr<int> x(new int); // 创建一个 shared_ptr,指向一个整数
这行代码中,std::shared_ptr<int>
表示创建一个 shared_ptr
,它管理一个 int
类型的整数。new int
在堆上分配了内存,并将地址传递给 shared_ptr
,这时 x
拥有对这块内存的所有权。
方法二:使用 make_shared
函数
为了简化代码,可以使用 make_shared
函数:
auto x = std::make_shared<int>(10); // 创建一个 shared_ptr,指向值为 10 的整数
make_shared<int>(10)
创建了一个指向整数 10 的 shared_ptr
。此时我们使用了 auto
关键字来让编译器自动推导类型,避免了重复写类型的麻烦。
shared_ptr
一旦创建了 shared_ptr
,我们可以像普通指针一样使用它:
std::cout << *x << std::endl; // 输出 shared_ptr 指向的整数值
*x = 20; // 修改 shared_ptr 指向的整数值
std::cout << *x << std::endl; // 输出修改后的值
shared_ptr
会自动管理内存,当所有 shared_ptr
不再使用该内存时,内存会被自动释放。
shared_ptr
指向同一内存最特别的地方是,多个 shared_ptr
可以共享同一块内存。当多个指针指向同一内存时,内存会被保留直到最后一个指向它的 shared_ptr
被销毁为止。
std::shared_ptr<int> x = std::make_shared<int>(10); // 创建第一个 shared_ptr,指向整数 10
std::shared_ptr<int> y = x; // 创建第二个 shared_ptr,指向同一内存
std::cout << *x << std::endl; // 输出 10
std::cout << *y << std::endl; // 也输出 10
在这个例子中,x
和 y
都指向同一块内存,因此输出的值都是 10。
shared_ptr
的强大之处在于它的内存管理。它会自动跟踪有多少个 shared_ptr
指向同一块内存,只有当最后一个指针被销毁时,内存才会被释放。这解决了手动管理内存的问题,避免了内存泄漏。
shared_ptr
允许多个指针共享对同一块内存的所有权,内存会在最后一个 shared_ptr
被销毁时自动释放。make_shared
函数可以简化 shared_ptr
的创建,并且通过 auto
关键字进一步减少冗余代码。shared_ptr
自动管理内存,减少了手动调用 delete
的风险,避免了内存泄漏。通过 shared_ptr
,我们可以在 C++ 中轻松地实现内存共享和自动管理,尤其在需要多个对象共同访问数据时,它是一个非常有用的工具。
在这一部分中,我们将开始介绍 C 语言中的字符串,并简要讲解它们是如何工作的以及它们的局限性。接下来,我们将深入了解 C++ 中的字符串,并展示它们的优势。我们还将讨论一些常见的操作技术,例如修改字符串、搜索字符串、提取子字符串以及字符串与数字之间的转换等。让我们开始吧!
在 C 语言中,字符串是通过字符数组来表示的。每个字符串都是以 \0
(空字符)结束的字符序列。你需要手动处理字符串的长度和内存管理,这对于新手来说可能会有一些挑战。
char str[50]; // 定义一个字符数组,大小为 50
strcpy(str, "Hello, World!"); // 使用 strcpy 函数来复制字符串
在上面的例子中,我们定义了一个字符数组 str
,它有足够的空间来存储 50 个字符。然后,使用 strcpy
函数将字符串 "Hello, World!"
复制到该数组中。
C++ 引入了 std::string
类,提供了更高级和方便的字符串操作。与 C 语言的字符数组不同,std::string
使得字符串的创建、操作和管理变得更加简便和安全。
#include <iostream>
#include <string>
int main() {
std::string str = "Hello, C++!"; // 使用 std::string 初始化字符串
std::cout << str << std::endl; // 输出字符串
return 0;
}
在这个例子中,std::string
自动管理内存,不需要显式地指定大小,并且提供了许多方便的成员函数来操作字符串。
std::string
会自动处理内存的分配和释放,不需要手动管理内存。std::string
可以动态调整大小,当你向字符串中添加更多内容时,它会自动扩展。std::string
提供了许多用于修改、搜索、连接等操作的成员函数,大大简化了字符串的处理。std::string
提供了多种方法来修改字符串,例如:
std::string str = "Hello, World!";
str.replace(7, 5, "C++"); // 将 "World" 替换为 "C++"
std::cout << str << std::endl; // 输出 "Hello, C++!"
我们可以使用 find
方法在字符串中搜索子串:
std::string str = "Hello, C++!";
size_t pos = str.find("C++"); // 查找 "C++" 在字符串中的位置
if (pos != std::string::npos) {
std::cout << "Found C++ at position: " << pos << std::endl;
}
通过 substr
方法,我们可以从字符串中提取子字符串:
std::string str = "Hello, C++!";
std::string sub = str.substr(7, 3); // 提取从位置 7 开始的 3 个字符
std::cout << sub << std::endl; // 输出 "C++"
C++ 中可以使用 std::stoi
(字符串转整数)、std::stof
(字符串转浮点数)等方法进行字符串与数字之间的转换。
std::string str = "123";
int num = std::stoi(str); // 字符串转整数
std::cout << num << std::endl; // 输出 123
std::string
类,支持自动内存管理、动态大小调整,并提供了丰富的成员函数,使得字符串的操作更加简单和高效。接下来的部分,我们将进一步深入讨论如何利用 C++ 字符串处理技术解决更复杂的问题,敬请期待!
在整个课程中,我们简要提到过 string
类型。这种类型并不是 C++ 语言本身的一部分,而是标准库的一部分,定义在一个名为 string
的文件中。我们之前从未显式包含这个文件,因为每次包含 iostream
时,string
文件都会自动包含进来。
但在 C++ 中,还有另一种表示字符串的方式,那就是 C 风格字符串(C-Style Strings),它是一个你应该尽量避免的方式。总是使用 std::string
类型会更好,但在本节中,我会简要讲解 C 风格字符串,主要有两个原因:一是你可能需要维护一些 C++ 代码,这些代码在 string
类型创建之前编写;二是很多大学课程会教授 C 风格字符串,你可能需要学习它们作为课程的一部分。
C 风格字符串实际上是一个特殊的字符数组。我们通过定义一个字符数组来表示字符串,并为它指定一个初始大小。由于我们依赖的是数组,因此我们必须确保为字符串分配足够的空间。否则,如果分配的空间过小,可能会发生内存溢出;如果分配的空间过大,就可能浪费内存。这也是为什么总是建议使用 std::string
类型的原因,因为它会动态地调整内存的使用。
假设我们要声明一个大小为 5 的字符数组,这意味着该数组最多可以存储 4 个字符,因为最后一个字符必须是我们称之为“空终止符”的特殊字符 \0
。
char name[5] = {'M', 'O', 'S', '\0'}; // 这样初始化字符数组
我们也可以使用更简便的方式来初始化:
char name[] = "MOS"; // 这样初始化会自动添加 \0 作为结尾符
在这个例子中,我们声明了一个字符数组 name
,并通过字符串字面量 "MOS"
初始化它。字符串字面量会自动在末尾添加一个空终止符 \0
,所以我们不需要手动添加。
由于我们在处理的是字符数组,我们可以像访问数组中的其他元素一样通过索引访问字符串中的字符。例如,我们可以修改字符串中的第一个字符:
name[0] = 'm'; // 修改第一个字符为小写 'm'
字符数组中的字符是通过“字符字面量”来表示的,字符字面量是用单引号括起来的字符:
name[0] = 'm'; // 这里的 'm' 是字符字面量
我们还可以读取字符串中的字符,例如:
std::cout << name[0]; // 输出 'm'
C 风格字符串有一组函数可以帮助我们操作它们,这些函数定义在另一个文件 cstring
中。iostream
文件会默认包含 cstring
文件,所以我们无需显式地包含它。
使用 strlen
函数可以获取 C 风格字符串的长度(不包括空终止符)。
#include <cstring>
std::cout << strlen(name); // 输出字符串的长度,不包括 '\0'
我们不能像 std::string
那样使用 +
运算符来拼接 C 风格字符串。如果要拼接字符串,我们需要使用 strcat
函数。strcat
函数将第二个字符串连接到第一个字符串的末尾:
char lastName[10] = "Dolly"; // 定义另一个字符数组
strcat(name, lastName); // 将 lastName 拼接到 name 后面
std::cout << name; // 输出 "MOSDolly"
这里有一个潜在的问题:我们声明的数组 name
只有 5 个元素,但拼接后的字符串长度超过了 5,因此会越界写入,可能会导致内存错误。为了避免这种问题,我们应该确保为数组分配足够的空间,例如 50 个元素。
复制字符串时,不能直接使用赋值操作符。要复制一个字符串到另一个字符串中,我们需要使用 strcpy
函数:
char lastName[10];
strcpy(lastName, name); // 将 name 复制到 lastName 中
std::cout << lastName; // 输出 "MOS"
同样,这里也存在一个潜在的问题:如果目标数组的大小不足以容纳源字符串,我们将越界写入。为了避免这种情况,我们应该确保目标数组足够大。
C 风格字符串不能直接使用 ==
运算符进行比较,因为 ==
比较的是两个数组的内存地址,而不是它们的内容。要比较两个字符串,我们需要使用 strcmp
函数:
if (strcmp(name, lastName) == 0) {
std::cout << "Equal!" << std::endl;
} else {
std::cout << "Not Equal!" << std::endl;
}
strcmp
会返回一个整数。如果两个字符串相等,它返回 0;如果第一个字符串在字典顺序上小于第二个字符串,则返回一个负数;如果第一个字符串大于第二个字符串,则返回正数。
尽管 C 风格字符串提供了一些基本的功能,但它们有许多局限性:
std::string
类型取代,后者提供了更高效、更安全的内存管理和操作方式。在接下来的课程中,我们将深入探讨 std::string
的使用方法,详细讲解它的优势和各种操作技巧。
std::string
)C++ 提供了一个比 C 风格字符串更加方便和强大的字符串类型——std::string
。我们将讨论如何使用它以及它的优势。
std::string
的基础首先,我们声明一个名为 name
的字符串变量,并初始化它。因为 std::string
类型是标准库的一部分,如果你没有在文件顶部包含 std
命名空间,你就需要使用 std::
来限定它。
#include <string> // 包含字符串头文件
std::string name = "Josette"; // 声明并初始化字符串
std::string
实际上是一个 C++ 类,它是 C 风格字符串(C-Style String)的包装器。内部实现上,std::string
使用一个字符数组(即 C 风格字符串)来存储字符串字面量,但你并不需要直接操作这个字符数组。C++ 中没有内建的 string
类型,只有像 int
、float
和 char
这样的基本数据类型。std::string
类隐藏了与字符数组相关的复杂性,使得我们不再需要担心数组的大小、内存浪费或数组越界等问题。所有这些复杂性都被该类内部处理了。
std::string
的优势std::string
会根据需要自动调整内存,避免了内存浪费和数组越界的问题。std::string
提供了丰富的成员函数来简化这些任务。std::string
更加安全,因为它会自动处理内存和字符数组边界。std::string
进行操作就像 C 风格字符串一样,std::string
也允许通过索引访问单个字符。索引从 0 开始,因此可以直接修改字符串中的某个字符。
std::string name = "Josette";
name[0] = 'M'; // 修改字符串的第一个字符
std::cout << name[0]; // 输出 M
通过调用 std::string
的 length()
函数,可以轻松获取字符串的长度。
std::cout << name.length(); // 输出字符串的长度
std::string
提供了非常简单的拼接方式,我们可以直接使用 +
运算符来将两个字符串连接起来,而无需像 C 风格字符串那样使用 strcat
。
std::string firstName = "Josette";
std::string lastName = "Dolly";
firstName += lastName; // 直接拼接
std::cout << firstName; // 输出 "JosetteDolly"
与 C 风格字符串不同,std::string
不需要使用 strcpy
来复制字符串。我们可以像复制其他基本数据类型一样直接赋值。
std::string copyName = firstName; // 直接复制字符串
std::cout << copyName; // 输出 "JosetteDolly"
std::string
支持直接使用比较运算符(如 ==
、<
、>
)来比较两个字符串,而不需要像 C 风格字符串那样使用 strcmp
。
std::string name1 = "Josette";
std::string name2 = "Dolly";
if (name1 == name2) {
std::cout << "Names are equal.";
} else {
std::cout << "Names are not equal."; // 输出 "Names are not equal."
}
你也可以使用其他比较操作符:
if (name1 < name2) {
std::cout << "name1 comes before name2";
} else {
std::cout << "name1 does not come before name2";
}
std::string
的一些其他有用函数检查字符串是否为空:使用 empty()
函数检查字符串是否为空。
std::string name = "";
if (name.empty()) {
std::cout << "The string is empty."; // 输出 "The string is empty."
}
检查字符串是否以某个字符或子字符串开头:starts_with()
函数可以检查字符串是否以指定的字符或子字符串开始。
std::string name = "Josette";
if (name.starts_with("J")) {
std::cout << "The string starts with 'J'."; // 输出 "The string starts with 'J'."
}
检查字符串是否以某个字符或子字符串结尾:ends_with()
函数可以检查字符串是否以指定的字符或子字符串结尾。
std::string name = "Josette";
if (name.ends_with("tte")) {
std::cout << "The string ends with 'tte'."; // 输出 "The string ends with 'tte'."
}
获取字符串的第一个字符:front()
函数返回字符串的第一个字符。
std::cout << name.front(); // 输出 "J"
获取字符串的最后一个字符:back()
函数返回字符串的最后一个字符。
std::cout << name.back(); // 输出 "e"
std::string
类使得处理字符串变得简单,它隐藏了与字符数组相关的复杂性。std::string
提供了丰富的成员函数,支持字符串的拼接、复制、比较、修改和查询。相比于 C 风格字符串,C++ 的 std::string
类型在性能和易用性上都有显著的提升,是现代 C++ 编程中处理字符串的首选方式。
在本节课中,我们将讨论 std::string
类的一些常用函数,尤其是那些用于修改字符串的函数。你不需要记住这些函数的细节,只需观看视频并了解如何使用它们。如果你忘记了某些功能,可以随时返回本节课作为参考,或通过 Google 查找相关信息,学习更多函数。通过搜索 “C++ string” 你可以找到许多有用的参考资料。
std::string
类的常用函数append
函数std::string
类提供了 append
函数,用于在字符串的末尾添加另一个字符串。append
函数是重载的,有多个版本。第一个版本接受一个常量字符串引用作为参数。
为什么要使用引用参数?因为使用引用可以避免复制字符,提高效率。
std::string name = "Marcia";
name.append(" Missouri"); // 将 " Missouri" 添加到 "Marcia" 后面
std::cout << name; // 输出 "Marcia Missouri"
insert
函数insert
函数允许你在字符串的特定位置插入另一个字符串。它同样有多个重载版本,可以根据需求选择使用。
std::string name = "Marcia";
name.insert(0, "Missouri "); // 在索引 0 处插入 "Missouri "
std::cout << name; // 输出 "Missouri Marcia"
erase
函数erase
函数允许你删除字符串中的一部分。你可以指定起始位置以及删除的字符数。
std::string name = "Marcia Missouri";
name.erase(0, 2); // 从位置 0 开始,删除 2 个字符
std::cout << name; // 输出 "rcia Missouri"
clear
函数clear
函数用于清空字符串,即将其设置为空字符串。
std::string name = "Marcia Missouri";
name.clear(); // 清空字符串
std::cout << name; // 输出空字符串
replace
函数replace
函数用于替换字符串中的一部分。你可以指定要替换的起始位置以及替换的字符数,然后提供新的字符串来替换。
std::string name = "Marcia";
name.replace(0, 2, "MO"); // 从位置 0 开始,替换 2 个字符为 "MO"
std::cout << name; // 输出 "MOcia"
在本节课中,我们介绍了几个 std::string
类的函数,这些函数可以帮助我们在 C++ 中方便地修改字符串:
append
:在字符串末尾追加另一个字符串。insert
:在字符串的指定位置插入一个字符串。erase
:删除字符串中的指定部分。clear
:清空字符串。replace
:替换字符串中的指定部分。这些函数使得在 C++ 中操作字符串变得更加简单和灵活。
在本节课中,我们将讨论 C++ 字符串类中用于查找字符串的几个函数。通过这些函数,我们可以在字符串中查找特定字符或子字符串的位置。
find
函数find
函数用于查找字符串中第一次出现某个字符或子字符串的位置。如果找到,返回其位置;如果未找到,则返回一个特殊的值,通常是 -1
或者一个非常大的数字。
例如,查找字符串中的小写字母 "a":
std::string name = "Marcia Missouri";
size_t position = name.find("a"); // 查找 'a' 在字符串中的位置
std::cout << position; // 输出 6
你还可以指定查找的起始位置:
size_t position = name.find("a", 7); // 从位置 7 开始查找
std::cout << position; // 输出 10
find
函数是区分大小写的。因此,如果你传入大写字母 "A",就无法找到它:
size_t position = name.find("A");
std::cout << position; // 输出一个非常大的数字,类似于 -1
这是因为返回值类型是 size_t
(或者 size_t
的别名),它是一个无符号整数类型,不能存储负值。所以,当找不到字符时,返回的会是一个非常大的值,代表 “未找到” 的意思。
你可以通过比较返回值与 std::string::npos
来检查是否找到了目标字符:
if (name.find("A") == std::string::npos) {
std::cout << "没有找到 A" << std::endl;
}
rfind
函数rfind
函数与 find
函数类似,不过它是从字符串的末尾开始查找。它返回最后一次出现指定字符或子字符串的位置。
例如,查找字符串中最后一次出现的小写字母 "a":
size_t position = name.rfind("a");
std::cout << position; // 输出字符串中最后一个 'a' 的位置
find_first_of
函数find_first_of
函数用于查找字符串中第一次出现指定字符集合中的任何一个字符的位置。你可以传入一个字符集合,函数将返回第一个匹配的字符的位置。
例如,查找第一个出现的逗号、句号或分号:
std::string name = "Marcia Missouri";
size_t position = name.find_first_of(",.;");
std::cout << position; // 如果没有逗号、句号或分号,返回 `std::string::npos`,否则返回第一个匹配的字符的位置
如果在字符串中找到了这些字符,返回它们的第一个出现位置。如果找不到,则返回 std::string::npos
。
find_last_of
函数find_last_of
函数与 find_first_of
类似,不过它是查找最后一次出现指定字符集合中的任何一个字符的位置。
例如,查找最后一个逗号、句号或分号的位置:
size_t position = name.find_last_of(",.;");
std::cout << position; // 输出最后一个匹配的字符的位置
find_first_not_of
函数find_first_not_of
函数返回第一个不属于指定字符集合的字符的位置。它与 find_first_of
相对,后者返回的是匹配的字符。
例如,查找第一个不是逗号、句号或分号的字符:
size_t position = name.find_first_not_of(",.;");
std::cout << position; // 输出第一个不匹配字符的索引
find_last_not_of
函数find_last_not_of
函数与 find_first_not_of
类似,不过它是查找最后一次不属于指定字符集合的字符的位置。
size_t position = name.find_last_not_of(",.;");
std::cout << position; // 输出最后一个不匹配字符的索引
我们讨论了多个用于查找字符串中字符或子字符串的函数,以下是这些函数的用途:
find
:查找指定字符或子字符串第一次出现的位置。rfind
:查找指定字符或子字符串最后一次出现的位置。find_first_of
:查找第一个出现指定字符集合中任意字符的位置。find_last_of
:查找最后一个出现指定字符集合中任意字符的位置。find_first_not_of
:查找第一个不属于指定字符集合的字符的位置。find_last_not_of
:查找最后一个不属于指定字符集合的字符的位置。这些函数为我们提供了灵活的工具,可以用来有效地搜索和操作字符串。
substr
有时我们需要从字符串中提取部分内容,这时可以使用 substr
(子字符串)函数。该函数有两个参数,都是 size_t
类型的值,表示起始位置和要提取的字符数。值得注意的是,两个参数都有默认值,所以它们是可选的。
substr
函数的基本用法substr
函数时不传入任何参数,它将返回字符串的副本。std::string name = "Mathai";
std::string copy = name.substr(); // 返回字符串的副本
std::cout << copy; // 输出 "Mathai"
std::string name = "Mathai";
std::string copy = name.substr(5); // 从位置 5 开始提取
std::cout << copy; // 输出 "ai"
substr
将从指定位置开始,提取该数量的字符。std::string name = "Mathai";
std::string copy = name.substr(0, 3); // 从位置 0 开始,提取 3 个字符
std::cout << copy; // 输出 "Mat"
假设我们有一个变量包含某个人的全名,下面是一个简单的例子,展示如何提取名字和姓氏。
find
查找空格的位置,然后从空格后开始提取姓氏。首先,声明一个名为 firstname
的字符串,然后通过查找空格的位置来提取名字。
std::string name = "Marcia Missouri";
std::string firstname = name.substr(0, name.find(' ')); // 提取空格之前的部分作为名字
std::cout << "(" << firstname << ")"; // 输出 "(Marcia)"
此时,firstname
会包含全名中的第一个名字。如果字符串中没有空格,find
函数会返回 std::string::npos
,并且 substr
会提取整个字符串。
接下来,我们需要提取姓氏。首先找到空格的位置,然后提取从空格之后到字符串末尾的部分。
size_t index = name.find(' '); // 查找空格的位置
std::string lastname = name.substr(index + 1); // 从空格后开始提取
std::cout << "(" << lastname << ")"; // 输出 "(Missouri)"
通过这种方式,我们提取了姓氏。
考虑到某些人有中间名,我们需要做一些改动。如果中间名存在,程序将不再只提取第一个空格后的部分作为姓氏。为了应对这种情况,我们可以使用 rfind
来查找最后一个空格,从而提取最后的姓氏。
std::string name = "Marcia Smith Missouri";
size_t index = name.rfind(' '); // 查找最后一个空格
std::string lastname = name.substr(index + 1); // 提取最后一个空格后的部分作为姓氏
std::cout << "(" << lastname << ")"; // 输出 "(Missouri)"
此时,无论中间名是什么,程序都会正确提取姓氏。
通过使用 substr
函数,我们能够从一个字符串中提取指定位置和长度的子字符串。以下是提取姓名的完整代码示例:
std::string name = "Marcia Smith Missouri";
// 提取名字
std::string firstname = name.substr(0, name.find(' '));
std::cout << "(" << firstname << ")" << std::endl; // 输出 "(Marcia)"
// 提取姓氏
size_t index = name.rfind(' '); // 查找最后一个空格
std::string lastname = name.substr(index + 1); // 提取最后一个空格后的部分
std::cout << "(" << lastname << ")" << std::endl; // 输出 "(Missouri)"
这个程序处理了包含中间名的情况,通过使用 find
和 rfind
来查找空格的位置,从而精确提取名字和姓氏。
终极C++
终极C++ 第1部分 掌握 C++ 的基础知识
0001 1 欢迎
0002 2 课程结构
0003 1 C语言简介
0004 2 常见的集成开发环境 (IDE)
0005 3 你的第一个 C 程序
0006 4 编译和运行 C 程序
0007 5 更改主题
0008 1 介绍
0009 2 变量
0010 3 常量
0011 4 命名规范
0012 5 数学表达式
0013 6 运算符的优先级
0014 7 向控制台输出内容
0015 8 从控制台读取内容
0016 9 使用标准库
0017 10 注释
0018 1 介绍
0019 2 基本数据类型
0020 3 变量初始化
0021 4 数字运算
0022 5 缩小类型
0023 6 生成随机数
0024 7 格式化输出
0025 8 数据类型的大小和限制
0026 9 布尔值运算
0027 10 字符和字符串操作
0028 11 数组操作
0029 12 类型转换
0030 1 介绍
0031 2 比较运算符
0032 3 逻辑运算符
0033 4 逻辑运算符的优先级
0034 5 if 语句
0035 6 嵌套 if 语句
0036 7 条件运算符
0037 8 switch 语句
0038 1 介绍
0039 2 for 循环
0040 3 基于范围的 for 循环
0041 4 while 循环
0042 5 do while 循环
0043 6 break 和 continue 语句
0044 7 嵌套循环
0045 1 介绍
0046 2 定义和调用函数
0047 3 带默认值的参数
0048 4 函数重载
0049 5 按值或按引用传递参数
0050 6 局部变量与全局变量
0051 7 函数声明
0052 8 在文件中组织函数
0053 9 使用命名空间
0054 10 调试 C 程序
0055 13 课程总结
终极C++第2部分 中级,了解有关数组、指针、字符串、结构和流的所有信息 Ultimate C++ Part 2 Intermediate
0001 1 欢迎
0002 1 介绍
0003 2 创建和初始化数组
0004 3 确定数组的大小
0005 4 复制数组
0006 5 比较数组
0007 6 将数组传递给函数
0008 7 理解 size_t
0009 8 解包数组
0010 9 搜索数组
0011 10 排序数组
0012 11 多维数组
0013 1 介绍
0014 2 什么是指针
0015 3 声明和使用指针
0016 4 常量指针
0017 5 将指针传递给函数
0018 6 数组与指针的关系
0019 7 指针运算
0020 8 比较指针
0021 9 动态内存分配
0022 10 动态调整数组大小
0023 11 智能指针
0024 12 使用唯一指针
0025 13 使用共享指针
0026 1 介绍
0027 2 C 字符串
0028 3 C 字符串
0029 4 修改字符串
0030 5 搜索字符串
0031 6 提取子字符串
0032 7 操作字符
0033 8 字符串数值转换函数
0034 9 转义序列
0035 10 原始字符串
0036 1 介绍
0037 2 定义结构体
0038 3 初始化结构体
0039 4 解包结构体
0040 5 结构体数组
0041 6 嵌套结构体
0042 7 比较结构体
0043 8 使用方法
0044 9 操作符重载
0045 10 结构体与函数
0046 11 结构体指针
0047 12 定义枚举类型
0048 13 强类型枚举
0049 1 介绍
0050 2 理解流
0051 3 写入流
0052 4 从流中读取
0053 5 处理输入错误
0054 6 文件流
0055 7 写入文本文件
0056 8 从文本文件读取
0057 9 写入二进制文件
0058 10 从二进制文件读取
0059 11 使用文件流
0060 12 字符串流
0061 13 将值转换为字符串
0062 14 解析字符串
终极C++第3部分 高级,掌握 C++ 的面向对象编程 Ultimate C++ Part 3 Advanced
01 欢迎
02 介绍
03 面向对象编程简介
04 定义类
05 创建对象
06 访问修饰符
07 获取器和设置器
08 构造函数
09 成员初始化列表
10 默认构造函数
11 使用 explicit 关键字
12 构造函数委托
13 拷贝构造函数
14 析构函数
15 静态成员
16 常量对象和函数
17 对象指针
18 对象数组
19 介绍
20 重载相等运算符
21 重载比较运算符
22 重载航天船运算符
23 重载流插入运算符
24 重载流提取运算符
25 类的朋友
26 重载算术运算符
27 重载复合赋值运算符
28 重载赋值运算符
29 重载一元运算符
30 重载下标运算符
31 重载间接寻址运算符
32 重载类型转换运算符
33 内联函数
34 介绍
35 继承
36 保护成员
37 构造函数与继承
38 析构函数与继承
39 基类和派生类之间的转换
40 方法重写
41 多态
42 多态集合
43 虚析构函数
44 抽象类
45 最终类和方法
46 深度继承层次
47 多重继承
48 介绍
49 什么是异常
50 抛出异常
51 捕获异常
52 捕获多个异常
53 异常捕获的位置
54 重新抛出异常
55 创建自定义异常
56 介绍
57 定义函数模板
58 显式类型参数
59 多参数模板
60 定义类模板
61 更复杂的类模板
62 下一步
终极C++第1部分 掌握 C++ 的基础知识 Ultimate C++ Part 1 Fundamentals
0001 1 Welcome 0002 2 Course Structure 0003 1 Introduction to C 0004 2 Popular IDEs 0005 3 Your First C Program 0006 4 Compiling and Running a C Program 0007 5 Changing the Theme 0008 1 Introduction 0009 2 Variables 0010 3 Constants 0011 4 Naming Conventions 0012 5 Mathematical Expressions 0013 6 Order of Operators 0014 7 Writing Output to the Console 0015 8 Reading from the Console 0016 9 Working with the Standard Library 0017 10 Comments 0018 1 Introduction 0019 2 Fundamental Data Types 0020 3 Initializing Variables 0021 4 Working with Numbers 0022 5 Narrowing 0023 6 Generating Random Numbers 0024 7 Formatting Output 0025 8 Data Types Size and Limits 0026 9 Working with Booleans 0027 10 Working with Characters and Strings 0028 11 Working with Arrays 0029 12 Type Conversion 0030 1 Introduction 0031 2 Comparison Operators 0032 3 Logical Operators 0033 4 Order of Logical Operators 0034 5 If Statements 0035 6 Nested If Statements 0036 7 The Conditional Operator 0037 8 The Switch Statement 0038 1 Introduction 0039 2 The for Loop 0040 3 Range based for Loops 0041 4 While Loops 0042 5 Do while Loops 0043 6 Break and Continue Statements 0044 7 Nested Loops 0045 1 Introduction 0046 2 Defining and Calling Functions 0047 3 Parameters with a Default Value 0048 4 Overloading Functions 0049 5 Passing Arguments by Value or Reference 0050 6 Local vs Global Variables 0051 7 Declaring Functions 0052 8 Organizing Functions in Files 0053 9 Using Namespaces 0054 10 Debugging C Programs 0055 13 Course Wrap Up
终极C++第2部分 中级,了解有关数组、指针、字符串、结构和流的所有信息 Ultimate C++ Part 2 Intermediate
0001 1 Welcome 0002 1 Introduction 0003 2 Creating and Initializing Arrays 0004 3 Determining the Size of Arrays 0005 4 Copying Arrays 0006 5 Comparing Arrays 0007 6 Passing Arrays to Functions 0008 7 Understanding size t 0009 8 Unpacking Arrays 0010 9 Searching Arrays 0011 10 Sorting Arrays 0012 11 Multi dimensional Arrays 0013 1 Introduction 0014 2 What is a Pointer 0015 3 Declaring and Using Pointers 0016 4 Constant Pointers 0017 5 Passing Pointers to Functions 0018 6 The Relationship Between Arrays and Pointers 0019 7 Pointer Arithmetic 0020 8 Comparing Pointers 0021 9 Dynamic Memory Allocation 0022 10 Dynamically Resizing an Array 0023 11 Smart Pointers 0024 12 Working with Unique Pointers 0025 13 Working with Shared Pointers 0026 1 Introduction 0027 2 C Strings 0028 3 C Strings 0029 4 Modifying Strings 0030 5 Searching Strings 0031 6 Extracting Substrings 0032 7 Working with Characters 0033 8 String Numeric Conversion Functions 0034 9 Escape Sequences 0035 10 Raw Strings 0036 1 Introduction 0037 2 Defining Structures 0038 3 Initializing Structures 0039 4 Unpacking Structures 0040 5 Array of Structures 0041 6 Nesting Structures 0042 7 Comparing Structures 0043 8 Working with Methods 0044 9 Operator Overloading 0045 10 Structures and Functions 0046 11 Pointers to Structures 0047 12 Defining Enumerations 0048 13 Strongly Typed Enumerations 0049 1 Introduction 0050 2 Understanding Streams 0051 3 Writing to Streams 0052 4 Reading from Streams 0053 5 Handling Input Errors 0054 6 File Streams 0055 7 Writing to Text Files 0056 8 Reading from Text Files 0057 9 Writing to Binary Files 0058 10 Reading from Binary Files 0059 11 Working with File Streams 0060 12 String Streams 0061 13 Converting Values to Strings 0062 14 Parsing Strings
终极C++第3部分 高级,掌握 C++ 的面向对象编程 Ultimate C++ Part 3 Advanced
01 Welcome 02 Introduction 03 An Introduction to Object-oriented Programming 04 Defining a Class 05 Creating Objects 06 Access Modifiers 07 Getters and Setters 08 Constructors 09 Member Initializer List 10 The Default Constructor 11 Using the Explicit Keyword 12 Constructor Delegation 13 The Copy Constructor 14 The Destructor 15 Static Members 16 Constant Objects and Functions 17 Pointer to Objects 18 Array of Objects 19 Introduction 20 Overloading the Equality Operator 21 Overloading the Comparison Operators 22 Overloading the Spaceship Operator 23 Overloading the Stream Insertion Operator 24 Overloading the Stream Extraction Operator 25 Friends of Classes 26 Overloading the Arithmetic Operators 27 Overloading Compound Assignment Operators 28 Overloading the Assignment Operator 29 Overloading Unary Operators 30 Overloading the Subscript Operator 31 Overloading the Indirection Operator 32 Overloading Type Conversions 33 Inline Functions 34 Introduction 35 Inheritance 36 Protected Members 37 Constructors and Inheritance 38 Destructors and Inheritance 39 Conversion between Base and Derived Classesp 40 Overriding Methods 41 Polymorphism 42 Polymorphic Collections 43 Virtual Destructors 44 Abstract Classes 45 Final Classes and Methods 46 Deep Inheritance Hierarchies 47 Multiple Inheritance 48 Introduction 49 What are Exceptions 50 Throwing an Exception 51 Catching an Exception 52 Catching Multiple Exceptions 53 Where to Catch Exceptions 54 Rethrowing an Exception 55 Creating Custom Exceptions 56 Introduction 57 Defining a Function Template 58 Explicit Type Arguments 59 Templates with Multiple Parameters 60 Defining a Class Template 61 A More Complex Class Template 62 What's Next