WangShuXian6 / blog

FE-BLOG
https://wangshuxian6.github.io/blog/
MIT License
46 stars 10 forks source link

终极C++[进行中] #222

Open WangShuXian6 opened 1 week ago

WangShuXian6 commented 1 week ago

终极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

WangShuXian6 commented 1 week ago

终极C++ 第1部分 掌握 C++ 的基础知识

0001 1 欢迎

欢迎来到终极 C++ 课程

在本课程中

你将学习关于 C++ 的所有知识

从基础到更高级的概念。 这样,在课程结束时,你将能够自信地编写 C++ 代码。

如果你正在寻找一门

你不需要任何关于 C++ 或编程的一般知识

本课程涵盖了你需要了解的所有 C++ 知识,你不需要在各种随机教程之间来回跳跃。

我的名字是 Mash Hama Donny

我是一名软件工程师,拥有超过二十年的经验,并且通过这个频道和我的在线学校 Marsh.com,已经教会了数百万的人如何编程。

如果你是新来的

一定要订阅我的频道,因为我会不断上传新的教学视频。

现在让我们开始吧!

0002 2 课程结构

现在让我为你快速概述一下这门课程的结构

这样你可以最大限度地从中受益

这门课程是我完整 C++ 系列的第一部分。每一部分大约有三到四个小时长,所以你可以轻松地在一两天内完成。

在第一部分

你正在观看的这一部分,我们将探索 C++ 的基础知识。

在这一部分中,你将学习编程的基本概念,包括:

现在

在整个课程中,我将给你提供大量的练习,帮助你培养问题解决能力,并提高编写代码的自信心。

事实上,许多练习题都是常见的面试题。

在第二部分

我们将探索中级概念,例如:

最后

在最后一部分,我们将讨论高级概念,例如:

通过这整个系列的学习,最终你将对 C++ 有一个扎实的理解,并准备好将其应用到实际生活中。

例如

如果你想使用 Unreal Engine(一个流行的游戏引擎)来开发游戏,你将拥有必要的 C++ 技能来构建游戏。你只需要学习 Unreal Engine 的相关知识。

所以我希望你能坚持下去,掌握 C++ 这门语言,它是目前最快和最高效的编程语言之一。

0003 1 C语言简介

在我们开始编写代码之前

让我们花几分钟时间讨论一下 C++,它能做什么,以及如何掌握它。

C++ 是世界上最流行的编程语言之一,它是构建性能关键型应用程序的首选语言。你可以用 C++ 来开发:

这就是为什么大型公司如:

每三年我们就会发布一个新的 C++ 版本

目前最新的版本是 C++ 20,下一版本将在明年发布。

有一些人,比如我们著名的超级开发者 Sean Smith

认为 C++ 已经不再相关,因为出现了像 Java 或 C# 这样的新语言,但这是不正确的。

C++ 仍然是最快、最高效的编程语言之一,所以,如果你想开发一个需要快速且高效利用内存的应用程序,C++ 是一个非常好的选择。

这是 C++ 相对于 C# 和 Java 的一个优势。

C++ 也是计算机科学或软件工程专业学生经常学习的第一门语言,因为它影响了许多编程语言,如:

所以,如果你想成为一名软件工程师,学习 C++ 是一个很好的投资,它为你打开了很多就业机会。

根据 Indeed.com 的数据,美国 C++ 程序员的平均年薪超过 17 万美元。

要精通 C++,你需要学习两件事:

  1. C++ 语言本身,即语言的语法或语法规则。
  2. C++ 标准库(STL),它是一个包含许多预先编写的 C++ 代码的集合,提供了许多应用程序需要的功能。

标准库的示例包括:

这些功能在几乎每个应用程序中都是必需的。因此,我们不需要每次都从头开始编写这些功能,而是可以重用标准库中一些现成的 C++ 代码,快速构建应用程序。

在本课程中,我们将探索标准库中的主要功能,

但标准库非常庞大,所以我们只能触及它的表面。如果你想深入了解,有专门的书籍可以学习这个话题。

现在,很多人觉得 C++ 有点复杂且令人生畏,

但实际上,你不需要掌握 C++ 的所有内容就能编写出有意义的程序。

就像你不需要了解电视提供的每一个功能才能使用和享受它一样。

所以在这门课程中,我们将一步步地探索 C++,

随着学习的进展,我将向你展示如何在学习 C++ 的同时编写一些非常酷的程序。

我还将为你提供大量的练习,帮助你更好地理解和记住这些概念。

你会发现 C++ 实际上并不那么难。

所以,如果你跟着课程走,到课程结束时,

你将能够自信地编写 C++ 代码。

好的,接下来我们将讨论你需要的工具,以便编写 C++ 程序。

0004 2 常见的集成开发环境 (IDE)

创建 C++ 程序所需的工具

为了创建 C++ 程序

我们使用集成开发环境(IDE),它基本上是一个包含代码编辑器、构建工具和调试工具的应用程序。

现在市面上有很多不同的 IDE

其中一些是免费的,其他的则是商业软件。不过,排名前几的 IDE 包括:

  1. Microsoft Visual Studio(Windows 专用)

    • 其社区版是免费的,你可以通过屏幕上的链接下载。
    • Mac 版本也有,但 Mac 版的体验并不是那么好。
  2. Xcode(Mac 专用)

    • 你可以通过 App Store 获取 Xcode,它是苹果官方的开发工具。
  3. CLion(跨平台)

    • CLion 支持 Windows、Mac 和 Linux。
    • 你可以免费试用 30 天,之后需要购买许可证。

如果你不想为许可证付费

你可以使用一些免费的替代工具。市面上有很多不同的 IDE 可供选择来创建 C++ 程序。

在本课程中,我将使用 CLion

但你并不需要使用它来跟随课程进度,你可以使用任何你喜欢的工具,因为我们在这里的重点是 C++ 语言本身,而不是工具。

如果你是完全的新手,从未编写过代码

我建议你下载 CLion 的免费版本,这样你可以轻松跟随课程学习。之后,你可以选择购买许可证,或者使用其他免费的替代工具。

访问 jetbrains.com 下载页面

注意事项

如果你是 Mac 用户,请确保下载正确的 DMG 文件,因为 Mac 上有两种不同的构建版本:

根据你 Mac 的处理器类型,确保下载适合你设备的 DMG,因为性能差异非常明显。

好的

请安装并准备好,在下一课中,我们将一起创建我们的第一个 C++ 程序。

0005 3 你的第一个 C 程序

第一次使用 CLion 创建 C++ 程序

启动 CLion 后的操作

  1. 首次打开 CLion 时,你会看到一个弹出框,要求激活许可证。

  2. 暂时选择“开始试用”

  3. 你需要登录你的 JetBrains 账户

    • 你可以选择登录创建账户
    • 这非常简单,几分钟内就可以完成。
  4. 现在,回到 CLion,开始试用

创建新项目

  1. 在页面上,点击新建项目

  2. 在顶部,你可以指定项目的位置

    • 比如在我的 Mac 上,项目将存储在 Users / myname / CLionProjects
  3. 给项目命名为hello_world(不带空格,全部小写)。

  4. 接下来,你可以指定 C++ 的语言标准

    • 默认情况下是选择 C++14,但你可以将其更改为更高的版本,如 C++20 或 C++23。
    • 因为 C++23 还未正式发布,建议选择 C++20。
  5. 点击创建项目

初次创建的 C++ 程序

  1. 这时,你会看到一个名为 main.cpp 的文件,里面已经有一些代码。
    • 如果你不小心关闭了它,可以通过项目窗口轻松找到。
    • 展开文件夹,你会看到 main.cpp 文件。
  2. 另外,项目中还有一个文件叫做 CMakeLists.txt,我们暂时不需要它,可以关闭。

删除现有代码并从头开始编写

  1. 我们将删除现有代码,因为我们将从头开始编写程序,以便理解 C++ 如何工作。

函数的类比:电视遥控器

  1. 类比一下你的电视:电视有多个功能,例如换频道、调节音量等。
  2. 同样,C++ 程序也由数十个、数百个甚至数千个函数组成,每个函数有自己的作用。
  3. 其中有一个特殊的函数叫做 main,它是程序的入口点,就像电视的电源按钮一样。

C++ 是大小写敏感的

  1. C++ 是大小写敏感的,所以你必须精确地输入代码。
    • 如果你在 main 之前使用了大写字母 M,这会改变程序的含义。

定义 main 函数

  1. 我们在 main 函数前面需要指定它的返回值类型

    • main 函数应该返回类型为 int 的值,int 是整型的缩写,表示一个整数(例如 1、2、3 等)。
  2. 当你运行程序时,操作系统(如 Windows 或 macOS)将执行这个函数。该函数返回的值会告诉操作系统程序是否正常终止。

    • 如果返回 0,表示程序正常结束;如果返回其他值,表示程序遇到错误。
  3. C++ 中,空格通常不重要,因此无论你用一个空格还是十个空格都没关系,但为了代码的可读性和格式化,我们建议使用一个空格。

编写代码并格式化

  1. main 函数的花括号 {} 内部编写代码。

    • 所有在这个函数内编写的代码都会在操作系统执行 main 函数时被执行。
  2. 代码格式化:在代码的花括号位置,有两种流派:

    • 一些人喜欢将左花括号放在函数定义的同一行。
    • 另一些人则将左花括号放在新的一行。

    没有对错,只要保持一致即可。在本课程中,我将左花括号放在函数定义的同一行。

输出 "Hello, World!"

  1. 我们要编写代码将“Hello, World!”打印到屏幕上。为此,我们将使用 C++ 标准库。

  2. 标准库提供了我们在大多数应用中需要的功能。我们需要的功能是输出信息到屏幕。

    • 在文件顶部,我们输入 #include,后跟尖括号 <>,并指定文件名 iostream(代表输入输出流)。
    • 在这个文件中,我们可以找到用于打印信息到屏幕的功能。
  3. 标准库就像超市有不同的区域一样,每个文件也有不同的功能。在课程中,你会学到标准库中的其他文件。

使用 std::cout 输出

  1. 回到 main 函数后,我们输入 std::,它代表标准库。

    • std::cout 是用于输出字符到屏幕的功能。有些人认为这是“控制台输出”,但实际上这是不对的。
  2. 使用 std::cout 对象,我们可以在屏幕上输出一个或多个字符。

    • std::cout 后面,输入两个左尖括号 <<,然后在双引号内输入要打印的文本:"Hello, World!"
  3. 代码结束时,我们使用分号 ; 来结束语句,就像我们在写句子时使用句号一样。

    • 每条语句都应该以分号结束。

返回值

  1. 最后,我们使用 return 0; 来返回一个整数值 0,表示程序正常结束。
  2. 如果返回其他值,表示程序出现错误。

总结

  1. 在标准库中包含了用于打印信息的文件 iostream
  2. 定义了 main 函数,它是程序的入口点。
  3. main 函数返回一个整数类型的值(例如 0)。
  4. 在大括号内编写代码,第一个语句是打印 "Hello, World!",第二个语句返回 0,表示程序成功结束。

下一步

在下一课中,我将向你展示如何编译并运行这个程序。

0006 4 编译和运行 C 程序

编译和运行 C++ 程序

编译程序

  1. 要运行这个程序,首先我们需要将代码编译成机器代码,让计算机操作系统能够运行它。
    • 机器代码是计算机操作系统能理解的本地语言,不同操作系统的机器代码是不同的。
    • 如果我们在 Windows 机器上编译这段代码,我们会得到一个可执行文件(exe),但这个文件只能在 Windows 上运行。
    • 如果我们想要在 Mac 或 Linux 上运行程序,就需要在这些平台上重新编译程序,生成对应平台的可执行文件。

运行程序

  1. 回到我们的代码,要运行程序,我们点击工具栏上的播放图标

    • 在 Mac 上的快捷键是 Control + R,我强烈推荐使用快捷键,因为它能让你事半功倍。
  2. 点击运行后,在屏幕底部会出现一个小窗口,这是我们程序的控制台终端窗口

    • 控制台窗口就是用来查看程序输出的地方。
    • 在窗口中,你可以看到 Hello, World! 的消息被成功输出。
  3. 由于我们使用的是控制台应用程序,控制台应用程序相对更容易创建,特别是对于刚开始学习编程的人来说。

    • 相比之下,构建一个图形用户界面(GUI)应用程序要复杂得多。
    • 一旦你掌握了 C++ 的基础,你就可以尝试编写带有图形界面的应用程序,如果你有这个需求的话。

引发错误并调试

  1. 最小化这个窗口,故意在代码中制造一个小问题

    • 例如,我将删除一个分号。
  2. 现在,重新运行程序。

    • 这时,我们会看到一个编译错误,并且错误信息指向了这行代码。
    • 错误提示告诉我们忘记加分号了。
  3. 如果你是刚开始学习编程,遇到这些错误是完全正常的,可能是拼写错误或漏掉了分号等等。不要因此气馁。

    • 耐心是成为一名好程序员的第一要素
    • 如果你的代码没有成功编译,仔细观察这个视频,看看我输入的内容。
    • 比较一下你自己代码与我写的代码,如果仔细比对,你会发现问题并能够自行解决。
  4. 修复错误后,我们可以继续进行下一步,学习更多内容。

下一步

在下一课中,我们将继续深入学习。

0007 5 更改主题

改变主题和界面设置

主题设置

  1. 接下来,我想改变编辑器的颜色,并展示如何进行设置,因为很多人问我视频中使用的主题是什么。

    • 如果你没有使用 SeaLion,可以跳过这个部分,继续学习下一课。
  2. 打开SeaLion,点击首选项(Preferences)菜单。

    • 然后在外观和行为(Appearance and Behavior)选项下选择外观(Appearance)
  3. 在这里,你可以看到已经安装的主题。

    • 默认情况下,我们只有四种主题。
    • 但是,我们可以通过点击屏幕上的链接来获取更多主题。
  4. 在这个页面中,我将根据下载次数对主题进行排序,这样可以查看最受欢迎的主题。

    • 我选择Dracula主题,这个主题非常受欢迎。
    • 当然,你也可以尝试其他主题,找到你自己喜欢的。
  5. 选择好主题后,点击安装。

    • 安装完成后,点击接受(Accept)

使用新主题

  1. 现在,你可以看到Dracula主题已经应用,这个主题比 SeaLion 默认的主题好看多了。

总结

0008 1 介绍

C++基础知识

课程概述

  1. 欢迎回到终极C++课程
  2. 在这一部分,我们将讨论C++的基础知识,内容包括:
    • 变量和常量
    • 命名规范
    • 编写数学表达式
    • 向控制台输出和从控制台读取
    • 使用标准库和注释

学习目标

  1. 到这一部分结束时,你将能够编写简单但非常实用的C++程序。

让我们开始吧!

  1. 现在,让我们直接进入正题,开始学习。

0009 2 变量

变量和常量

变量的定义

  1. 现在,让我们来谈谈编程中的变量
  2. 我们使用变量来暂时存储数据在计算机的内存中。
  3. 技术上讲,变量是内存中某个位置的名称,在这个位置我们可以存储一些值。
  4. 这些值是可以变化的,所以我们称之为变量。

声明变量

  1. 在C++中,声明变量时,首先需要指定我们要存储的数据类型。比如,intinteger 类型用于存储整数。
  2. 然后,我们为变量取一个有意义的名字,比如 file_size
  3. 在命名变量时,我们有不同的命名规则。稍后我们将讨论这些规则。

变量命名规范

  1. 我想强调的是,始终使用有意义的变量名,不要使用像 fs 这样的缩写,因为别人看你的代码时,可能不清楚 fs 是什么意思。
  2. 另外,像 f1f2thing 这样的名字也是不可取的,这些名字过于简短且不具备任何含义。
  3. 所以我们将变量命名为 file_size,并将其赋值为 100。

初始化变量

  1. 我们可以在声明变量时,直接给它赋初值。这样做叫做初始化变量
  2. 这使得代码更加简洁,我们不需要再单独写一行来初始化变量。

浮动类型的变量

  1. 接下来,我们再声明一个用于存储小数的变量。对于这种情况,我们使用 double 类型。
  2. 在本节课程中,我们只使用整数(int)和双精度浮动类型(double)。

输出变量

  1. 我们有了两个变量后,接下来可以使用它们打印输出。
  2. 比如,我们可以打印 file_size,输出结果就是 100。

没有初始化的变量

  1. 初始化变量并不是强制性的,但它是一个好的编程习惯。
  2. 如果我们没有初始化 file_size,并且尝试立即打印它,IDE 会发出警告。
  3. 警告指出:file_size 变量在使用时未初始化。

编译错误与垃圾值

  1. 如果我们运行程序,将看到一个随机值,这是所谓的垃圾值
  2. 这是因为内存中尚未初始化的变量会使用内存中其他数据的残留值。
  3. 作为最佳实践,我们应该始终在使用变量之前对其进行初始化。

初始化多个变量

  1. 我们还可以在同一行中初始化多个变量。
  2. 但是,通常不推荐这么做。最佳实践是将每个变量声明放在单独的一行。

编程练习:交换变量的值

  1. 现在,我有一个小练习给你:交换两个变量的值,这是一个常见的面试题。
  2. 假设我们声明了两个变量 ab,并且 a 的值为 1,b 的值为 2。
  3. 我希望你编写代码交换这两个变量的值。也就是说,执行交换后 a 的值应为 2,b 的值应为 1。
  4. 注意:不允许直接重新赋值给这些变量(例如:a = 2b = 1)。
  5. 提示:可以通过想象两个桶的内容来帮助你思考如何交换它们的内容。

解答与思路

  1. 为了交换这两个变量的值,我们需要一个额外的第三个变量来存储其中一个变量的值。
  2. 我们可以声明一个名为 temp 的新变量,将 a 的值存储在其中。
  3. 然后将 a 的值设置为 b 的值,最后将 b 的值设置为 temp
  4. 这样就完成了变量值的交换。
  5. 运行程序后,我们验证输出的 ab 值是否交换成功。

总结

  1. 恭喜你完成了你的第一个编程问题!
  2. 如果你遇到困难,不要担心,随着课程的进行,你将会掌握更多的编程技巧。
  3. 接下来,我们将讨论常量

0010 3 常量

常量的使用

什么时候使用常量

  1. 有时候我们希望某个变量的值保持不变,这时候我们使用常量
  2. 例如,我们可以声明一个double 类型的变量,命名为 pi,并将其赋值为 3.14159。
  3. 我们可以利用这个常量计算圆的面积。
  4. 但如果在程序的某个地方不小心把 pi 的值修改为其他数值,比如设置为 0,那么计算将会出错。

使用常量来防止修改

  1. 为了防止这种错误,我们可以将 pi 定义为常量,这样它的值就不能再被修改。
  2. 如何定义常量呢?很简单。
  3. 我们只需要在声明变量时,在类型前加上 const 关键字。

编译错误

  1. 但是,当我们在第五行修改 pi 的值时,IDE 会给出一个编译错误提示。
  2. 具体错误信息是:“不能给具有 const 修饰符的变量 pi 赋值。”
  3. 如果你运行程序,终端窗口也会显示一个错误,指出无法对常量类型的变量 pi 进行赋值。
  4. 这个错误是在 main.cpp 文件的第五行第八列发生的,错误信息是:“不能给 pi 赋值,因为它是常量。”

保护变量不被修改

  1. 通过将 pi 定义为常量,我们可以防止不小心修改该变量的值,确保它在程序中保持不变。

0011 4 命名规范

命名约定

不同的命名约定

  1. 在编程中,我们有不同的命名约定来命名变量和常量,而不同的团队可能会偏好不同的约定。
  2. 其实这里没有严格的对错之分,但是我可以向你展示一些常见的命名约定。

常见的命名约定

  1. 之前我们声明了一个变量,命名为 file_size
  2. 我们使用的是一种叫做 蛇形命名法(snake_case)的命名约定。
  3. 在这种命名约定中,我们需要使用小写字母来命名变量和常量。
  4. 如果变量名包含多个单词,我们需要用下划线 _ 来分隔它们。

Pascal 命名法

  1. 另一种常见的命名约定是 Pascal 命名法
  2. 在 Pascal 命名法中,我们需要将每个单词的首字母大写。
  3. 比如,如果用 Pascal 命名法命名 file_size,它就会变成 FileSize
  4. 顺便提一下,我在代码中用两条斜杠 // 开头的文本是 注释
  5. 注释的作用是描述我们的代码,但它们不会被编译。
  6. 我们稍后会讨论注释的使用。

驼峰命名法

  1. 还有一种命名约定叫做 驼峰命名法(camelCase)。
  2. 它与 Pascal 命名法类似,唯一的不同是 驼峰命名法 中第一个单词的首字母要小写。
  3. 所以 file_size 在驼峰命名法中会变成 fileSize

匈牙利命名法

  1. 还有一种叫做 匈牙利命名法 的旧式命名约定。
  2. 这种命名法已经不再流行,但仍然有些人使用它。
  3. 匈牙利命名法的规则是:在变量名的前面加上一个字母,表示变量的类型。
  4. 比如,如果变量是整数类型,我们会用小写字母 i 来表示。
  5. 所以一个整数类型的变量 file_size 就会被命名为 iFileSize
  6. 然而,这种命名方式已经过时,主要是因为现代的编辑器都非常强大。
  7. 在过去,如果我们声明了一个变量,想要知道它的类型,我们可能需要往回滚动查看。
  8. 但现在,只要将鼠标悬停在变量上,我们就能直接看到它的类型。

选择命名约定

  1. 在本课程中,我将使用 驼峰命名法(camelCase)来命名变量和常量,而使用 Pascal 命名法 来命名类。
  2. 类的定义我们将在后面讲解。
  3. 如果你不喜欢这些命名约定,使用 蛇形命名法(snake_case)也是完全可以的。
  4. 但重要的是要保持一致性。确保你始终遵循自己的命名约定。
  5. 代码风格越一致,代码就越容易阅读和维护。

总结

  1. 所以,在这个课程中,你可以根据自己的喜好选择命名约定,但务必保证代码的一致性,这样团队成员或自己将来阅读和维护代码时会更轻松。

0012 5 数学表达式

数学表达式

声明变量并进行计算

  1. 你已经学会了如何声明变量和常量,接下来让我们看看如何使用数学表达式进行计算,这是非常有趣的部分。
  2. 首先,我会声明两个变量 XY
  3. 接着,我们可以再声明一个第三个变量,并将它的值设为 X + Y
  4. 在这里,+ 是加法运算符,而 XY 被称为操作数(operands)。
  5. 现在,让我们在终端上打印出变量 Z 的值。
  6. 使用 std::cout 来输出结果,看看我们得到什么。

加法运算符

  1. 我们会看到 Z = 13,漂亮!此外,我们也可以使用减法、乘法和除法运算符。
  2. 但需要注意的是,除法运算在处理整数时有些复杂。

除法运算

  1. 由于我们使用的是两个整数,除法结果将是一个整数。
  2. 即使现实中,10 ÷ 3 的结果是带有小数点的浮动数值(也称浮点数)。
  3. 如果我们运行程序,会看到 3
  4. 那么,如果我们想看到浮点数该怎么办呢?
  5. Z 的类型改为 double 并不能解决这个问题,因为正如我之前所说的,如果参与运算的两个数都是整数,除法的结果也会是整数。
  6. 为了得到浮动数,我们需要将其中一个数转换为浮点数。

转换为浮点数

  1. 我们可以通过将其中一个变量转换为 double 类型来解决这个问题。
  2. 看,看,警告消失了!
  3. 现在重新运行程序,输出结果将是 3.333333,这是正确的浮点数结果。

模余运算

  1. 现在让我们回到整数和整数的除法。
  2. 我们有一个新的运算符,叫做 取余运算符(modulus),它返回除法的余数。
  3. 比如 10 ÷ 3 的余数是 1
  4. 让我们验证一下这个操作,结果正是 1

修改变量的值

  1. 使用这些运算符,我们还可以修改变量的值。
  2. 让我演示一下。
  3. 为了简化起见,我会去掉 YZ,只保留 X,然后打印它的值。
  4. 假设我们想将 X 增加 5,下面是如何操作的:
    X = X + 5;
  5. 这个表达式会先计算出 X + 5 的结果(即 15),然后将结果存回 X
  6. 同样地,我们也可以进行减法、乘法等运算。

自增与自减运算符

  1. 除了这些基础运算符外,我们还有两个非常常见的运算符,分别是自增运算符(++)和自减运算符(--)。
  2. 比如,我们想给 X 加 1:
    X = X + 1;
  3. 这是完全可以的,但有一种更简洁的写法:
    X++;
  4. 这就是自增运算符。同样,我们也有自减运算符(--),但是没有与乘法或除法运算符等效的自增自减运算符。

自增运算符的前置与后置用法

  1. 自增运算符有两种用法:前置(++X)和后置(X++)。让我们看看它们的区别。

后置自增

  1. 首先,删除之前的两行代码。
  2. 声明一个新变量 Y,并将其设置为 X++
  3. 在后置自增的情况下,首先会将 X 的当前值赋给 Y,然后 X 会增加 1。
  4. 如果运行程序,Y 会是 10,而 X 会变为 11

前置自增

  1. 现在,声明一个新的变量,并设置它为 ++X
  2. 在前置自增的情况下,首先 X 的值会加 1,然后结果会被存储到 Z 中。
  3. 因此,在这种情况下,XZ 都会是 11

总结

  1. 总结一下:
    • 如果使用 后置自增X++),首先将 X 的当前值赋给变量,然后再自增。
    • 如果使用 前置自增++X),则先将 X 自增,再将其值赋给变量。

0013 6 运算符的优先级

运算符优先级

运算符优先级的重要性

  1. 在编写数学表达式,尤其是比较复杂的表达式时,你需要考虑运算符的顺序或优先级。
  2. 让我给你演示一下是什么意思。

演示

  1. 我将声明一个名为 X 的变量,并将其设置为 1 + 2 × 3
  2. 然后我们在控制台打印 X
  3. 你认为我们会得到什么结果呢?暂停视频,思考一下。
  4. 答案是 7

乘法和加法的优先级

  1. 这是一个非常简单的数学问题,但不幸的是很多人都会搞错。
  2. 乘法和除法运算符的优先级更高。
  3. 所以在计算这段代码或这个表达式时,首先会计算乘法部分。
  4. 2 × 3 = 6,然后 6 + 1 = 7,所以结果是 7
  5. 让我们验证一下。
  6. 我会运行程序,看看结果。

验证计算

  1. 结果是 7,非常好!

运算符优先级总结

  1. 记住,在数学或任何编程语言中,乘法和除法运算符的优先级总是高于加法和减法运算符。
  2. 但是我们可以通过使用括号来改变运算符的优先顺序。
  3. 比如,如果我们将这段代码用括号括起来,首先会计算括号内的部分。
  4. (1 + 2) × 3,首先计算 1 + 2 = 3,然后 3 × 3 = 9,结果就是 9
  5. 让我们再验证一下。

通过括号修改优先级

  1. 重新运行程序,我们会得到 9

练习

练习题

  1. 这是本节的练习题:根据给定的数学表达式实现它。
  2. 假设 X = 10Y = 5,并计算 Z 的值。
  3. 如果正确实现,Z 应该等于 13
  4. 暂停视频,尝试自己完成这道题,几分钟后再回来。

解答

  1. 好了,这里是解决方案。
  2. 我将声明 X 并将其设置为 10
  3. 然后声明 Y 并设置为 5
  4. 接下来是 Z,首先我们需要对 XY 做一些计算。需要注意的是,我们必须使用括号。
  5. 因为整个表达式将作为分子,所以首先计算 (X + Y) ÷ 3 × Y
  6. 你必须用括号将整个分母部分括起来,否则除法运算会优先执行,导致结果不同。
  7. 如果我们不加括号,分母会变成 3,结果会不一样。

打印结果

  1. 现在让我们打印 Z 到终端,输出结果应该是 13
  2. 如果你没有成功解决这个问题,不要灰心。
  3. 不要让它打击你,记住你是学生,正在学习。
  4. 如果你知道所有的东西并能解决所有的问题,那么你就该是老师了,对吧?
  5. 所以不要灰心,继续努力。

下一课

  1. 让我们继续学习下一节内容。

0014 7 向控制台输出内容

写入控制台

基本输出

  1. 你已经学习了如何向控制台或终端写入内容,现在我将向你展示一些写入控制台的其他技巧。
  2. 我们首先来声明一个变量。
  3. 假设我们有一个变量 X,并且我们想要打印出 X = 10
  4. 那我们怎么做呢?

使用 std::cout 输出

  1. 首先我们进入 std 命名空间并使用 std::cout,这是一个表示标准输出流的对象。
  2. 听起来可能有点复杂,但我会为你解释清楚。
  3. 在编程中,(stream)表示一串字符。
  4. 标准输出 是我们的控制台或终端窗口。
  5. 所以,使用 std::cout,我们可以将一串字符写入标准输出,即控制台窗口。

输出操作符

  1. 这两个 左尖括号 被称为 流插入运算符(stream insertion operator),它用于将内容插入到输出流中。
  2. 在这个例子中,我们将输出一串字符,字符内容由 双引号("")括起来。
  3. 在 C++ 中,这被称为一个 字符串,我们稍后会详细讨论字符串。

打印变量

  1. 现在我们要打印 X =,然后打印 X 的实际值。
  2. 我们结束这条语句,再次使用 std::cout 来输出 X

运行程序

  1. 让我们运行程序并查看输出结果。
  2. 输出应该是 X = 10,很漂亮,对吧?

简化输出代码

  1. 我们可以将这些语句合并为一条语句。
  2. 这样,我们就可以去掉第二个 std::cout 和分号,然后将所有内容连接在一起。
  3. 通过链式调用多个流插入运算符。

运行验证

  1. 运行程序后,我们会得到相同的输出结果 X = 10,这样代码变得更加简洁。

添加第二个变量

声明第二个变量

  1. 现在我们来声明第二个变量 Y,并将其设置为 20,然后重复上述步骤。
  2. 现在我们打印 Y = 20

输出格式改进

  1. 输出结果应该是 Y = 20,但如果我们希望 Y = 20 显示在新的一行上,该怎么办呢?
  2. 为了实现这一点,我们需要添加一个换行符。

使用 std::endl 换行

  1. 使用流插入运算符,我们可以添加 std::endl,它表示行尾并会自动换行。

运行程序

  1. 运行程序后,我们会得到更好的输出格式。

合并代码

  1. 现在我们再次简化代码,通过将这两条语句合并,去掉第二个 std::cout 和分号。
  2. 运行程序后,输出格式依旧正确。

排版和代码美化

提高代码可读性

  1. 尽管代码能够正确运行,但它的格式不太容易阅读。
  2. 让我们使用 制表符空格 来对齐这些运算符,使代码看起来更加整齐。
  3. 通过这种方式,代码的排版与输出结果更一致。

删除重复的 std:: 命名空间

  1. 现在有一个小问题,代码中出现了重复的 std::
  2. 为了简化代码,我们可以使用 using 指令来引用 std 命名空间。

使用 using 指令

  1. main 函数之前,我们可以添加 using namespace std;,这样就不需要在每次使用 std:: 时重复书写它。

优化后的代码

  1. 这样,我们的代码变得更简洁、更易读。

练习

税务练习

  1. 假设你拥有一家商店,销售总额为 95,000 美元。
  2. 现在,作为税务申报的一部分,你需要按不同的税率支付州税和县税。
  3. 州税是 4%,县税是 2%

编写代码计算税额

  1. 请你编写代码,显示总销售额、州税、县税以及你需要支付的总税额。
  2. 暂停视频,花五分钟完成这道练习,然后回来查看我的解答。

解答

声明变量并计算
  1. 首先,我们需要为总销售额声明一个变量,使用 double 类型(即使没有小数值)。
  2. 对于货币值,我们应该总是使用 double 类型。
  3. 我声明 double sales = 95000;
打印总销售额
  1. 然后,我们打印总销售额,并加上美元符号。
  2. 使用流插入运算符输出销售额,并在后面加上换行符。
计算州税
  1. 接下来,我们声明一个变量 state_tax,并计算州税:sales × 0.04
  2. 输出州税的金额,并在后面加上换行符。
验证程序
  1. 运行程序,确保所有输出结果正确。

提高代码格式

  1. 我们改进代码格式,将输出部分分开,使用垂直线将每个部分分隔开来,就像文章中的段落一样。
  2. 这有助于使代码的结构更加清晰。

使用常量变量

  1. 为了避免硬编码数字(魔术数字),我们将税率存储在单独的变量中。
  2. 我们声明 double state_tax_rate = 0.04; 来表示州税率,并将其应用到计算中。

改善代码可读性

  1. 这样,代码更加具有表达性,也方便日后修改税率时,避免在多个地方修改。
计算县税
  1. 复制并粘贴计算州税的代码,计算县税,税率是 2%
  2. 使用 county_tax_rate 来表示县税率。

最终输出总税额

  1. 最后,我们声明一个 total_tax 变量,它等于州税加县税,并打印总税额。

运行验证

  1. 运行程序,确保一切正常,最终输出县税和总税额。

代码命名规范

  1. 我强调了变量命名的重要性,确保每个变量都有清晰的含义,避免使用无意义的变量名(例如 t1t2)。

0015 8 从控制台读取内容

从控制台读取输入

标准输入流

  1. 现在我们来看一下如何从控制台读取输入。
  2. 你将了解到,std::cout 表示标准输出流(我们之前已经讨论过)。在 I/O 流 中,我们还有另一个对象叫做 std::cin,它代表标准输入流。
  3. 使用 std::cin,我们可以从控制台读取数据。

使用 std::cin 读取输入

  1. 让我来给你展示如何做。
  2. 首先,我们使用 std::cout 打印一个标签到屏幕上,提示用户输入一个值。
  3. 然后,使用 std::cin 来读取这个值,并将其存储到一个变量中。

声明变量

  1. 我们需要首先声明一个变量。
  2. 例如,我们声明一个整数类型的变量 value
  3. 然后,我们使用 std::cin 和流提取运算符(>>)来读取这个值,并将其存储到 value 变量中。

流提取运算符

  1. 这个 >> 运算符叫做 流提取运算符,它是 流插入运算符<<)的反向操作。
  2. 我知道这可能有点混淆,但记住它的最简单方法是思考数据流的方向。
  3. 在这种情况下,我们是从控制台读取数据,将其放入变量中。
  4. 相比之下,std::cout 是将一串字符输出到控制台。
  5. 这样,你就可以理解为什么读取数据的方向是 从控制台到变量,而输出数据则是 从变量到控制台

验证程序

  1. 为了验证程序是否正常工作,我们打印出刚刚读取的值。
  2. 运行程序,输入一个数字,比如 10
  3. 结果应该输出 10,程序正常工作。

读取浮点数

  1. 那么,如果我们输入一个带有小数点的浮动数字会发生什么呢?
  2. 比如我输入 10.1
  3. 由于我们将 value 声明为整数类型,程序会丢弃小数部分,因此输出为 10
  4. 如果我们想读取带有小数的数字,我们需要将 value 的类型改为 double

使用 double 类型

  1. 让我们把 value 改成 double 类型。
  2. 现在,如果我们输入 10.5,程序会正确读取并输出 10.5

读取多个值

  1. 我们还可以一次读取多个值。
  2. 比如,将标签更改为 "Enter values for X and Y"。
  3. 然后声明两个变量 XY,并分别读取它们的值。

输出计算结果

  1. 最后,我们可以打印出 X + Y 的结果,这就像一个简单的计算器。
  2. 输入 1020 后,结果应该是 30

支持空格分隔

  1. 我们还可以使用空格分隔输入的数值,程序仍然能正确工作。
  2. 例如,输入 10 20,无论用一个空格还是多个空格,程序都会正确解析,并输出 30

链式读取

  1. 类似于 std::cout,我们也可以将多个输入语句链接在一起。
  2. 这样,我们就可以去掉第二个 std::cin 语句,通过链式调用 流提取运算符 来读取第二个值。

精简代码

  1. 通过这种方式,我们的代码变得更加简洁。
  2. 现在我们可以一次性读取多个值并存储到对应的变量中。

编写温度转换程序

练习任务

  1. 现在是你的练习时间了。
  2. 我希望你编写一个程序,将温度从华氏度转换为摄氏度。
  3. 当你运行程序时,它应该要求用户输入一个华氏温度,并将其转换为摄氏度,并将结果打印到终端上。

暂停并尝试

  1. 暂停视频,花几分钟时间完成这道练习,然后回来查看我的解答。

解答

打印标签
  1. 首先,我们使用 std::cout 打印标签 "Enter temperature in Fahrenheit"。
  2. 然后,我们声明一个变量 Fahrenheit 来存储输入的华氏温度。
读取输入
  1. 使用 std::cin 读取用户输入的华氏温度并存入 Fahrenheit 变量。
声明摄氏度变量
  1. 接下来,我们声明一个 double 类型的变量 celsius 来存储转换后的摄氏温度。
  2. 使用 double 类型是因为计算可能会得到浮动的小数值。
计算摄氏温度
  1. 这里是转换公式:(Fahrenheit - 32) / 1.8
  2. 我们将 Fahrenheit - 32 包裹在括号中,然后除以 1.8 来得到摄氏温度。
输出结果
  1. 最后,使用 std::cout 打印出转换后的摄氏温度。

测试程序

  1. 运行程序,输入 90 华氏度,程序应该输出 32.2 摄氏度。

0016 9 使用标准库

数学函数和圆的面积计算

数学函数介绍

  1. 如你所见,标准库为我们提供了读取和写入的功能。
  2. 本节课,我们将学习标准库的另一个部分,它包含了若干有用的数学函数。

引入数学库

  1. 在这一部分,我们会使用 #include 指令来再次引入一个名为 <cmath> 的文件。
  2. <cmath> 文件声明了一些有用的数学函数。

查找数学函数

  1. 如果你对这些函数感兴趣,可以去 Google 上搜索 "C++ Math functions",会有很多网站提供 C++ 函数的参考。
  2. 其中一些参考网站包括 cpluspluss.comcppreference.com 等。

例子:查看数学函数

  1. 以这个页面为例,你可以看到 <cmath> 库中声明的所有函数。
  2. 在本节课,我们将重点看几个函数,其中之一是 ceil,它会将一个值向上取整。
  3. 另一个函数是 floor,它会将一个值向下取整。

更多函数信息

  1. 如果你点击任何一个函数链接,你可以了解更多信息。
  2. 页面上会显示不同版本的 C 和 C++。
  3. 例如,C 语言有 C99 版本,C++ 98 是 C++ 的早期版本,发布于1998年。
  4. C++11 版本发布于2011年,你可以看到这些函数在不同版本的 C 和 C++ 中是如何演变的。
  5. 但不需要过多纠结这些细节,我只需要你注意的是,这些函数的输入类型是 double,并返回一个 double 类型的值。

示例:使用 floor 函数

  1. 现在,让我们来看看如何使用这些函数。
  2. 在代码的 main 函数中,使用 floor 函数时,我们写 floor(),然后在括号中提供一个输入值。
  3. 输入值是函数的参数。比如我们传入 1.2,那么它会返回一个 double 类型的值,我们可以将其存储在一个变量中。

声明变量并存储返回值

  1. 我们声明一个 double 类型的变量 result,并将 floor() 函数的返回值赋值给它。
  2. 然后,我们用分号结束这一语句。
  3. 这样,我们就完成了对 floor 函数的调用,它会接受一个值并返回一个新值。

打印结果

  1. 最后,我们像之前一样打印结果,输出 result
  2. 如果输入值是 1.2,输出将是 1.0

示例:使用 pow 函数

  1. 有些函数需要多个参数,pow 函数就是其中之一。
  2. pow 函数要求两个参数,我们需要传递两个值,用逗号分隔。
  3. 比如,如果我们输入 2, 3,则意味着 2 的 3 次方

代码示例:使用 pow

  1. 使用 pow() 时,我们可以看到 xy 分别是此函数的参数。
  2. 运行后,输出应该是 8,即 2 的 3 次方

练习任务:计算圆的面积

编写程序

  1. 作为练习,我希望你编写一个程序,要求用户输入圆的半径,并计算该圆的面积。
  2. 这个任务很简单,你可以在几分钟内完成。

解答步骤

  1. 好的,下面是我的解答。
  2. 首先,我们使用 std::cout 打印提示标签 "Enter radius"。
  3. 然后,我们声明一个 double 类型的变量 radius 来存储输入的半径值。
  4. 接下来,我们使用 std::cin 读取用户输入的半径值,并将其存储到 radius 变量中。

计算面积

  1. 我们再声明一个变量 area 来存储计算结果。
  2. 圆的面积计算公式是 *π r²**,因此我们可以直接使用公式来计算。
  3. 为了避免使用神秘数字(magic numbers),我们将 π 存储在一个单独的变量中。
  4. 或者,更好的做法是,将 π 作为常量进行定义。

使用 pow 函数

  1. 使用 pow(radius, 2) 计算 radius 的平方,然后将其与 π 相乘,得到圆的面积。

打印结果

  1. 最后,我们使用 std::cout 输出计算得到的面积。
  2. 假设输入的半径是 4,那么程序输出的面积应该是 50.24

0017 10 注释

注释的使用

注释的目的

  1. 本节课的最后一个主题是注释。
  2. 我们使用注释来澄清代码并使其更易于理解。
  3. 正如我之前提到的,注释不会被编译。

注释的类型

  1. 在 C++ 中,我们有几种不同的注释写法。
  2. 一种常见的写法是使用两个斜杠 //,之后我们在这两个斜杠后面写的内容将会被视为注释。
  3. 我们可以将这些斜杠放在一行的上方或前面。
  4. 两种方式都可以工作,但如你所见,这里空间有限,因为我们只能使用剩余的部分。
  5. 当然,我们可以写更长的注释,但这样会导致我们不得不不断左右滚动来查看内容。
  6. 因此,通常建议将注释放在一行的上方。

多行注释

  1. 如果你想使用多行注释,可以像 C++ 中那样使用另一种方式来书写。
  2. 除了使用两个斜杠,我们还可以用一个斜杠加上星号 *`/`** 来开始注释块。
  3. 然后按回车键,C++ 会自动生成这个注释块。
  4. 这个注释块的开始由 /* 表示,结束由 */ 表示。
  5. 我们在这两个符号之间写的内容都将被视为多行注释。

注释风格

  1. 不同的团队可能对注释的风格有不同的偏好。
  2. 所以,实际上并没有绝对的对错之分。
  3. 只要选择一种风格并坚持使用即可。

关于注释的使用建议

  1. 我想特别强调的是,不要过度使用注释。
  2. 过多的注释会让代码变得更难理解和维护。
  3. 注释应该仅用于解释“为什么”和“如何”,而不是“做了什么”。

注释示例

  1. 让我给你演示一下我是什么意思。
  2. 比如,我可以写一个注释:“声明一个变量并将其初始化为零”。
  3. 其实很明显我们在下一行做的就是这个操作。
  4. 这样的注释是完全不必要的,它只是让我们的代码显得有些冗余。
  5. 我们不希望代码中充满这些不必要的注释。
  6. 相反,我们应该用注释来解释“为什么”和“如何”。
  7. 如果在写代码时做了某些假设,我们应该注释这些假设。
  8. 这样,在未来我们回顾代码时,可以知道为什么按某种方式写代码。

总结

  1. 这就是关于注释的所有内容,今天的部分也就到这里。
  2. 我们下次再见!

0018 1 介绍

欢迎回来

  1. 欢迎回到终极 C++ 课程。
  2. 在本节课中,我们将详细探讨 C++ 中的基本数据类型。
  3. 我们将讨论各种内置数据类型,以及它们的大小和限制。
  4. 更具体地说,我们将探索表示数字的各种类型及其差异。
  5. 你将学习如何生成随机数,这是一项非常有用的技术,尤其是在构建游戏时。
  6. 你还将学习如何处理布尔值、字符和字符串。
  7. 以及数组,它们用于存储一组值。
  8. 到本节结束时,你将深入了解这些基本数据类型,并学会如何利用它们编写有用的程序。
  9. 那么,现在让我们开始吧!

0019 2 基本数据类型

在 C++ 中声明变量

  1. 如你所见,在 C++ 中声明一个变量时,我们需要指定其类型。
  2. 这就是我们所说的 C++ 是一种静态类型语言的原因。
  3. 这意味着在声明一个变量时,我们需要指定它的类型。
  4. 并且这个类型在程序的生命周期内是不能改变的。
  5. 例如,静态类型语言包括 C#、Java、TypeScript 等。
  6. 与静态类型语言相比,我们还有动态类型语言,如 Python、JavaScript 和 Ruby 等。
  7. 在这些语言中,我们不需要为变量指定特定的类型。
  8. 类型会根据我们赋给这些变量的值来确定。
  9. 并且这个类型可以在程序的生命周期内改变。

静态类型与动态类型语言的区别

  1. 所以这就是静态类型语言与动态类型语言之间的区别。
  2. 现在在 C++ 中,我们有许多不同的数据类型。
  3. 到目前为止,你只见过 int 和 double 类型。
  4. 但我们还有更多内置的数据类型,将在本节中介绍。
  5. 在这一课中,我将给你这些类型的基本概述。
  6. 但随着我们继续深入本节,你会对这些类型越来越熟悉。

存储整数

  1. 用于存储整数的类型是 int。
  2. 在大多数系统中,int 类型占用 4 个字节的内存,这不是硬性规定。
  3. 根据实现的不同,整数占用的字节数可以从一个系统到另一个系统有所变化。
  4. 但大多数情况下,你可以假定一个整数占用 4 个字节的内存。
  5. 在 4 个字节中,我们可以存储从负 20 亿到正 20 亿的数值。

存储更小的整数

  1. 如果你想存储一个较小的数值,没必要浪费 4 个字节的内存。
  2. 所以我们可以使用 short 类型。
  3. short 类型占用 2 个字节的内存。
  4. 在 2 个字节中,我们可以存储从负 32,768 到正 32,767 的值。

存储更大的整数

  1. 用于存储更大整数的类型是 long。
  2. 在大多数系统中,long 类型通常与 int 类型相同。
  3. 还有 long long 类型,它占用 8 个字节的内存,可以存储非常大的数值。
  4. 根据经验,大多数情况下你会使用 short 或 int 类型。
  5. 除非你在处理涉及复杂数学计算的程序。

存储浮动小数的数字

  1. 对于带有小数的数字,我们称之为浮点数。
  2. 我们有 double 类型,这是你之前已经见过的。
  3. double 类型占用 8 个字节的内存。
  4. 我们还可以使用 float 类型,它占用 4 个字节的内存。
  5. 还有 long double 类型,它也占用 8 个字节的内存。
  6. 再次强调,大多数时候你会使用 double 类型,尤其是在存储货币值时。
  7. float 类型可能会导致精度丧失。

布尔值和字符类型

  1. 我们还有一个布尔类型 (bool),用于存储 true 和 false 值,通常用于表示某种条件。
  2. 比如:这个人是否符合贷款条件等。
  3. 我们还可以使用另一个内置类型 char 来存储单个字符。

总结

  1. 这就是 C++ 中基本数据类型的概览。
  2. 如前所述,随着我们继续进行本节内容,你将会更深入地探索这些数据类型。

0020 3 变量初始化

变量声明与初始化方式

现在你已经了解了 C++ 中的一些基本内建类型,接下来我们将看看几种不同的变量声明与初始化方式。

声明一个 double 类型变量

我们从声明一个名为 pricedouble 类型变量开始,并将其初始化为 99.99。到这里没有什么新内容。

声明一个 float 类型变量

那么,如何声明一个 float 类型的变量呢?我们可以声明一个名为 interest_ratefloat 类型变量,并将其初始化为 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 类型变量

现在,声明一个存储字符的变量,我们声明一个名为 letterchar 类型变量,并使用单引号表示字符,比如 'a'

声明一个 boolean 类型变量

最后,声明一个 boolean 类型的变量,我们将其命名为 is_valid,并将其值设置为 truefalseboolean 类型的变量只接受这两个值。

使用 auto 关键字

对于这些类型的变量,我们还可以使用 auto 关键字,让编译器自动推断变量的类型。例如,如果将 bool 改为 auto,然后将鼠标悬停在 is_valid 上,编译器就能识别 is_validbool 类型。同样的,如果将 char 改为 auto,然后查看变量的类型,编译器就会显示它是 char 类型。

对于 long 类型的变量,如果将 long 改为 auto,编译器会根据你添加的 l 后缀,知道它是 long 类型;如果没有加 l 后缀,file_size 会被当作 int 类型来处理。同样,对于 float 类型,如果使用 auto,编译器会知道 interest_ratefloat 类型,而如果没有加 f 后缀,它就会被当作 double 类型处理。

使用 auto 的好处是可以让代码更简洁一致,你不必手动指定类型。虽然你可以不使用 auto,但在处理更复杂类型时,auto 关键字特别有用,未来我们会讨论到这一点。

使用大括号进行变量初始化

在现代 C++ 中,还有一种初始化变量的方式,称为“大括号初始化”。假设我们声明一个名为 number 的整数,并将其初始化为 12。这时我们会看到一个警告,代码仍然可以编译并执行。输出的结果是 1,因为小数部分被去掉了。

另一种初始化变量的方式是使用大括号而非赋值操作符。将数值放在大括号里。现在,编译器会报告一个错误,因为我们没有正确初始化变量。因此,大括号初始化能够帮助我们避免这种错误。并且,如果不提供值,变量会被默认初始化为零。因此,如果运行程序,number 的值就是 0。

但是,如果你移除大括号初始化并运行程序,你会看到一个随机值,这通常称为“垃圾值”。每次运行程序时,这个值都可能不同,导致程序行为不可预测。因此,我们应该确保要么使用赋值操作符为变量初始化一个正确的值,要么使用空的大括号初始化。

0021 4 数字运算

数字系统与编程中的应用

在数学和编程中,我们有不同的数字系统,它们有各自的用途。

十进制数字系统

在日常生活中,我们使用的是十进制系统(即基数为 10),其中的数字从 0 到 9。计算机并不理解这些数字,它们只理解 0 和 1,这就是为什么我们有了二进制系统(基数为 2)。所以,在二进制系统中,数字只能是 0 或 1。

二进制数字系统

我们可以将任何数字表示为二进制。例如,十进制中的 55 在二进制中是 110111,这是一个很长的数字。因此,为了简化表示,我们使用十六进制系统(基数为 16)。

十六进制数字系统

十六进制系统的数字可以包含从 0 到 9 的数字以及从 a 到 f 的字母。正如我们所见,十六进制数字比二进制数字更加简洁。通常在编程中,我们使用十六进制数字来表示颜色。

使用十六进制表示颜色

你可能听说过 RGB(红绿蓝)颜色模型,使用六位数字的十六进制数来表示颜色。在十六进制中,红色、绿色和蓝色的值分别可以从 00 到 ff。通过这种方式,我们可以表示任何颜色,这在编程中非常有用,因为我们无需处理非常大的十进制或二进制数字。

在 C++ 中表示数字

接下来,我们来看如何在 C++ 中表示这些数字。假设我要声明一个名为 code_number 的整数并将其初始化为 55。如果我们想用二进制表示这个数字,可以在数字前加上 0b 前缀,然后输入二进制数。例如:0b110111

当我们打印该数字时,输出将是 55,正如预期的那样。

如果要用十六进制表示同样的数字,则在数字前加上 0x 前缀,然后输入十六进制数。在这个例子中,55 的十六进制表示为 0x37。同样,输出的结果会是 55。

十进制、二进制和十六进制的使用

在编程中,我们大多数时候使用的是十进制数字。我会说 99% 的时间我们都使用十进制数字,但根据应用程序的类型,在某些情况下,使用二进制或十六进制来表示数字可能更合适。

正负数表示

无论我们使用哪种表示方式,数字都可以是正数或负数。如果是正数,我们不需要显式写出正号,默认就是正数。然而,对于负数,我们必须显式地写出负号。

无符号类型 unsigned

在 C++ 中,有一个特殊的关键字 unsigned。如果将 unsigned 应用于一个数字类型,该类型就不能接受负值。表面上看,这似乎是一个好功能,但它实际上可能会导致一些难以发现的编程问题。

例如,如果你在程序中声明一个 unsigned 类型的变量并打印它,可能会得到一个非常大的正数。另一个例子是,如果你将该数字初始化为零,并在程序的其他地方递减它,当你打印它时,可能不会得到预期的负数,而是得到一个非常大的正数。这是因为 unsigned 类型不能表示负数,超出范围的值会变成一个非常大的正数。

建议

因此,我的建议是避免使用 unsigned 关键字。尽管 C++ 提供了这个特性,但并不意味着你应该使用它。正如在课程开始时我所说的,构建有用和实质性的程序时,你不需要学习 C++ 中的所有特性。

所以,尽量避免使用 unsigned 关键字。

0022 5 缩小类型

数字类型的缩小转换与数据丢失

在处理数字时,有一个概念是必须理解的,那就是 缩小转换(narrowing)。它发生在使用较大类型初始化较小类型的变量时。

缩小转换的示例

举个例子,我们声明一个整数 number,并将其设置为 1000000(百万)。为了让代码更具可读性,我们可以使用单引号分隔这些数字,这样看起来更加清晰。

接下来,我们声明一个 short 类型的变量 another,并将 number 的值赋给它。这时我们会看到一个警告:“从 intshort 的缩小转换(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 字节。存储一个较小的数字在更大的内存空间中是完全安全的,额外的内存空间将被填充为零。因此,这种操作通常不会导致数据丢失。

总结

0023 6 生成随机数

随机数生成与骰子模拟

在 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 随机数库的改进

在 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;

这样,当你查看代码时,就能清楚地知道 16 分别代表了骰子的最小和最大值。

完整的骰子模拟代码

#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++ 中生成随机数、限制其范围,并模拟了一个掷骰子的程序。掌握这些基本的随机数操作对于编写游戏和其他随机应用程序非常重要。

0024 7 格式化输出

控制输出格式:列对齐与浮点数格式化

打印标签并对齐

我们已经学习了如何使用 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 一旦被应用,所有后续输出都会左对齐,直到我们调用 rightsetw 来更改它。

示例:打印带有列对齐的标签

我们可以进一步整理代码,使其格式更美观。通过分开设置每一列的对齐方式,我们可以控制打印格式。例如:

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) 可以让数字显示更多的小数位。

流操控符的持久性(继续)

同样,fixedsetprecision 也是粘性的。这意味着一旦应用了这些操控符,后续的浮动数字将会使用这些设置,直到你更改它们。例如:

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;

此时,我们的输出表格已经完成,文本左对齐,数字右对齐。

0025 8 数据类型的大小和限制

数据类型的大小和溢出

系统中数据类型的大小

之前我们提到过,数据类型的大小与系统有关。通常情况下,可以假设 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 的最大值),因为值太小,导致回绕。

练习:查找其他数据类型的大小和极限

作为练习,我希望你能够利用本节课学到的内容,查找你机器上其他数据类型的大小和极限。你可以使用类似 sizeofnumeric_limits 的方法来获取 shortlongfloat 等数据类型的信息。

例如:

std::cout << "short 的大小: " << sizeof(short) << std::endl;
std::cout << "long 的大小: " << sizeof(long) << std::endl;
std::cout << "float 的最大值: " << std::numeric_limits<float>::max() << std::endl;

通过这些方法,你可以探索出更多关于不同数据类型在你机器上的表现,理解它们的大小和溢出行为。

0026 9 布尔值运算

布尔类型和流操作符

布尔类型的基本使用

在 C++ 以及几乎所有其他编程语言中,我们都有一种称为 布尔类型 的数据类型,用于表示 truefalse 值,这对于编写条件判断非常有用。比如,我们可以声明一个布尔变量 is_new_user,并将它设置为 truefalse。这些是 C++ 中的关键字。

bool is_new_user = true;

在代码中,我们打印 is_new_user 变量的值并运行程序,可能会看到如下的输出结果:

cout << is_new_user << endl;

布尔值的内部表示

虽然在代码中我们使用 truefalse,但在计算机内部,布尔值通常是用 1 和 0 来表示的。也就是说:

因此,上面的代码实际上输出的就是数字 10,具体取决于 is_new_user 的值。

// 输出 1,因为布尔值 true 被表示为 1
cout << is_new_user << endl;

使用 0 或 1 来代替 true 或 false

如果我们用 01 来代替 truefalse,程序仍然可以正常工作:

bool is_new_user = 1; // 或者 bool is_new_user = 0;
cout << is_new_user << endl;

然而,这种方式虽然可行,但并不是最佳实践。因为它可能导致代码的可读性差,尤其是当变量名或程序逻辑变得复杂时。因此,虽然使用 10 不会报错,但它并不推荐作为布尔值的表示方式。

使用流操作符(Stream Manipulators)来控制输出格式

为了更清晰地输出布尔值的 truefalse,C++ 提供了一个流操作符(stream manipulator),叫做 boolalpha。它的作用是在打印布尔值时,将 truefalse 显示为文字而不是数字。

在打印布尔变量之前,我们可以使用 boolalpha 来启用这个显示方式:

cout << std::boolalpha << is_new_user << endl;  // 输出 true

这样,当我们打印布尔值时,它将显示为 truefalse,而不是 10

boolalpha 是一个粘性操作符

boolalpha 是一个粘性操作符(sticky manipulator),意味着一旦我们应用它,之后打印的所有布尔值都会以 truefalse 形式显示,直到我们显式关闭它为止。为了关闭 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

总结

在后面的决策部分(Decision Making)中,我们将更深入地使用布尔类型进行条件判断,帮助你更好地理解其实际应用。

0027 10 字符和字符串操作

字符类型(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 函数确保能够正确地读取包含空格的字符串。

总结

在后续课程中,我们将进一步探索字符串的高级操作和字符处理技术。

0028 11 数组操作

数组(Arrays)

数组的基本概念

在 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

这个程序的关键点在于:

总结

0029 12 类型转换

数据类型转换与强制类型转换(Casting)

类型转换的基本概念

在编写更多代码时,我们可能会遇到需要将一种数据类型转换为另一种数据类型的情况,这就是所谓的 类型转换(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。这正是我们所期望的结果。

背后的机制:

显式类型转换与数据丢失

现在,假设我们将 z 的类型从 double 改为 int。这时,程序会发出警告,因为将一个 double 类型的值赋给 int 类型的变量时,可能会导致数据丢失。让我们来看一个例子:

int x = 1;
double y = 2.2;
int z = x + y;  // 可能会丢失数据

背后的机制:

当我们运行这个程序时,输出会是 3,但是这可能不是我们想要的结果。为了防止数据丢失,我们可以显式地告诉编译器,我们知道会发生数据丢失,并强制执行类型转换。

强制类型转换(Casting)

强制类型转换是指我们明确告诉编译器如何转换数据类型。有两种主要的方式来进行强制类型转换:

  1. C 风格的强制类型转换: 这种方式使用一个括号,括号内指定目标类型,例如:

    int z = (int)(x + y);  // C 风格的强制类型转换

    这种方式的问题是,如果转换无法进行,程序会在运行时发生错误。因此,我们必须在运行时测试程序,以确保转换是有效的。

  2. C++ 风格的强制类型转换: C++ 提供了更安全的方式,即使用 static_cast 运算符。它能够在编译时捕获错误,这样我们就不必在运行时测试程序。

    int z = static_cast<int>(x + y);  // C++ 风格的强制类型转换

    使用 static_cast 比 C 风格的转换更安全,因为如果转换无法进行,编译器会告诉我们错误。

练习:修改整数除法结果

假设我们有两个整数 xy,我们要将 x 除以 y,并将结果存储在一个 double 类型的变量 z 中。让我们看一下代码:

int x = 5;
int y = 2;
double z = x / y;  // 结果是 2,因为整数相除会丢失小数部分

如果我们打印 z,输出将是 2,而不是我们预期的 2.5,因为 xy 都是整数类型,整数相除会舍弃小数部分。

如何修改代码,使得结果包含小数部分? 我们可以强制转换 xy 中的一个值为 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 类型,从而执行浮点数除法。

总结

通过掌握类型转换,我们可以更灵活地处理数据类型,并避免潜在的错误和数据丢失。

0030 1 介绍

决策结构:条件判断和流程控制

欢迎回到《终极 C++ 课程》的另一章节。在这一部分中,你将学习如何编写能够做出决策的程序。我们将讨论比较运算符和逻辑运算符,用于编写条件判断和业务规则。接着,我们会讲解如何使用 ifswitch 语句来控制程序的流程。你还将学会如何使用条件运算符来简化 if 语句。通过这一部分的学习,你将能够编写更有用的 C++ 程序。

让我们开始吧!

0031 2 比较运算符

比较运算符:用于比较值的运算符

在这一部分,我们将讨论 比较运算符,这些运算符用于比较值。首先,假设我们声明了一个变量 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 后,布尔值会以 truefalse 的形式显示,增加代码的可读性。

注意:检查相等性时要使用 ==

有一个很常见的错误是,初学者容易将 === 混淆。== 是检查相等性的运算符,而 = 是赋值运算符。例如,如果你写 X = Y,那么实际上是将 Y 的值赋给了 X,而不是比较它们的值。对于布尔表达式,使用单个等号会导致 X 的值被改变,这是非常容易引发错误的地方。

类型不同的比较

通常我们会比较相同类型的值。例如,两个整数之间的比较。然而,C++ 允许将不同类型的值进行比较,编译器会自动将较小或不精确的类型转换为更大的、更精确的类型。让我们看看一个例子,比较一个整数和一个浮动值:

int X = 10;
double Y = 5.0;
bool result = (X == Y);

运行程序时,我们会得到 false,这是因为 10 并不等于 5.0,但这里有一个有趣的地方,编译器会自动将 Xint 转换为 double,以便执行比较。因此,当比较不同类型的值时,编译器会自动进行类型转换。

字符比较

你也可以使用比较运算符来比较字符。举个例子,我们可以声明两个字符变量并比较它们:

char first = 'a';
char second = 'A';
bool result = (first == second);

此时,程序会输出 0,表示 false。因为在 C++ 中,字符比较是区分大小写的——小写字母 'a' 和大写字母 'A' 是不同的字符。所以,字符比较是大小写敏感的

通过这些运算符,你可以在 C++ 中进行各种类型的数据比较,用于编写有条件的决策逻辑。

0032 3 逻辑运算符

逻辑运算符:用于组合布尔表达式或条件的运算符

接下来我们将讨论 逻辑运算符。逻辑运算符用于组合两个或多个布尔表达式或条件。我们常见的逻辑运算符有 &&(与运算符)、||(或运算符)和 !(非运算符)。

示例 1:与运算符 (&&)

假设我们声明一个变量 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++ 编译器会从左到右开始评估表达式。如果第一个条件已经是假的,编译器就不再评估第二个条件,这称为 短路求值

示例 2:或运算符 (||)

与运算符的对立面是 或运算符 (||)。如果任何一个条件为真,整个表达式的结果就会为真。例如,如果我们将 age 设置为 10,第二个条件永远为真(假设我们检查 age < 100),那么整个表达式的结果会为真:

age = 10;
isEligible = (age > 18) || (age < 100);
std::cout << std::boolalpha << isEligible;

在这种情况下,第二个条件是 true,所以无论第一个条件是否为真,最终结果都会是 true。然而,编译器也会应用短路求值,首先评估第一个条件。如果第一个条件为真,它将跳过对第二个条件的评估。

示例 3:非运算符 (!)

非运算符 (!) 用于反转布尔值。如果布尔变量 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,条件就成立。虽然这种逻辑在现实中可能不太符合常理,但它演示了如何使用不同的运算符来组合多个条件。

总结

逻辑运算符(&&, ||, !)使得我们能够组合多个布尔条件进行更复杂的决策和判断。它们广泛应用于需要多重条件评估的场景。使用逻辑运算符时,始终注意运算符的优先级,并尽可能地使用括号来明确各个条件的优先级,从而提高代码的可读性。

0033 4 逻辑运算符的优先级

运算符优先级和括号的使用

在之前的课程中,我们讨论了数学表达式时,乘法和除法运算符的优先级高于加法和减法运算符。逻辑运算符也有类似的优先级顺序,记住这一点非常重要。逻辑运算符的优先级如下:

  1. 非运算符 (!)
  2. 与运算符 (&&)
  3. 或运算符 (||)

就像数学运算一样,我们可以通过括号来改变运算顺序。

示例 1:非运算符与与运算符的优先级

让我们声明三个布尔变量 a, bc,并使用抽象的名字(如 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,因为 atrue,所以 !a 会变为 false。然后,b && false 结果为 false,最终 result 的值是 false

示例 2:与运算符与或运算符的优先级

接下来,我们更改表达式,使用 a || b && c,并看看 C++ 是如何评估这个表达式的:

result = a || b && c;
std::cout << std::boolalpha << result;

请停下来思考一下这个表达式的执行顺序。

答案是:首先会评估 b && c,因为 与运算符 (&&) 的优先级高于 或运算符 (||)。由于 btruecfalse,所以 b && c 的结果为 false。接着评估 a || false,由于 atrue,最终结果为 true

示例 3:使用括号改变优先级

为了改变运算顺序,我们可以使用括号。例如,若要确保 a || b 先被评估,可以加上括号:

result = (a || b) && c;
std::cout << std::boolalpha << result;

现在,a || b 先被评估,然后再与 c 进行 && 运算。如果 abtrue,就会进入 && 操作。

编写布尔表达式:判断是否符合工作要求

假设我们需要判断一个申请人是否符合工作要求:该申请人必须是美国公民,并且必须拥有 学士学位 或者 至少两年工作经验

我们可以使用以下变量来编写逻辑表达式:

  1. isCitizen:判断是否为美国公民。
  2. hasBachelorDegree:判断是否拥有学士学位。
  3. 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 变量,测试不同的输入值来确保程序的正确性。

  1. 如果将工作经验设为 2 年,结果应该仍然是 true,因为工作经验已满足条件。
  2. 如果将工作经验设为 1 年,结果应该是 false,因为条件要求至少 2 年的工作经验。
  3. 如果申请人有学士学位,则无论工作经验多少,都应该是 true
  4. 如果申请人不是美国公民,则无论其他条件如何,结果应该是 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,因为不是美国公民

总结

  1. 运算符优先级:理解运算符的优先级(如非运算符、与运算符、或运算符)对于正确评估逻辑表达式至关重要。你可以通过括号来显式改变运算顺序。
  2. 布尔表达式:通过合理的布尔表达式,你可以判断申请人是否符合工作要求,确保逻辑正确,特别是在涉及多个条件时。
  3. 调试与验证:每当修改输入值时,测试程序的不同情况以确保逻辑运算的正确性。

0034 5 if 语句

if语句的使用

if 语句用于控制程序中的逻辑流。通过使用 if 语句,我们可以根据不同的条件执行不同的代码块。接下来,我们通过一些示例来解释如何使用 if 语句。

示例 1:简单的 if 语句

假设我们有一个变量 temperature,并将其设置为 70。根据 temperature 的值,我们希望打印不同的消息。例如,如果温度低于 60,我们想要打印“穿外套”。

我们可以使用如下代码:

int temperature = 70;

if (temperature < 60) {
    std::cout << "Wear a coat" << std::endl;
}

此时,temperature 的值是 70,它不小于 60,所以条件为假,程序不会打印任何信息。

示例 2:添加 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”。

示例 3:多重条件的使用 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”。

示例 4:实现基于销售额计算佣金

假设我们需要根据销售额来设定佣金。例如:

我们可以使用以下代码来实现这个要求:

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

示例 5:简化逻辑

在编写条件语句时,有时我们可以简化代码。例如,下面的代码:

if (sales > 10000 && sales <= 15000) {
    commission = sales * 0.15;
}

如果第一个条件 sales > 10000 不成立,我们就知道 sales 已经大于 10,000。所以我们可以去掉重复的部分,只保留后面的条件:

else if (sales <= 15000) {
    commission = sales * 0.15;
}

这样,代码就更简洁了,避免了冗余的检查。

总结

通过这些示例,我们可以灵活地使用 if 语句来控制程序逻辑,处理各种不同的情况。

0035 6 嵌套 if 语句

嵌套 if 语句

在某些情况下,我们需要在一个 if 语句中嵌套另一个 if 语句,这就是所谓的 嵌套 if 语句。嵌套 if 语句能够让我们处理更复杂的逻辑条件。接下来,我们通过一个示例来演示如何在 C++ 中使用嵌套的 if 语句。

示例:计算大学生的学费

假设我们需要根据学生的国籍和居住地来计算学费。规则如下:

  1. 如果学生是美国公民,并且是加利福尼亚州的居民,那么学费为 0。
  2. 如果学生是美国公民,但不是加利福尼亚州的居民,那么学费为 1000。
  3. 如果学生不是美国公民,则学费为 3000。

步骤 1:声明基本变量

首先,我们需要两个变量来检查学生的身份和居住地。我们用布尔变量来表示学生是否是美国公民,以及是否是加利福尼亚州的居民。

bool isCitizen = true;  // 学生是否是美国公民
bool isCaliforniaResident = true;  // 学生是否是加利福尼亚州的居民
int tuition = 0;  // 学费初始为0

步骤 2:使用嵌套的 if 语句

我们可以先检查学生是否是美国公民。如果是美国公民,那么进一步检查他们是否是加利福尼亚州的居民。如果他们是加利福尼亚州的居民,学费为 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。

步骤 3:简化代码

在有时,嵌套的 if 语句可能会变得冗长和难以阅读。我们可以通过简化表达式来避免不必要的嵌套。

例如,考虑下面的修改:

  1. 我们可以初始化学费为 0,这样就不需要再次在每个条件中重新设置。
  2. 我们可以使用逻辑“非”运算符 (!) 来简化判断学生是否为非加利福尼亚州居民。

简化后的代码如下:

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 语句在处理复杂逻辑时非常有用,但也需要注意过度嵌套可能导致代码的复杂性增加。

0036 7 条件运算符

条件运算符

在 C++ 中,有一个特殊的运算符,叫做 条件运算符?:),它可以用来简化我们的 if 语句。这个运算符在许多源自 C++ 的编程语言中都有使用,如 C#、Java、JavaScript 等。接下来,我们通过一个示例来演示如何使用条件运算符。

示例:根据销售额计算佣金

首先,我们定义一个整数 sales 来表示销售额,并将其设为 11,000。然后,我们定义一个双精度浮点数 commission 来表示佣金。假设如果销售额大于 10,000,我们将佣金设置为 0.1,否则设置为 0.05。

步骤 1:使用 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;
步骤 2:使用条件运算符简化代码

现在,我们可以用条件运算符来简化上面的代码。条件运算符的语法是:

condition ? value_if_true : 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

通过这种方式,我们可以将一个 if-else 语句简化为一行代码,从而提高代码的简洁性和可读性。

示例:比较两个数并输出较大的数

接下来,我们通过一个练习来进一步掌握条件运算符的使用:

题目:编写一个程序,要求用户输入两个值,找出较大的值并打印出来。首先使用传统的 if 语句实现,然后使用条件运算符简化代码。

步骤 1:使用 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;
}

在这个例子中,我们让用户输入两个数 firstsecond,然后用 if 语句比较两个数的大小,最后输出较大的那个数。

步骤 2:使用条件运算符简化代码

我们可以通过条件运算符将 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 变量中。

总结

通过掌握条件运算符,您可以减少代码的冗余,使程序更加简洁明了。

0037 8 switch 语句

Switch 语句

在 C++ 中,除了 if 语句之外,还有一个用于做决策的语句,那就是 switch 语句。它可以用来替代多个 if-else 语句,尤其在需要判断一个变量的多个可能值时,switch 语句可以让代码更加简洁和清晰。我们通过一个示例来演示如何使用 switch 语句。

示例:菜单选择

我们首先打印一个菜单给用户,如果用户选择不同的选项,则执行不同的操作。例如:

步骤 1:使用 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;
}

在这个例子中,我们首先输出一个菜单,接着根据用户的输入选择相应的操作。

步骤 2:使用 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 语句的工作原理

  1. switch 语句

    • 在括号内放置要进行比较的变量。
    • 每个 case 标签后面跟着一个值,表示要比较的目标值。如果 switch 中的变量和 case 后的值相等,就执行该 case 后的语句。
    • 每个 case 后需要使用 break 语句来结束当前 case,避免执行到下一个 case
    • default 标签是可选的,如果没有任何 case 匹配时,就会执行 default 部分的代码。
  2. break 语句

    • break 用来终止 switch 语句的执行。如果没有 break,则代码会继续执行下一个 case,即使条件不匹配(这就是所谓的“贯穿”)。

switchif 语句的比较

switch 语句与 if 语句有一些关键区别:

不过,在面对多种选择的情境时,switch 语句的结构更加清晰,代码也更简洁。

实践题:简单计算器

现在,作为练习,我们编写一个简单的计算器程序,要求用户输入两个数字和一个运算符(加、减、乘、除)。根据运算符的不同,计算并输出结果。如果用户输入了一个无效的运算符,则输出“无效运算符”的提示。

步骤 1:使用 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;
}
流程说明
  1. 输入两个数字和运算符
    • 用户输入两个数字 firstsecond,以及一个运算符 oper,然后根据运算符进行相应的计算。
  2. switch 语句
    • 根据运算符选择执行对应的 case,如果是加法就执行加法操作,以此类推。
    • 如果用户输入无效的运算符,执行 default 块,输出“无效运算符”。
    • 对于除法操作,增加了对除数为零的特殊处理。
示例运行

总结

switch 语句在处理多个条件判断时非常有用,尤其是当判断的条件是一个变量的多个离散值时,可以让代码更易读和管理。

0038 1 介绍

循环结构

欢迎回来!在这节课中,我们将探索 C++ 中的各种循环结构。你将学习如何使用 for 循环、基于范围的 for 循环,以及 whiledo-while 循环。同时,我们还会讨论如何使用 breakcontinue 语句来改变程序的控制流。

如果你跟着练习并完成所有的任务,你将能够编写更复杂的程序。事实上,计算机科学学生学习的大多数算法都可以通过循环和条件语句来实现。因此,掌握循环的使用是非常重要的,让我们马上开始吧!


for 循环

for 循环是最常见的循环结构之一,它允许我们在某个条件下重复执行一段代码。for 循环通常用于已知次数的循环。

for 循环的基本结构:

for (初始化; 条件; 更新) {
    // 循环体
}

示例:打印 1 到 10 的数字

#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 循环

基于范围的 for 循环(Range-based for loop)是 C++11 引入的一种循环方式,用于遍历容器中的元素(如数组、vector 等)。

语法结构:

for (元素类型 变量名 : 容器) {
    // 循环体
}

示例:遍历数组

#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 循环是一种基础的循环结构,适用于当你不知道循环会执行多少次时,可以使用 while 循环,直到某个条件为 false

while 循环的基本结构:

while (条件) {
    // 循环体
}

示例:打印 1 到 10 的数字

#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 循环

do-while 循环与 while 循环非常相似,不同之处在于 do-while 循环至少会执行一次循环体,因为条件判断是在循环体执行之后才进行的。

do-while 循环的基本结构:

do {
    // 循环体
} while (条件);

示例:打印 1 到 10 的数字

#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 语句

在循环结构中,breakcontinue 语句用于改变程序的控制流。

break 语句

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 用于跳过当前循环的剩余部分,立即进入下一次循环的条件判断。

示例:使用 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 循环。每种循环有其适用的场景:

此外,我们还介绍了 breakcontinue 语句,它们可以用来改变循环的控制流。在实际编程中,掌握这些循环结构和控制语句将使你能够编写更高效、更灵活的程序。

0039 2 for 循环

for 循环基础

在本节中,我们将讨论 for 循环,它是用来重复执行一组指令的一种结构。首先,假设我们想打印数字从 1 到 5,这是一个常见的需求。我们可以使用 cout 打印数字 1,然后依次打印数字 2、3、4 等。为了实现这个,我们可能需要复制粘贴多次相同的语句。显然,这不是一个理想的编程方法,因为如果我们需要打印从 1 到 100 万的数字,那我们就需要写 100 万行代码来完成这一任务。

幸运的是,循环提供了更好的解决方案,能够自动重复执行一段代码。因此,我们可以用一个循环来替代重复写代码的繁琐工作。

使用 for 循环

for 循环可以帮助我们重复执行一个或多个语句,并且它的结构非常简洁。我们可以通过一个变量来控制循环的执行次数,并且每次循环时,变量的值会发生变化。接下来,我将展示如何使用 for 循环来完成这一任务。

for 循环的结构

for 循环的基本语法如下:

for (初始化; 条件; 更新) {
    // 循环体
}

示例:打印 0 到 4 的数字

#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 的数字。

循环的执行过程

让我们逐步解释这两行代码的执行过程:

  1. 初始化i 被初始化为 0。
  2. 条件检查i < 5 这个条件被检查,第一次检查时,i 是 0,条件成立,因此进入循环体。
  3. 循环体执行:打印当前 i 的值。
  4. 更新:执行 i++,将 i 增加 1。
  5. 重复:程序再次回到条件检查部分,检查是否继续执行。这个过程会重复,直到条件不成立为止。

改变循环范围

如果我们希望打印从 1 到 5 的数字,可以通过初始化 i 为 1,而不是 0。同样,如果我们希望打印从 5 到 1 的数字,我们可以设置 i 从 5 开始,然后每次减少 1。

示例:打印 1 到 5 的数字

#include <iostream>
using namespace std;

int main() {
    for (int i = 1; i <= 5; i++) {
        cout << i << " ";
    }
    return 0;
}

输出:

1 2 3 4 5

示例:打印 5 到 1 的数字

#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 之间的所有奇数数字,我们可以使用模运算来判断当前数字是否为奇数。

示例:打印 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 = 64! = 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;
}

阶乘的输出示例

解释

  1. 程序要求用户输入一个正整数。
  2. 如果输入的数字小于零,程序会输出错误信息。
  3. 如果输入的数字是正数或零,程序会使用 for 循环计算该数字的阶乘。
  4. 计算时,我们从 1 开始,逐步将每个数字与当前阶乘结果相乘,直到数字为止。
  5. 最后,输出计算结果。

总结

在本节课中,我们学习了如何使用 for 循环来重复执行代码,如何控制循环次数,以及如何结合条件判断来改变程序的行为。通过这些基础知识,我们能够编写更灵活的程序,自动执行重复性任务,如打印数字、计算阶乘等。

0040 3 基于范围的 for 循环

Range-Based 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;
}

解释:

  1. 我们首先声明一个包含温度值的数组 temperatures,并初始化它。
  2. 然后,我们通过基于范围的 for 循环遍历数组中的每个温度,并将其累加到 sum 变量中。
  3. 最后,我们通过将 sum 除以数组中的元素数量 count 来计算平均值,并打印结果。

关键点:

通过这些方法,你可以更加高效和简洁地处理数组、字符串以及其他容器中的数据。

0041 4 while 循环

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;
}

解释:

  1. 我们声明了两个变量,一个是 secret(秘密数字),另一个是 guess(用户的猜测)。
  2. 使用 while 循环,条件是用户的猜测不等于秘密数字 secret,只要条件为真,循环就会继续。
  3. 每次循环,程序提示用户输入一个数字,并检查用户的猜测。如果猜错了,程序会提示 "Wrong guess! Try again."。
  4. 一旦用户猜对了数字,循环结束,程序会输出祝贺信息。

示例输出:

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!

结论

0042 5 do 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-whilewhile 的区别

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;
}

解释:

  1. 我们首先声明了变量 number,并初始化为 0。
  2. do-while 循环中,我们要求用户输入一个数字。然后,我们检查数字是否在 1 到 5 之间。
  3. 如果输入的数字无效(小于 1 或大于 5),程序会重复提示用户输入,直到用户输入有效数字为止。
  4. 如果用户输入有效数字,循环会结束,程序输出用户输入的有效数字。

示例输出:

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)为止。

注意事项

  1. do-while 循环中,变量 number 必须在循环体外部声明,因为在 do 块内,变量 number 不能被直接访问。我们需要确保该变量的作用域覆盖整个循环体。
  2. 由于 do-while 循环至少会执行一次,所以它特别适合需要至少进行一次用户输入或执行某项操作的场景。

总结

do-while 循环通常用于那些必须执行一次操作之后再判断是否继续的场景,例如用户输入验证、菜单选择等。

0043 6 break 和 continue 语句

循环的额外控制:breakcontinue 语句

在 C++ 中,除了常规的循环控制语句,我们还可以使用两个额外的控制语句:breakcontinue。这些语句能帮助我们更灵活地控制循环的执行过程。

break 语句

break 语句用于 提前终止循环。当循环体中的某个条件满足时,可以使用 break 来跳出循环,直接跳到循环体外的代码执行。通常,break 语句常用于在某些特定条件下提前结束循环。

continue 语句

continue 语句用于 跳过当前循环中的迭代,并继续执行下一次迭代。与 break 不同,continue 不会终止整个循环,而是跳过当前的循环步骤,直接开始下一次迭代。

示例:使用 breakcontinue

让我们通过一个简单的例子来演示 breakcontinue 的使用。

#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;
}

解释:

  1. 第一部分:我们使用了一个简单的 for 循环,打印从 1 到 5 的数字。但是通过 if (i % 3 != 0) 判断,只打印不是 3 的倍数的数字。结果输出是:1、2、4、5。

  2. 第二部分:我们使用 continue 语句来跳过每一个 3 的倍数。如果 i % 3 == 0,那么程序会跳过当前的迭代,继续下一次迭代。输出结果是:1、2、4、5。

  3. 第三部分:我们使用 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;
}

解释:

  1. 我们创建了一个无限循环 while (true),这个循环会一直执行,直到用户输入一个有效的数字(在 1 到 5 之间)。
  2. 如果用户输入的数字在有效范围内,我们使用 break 语句终止循环,并打印有效的数字。
  3. 如果输入无效(小于 1 或大于 5),程序会输出错误信息,并继续请求用户输入。

输出示例:

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 循环并在循环外部进行条件判断,我们可能会面临重复代码的问题。

通过这种方式,我们的程序更加清晰且易于维护。

0044 7 嵌套循环

嵌套循环:构建更复杂的算法

在 C++ 中,除了基本的单一循环结构外,我们还可以使用 嵌套循环 来处理更加复杂的任务。嵌套循环指的是将一个循环结构放在另一个循环结构内,这种技术非常强大,能够帮助我们实现一些复杂的算法。

嵌套循环示例:生成坐标对

假设我们想要生成从 1 到 5 的 XY 坐标组合,具体地,我们希望生成类似于以下的坐标对:

(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 到 5 的 X 坐标。
  2. 内部循环:在每次外部循环迭代时,生成从 1 到 5 的 Y 坐标。
  3. 打印坐标对:每次内循环迭代时,打印一个坐标对。
  4. 换行:每次外部循环结束后,打印一个换行符,让坐标对按行分布。

输出:

(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) 

编写一个星号三角形程序

在这个练习中,我们将编写一个程序来打印一个星号三角形。用户输入行数,程序根据用户的输入生成相应的三角形。

程序设计

  1. 用户输入行数:程序首先要求用户输入行数(例如 5 行)。
  2. 外部循环:外部循环控制行数的数量。每一行需要打印一定数量的星号。
  3. 内部循环:根据当前行数打印相应数量的星号。
  4. 换行:每打印完一行,换行到下一行。

代码示例:

#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;
}

解释:

  1. 用户输入行数:首先,我们让用户输入一个整数 rows,表示三角形的行数。
  2. 外部循环for (int i = 1; i <= rows; ++i),外部循环控制打印的行数,行数从 1 到 rows
  3. 内部循环for (int j = 1; j <= i; ++j),内部循环控制每行星号的数量。每行的星号数量与当前行数 i 相等。
  4. 换行:每行打印完后,调用 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;
}

解释:

  1. 输入验证:我们增加了一个检查,如果用户输入的行数小于等于 0,程序会提示用户输入一个正数并终止程序。
  2. 三角形打印:如果输入有效,程序会按照预期打印三角形。

输出示例:

Enter the number of rows: 5
*
**
***
****
*****

总结

  1. 嵌套循环 是实现复杂算法的强大工具。我们可以将一个循环放在另一个循环中,来处理多维度的任务,例如生成坐标组合或者打印动态星号图形。
  2. 星号三角形问题 是经典的面试题之一,关键在于理解如何通过外部循环和内部循环配合来动态打印图形。
  3. 输入验证边界条件处理 是编写健壮程序的必要步骤,特别是对于用户输入的情况,需要确保程序的稳定性。

通过这些示例,你可以看到嵌套循环在实际问题中的应用,同时也能够学会如何处理一些常见的编程问题,如用户输入的验证和程序的扩展性。

0045 1 介绍

函数:让程序更加模块化和可重用

随着我们编写的程序变得越来越复杂,我们需要将代码组织成更小的、可重用的部分。这就是函数的作用所在。在本节中,你将学习如何定义和调用函数,如何为函数分配默认值参数,如何重载函数,如何通过值或引用传递参数,还会了解局部变量和全局变量的区别,以及如何在不同文件中组织函数。

1. 定义和调用函数

函数是实现特定任务的代码块。你可以通过调用函数来执行这些任务。在 C++ 中,定义函数的一般格式如下:

// 函数的声明
返回类型 函数名(参数列表) {
    // 函数体
    // 执行某些操作
}

示例:简单的函数定义和调用

#include <iostream>
using namespace std;

// 定义一个简单的函数
void greet() {
    cout << "Hello, world!" << endl;
}

int main() {
    greet();  // 调用函数
    return 0;
}

在这个示例中,greet 函数没有参数,也没有返回值。我们在 main 函数中调用它,它会打印一条欢迎信息。

2. 函数参数与默认值

函数可以接受参数,这些参数在调用时提供给函数,函数可以根据这些参数来执行特定的任务。你还可以为参数赋予默认值,这样在调用函数时,如果没有提供相应的值,函数就会使用默认值。

示例:带参数的函数

#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"

3. 函数重载

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;
}

4. 通过值或引用传递参数

在 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;
}

5. 局部变量和全局变量

在 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 是一个全局变量,它的值在多个函数调用之间保持不变。

6. 函数分文件组织

在大型程序中,为了提高代码的可维护性和可读性,我们常常将函数分离到不同的文件中。这样可以避免在一个文件中包含所有代码,提高代码的模块化程度。

示例:将函数分离到不同文件

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;
}

总结

  1. 函数:使得代码更加模块化和可重用,提升了程序的组织结构。
  2. 函数参数:可以设置默认值,也可以重载函数以处理不同的参数类型和数量。
  3. 参数传递方式:可以选择值传递或引用传递,决定了是否修改原始数据。
  4. 局部变量与全局变量:局部变量只在函数内有效,全局变量在整个程序中有效。
  5. 函数的文件组织:通过将函数声明和定义分离到不同的文件中,增强程序的可维护性和可扩展性。

掌握这些函数的基本概念和技巧,会让你在编写复杂程序时更加得心应手。

0046 2 定义和调用函数

函数的创建与调用

在课程的早期,我们提到过电视机有几个功能,每个功能负责完成特定的任务。电视机有更换频道、控制音量、调节亮度等功能。同样地,我们的程序往往也有几十、几百甚至上千个函数,每个函数负责一个独立的任务。到目前为止,我们的程序只有一个函数,那就是 main 函数。现在,让我们来看一下如何创建其他函数来实现不同的任务。

1. 创建一个简单的函数

就像我们定义 main 函数一样,创建其他函数的方法是类似的。我们首先定义函数的返回类型,返回类型可以是你已经学过的任何类型,比如 intdoublebooleanstring 等等。如果函数没有返回值,我们需要使用 void 关键字作为返回类型。

示例:创建一个简单的函数

#include <iostream>
using namespace std;

// 定义一个函数,返回类型为 void
void greet() {
    cout << "Hello, world!" << endl;
}

int main() {
    greet();  // 调用 greet 函数
    return 0;
}

在上面的代码中,greet 函数没有参数,并且不返回任何值。当我们在 main 函数中调用它时,它会打印 "Hello, world!"。

2. 为函数添加参数

我们可以为函数添加一个或多个参数,用来传递值。在调用函数时,这些参数将被传递给函数,函数会根据这些参数来执行任务。

示例:带参数的函数

#include <iostream>
using namespace std;

// 定义一个带参数的函数
void greet(string firstname, string lastname) {
    cout << "Hello, " << firstname << " " << lastname << "!" << endl;
}

int main() {
    greet("John", "Doe");  // 调用函数,并传递参数
    return 0;
}

在这个例子中,greet 函数接收两个参数:firstnamelastname,并将它们组合打印出来。我们在 main 函数中调用时传递了实际的名字。

3. 参数和实参的区别

例如,在上面的 greet 函数中,firstnamelastname 是参数,而 "John""Doe" 则是实参。

4. 返回值的函数

有些函数需要返回一个值,而不仅仅是执行某些操作。在这种情况下,我们需要在函数中使用 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 函数接收 firstnamelastname 两个参数,并返回它们的组合(全名)。我们在 main 函数中调用该函数并存储返回值。

5. 使用多个函数进行任务分解

通过将不同的任务分配给不同的函数,我们可以使程序更具可读性和可维护性。比如,可以创建一个函数来生成全名,然后另一个函数来打印问候语。

示例:组合多个函数

#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 函数来打印问候语。

6. 使用条件运算符简化代码

在 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 函数使用条件运算符来返回 firstsecond 中较大的值。

7. 测试函数

编写函数时,测试不同的输入和场景是很重要的。确保函数能正确处理各种边界情况,例如相同的值、不同的顺序、负数等。

示例:测试 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 函数的不同输入,确保它在不同场景下都能正确工作。

总结

  1. 创建函数:可以通过定义返回类型、函数名和参数列表来创建函数。
  2. 函数调用:通过调用函数名并传递实参来执行函数。
  3. 参数和实参:参数是函数定义时的占位符,而实参是调用函数时传递给函数的实际值。
  4. 返回值函数:可以通过 return 语句返回函数的结果。
  5. 函数组合:通过将任务拆分为多个函数,可以使程序更清晰、易于维护。
  6. 条件运算符:使用条件运算符可以简化代码,特别是处理简单的条件判断。
  7. 函数测试:测试函数时要考虑多种情况,确保代码的健壮性。

通过这些技巧,你可以编写更模块化、可维护的代码,并有效地解决各种编程问题。

0047 3 带默认值的参数

使用默认值参数

有时我们需要为函数的参数指定默认值。这样,在调用函数时,如果没有提供某个参数的值,系统会自动使用这个默认值。下面是一个关于如何为参数设置默认值的例子。

1. 给参数设置默认值

假设我们需要创建一个计算税额的函数。这个函数应该返回一个 double 类型的值,函数名为 calculateTax,并接受两个参数:incometaxRate。在函数内部,我们会将 incometaxRate 相乘来计算税额。

示例:带默认值的参数

#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 函数接受两个参数:incometaxRate,其中 taxRate 的默认值是 0.2。当我们调用 calculateTax 函数时,如果没有提供税率,默认税率 0.2 会被使用;如果我们提供了税率(例如 0.3),则默认值会被新的税率覆盖。

2. 参数默认值的规则

示例:调用带默认值的函数

3. 默认值参数的顺序

需要注意的是,具有默认值的参数必须出现在没有默认值的参数之后。如果在具有默认值的参数前面有没有默认值的参数,编译器会报错。

示例:错误的默认值顺序

#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 有默认值。这样的顺序是合法的。

总结

  1. 默认值参数:你可以为函数的参数设置默认值,这样在调用函数时,如果没有传递该参数的值,系统会使用默认值。
  2. 覆盖默认值:如果你传递了参数的值,那么该值会覆盖默认值。
  3. 参数顺序:具有默认值的参数应该始终放在没有默认值的参数之后,否则会引发编译错误。

通过为函数参数设置默认值,我们可以简化函数调用,使得某些参数变得可选,从而提高程序的灵活性。

0048 4 函数重载

函数重载

有时候我们需要创建多个名称相同,但参数不同的函数,这种技术称为函数重载(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 函数不同,因为它接受两个参数。

2. 如何调用重载函数

当我们调用重载函数时,编译器会根据传递的参数类型来决定调用哪个版本的函数。如果我们只传递一个名字参数,那么编译器会调用第一个版本;如果我们传递称谓和名字参数,那么编译器会调用第二个版本。

示例:调用重载函数

int main() {
    greet("Mosh");       // 调用第一个版本,输出 "Hello Mosh"
    greet("Mr.", "Mosh");  // 调用第二个版本,输出 "Hello Mr. Mosh"

    return 0;
}

3. 函数签名

重载函数时,函数签名是一个非常重要的概念。函数签名由函数名称参数的数量及类型组成。函数参数的名称并不影响函数签名,只有参数的数量和类型才决定签名是否唯一。

示例:函数签名

签名的关键点是:每个重载函数必须有唯一的签名,否则编译器无法区分这些函数,导致编译错误。

错误示例:签名冲突

#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 类型的参数,因此会导致编译错误。即使参数的名称不同,编译器仍然会认为它们是相同的函数。

4. 总结

通过函数重载,我们可以提高程序的灵活性,允许我们在不同的情况下使用相同的函数名。

0049 5 按值或按引用传递参数

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

在 C++ 中,理解如何传递参数是非常重要的概念。参数可以通过值传递(pass by value)或引用传递(pass by reference)来传递。下面我们来详细讲解这两种方式的区别,以及它们的实际应用。

值传递(Pass by Value)

在值传递中,函数接收的是实参的一个副本。也就是说,函数内部操作的只是副本,而不是原始变量。因此,在函数内部对参数进行修改时,原始变量的值不会受到影响。

示例:值传递

我们定义一个函数 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 变量中。

引用传递(Pass by Reference)

在引用传递中,函数接收的是实参的地址,也就是说,函数内部操作的是原始变量。因此,函数内部对参数的修改会直接影响到原始变量。

示例:引用传递

我们可以通过引用传递来避免复制操作,并直接在原始变量上进行修改。我们在函数的参数类型后加上一个 & 来表示引用传递。

#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 变量的值。

何时使用引用传递?

示例:引用传递与字符串

#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

常量引用(Constant Reference)

如果不希望在函数中修改传递给函数的参数,可以将参数声明为常量引用(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 是通过常量引用传递的,因此在函数内部不能修改它,确保了函数不会意外更改传递给它的值。

总结

了解值传递与引用传递的差异,可以帮助我们在实际编程中做出更合适的选择,优化代码的性能与可读性。

0050 6 局部变量与全局变量

本地变量与全局变量

在 C++ 中,变量的作用范围(Scope)决定了它在哪些地方可被访问。我们可以将变量分为 本地变量(Local Variables)全局变量(Global Variables),它们各自有不同的作用范围和使用场景。

本地变量(Local 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 超出了它的作用域。

全局变量(Global Variables)

全局变量是在所有函数之外声明的变量,因此它们对程序中的所有函数都是可见的。换句话说,全局变量的作用范围是整个程序。

示例:全局变量
#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 是一个普通的全局变量,所以它的值可以在程序的任何地方被修改,从而引发了这个错误。

最佳实践

尽量避免使用全局变量,尤其是在没有特别必要的情况下。如果必须使用全局变量:

总结

总的来说,避免使用全局变量,尤其是可修改的全局变量,可以提高程序的可靠性和可维护性。

0051 7 函数声明

函数声明与定义

在 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 函数之后。

函数声明与定义的区别

总结

通过声明函数,编译器就能在调用该函数时知道它的存在,并且可以根据函数的声明来进行调用。

0052 8 在文件中组织函数

组织代码文件:分离函数到不同文件

随着程序的复杂化,main 文件通常会变得越来越庞大,难以维护。因此,为了提高代码的可读性和可维护性,我们可以将代码分离到不同的文件中,每个文件专注于一个功能模块,就像我们将物品组织到不同的容器中一样。在本节课中,我们将展示如何将一个 greet 函数从 main 文件中分离出来,放到一个单独的文件中。

优点

将函数分离到不同文件的好处有两个:

  1. 减小文件大小:将功能提取到不同的文件后,main 文件变得更小,更容易管理。
  2. 提高复用性:将功能放在单独的文件中之后,我们可以在其他项目中复用该文件,而无需复制和粘贴代码。

创建新文件

让我们开始操作,创建一个新的文件来存放 greet 函数。

  1. 创建一个新目录

    • 在项目窗口中,右键点击项目文件夹,选择添加新目录(在 Xcode 中,这个操作叫做“添加新组”)。我们可以将这个目录命名为 utils,表示其中存放实用功能的文件。
  2. 添加两个新文件

    • 头文件(Header File):用于存放函数声明。
    • 实现文件(Source File):用于存放函数的具体实现。
  3. 创建 .cpp 文件

    • utils 目录下创建一个 greet.cpp 文件,存放 greet 函数的实现。
#include <iostream>
using namespace std;

void greet(string name) {  // greet 函数的定义
    cout << "Hello " << name << endl;
}
  1. 处理编译错误
    • 由于我们在 greet.cpp 中使用了 stringcout,我们需要在文件顶部引入适当的头文件。
#include <iostream>
#include <string>
using namespace std;

void greet(string name) {
    cout << "Hello " << name << endl;
}
  1. 创建头文件
    • utils 目录下创建一个 greet.h 文件,存放 greet 函数的声明。
#ifndef GREET_H  // 防止头文件重复包含
#define GREET_H

#include <string>

void greet(std::string name);  // 函数声明

#endif
  1. 更新主文件
    • 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)需要更新,以便将新的源文件包含进构建过程。

  1. 打开 CMakeLists.txt 文件。
  2. 添加 greet.cpp 文件,以确保它被包含在构建过程中:
add_executable(MyProject main.cpp utils/greet.cpp)

使用 IDES 的快捷方式

虽然手动创建文件和添加代码可能比较繁琐,许多 IDE 提供了快捷方式来自动生成头文件和源文件。比如在 CMake 或 Visual Studio 中,我们可以右键点击项目目录,选择“新建源文件”或“新建头文件”,IDE 会自动生成必要的模板代码并添加到 CMakeLists.txt 中。

总结

通过将代码分离到多个文件中,我们不仅提高了代码的可维护性,还使得代码更具复用性。分离的过程包括:

  1. 创建源文件和头文件
  2. 声明函数 在头文件中,并在源文件中实现它们。
  3. 防止头文件重复包含:使用 #ifndef#define
  4. 更新构建配置:确保新的源文件被包含进构建过程中。

通过这些步骤,我们能够更好地组织代码并提高其扩展性。

0053 9 使用命名空间

使用命名空间解决命名冲突

我们将 greet 函数移动到一个单独的文件中,这样我们就能在其他项目中复用了。但是,复用别人的代码时可能会遇到一个问题:如果别人也写了一个 greet 函数,或者有一个同名函数,如何避免冲突呢?这时我们就需要使用命名空间。

命名空间(Namespace)就像一个容器,帮助我们将函数和类组织在一个特定的空间里,从而避免与其他代码库中的同名函数发生冲突。我们已经见过命名空间的使用,像 std 命名空间,它包含了标准库中的所有类和函数。例如,标准库中的 string 类是定义在 std 命名空间中的,如果我们自己定义了一个名为 string 的类,它将与标准库中的 string 类区分开来,因为我们定义的 string 类不会处于 std 命名空间中。

如何将函数放入命名空间

现在,让我们把 greet 函数放入一个命名空间中,以避免未来的命名冲突。

  1. 创建命名空间

    • 首先,在头文件和源文件中声明和定义 greet 函数时,我们都要将它放入一个命名空间。例如,我们可以创建一个名为 messaging 的命名空间,因为这个命名空间下的所有函数都与显示消息相关。
  2. 更新函数声明和定义

    • 我们在头文件 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;
       }
    }

处理命名空间中的函数调用

  1. 在主文件中调用函数

    • 如果我们不使用命名空间,编译器将无法找到 greet 函数。为了调用 greet 函数,我们有两种方式:

    • 方法 1:显式地指定命名空间:我们每次调用函数时都需要加上 messaging:: 前缀。

    #include <iostream>
    #include "utils/greet.h"
    
    using namespace std;
    
    int main() {
       messaging::greet("Mosh");  // 使用命名空间前缀
       return 0;
    }
    • 方法 2:导入命名空间:在文件的顶部,我们可以使用 using 指令来导入整个命名空间,这样我们就不需要在每次调用时都加上命名空间前缀了。
    #include <iostream>
    #include "utils/greet.h"
    
    using namespace std;
    using namespace messaging;  // 导入命名空间
    
    int main() {
       greet("Mosh");  // 现在可以直接调用
       return 0;
    }

    使用 using namespace messaging; 之后,所有位于 messaging 命名空间中的函数和类都可以直接使用,无需每次都加上命名空间前缀。

处理命名冲突

  1. 避免命名冲突: 如果我们在不同的命名空间中定义了相同名称的函数,例如在 messaging 命名空间中和在另一个命名空间中都定义了 greet 函数,那么这时就会发生命名冲突。为了避免这种情况,我们可以使用更加精细的控制。

    • 方法 1:仅导入需要的函数:如果我们只需要使用某个特定的函数,而不想导入整个命名空间,可以通过 using 指令仅导入需要的函数。例如:
    using messaging::greet;  // 仅导入 greet 函数

    这样,只有 greet 函数可以直接使用,其他 messaging 命名空间中的函数则不会受到影响。

    • 方法 2:只导入特定对象:同样,我们可以使用 using 指令仅导入某些对象,例如 std::coutstd::cin,而不导入整个 std 命名空间。
    using std::cout;  // 仅导入 cout 对象

    这样,在文件中,我们只需要使用 cout,而不必每次都加上 std:: 前缀。

总结

  1. 使用命名空间:命名空间可以帮助我们避免不同模块或库之间的命名冲突,将函数和类放入不同的命名空间中。

  2. 导入命名空间

    • 通过 using namespace 导入整个命名空间,可以简化代码,使得不需要每次都加上命名空间前缀。
    • 为了避免命名冲突,我们也可以选择仅导入特定的函数或对象。
  3. 命名冲突处理:当两个命名空间中有相同名称的函数时,可以通过显式指定命名空间或仅导入特定函数来解决。

命名空间是一种组织代码的有效工具,特别是在需要复用别人代码或在大型项目中开发时,它帮助我们保持代码整洁并避免混乱。

0054 10 调试 C 程序

调试:定位程序中的错误

调试是定位程序错误的一种技术,是每个程序员必须掌握的关键技能。所以请仔细阅读下面的内容。

创建一个包含错误的示例

在本节中,我们将创建一个打印奇数的函数。该函数接受一个限制值,并使用一个 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)中的调试工具。几乎所有的开发环境都有调试工具,唯一的区别可能在于快捷键或图标。

  1. 插入断点

    • 调试的第一步是插入一个断点,断点是你希望程序暂停执行的位置。点击左侧行号旁边的位置,就能插入一个断点。在这里,我们将在 printOddNumbers 函数内部插入一个断点,观察程序的执行。
  2. 开始调试

    • 调试工具中有一个按钮可以启动调试会话,通常与“运行”按钮相邻。你也可以使用快捷键来启动调试。在 SeaLion 中,调试的快捷键是 Ctrl + R(开始程序),接着使用 F7F8 来单步调试。
  3. 逐步执行(Step Over 和 Step Into)

    • 当程序在断点处暂停时,你可以选择逐步执行代码。Step Over(F8)让程序跳过当前行,直接执行到下一行。
    • Step Into(F7)则进入函数内部,逐行执行。这对于想要了解函数内部执行过程时非常有用。
  4. 查看和修改变量

    • 在调试过程中,你可以查看当前作用域中的所有变量以及它们的值。比如,在我们的示例中,可以查看 ilimit 的值。
    • 如果你想观察某个变量的变化,可以在调试窗口中添加监视(Watch)。在 SeaLion 中,你可以右键点击调试窗口,选择 "Add Watch" 来添加一个新的监视表达式。

调试的步骤演示

  1. 运行程序并暂停: 当我们启动调试时,程序会在断点处暂停。此时我们可以查看控制台输出,发现它显示的是偶数而不是奇数。

  2. 逐行调试: 我们按 F8(Step Over)逐行调试,发现程序开始时的 i 值是 0。这时程序打印了 0,这是错误的,因为我们应该打印的是奇数。

  3. 查看变量值: 我们在调试窗口中添加了一个监视表达式,用来查看 i % 2 == 0 这个条件的值。我们发现当 i 为 0 时,这个条件的值为 true,导致程序错误地打印了偶数。

  4. 修改并修复错误: 通过调试,我们发现问题的根源是我们使用了 ==(等于)运算符,而应该使用 !=(不等于)。修改代码如下:

    if (i % 2 != 0)  // 改为判断是否为奇数
  5. 重新启动调试: 修改代码后,我们需要停止调试并重新启动,以便查看修改后的效果。通过继续逐步执行,我们可以看到程序最终正确地打印了奇数。

  6. 移除断点并结束调试: 一旦调试完成,记得移除所有的断点,并停止调试会话。这样就能确保程序回到正常的执行状态。

小结

调试是查找和修复程序错误的有力工具。通过逐行执行代码,检查变量的值,并使用监视和断点等功能,我们可以精准地定位问题所在。在调试过程中,我们学到了如何:

掌握调试技能能够大大提高你编程时解决问题的效率,它是每个程序员必备的核心技能。

0055 13 课程总结

课程结束:第一部分总结与第二部分预告

感谢你们参与并完成了本课程的第一部分!在此,我衷心感谢你们让我担任你们的讲师。我希望你们在这一部分中学到了很多有用的知识。

接下来,我们将在第二部分的课程中探讨更为深入的中级主题,包括:

如果你喜欢这门课程,并且从中受益,请支持我,通过向他人推荐我的编程学校来帮助我继续发展。非常感谢你的支持!

这就是第一部分的全部内容。我们很快会在第二部分见面!

WangShuXian6 commented 1 week ago

终极C++第2部分 中级,了解有关数组、指针、字符串、结构和流的所有信息 Ultimate C++ Part 2 Intermediate

0001 1 欢迎

欢迎来到《终极 C++ 课程》第二部分

在这一部分,你将学习中级概念,例如:

要顺利学习本部分内容,你需要先完成第一部分的学习,掌握我在第一部分中讲解的基础概念。你应该了解基本数据类型、条件语句、循环和函数等内容。

我非常激动能成为你们这部分课程的讲师,接下来让我们马上开始吧!

0002 1 介绍

欢迎回来,欢迎进入《终极 C++ 课程》的另一个部分

在本系列的第一部分中,你已经学习了数组的基础知识。在这一部分,我们将更详细地探讨数组的使用。你将学习:

让我们开始吧!

0003 2 创建和初始化数组

回顾:第一部分中你学到的数组基础知识

在本课程的第一部分中,你了解了数组的基本概念。简单来说,数组用于在内存中存储一系列对象,比如一系列数字、字符串等。这里是一个例子:

我们声明了一个整数类型的数组,叫做 numbers。建议在命名数组时使用复数形式,因为它存储的是多个对象。例如:

int 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

打印数组:

如果我们尝试直接打印数组而不是访问它的单个元素,输出的将不是数组的内容,而是一个十六进制地址值,这是该数组在内存中的地址。这被称为 指针。我们将在后面进一步讨论指针的概念。

这就是第一部分中关于数组的主要内容。

接下来,我们将讨论如何确定数组的大小。

0004 3 确定数组的大小

遍历数组并打印元素

在这一节中,我们将继续学习如何遍历数组并打印出其中的每个元素。如果你有一个包含两个元素的整数数组,并希望打印每个元素,最简单的解决方案是使用范围基的 for 循环。回顾一下:

for (int number : numbers) {
    std::cout << number << " ";
}

我们还可以使用 auto 关键字来让编译器推断出变量的类型:

for (auto number : numbers) {
    std::cout << number << " ";
}

何时不能使用范围基 for 循环?

有些情况下,我们不能使用范围基 for 循环进行迭代。比如,当我们需要使用索引来访问数组的单独元素时,就需要用到普通的 for 循环。这些情况通常出现在需要复制、比较数组元素等操作时。在下一个小节中,我们将讨论这些情况的具体例子。

使用传统的 for 循环和动态计算数组大小

接下来,我们看一下如何使用传统的 for 循环遍历数组。我们首先声明一个循环变量 i 作为索引,然后遍历数组。

for (int i = 0; i < sizeof(numbers) / sizeof(numbers[0]); i++) {
    std::cout << numbers[i] << " ";
}

在这里,我们需要知道数组的大小。为了避免硬编码数组大小,假设你将来决定往数组里添加新元素,这时你需要手动更新数组大小。为了避免这种麻烦,我们可以通过动态计算数组的大小。

使用 std::size 简化计算

在标准库中,我们还有一个更简单的方式来计算数组的大小。我们可以使用 std::size 函数来获取数组的大小,而不需要手动使用 sizeof 操作符。代码如下:

for (int i = 0; i < std::size(numbers); i++) {
    std::cout << numbers[i] << " ";
}

注意: 由于 std::size 是定义在 std 命名空间中的,所以如果你没有在文件顶部引入 std 命名空间,需要使用 std::size 来访问它。

#include <iostream>
#include <array>  // 需要包含此头文件

总结

这就是我们在这一部分学习的关于数组遍历的内容。

0005 4 复制数组

复制数组

在这节课中,我们将讨论如何在 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];  // 逐个复制每个元素
}

打印第二个数组来验证

为了验证我们的实现是否正确,我们可以再次遍历 second 数组并打印每个元素:

for (int number : second) {
    std::cout << number << " ";
}

总结

0006 5 比较数组

比较数组

在本节中,我们将讨论如何比较两个数组。在 C++ 中,如果我们想比较两个数组,我们不能直接使用 == 运算符,因为这只会比较数组的内存地址,而不是数组中的实际值。因此,我们需要逐个元素地比较数组的值。

直接比较数组会导致错误

假设我们有两个数组,它们的值是相同的:

int first[] = {10, 20, 30};
int second[] = {10, 20, 30};

如果我们直接使用 == 运算符来比较这两个数组:

if (first == second) {
    std::cout << "Equal";
}

编译器会给出警告,表示条件总是 false。这是因为 firstsecond 代表的是数组的内存地址,而不是数组的值。即使它们的值相同,内存地址仍然不同。所以,编译器会指出这两者不相等。

数组名与地址

我们可以打印这两个数组的地址来看发生了什么:

std::cout << "first: " << first << "\n";
std::cout << "second: " << second << "\n";

输出的可能类似于:

first: 0x7ffee4baf540
second: 0x7ffee4baf560

可以看到,两个数组的地址几乎相同,但最后两位(例如 F0F6)是不同的,这就是为什么编译器给出警告的原因。

正确的数组比较方法

为了正确地比较数组,我们必须逐个元素进行比较。可以使用 for 循环来实现这一点:

bool areEqual = true;
for (int i = 0; i < sizeof(first) / sizeof(first[0]); i++) {
    if (first[i] != second[i]) {
        areEqual = false;
        break;  // 一旦发现不同的元素,直接退出循环
    }
}

在这个例子中:

测试我们的实现

为了测试我们的实现,我们可以打印 areEqual 变量的值:

std::cout << "Arrays are " << (areEqual ? "equal" : "not equal") << "\n";

例如,修改 first 数组中的一个元素:

first[0] = 99;

再运行程序,输出将变为:

Arrays are not equal

总结

0007 6 将数组传递给函数

传递数组到函数时的问题

在 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 和它的大小 sizeprintNumbers 函数。这里的关键点是,当我们将数组传递给函数时,数组会被转换为指针,而指针无法提供数组的大小信息。因此,我们必须手动传递数组的大小作为函数的另一个参数。

如果不传递数组的大小,函数就无法知道数组的元素数量,这样我们就无法有效地访问数组中的每个元素。我们需要明确地提供大小,以确保函数能够正确地访问和操作数组。

解决方案总结

通过这种方式,我们可以避免编译错误,并成功地遍历和操作传递给函数的数组。

0008 7 理解 size_t

关于 size_t 类型

在 C++ 中,size_tsizeof 操作符返回的值都是 size_t 类型。size_t 是一种专门用于表示对象大小的数据类型,它的主要用途是存储对象的大小,且保证足够大以容纳系统可以处理的最大对象的大小。

size_t 的定义与作用

size_t 是标准库定义的一个数据类型,用来表示对象的大小。其作用是确保其大小足够大,可以表示当前系统能够处理的最大对象的大小。通常情况下,size_t 的大小要比普通的整数类型(如 int)更大,它能够容纳更大的值。

示例:比较 intsize_t 的大小

我们可以通过打印 intsize_t 的大小来查看它们占用的内存大小。假设我们在代码中加入如下内容:

std::cout << "Size of int: " << sizeof(int) << " bytes" << std::endl;
std::cout << "Size of size_t: " << sizeof(size_t) << " bytes" << std::endl;

在不同的机器上,intsize_t 的大小可能不同。在 64 位机器上,int 通常占用 4 个字节,而 size_t 占用 8 个字节,因为 size_t 的大小足够大,可以表示更大的数字。

size_tlong long 类型的比较

size_t 在一些系统中等价于 unsigned long long 类型。具体来说,size_t 总是无符号的,这意味着它只能表示正整数和零,而不能表示负数。相比之下,long long 类型是带符号的,它可以表示负数、零和正数。

我们可以使用 numeric_limits 类来查看 long longsize_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 的大小依赖于系统架构,但它始终足够大,能够表示系统中可以处理的最大对象的大小。

结论

0009 8 解包数组

关于数组解包(Unpacking Arrays)

在 C++ 中,数组解包是一种非常实用的技术,允许我们将数组中的元素快速赋值给多个变量。这种技术使得代码更加简洁和易于理解。下面,我们将通过一个示例来讲解如何使用这种技术。

1. 传统的赋值方式

假设我们有一个包含三个元素的整数数组,我们将它们视为 xyz 坐标。首先,我们可以手动声明三个变量 xyz,并将数组中的值逐个赋给它们:

int values[3] = {10, 20, 30};
int x = values[0];
int y = values[1];
int z = values[2];

这是一种直接且明确的方法,但它需要写很多重复的代码,尤其当数组的大小增大时。

2. 解包数组(Unpacking Arrays)

为了简化这种情况,C++ 提供了一个简洁的技术——结构绑定(Structured Binding)。虽然这个名字有点儿拗口,在 Python 中我们称之为解包(Unpacking)。这种方法可以在一行代码中完成多个变量的赋值。

使用这种技术,我们可以将数组直接解包到多个变量中,而无需写多行代码。具体实现方法如下:

auto [x, y, z] = values;

这里,auto 关键字会自动推断变量 xyz 的类型,然后我们使用结构绑定的语法将 values 数组的元素解包到这三个变量中。

3. 验证解包是否成功

我们可以通过打印这些变量的值来验证解包是否成功:

std::cout << "x: " << x << ", y: " << y << ", z: " << z << std::endl;

输出将会是:

x: 10, y: 20, z: 30

这表明我们成功地将数组 values 的值解包到了 xyz 变量中。

4. 解包的优点

5. 其他解包场景

结构绑定不仅限于数组,还可以用于解包其他类型的数据结构。例如,可以用它来解包 std::tuplestd::pair

例如,对于一个包含两个元素的 std::pair

std::pair<int, int> coordinates = {10, 20};
auto [x, y] = coordinates;

这样,我们就可以直接通过 xy 访问 coordinates 中的值。

结论

数组解包是一种非常实用的技术,它能够减少代码量并提高代码的可读性。通过使用结构绑定(或解包),我们可以轻松地将数组的多个值赋给多个变量,而无需手动逐个赋值。

0010 9 搜索数组

线性搜索算法

在计算机科学中,线性搜索(Linear Search)是一种最简单的搜索算法,用于查找数组或列表中的目标值。我们将逐步解释这一算法及其实现方法。

1. 线性搜索算法的工作原理

假设我们有一个整数数组,并且我们想要查找某个特定的值。线性搜索的基本思路是:

2. 最佳和最坏情况

在计算复杂度上,线性搜索的时间复杂度是 O(n),其中 n 是数组的大小。也就是说,随着数组大小的增加,线性搜索所需的比较次数也会线性增加。

3. 算法实现

我们可以使用 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;
}

4. 解释代码

5. 测试结果

对于上述代码,假设数组是 {5, 10, 15, 20, 25},我们分别查找目标值 1030

输出结果如下:

目标值 10 的索引是: 1
目标值 30 的索引是: -1

6. 总结

这是一个经典的线性搜索的实现,您可以尝试在不同的数据集上测试,理解它的行为和性能。

0011 10 排序数组

冒泡排序算法(Bubble Sort)

冒泡排序是所有排序算法中最简单的之一。它通过重复遍历数组,不断比较相邻元素,并交换它们的位置,直到数组完全有序。接下来,我们将详细讨论冒泡排序的工作原理,并展示如何在 C++ 中实现这个算法。

1. 冒泡排序的工作原理

假设我们有一个整数数组,目标是将这个数组按升序排列。在使用冒泡排序时,我们会从左到右扫描数组:

我们需要重复多次比较,直到数组完全排序。

2. 冒泡排序的步骤

第一轮比较:

第二轮比较:

重复这个过程,直到整个数组完全有序

3. 算法实现

接下来我们来看 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;
}

4. 代码解释

5. 测试案例

假设数组为 {30, 20, 10},执行排序后,输出结果为:

Sorted array: 10 20 30

如果数组已经排序好(例如 {10, 20, 30}),冒泡排序仍然能够正常工作,输出结果为:

Sorted array: 10 20 30

若数组只有两个元素或一个元素,冒泡排序也能够正确处理:

6. 总结

通过这个实现,您可以更好地理解冒泡排序的工作原理,并且能够在 C++ 中实现这一基本的排序算法。

0012 11 多维数组

我们到目前为止声明的所有数组都是一维的,也就是说它们只有一组值。但我们也可以声明多维数组。例如,我们可以使用二维数组来表示一个矩阵。这里有一个例子,假设我们要表示一个 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 并传入我们的矩阵。只需要传入数组,不需要为数组的大小或每个维度的大小传递单独的参数。好了,程序运行并确保一切都能正常工作。

这就是如何声明和初始化多维数组的过程。

0013 1 介绍

欢迎回来,进入终极 C++ 课程的另一个章节。在本章节中,我们将探索指针,这是 C++ 中一个强大但常常被误解的概念。我们将从讲解指针是什么以及为什么使用它们开始。接下来,我将向你展示如何声明和使用指针。一旦你掌握了指针的基本原理,我们将看到如何使用指针高效地在函数之间传递大量数据以及动态分配内存。最后,我们将讨论指针的问题,以及如何通过现代 C++ 中的智能指针来避免这些问题。

现在,让我们开始吧!

0014 2 什么是指针

那么,什么是指针呢?指针是一个特殊的变量,它保存了内存中另一个变量的地址。这里有一个例子,假设我们有一个名为 number 的变量,存储了值 10。你知道变量只是内存地址的标签,所以这个 number 变量可能存储在某个特定的内存位置。现在,我们可以声明另一个名为 pointer 的变量,它保存 number 的地址。

那么,为什么我们要这么做呢?使用指针有几个原因。第一个原因是为了高效地在函数之间传递大对象。在课程的第一部分,我们讨论了按值或按引用传递参数。我告诉过你,如果你想在函数之间传递大量数据,直接复制数据并不高效,应该通过引用传递。因此,引用参数或引用变量是高效传递大量数据的一种方式。指针是解决这个问题的另一种方法。我们稍后会在本节中看一个例子。

第二个原因是我们使用指针进行动态内存分配,这样我们的程序可以根据输入调整内存使用。例如,在本节的后面,我将展示如何在运行时动态调整数组的大小。当数组满了之后,我们可以重新调整它的大小,以便存储更多的数据。

使用指针的第三个原因是启用多态,这是面向对象编程的能力之一。这是一个高级话题,我们将在课程的下一部分讨论它。

现在,既然你理解了指针是什么以及为什么我们要使用它们,让我们看看如何声明和使用指针。

0015 3 声明和使用指针

好,我们来看一下如何创建和使用指针。首先,我将声明一个名为 number 的整数,并将其设置为 10。你知道,每个变量都有一个地址。如果你想打印 number 的地址,我们应该在它前面加上取地址运算符 &,这样就可以获取它的地址了。现在,我们打印 number 的地址。得到的是一个十六进制的数字。请注意,你在自己的机器上看到的值会不同,所以不要被这个值分心。

接下来,我们声明一个整数指针。我们可以通过在类型前面加上星号 * 来声明指针。所以,星号 * 表示这是一个指针,指针指向的类型是整数。接下来,我们给这个指针变量命名,我们可以称它为 PR 或其他名字,然后将它初始化为 number 的地址。这样我们就声明了一个整数指针,并且这个指针只能指向整数类型的变量。如果你把 number 的类型改为其他类型,例如 double,编译器会报错,提示不能将 int 类型的指针初始化为 double 类型的值。

所以我们需要把它改回 int 类型。就像其他变量一样,如果我们没有初始化这个指针,它将会保存垃圾值或者无效值。所以,为了避免这个问题,我们可以打印指针的值,而不是 number 的地址。第一次运行程序时,我们看到的是零,第二次运行时会看到一个不同的数字。未初始化的指针问题在于,如果我们使用它,可能会访问到不应该访问的内存区域,这时操作系统会终止程序,并提示内存访问违规。因此,最佳实践是始终初始化指针。我们可以把它初始化为另一个变量的地址,或者如果我们不确定它指向什么值,可以将其初始化为 nullptr(现代 C++ 中的空指针)。nullptr 是一个不指向任何内存位置的特殊关键字。

使用空指针的好处在于,过去的 C++ 或 C 版本中,我们可能会用 NULL0 来表示空指针,而现代 C++ 推荐使用 nullptr,这是首选的做法。作为最佳实践,始终初始化指针。如果我们没有指定指向哪个变量,最好将它初始化为 nullptr。在需要使用这个指针时,我们可以先检查它是否为 nullptr,如果不是空指针,再使用它。

现在,指针已经初始化为 number 的地址。那我们可以对这个指针做什么呢?我们有一个称为间接引用(dereferencing)或解引用(indirection)的运算符。我们可以在指针前加上星号 *,通过这个操作符,我们可以访问指针指向的内存位置上的数据。比如,如果我们打印出通过指针解引用后的值,就能看到 number 的值,10。

现在,使用这个语法,我们还可以更改目标内存位置上的值。我们可以通过解引用指针并将其设置为 20,这样我们就改变了目标内存位置上的值,所以如果我们打印 number,就会看到 20。

让我们快速回顾一下:我们在这行代码中使用的是解引用(dereferencing)或间接引用(indirection)运算符,而在那行代码中,& 是取地址运算符。

最后一个小细节,在这节课中我将星号 * 放在数据类型后面,但我们也可以将其放在变量名之前。之所以我不喜欢这种写法,是因为它可能会与解引用运算符混淆。因此,在声明指针时,我更倾向于将星号放在数据类型后面。

练习题: 现在给你一个练习题,猜猜这段程序的输出是什么。如果我们打印出 XY 的值,最后我们会看到什么呢?请花一分钟时间,理解每一行代码在做什么。

答案: 在第 6 和第 7 行,我们声明了两个整数变量。在第 8 行,我们声明了一个整数指针,并将其初始化为指向 X 的地址。然后,通过解引用运算符,我们访问到存储 X 的内存位置,得到它的值 10,然后将其乘以 2。所以,在这一点上,如果你打印 X,它的值应该是 20。

接着,在第 10 行,我们将指针指向 Y 的地址,所以现在这个指针指向了一个不同的变量。然后,同样地,我们通过解引用运算符,访问 Y 的内存位置,得到它的值 20,并将其乘以 3。最终,程序结束时,X 的值为 20,Y 的值为 60。

0016 4 常量指针

好的,现在我们来看一下指针与常量结合的三种情况。

第一种情况:数据是常量,但指针不是常量
在这种情况下,我们可以将指针指向其他的变量。换句话说,数据的值是常量,但是指针本身可以在之后指向其他地方。为了演示这个场景,我将声明一个整数 X,并声明一个指针,将其初始化为指向 X 的地址。现在,如果我们将 X 声明为常量整数,编译器会报错,因为我们不能让一个整数指针指向常量整数。此前,我提到过,如果 Xdouble 类型,编译器也会报错,因为一个整数指针不能指向一个 double 类型的值,类型必须一致。

如果 X 是常量整数,那么我们的指针应该是一个常量整数指针。换句话说,数据是常量,但指针不是常量。此时,我们可以使用间接引用运算符(解引用运算符)来访问 X 的内存位置,但尝试修改 X 的值会导致编译错误,因为 X 是常量。因此,如果我们声明一个新变量 Y,并将指针指向 Y,就没有问题了,因为我们的指针仍然可以指向不同的变量。

第二种情况:指针是常量,但数据不是常量
接下来,假设我们希望声明一个常量指针。要定义一个常量指针,我们应该把 const 关键字放在星号 * 后面,这样我们的指针就变成了常量指针。一旦我们声明并初始化了常量指针,就不能再改变它指向的地址了。如果我们尝试将指针重新指向 Y 的地址,编译器会报错,因为常量指针的值(即它指向的地址)是不可修改的。

在这种情况下,最好在声明常量指针时立即初始化它。如果我们不初始化它,编译器会报错,因为以后我们不能重新设置常量指针。

第三种情况:数据和指针都是常量
最后,我们来看第三种情况,数据和指针都是常量。在这种情况下,首先,我们将 X 声明为常量整数,并将指针也声明为常量指针。这时候,我们的指针指向的内存地址和数据的值都无法修改。

这种写法看起来可能有些奇怪,但它的意思是我们有一个常量指针,它指向一个常量整数。换句话说,我们的指针是常量,并且指向的值也是常量。

总结

通过这三种场景,我们可以更好地理解如何使用常量指针,并避免编程中的常见错误。

0017 5 将指针传递给函数

好,现在让我们来看一下指针的第一个应用。之前我提到过,使用指针可以高效地在函数之间传递大量数据。与其复制数据,不如传递数据的引用。课程的第一部分我们通过引用参数来解决这个问题,那是现代且推荐的方式,但我们也可以通过指针来解决这个问题。

使用指针传递数据

让我们先看一个例子。我定义了一个名为 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 的地址传递给这个函数。

总结来说,我们需要做三个修改来使用指针解决这个问题:

  1. 将参数声明为指针。
  2. 使用解引用运算符来访问指针指向的值。
  3. 使用地址运算符 & 将变量的地址传递给函数。

虽然使用指针来传递数据更复杂,但它可以解决相同的问题。最终,程序运行结果和使用引用参数时是一样的。

推荐做法

总的来说,像这种情况最好还是使用引用参数,因为它更简洁且不容易出错。但我在这里解释使用指针来传递数据,是因为你可能会遇到老旧项目中仍然使用指针来传递数据的情况。

练习题

现在,我想让你实现一个交换函数,使用指针交换两个变量的值。假设我们有两个整数指针作为参数。花几分钟理解这个问题,然后尝试自己实现。

解题思路: 我将定义一个名为 swap 的函数,接收两个整数指针参数 firstsecond。与之前一样,我们需要一个临时变量来交换这两个值。我将声明一个名为 temp 的整数变量,并用它来暂时存储数据。

我们将把第一个指针指向的值存储在 temp 中。然后我们将第一个指针指向的值设置为第二个指针指向的值。最后,我们将第二个指针指向的值设置为 temp 的值。

void swap(int* first, int* second) {
    int temp = *first;      // 将 first 指向的值存储到 temp 中
    *first = *second;       // 将 second 指向的值赋给 first
    *second = temp;         // 将 temp 的值赋给 second
}

接下来,在 main 函数中,我会声明两个整数 XY,并将它们的地址传递给 swap 函数。然后,打印 XY 的值,我们应该看到交换后的结果。

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。

通过这次练习,你能更好地理解如何使用指针来传递数据并在函数中修改它们的值。

0018 6 数组与指针的关系

数组与指针的关系

让我们来讨论一下数组和指针之间的关系,以便让你完全理解这一概念。我将声明一个整数数组并初始化它为三个数字。这里的 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++ 编译器总是通过指针的方式传递数组,这使得在函数之间传递数组变得高效。

总结

0019 7 指针运算

指针的数学运算

让我们来讨论一下我们可以对指针执行的数学运算。我将声明一个整数数组并初始化它为三个值。接着,我将声明一个整数指针并将其初始化为零。现在,这个指针指向数组的第一个元素。该指针所持有的地址是 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;  // 编译错误

这会导致编译错误,因为指针只能执行加法和减法运算,不能进行乘法和除法。这是因为乘法和除法操作在内存地址层面上并没有实际意义,指针的操作是基于它所指向的数据类型的大小进行的,不能直接通过数值的乘除来改变它指向的地址。

总结

通过这些操作,我们可以更灵活地操作内存中的数据和数组,使得代码更高效且易于理解。

0020 8 比较指针

指针的比较操作

现在我们可以使用所有你学过的比较操作符来对指针进行比较,但是你需要记住,实际上我们比较的是指针所存储的内存地址。下面是一个例子:

假设我们声明两个整数 xy,以及两个整数指针。我们可以将第一个指针命名为 prX,让它指向 x,将第二个指针命名为 prY,让它指向 y。如果我们写出类似于 prX < prY 的表达式,那么我们实际上是在比较这两个指针所存储的地址。

如果我们想要比较实际的值(比如 1020),那么我们必须解引用这些指针。也就是说,如果你想比较指针所指向的值,就需要通过解引用来获取值进行比较:

if (*prX < *prY) {
    // 比较实际的值
}

然而,并不总是需要解引用指针进行比较。有时候,我们可能只想比较地址。例如,我们可以将 prY 改为指向 x,这时我们有两个指针指向同一个变量。我们可以写出一个布尔表达式:

if (prX == prY) {
    // 如果两个指针指向相同的地址
}

这段代码的意思是,两个指针指向的是同一个内存位置。如果条件成立,我们可以输出类似“same”的消息:

std::cout << "same" << std::endl;

避免使用空指针

在进行指针操作时,特别是在使用指针之前,我们需要确保指针不是空指针。一个好的编程实践是在解引用指针之前先检查它是否为空。比如,在我们尝试解引用 prX 之前,我们应该先验证它是否为非空指针:

if (prX != nullptr) {
    std::cout << *prX << std::endl;
}

这种方式确保我们在使用指针之前,它已经指向了有效的内存地址。

练习:反向遍历数组

假设我们有一个整数数组,任务是创建一个指针指向数组的最后一个元素,并通过 while 循环反向遍历数组,打印每个元素。目标是打印数组中的元素,顺序是从后往前,比如 302010

解决方案

首先,我们声明一个整数数组并初始化它:

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。这是一个有效的反向遍历数组的例子。

总结

这些技巧使得指针操作更为灵活且高效,在处理内存和数组时非常有用。

0021 9 动态内存分配

指针的第二个应用:动态内存分配

当你声明一个整数数组,比如:

int numbers[10];

这个数组的大小是固定的,最多只能容纳 10 个数字。那么,如果在程序运行时,我们从用户那里获取的数据超过了 10 个数字,或者我们从文件中读取了更多的数据,怎么办呢?我们可以通过将数组的大小增加到 100 或 1000,甚至更多来解决这个问题,但这总是会有一个限制。更糟糕的是,如果用户只输入了一个数字,我们就浪费了大量的内存空间来存储这一单一值。

这时,动态内存分配就派上了用场。通过动态内存分配,我们可以在程序运行时根据实际需要调整数组的大小,而不是一开始就为可能不需要的元素分配内存空间。

动态内存分配的工作原理

动态内存分配允许我们在程序运行时根据需求分配内存。为了实现这一点,我们使用 new 运算符而不是常规的声明语法。下面是如何使用 new 运算符分配动态内存的示例:

int* numbers = new int[10];  // 动态分配一个能存储10个整数的数组

在这个例子中,new 运算符为 numbers 分配了一个大小为 10 的整数数组,并返回该数组的指针。这个指针被存储在 numbers 变量中。使用这种方式声明的变量存储在内存的堆区(heap)中,而不是栈区(stack)。堆和栈的区别如下:

堆和栈的区别

堆区的内存不会像栈区的内存那样自动回收。程序员需要负责释放堆区分配的内存,否则会造成内存泄漏(memory leak)。内存泄漏会导致程序占用越来越多的内存,最终导致程序崩溃。

例如:

delete[] numbers;  // 释放动态分配的内存

上述代码会释放通过 new 运算符分配的内存。在这里,numbers 是一个指向整数数组的指针,因此我们使用 delete[] 来释放这个数组的内存。

动态分配单个变量

除了分配数组外,new 运算符还可以用于分配单个变量。例如:

int* number = new int;  // 动态分配一个整数

这会在堆区分配一个整数,并返回该整数的指针。这时,我们不需要使用方括号,因为我们只分配了一个整数,而不是一个数组。虽然在这种情况下可以使用 new 来分配内存,但通常情况下我们更倾向于使用栈区的变量,因为栈区变量的生命周期由编译器自动管理,无需担心内存释放的问题。

释放内存

当我们不再使用动态分配的内存时,应该显式地释放它:

delete number;  // 释放单个整数的内存

重置指针

在释放内存之后,最好将指针重置为 nullptr,以避免悬挂指针问题。悬挂指针是指向已经释放内存的指针,如果继续使用它会导致不可预期的行为。

number = nullptr;  // 重置指针
numbers = nullptr;  // 同样可以重置数组指针

总结

这些基本概念为后续涉及更复杂的内存管理(如使用指针动态创建复杂数据结构)打下了基础。

0022 10 动态调整数组大小

动态调整数组大小

在这部分,我们将展示如何使用指针动态调整数组的大小。这个过程涉及到创建一个初始大小的数组,并在数组满时自动调整其大小以容纳更多元素。接下来,我会带你一步步完成实现。

1. 创建初始数组和指针

首先,我们声明一个大小为 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] << " ";
}

2. 动态调整数组大小

当用户输入的数字数量达到了数组的最大容量(例如 5 个数字)时,我们需要调整数组的大小以容纳更多的数据。这个过程包括以下几个步骤:

  1. 创建一个临时数组:它的大小是原数组的两倍。
  2. 复制数据:将原数组中的所有元素复制到临时数组中。
  3. 指针指向新数组:让原数组的指针指向新的数组。
  4. 释放原数组的内存:使用 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;    // 更新原指针指向新的数组
}

3. 改进实现,使用更灵活的容量

为了使数组的大小更加灵活,我们可以引入一个 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] << " ";
}

4. 释放内存

完成程序后,不要忘记释放所有动态分配的内存。最后,确保删除指向数组的指针:

delete[] numbers; // 释放动态分配的内存

总结

在这个例子中,我们学习了如何使用指针进行动态内存分配,并在数组满时自动调整数组大小。主要步骤如下:

  1. 使用 new 运算符动态分配内存。
  2. 在数组满时,创建一个新的、更大的数组,并将数据从旧数组复制到新数组。
  3. 使用 delete[] 运算符释放不再需要的内存,避免内存泄漏。

这种方式可以确保我们的程序能够根据需求动态调整数组大小,充分利用内存,并且避免内存浪费。

扩展阅读

虽然我们手动实现了动态内存分配和数组扩展的功能,但 C++ 标准库提供了更方便的容器类 std::vector,它自动处理了动态内存分配和调整大小。我们将在后续课程中讨论 std::vector 的使用。

0023 11 智能指针

内存泄漏与智能指针

在这部分,我们学习了当使用 new 运算符分配内存时,我们必须记得使用 delete 运算符来释放内存。如果我们忘记释放内存,程序将无法重用这些已分配的内存。随着内存的不断分配,程序将消耗越来越多的内存,最终会导致内存不足并崩溃,这就是我们所说的“内存泄漏”。内存泄漏意味着程序不断消耗内存而不释放,最终可能会导致程序崩溃。

1. 内存泄漏的示例

假设我们不释放内存,会导致什么情况发生。下面是一个简单的示例:

int* ptr = new int(10);  // 分配内存

// 忘记释放内存,导致内存泄漏

在这个例子中,我们使用 new 运算符分配了一个整数类型的内存,但如果没有调用 delete 来释放它,程序就会造成内存泄漏。

2. 双重删除导致的问题

另一个需要注意的情况是 双重删除,即尝试释放同一块内存两次。这是一个非常危险的操作,因为它可能导致程序崩溃。来看看下面的代码:

int* ptr = new int(10); // 分配内存

delete ptr;  // 第一次删除
delete ptr;  // 第二次删除,程序崩溃

这里,第一次 delete 会释放内存,而第二次 delete 尝试释放同一块内存,这会导致程序崩溃,因为该内存区域已经被释放,再次释放它是无效的,并且可能会破坏程序的内存管理系统。

3. 智能指针的引入

在现实中的 C++ 程序中,管理指针和内存非常复杂。我们可能会有数十、数百甚至数千个函数,这些函数可能会在各个地方创建指针来分配内存。在这种情况下,记住何时以及在哪里删除这些指针是非常困难的。而且,我们还需要小心,避免重复删除同一个指针。

为了解决这个问题,C++ 引入了 智能指针(Smart Pointers),它们让我们不再需要手动调用 delete 来释放内存。智能指针通过自动管理内存来减少内存泄漏和错误释放的问题。

智能指针的主要好处是,它们可以像普通的变量一样使用,不需要关心何时释放内存。智能指针会在其生命周期结束时自动释放内存,从而避免了内存泄漏。

4. 智能指针的两种类型

在现代 C++ 中,主要有两种类型的智能指针:unique_ptrshared_ptr

5. 智能指针的使用

下面是如何使用 unique_ptrshared_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 都销毁时,内存会自动释放
}

6. 总结

在现实世界中的 C++ 编程中,手动管理内存非常复杂且容易出错。为了避免内存泄漏和其他内存管理问题,C++ 提供了 智能指针,它们可以自动管理内存,减少我们在编程时的负担。

  1. unique_ptr 适用于独占所有权的情况;
  2. shared_ptr 适用于共享所有权的情况。

在后续的课程中,我们将详细讨论如何使用这两种智能指针来管理内存。

智能指针不仅使得内存管理变得更加安全,而且它们的使用也让代码更加简洁和易于维护。

0024 12 使用唯一指针

unique_ptr 介绍

unique_ptr 是 C++ 中的一种智能指针类型,它拥有它所指向的内存块。一个 unique_ptr 只能指向一块内存,这意味着不能有两个 unique_ptr 指向同一块内存。如果需要多个指针共享内存,应该使用 shared_ptr,这是下一个课题。现在,我们来看看如何在 C++ 中使用 unique_ptr

1. 引入头文件

为了使用 unique_ptr,我们需要在程序中引入一个标准库文件:

#include <memory>  // 引入内存管理功能

在这个文件中,C++ 提供了一个名为 unique_ptr 的类。我们将在后续的课程中详细讨论类的概念,但现在你可以将类视为包含多个功能(函数)的“构建块”。unique_ptr 是一个模板类,可以用于管理不同类型的内存。

2. 创建 unique_ptr

要创建 unique_ptr,我们需要指定它所管理的类型。接下来,我们通过 new 运算符来分配内存:

std::unique_ptr<int> x(new int);  // 创建一个 unique_ptr,管理一个整数

这行代码中,unique_ptr<int> 表示我们创建了一个管理 int 类型的智能指针 xnew int 用于在堆上分配一个整数,并将它的地址传递给 unique_ptr。这样,x 就拥有了这块内存,并且我们不需要担心手动删除内存,因为 unique_ptr 会自动处理。

3. 使用 unique_ptr

与常规指针类似,我们可以通过 unique_ptr 来访问指向的内存:

*x = 10;  // 通过 unique_ptr 设置值
std::cout << *x << std::endl;  // 打印指针指向的值

这样,我们就可以使用 *x 来访问 unique_ptr 指向的整数,并为其赋值。此时,我们可以像普通指针一样使用 unique_ptr,但是它会在作用域结束时自动释放内存。

4. 通过 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 关键字让编译器自动推导出类型,避免了重复声明类型的麻烦。

5. 创建指向数组的 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;  // 输出第一个元素的值

6. unique_ptr 的特点

7. 总结

通过 unique_ptr,我们可以更安全地管理内存,避免手动删除内存带来的错误和复杂性。C++ 的智能指针 unique_ptr 会自动管理内存,确保我们不必担心内存泄漏或重复删除内存。

下一个课题将会讨论 shared_ptr,它允许多个指针共享对同一块内存的所有权,并在所有者都不再使用该内存时自动释放它。

0025 13 使用共享指针

shared_ptr 介绍

在 C++ 中,shared_ptr 是另一种智能指针,它允许多个指针共享对同一块内存的所有权。这与 unique_ptr 不同,后者保证一个内存位置只能有一个指针拥有。在一些场景中,我们需要多个指针共享内存,此时 shared_ptr 就派上了用场。

1. 引入头文件

unique_ptr 类似,要使用 shared_ptr,我们首先需要引入 <memory> 文件:

#include <memory>  // 引入内存管理功能

在这个文件中,C++ 提供了一个名为 shared_ptr 的类,也同样是一个模板类,可以用于管理不同类型的内存。

2. 创建 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 关键字来让编译器自动推导类型,避免了重复写类型的麻烦。

3. 使用 shared_ptr

一旦创建了 shared_ptr,我们可以像普通指针一样使用它:

std::cout << *x << std::endl;  // 输出 shared_ptr 指向的整数值
*x = 20;  // 修改 shared_ptr 指向的整数值
std::cout << *x << std::endl;  // 输出修改后的值

shared_ptr 会自动管理内存,当所有 shared_ptr 不再使用该内存时,内存会被自动释放。

4. 多个 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

在这个例子中,xy 都指向同一块内存,因此输出的值都是 10。

5. 内存管理

shared_ptr 的强大之处在于它的内存管理。它会自动跟踪有多少个 shared_ptr 指向同一块内存,只有当最后一个指针被销毁时,内存才会被释放。这解决了手动管理内存的问题,避免了内存泄漏。

6. 总结

通过 shared_ptr,我们可以在 C++ 中轻松地实现内存共享和自动管理,尤其在需要多个对象共同访问数据时,它是一个非常有用的工具。

0026 1 介绍

欢迎回来!在这一节中,我们将详细探讨字符串(Strings)

在这一部分中,我们将开始介绍 C 语言中的字符串,并简要讲解它们是如何工作的以及它们的局限性。接下来,我们将深入了解 C++ 中的字符串,并展示它们的优势。我们还将讨论一些常见的操作技术,例如修改字符串、搜索字符串、提取子字符串以及字符串与数字之间的转换等。让我们开始吧!

C 语言中的字符串

在 C 语言中,字符串是通过字符数组来表示的。每个字符串都是以 \0(空字符)结束的字符序列。你需要手动处理字符串的长度和内存管理,这对于新手来说可能会有一些挑战。

1. 字符串声明与初始化

char str[50];   // 定义一个字符数组,大小为 50
strcpy(str, "Hello, World!");   // 使用 strcpy 函数来复制字符串

在上面的例子中,我们定义了一个字符数组 str,它有足够的空间来存储 50 个字符。然后,使用 strcpy 函数将字符串 "Hello, World!" 复制到该数组中。

2. 字符串的局限性

C++ 中的字符串

C++ 引入了 std::string 类,提供了更高级和方便的字符串操作。与 C 语言的字符数组不同,std::string 使得字符串的创建、操作和管理变得更加简便和安全。

1. 字符串声明与初始化

#include <iostream>
#include <string>

int main() {
    std::string str = "Hello, C++!";   // 使用 std::string 初始化字符串
    std::cout << str << std::endl;   // 输出字符串
    return 0;
}

在这个例子中,std::string 自动管理内存,不需要显式地指定大小,并且提供了许多方便的成员函数来操作字符串。

2. 字符串的优点

常见的字符串操作技术

1. 修改字符串

std::string 提供了多种方法来修改字符串,例如:

std::string str = "Hello, World!";
str.replace(7, 5, "C++");   // 将 "World" 替换为 "C++"
std::cout << str << std::endl;   // 输出 "Hello, C++!"

2. 字符串搜索

我们可以使用 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;
}

3. 提取子字符串

通过 substr 方法,我们可以从字符串中提取子字符串:

std::string str = "Hello, C++!";
std::string sub = str.substr(7, 3);  // 提取从位置 7 开始的 3 个字符
std::cout << sub << std::endl;   // 输出 "C++"

4. 字符串与数字的转换

C++ 中可以使用 std::stoi(字符串转整数)、std::stof(字符串转浮点数)等方法进行字符串与数字之间的转换。

std::string str = "123";
int num = std::stoi(str);   // 字符串转整数
std::cout << num << std::endl;   // 输出 123

总结

接下来的部分,我们将进一步深入讨论如何利用 C++ 字符串处理技术解决更复杂的问题,敬请期待!

0027 2 C 字符串

C风格字符串(C-Style Strings)

在整个课程中,我们简要提到过 string 类型。这种类型并不是 C++ 语言本身的一部分,而是标准库的一部分,定义在一个名为 string 的文件中。我们之前从未显式包含这个文件,因为每次包含 iostream 时,string 文件都会自动包含进来。

但在 C++ 中,还有另一种表示字符串的方式,那就是 C 风格字符串(C-Style Strings),它是一个你应该尽量避免的方式。总是使用 std::string 类型会更好,但在本节中,我会简要讲解 C 风格字符串,主要有两个原因:一是你可能需要维护一些 C++ 代码,这些代码在 string 类型创建之前编写;二是很多大学课程会教授 C 风格字符串,你可能需要学习它们作为课程的一部分。

C 风格字符串的工作原理

C 风格字符串实际上是一个特殊的字符数组。我们通过定义一个字符数组来表示字符串,并为它指定一个初始大小。由于我们依赖的是数组,因此我们必须确保为字符串分配足够的空间。否则,如果分配的空间过小,可能会发生内存溢出;如果分配的空间过大,就可能浪费内存。这也是为什么总是建议使用 std::string 类型的原因,因为它会动态地调整内存的使用。

初始化 C 风格字符串

假设我们要声明一个大小为 5 的字符数组,这意味着该数组最多可以存储 4 个字符,因为最后一个字符必须是我们称之为“空终止符”的特殊字符 \0

char name[5] = {'M', 'O', 'S', '\0'}; // 这样初始化字符数组

我们也可以使用更简便的方式来初始化:

char name[] = "MOS";  // 这样初始化会自动添加 \0 作为结尾符

在这个例子中,我们声明了一个字符数组 name,并通过字符串字面量 "MOS" 初始化它。字符串字面量会自动在末尾添加一个空终止符 \0,所以我们不需要手动添加。

访问和修改 C 风格字符串

由于我们在处理的是字符数组,我们可以像访问数组中的其他元素一样通过索引访问字符串中的字符。例如,我们可以修改字符串中的第一个字符:

name[0] = 'm';  // 修改第一个字符为小写 'm'

字符数组中的字符是通过“字符字面量”来表示的,字符字面量是用单引号括起来的字符:

name[0] = 'm';  // 这里的 'm' 是字符字面量

我们还可以读取字符串中的字符,例如:

std::cout << name[0];  // 输出 'm'

常用的 C 风格字符串函数

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 风格字符串的局限性

尽管 C 风格字符串提供了一些基本的功能,但它们有许多局限性:

总结

在接下来的课程中,我们将深入探讨 std::string 的使用方法,详细讲解它的优势和各种操作技巧。

0028 3 C 字符串

C++ 字符串(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 类型,只有像 intfloatchar 这样的基本数据类型。std::string 类隐藏了与字符数组相关的复杂性,使得我们不再需要担心数组的大小、内存浪费或数组越界等问题。所有这些复杂性都被该类内部处理了。

std::string 的优势

  1. 内存管理:不需要手动管理字符数组的大小和内存分配。std::string 会根据需要自动调整内存,避免了内存浪费和数组越界的问题。
  2. 简化操作:你不需要显式地处理字符串的拼接、复制或比较等操作,std::string 提供了丰富的成员函数来简化这些任务。
  3. 安全性:与 C 风格字符串相比,std::string 更加安全,因为它会自动处理内存和字符数组边界。

使用 std::string 进行操作

修改字符串

就像 C 风格字符串一样,std::string 也允许通过索引访问单个字符。索引从 0 开始,因此可以直接修改字符串中的某个字符。

std::string name = "Josette";
name[0] = 'M';  // 修改字符串的第一个字符
std::cout << name[0];  // 输出 M

获取字符串的长度

通过调用 std::stringlength() 函数,可以轻松获取字符串的长度。

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 的一些其他有用函数

总结

相比于 C 风格字符串,C++ 的 std::string 类型在性能和易用性上都有显著的提升,是现代 C++ 编程中处理字符串的首选方式。

0029 4 修改字符串

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++ 中方便地修改字符串:

这些函数使得在 C++ 中操作字符串变得更加简单和灵活。

0030 5 搜索字符串

字符串查找函数

在本节课中,我们将讨论 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;  // 输出最后一个不匹配字符的索引

总结

我们讨论了多个用于查找字符串中字符或子字符串的函数,以下是这些函数的用途:

这些函数为我们提供了灵活的工具,可以用来有效地搜索和操作字符串。

0031 6 提取子字符串

字符串提取函数:substr

有时我们需要从字符串中提取部分内容,这时可以使用 substr(子字符串)函数。该函数有两个参数,都是 size_t 类型的值,表示起始位置和要提取的字符数。值得注意的是,两个参数都有默认值,所以它们是可选的。

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"
std::string name = "Mathai";
std::string copy = name.substr(0, 3);  // 从位置 0 开始,提取 3 个字符
std::cout << copy;  // 输出 "Mat"

提取姓名的实例

假设我们有一个变量包含某个人的全名,下面是一个简单的例子,展示如何提取名字和姓氏。

  1. 提取名字:我们首先要找到空格的位置,然后提取空格前的所有字符作为名字。
  2. 提取姓氏:我们可以使用 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)"

这个程序处理了包含中间名的情况,通过使用 findrfind 来查找空格的位置,从而精确提取名字和姓氏。

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 解析字符串

WangShuXian6 commented 1 week ago

终极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 下一步