C++ 第四课:自定义数据类型与函数
课程目标:
- 掌握自定义数据类型的力量: 深入理解并熟练运用结构体 (struct)、枚举 (enum) 和联合体 (union),为处理复杂数据奠定基础。
- 理解函数的核心概念: 透彻理解函数作为代码组织和复用的基本单元,认识其在程序设计中的重要性。
- 精通函数的定义与调用: 掌握定义和调用函数的完整流程,包括返回值、参数传递等关键要素。
- 运用所学知识解决实际问题: 能够结合自定义数据类型和函数,编写结构清晰、功能完善的 C++ 程序,解决实际编程挑战。
课程内容:
第一部分: 自定义数据类型的魅力
在之前的课程中,我们学习了 C++ 提供的基本数据类型,如 int
、float
、char
等。但现实世界的数据往往更加复杂,需要我们自定义数据类型来更好地描述和管理。本部分将介绍三种强大的自定义数据类型:结构体、枚举和联合体。
结构体 (struct):将数据打包成有意义的整体 (约 40 分钟)
什么是结构体?
- 想象一下,你要描述一个学生的信息。仅仅用一个整数表示学号,一个字符串表示姓名,一个浮点数表示成绩,这些数据是分散的,难以在一个逻辑单元中进行管理。
- 结构体就是一种自定义的数据类型,它允许你将不同类型的数据组合在一起,形成一个有意义的新的数据类型。 这个新的数据类型就像一个“包裹”,将相关的各种信息捆绑在一起。
- 例如,我们可以创建一个名为
Student
的结构体,用来存储学生的姓名(字符串)、学号(整数)和成绩(浮点数)。
定义结构体:设计你的数据“包裹”
c++struct Student { std::string name; // 学生姓名 int id; // 学生学号 float score; // 学生成绩 };
struct
关键字表明我们正在定义一个结构体。Student
是这个结构体的名称,你可以根据需要自定义名称,但要遵循命名规范,使其具有描述性。- 花括号
{}
内部定义了结构体的成员(也称为字段或属性),每个成员都有自己的数据类型和名称。
使用结构体:创建和访问结构体变量
c++#include <iostream> #include <string> struct Student { std::string name; int id; float score; }; int main() { // 声明一个 Student 类型的变量 s1 Student s1; // 为 s1 的成员赋值 s1.name = "Alice"; s1.id = 2023001; s1.score = 95.5; // 访问并输出 s1 的成员 std::cout << "姓名: " << s1.name << std::endl; std::cout << "学号: " << s1.id << std::endl; std::cout << "成绩: " << s1.score << std::endl; return 0; }
- 要使用结构体,首先需要声明结构体类型的变量,就像声明
int
或float
类型的变量一样。 - 使用点运算符
.
来访问结构体变量的成员。例如,s1.name
表示访问s1
这个Student
结构体变量的name
成员。
- 要使用结构体,首先需要声明结构体类型的变量,就像声明
C++ 中的
struct
与 C 语言中的struct
的主要区别- 成员函数: C++ 中的
struct
不仅可以包含数据成员,还可以包含函数(称为成员函数或方法),用于操作结构体内部的数据。这使得结构体更像一个轻量级的类,能够将数据和行为封装在一起。而 C 语言的struct
只能包含数据成员。 - 默认访问权限: 虽然 C++ 中
struct
的默认成员访问权限是public
,与 C 语言相同,但这只是默认行为。C++ 的struct
同样可以使用public
、private
和protected
关键字来显式控制成员的访问权限,尽管在实践中,class
更常用于实现具有私有成员的抽象数据类型。
- 成员函数: C++ 中的
struct
和class
的异同- 在 C++ 中,
struct
和class
在功能上几乎完全相同。它们都可以包含数据成员和成员函数,并且都支持继承、多态等面向对象编程的特性。 - 核心区别在于默认的成员访问权限和继承权限:
struct
的默认成员访问权限是public
,默认继承方式是public
继承。class
的默认成员访问权限是private
,默认继承方式是private
继承。
- 最佳实践的约定: 通常,我们使用
struct
来表示一个简单的、由不同数据类型组成的数据集合,强调数据的组合。而class
则更常用于表示具有复杂行为和需要封装性的抽象数据类型。但这仅仅是一种约定,语法上两者并没有本质区别。
- 在 C++ 中,
结构体的应用场景
- 表示具有多个相关属性的实体,例如学生、书籍、商品、坐标点等。
- 作为函数参数和返回值,传递复杂的数据结构。
- 构建更复杂的数据结构,如链表、树等。
枚举 (enum):为整数赋予有意义的名称 (约 25 分钟)
什么是枚举?
- 在编程中,我们经常需要表示一组固定的、相关的常量值。例如,一周的七天、交通信号灯的三种状态、游戏角色的不同职业等。
- 枚举是一种用户定义的数据类型,它允许你为一组整型常量赋予有意义的名称。 这样可以提高代码的可读性和可维护性,避免使用难以理解的 magic number (魔法数字)。
定义枚举类型
c++enum Weekday { Monday, // 默认值为 0 Tuesday, // 默认值为 1 Wednesday, // 默认值为 2 Thursday, // 默认值为 3 Friday, // 默认值为 4 Saturday, // 默认值为 5 Sunday // 默认值为 6 };
enum
关键字表明我们正在定义一个枚举类型。Weekday
是这个枚举类型的名称。- 花括号
{}
内部列出了枚举常量,用逗号分隔。默认情况下,第一个枚举常量的值为 0,后续常量的值依次递增 1。
使用枚举类型
c++#include <iostream> enum Weekday { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday }; int main() { Weekday today = Wednesday; // 可以直接使用枚举常量 if (today == Wednesday) { std::cout << "今天星期三,要努力学习!" << std::endl; } // 枚举常量本质上是整型,可以进行比较和运算 if (today > Friday) { std::cout << "今天是周末!" << std::endl; } // 也可以显式获取枚举常量的值 std::cout << "Wednesday 的值是: " << Wednesday << std::endl; // 输出 2 return 0; }
- 声明枚举类型的变量与声明其他类型的变量类似。
- 可以直接使用枚举常量来赋值和比较。
- 注意:虽然枚举常量本质上是整型,但最好将枚举类型视为独立的类型,避免将枚举类型的值与普通的整型值进行直接运算,除非有明确的需要。
指定枚举常量的值
c++enum Status { Success = 0, Warning = 10, Error = 20, };
- 可以显式地为枚举常量指定整数值。如果只指定部分常量的值,后续常量的值仍然会依次递增。
枚举的应用场景
- 表示状态码或错误码。
- 表示选项或模式。
- 提高代码可读性,使代码更易于理解和维护。
联合体 (union):共享内存的特殊数据类型 (约 20 分钟)
- 什么是联合体?
- 联合体是一种特殊的数据类型,它允许在同一块内存空间存储不同类型的数据。 这意味着,联合体的所有成员共享同一块内存,任何时候只能有一个成员存储有效的值。
- 你可以把联合体想象成一个“房间”,这个房间可以用来存放不同类型的“物品”,但一次只能存放一件“物品”。
- 定义联合体c++
union Data { int intValue; float floatValue; char stringValue[20]; };
union
关键字表明我们正在定义一个联合体。Data
是这个联合体的名称。- 花括号
{}
内部定义了联合体的成员。
- 使用联合体c++
#include <iostream> #include <cstring> struct Data { int intValue; float floatValue; char stringValue[20]; }; int main() { Data data; // 存储整数值 data.intValue = 100; std::cout << "intValue: " << data.intValue << std::endl; // 存储浮点数值,此时 intValue 的值不再有效 data.floatValue = 3.14f; std::cout << "floatValue: " << data.floatValue << std::endl; // 此时访问 data.intValue 将会得到未定义的值 // 存储字符串值,之前的值都不再有效 strcpy(data.stringValue, "Hello"); std::cout << "stringValue: " << data.stringValue << std::endl; return 0; }
- 联合体的大小由其最大的成员的大小决定。
- 当给联合体的一个成员赋值时,其他成员的值会变得无效。 你需要清楚地知道当前联合体中哪个成员存储了有效的值。
- 联合体的应用场景
- 节省内存空间: 当需要在不同的时间存储不同类型的数据,但这些数据不会同时使用时,可以使用联合体来节省内存。
- 类型双关 (Type Punning): 在某些底层编程或需要直接操作内存的情况下,可以使用联合体来查看同一块内存的不同类型解释,但这需要非常小心,避免出现未定义的行为。
- 与结构体结合使用: 联合体常常与结构体结合使用,例如,在一个表示消息的结构体中,消息的内容可能是不同的类型,可以使用联合体来存储不同类型的消息内容。
- 使用联合体的注意事项
- 由于联合体的成员共享内存,因此在给一个成员赋值后,其他成员的值会变得无效。你需要维护当前有效成员的信息。
- 联合体不能包含带有构造函数或析构函数的类类型的成员,除非这些构造函数和析构函数是平凡的 (trivial)。
- 使用联合体需要谨慎,确保你清楚地知道当前存储在联合体中的数据类型,避免数据错误。
- 什么是联合体?
第二部分:函数的魔力:代码的组织者和复用者
随着程序规模的增大,将所有代码都写在一个 main
函数中会变得难以管理和维护。函数是解决这个问题的重要工具,它可以将代码分割成独立的可重用模块。
函数的概念:构建代码模块 (约 15 分钟)
- 什么是函数?
- 函数是一段独立的代码块,它执行特定的任务,并且可以被程序中的其他部分多次调用。 函数就像一个小型“加工厂”,你给它一些“原材料”(输入),它会按照预定的流程进行处理,然后给你“产品”(输出)。
- 例如,你可以创建一个函数来计算两个数的和,或者创建一个函数来打印一条欢迎消息。
- 为什么要使用函数?
- 避免代码冗余: 如果程序中有多处需要执行相同的操作,可以将这部分代码封装成一个函数,然后在需要的地方调用,避免重复编写相同的代码。
- 提高代码可读性: 将复杂的程序逻辑分解成多个小型、功能独立的函数,可以使代码结构更清晰,更容易理解...,提高代码的可读性。每个函数专注于完成一个特定的任务,使得代码的逻辑更加模块化,易于理解和维护。想象一下,一个很长的
main
函数包含了所有的逻辑,阅读起来会非常困难。而将不同的任务分配到不同的函数中,就像给一本书加上了章节标题,让读者更容易把握整体结构。 - 便于代码调试: 当程序出现错误时,如果代码被组织成多个函数,我们可以更容易地定位到出错的函数,并进行独立的测试和调试。这就像排查电路故障,将电路分成不同的模块可以更快地找到问题所在。
- 提高代码的复用性: 一个设计良好的函数可以在程序的多个地方被调用,甚至可以在不同的程序中复用,大大提高了代码的效率和可维护性。
- 什么是函数?
函数的定义:如何创建你的代码模块 (约 30 分钟)
函数定义的语法:
c++返回值类型 函数名(参数类型1 参数名1, 参数类型2 参数名2, ...) { // 函数体:包含函数要执行的代码 return 返回值; // 如果返回值类型不是 void }
- 返回值类型: 指定函数执行完成后返回给调用者的值的类型。可以是任何有效的 C++ 数据类型,例如
int
、float
、std::string
,甚至是自定义的结构体类型。如果函数不返回任何值,则返回值类型应声明为void
。 - 函数名: 函数的标识符,用于在程序中唯一地识别该函数。函数名应具有描述性,能够清晰地表达函数的功能。命名规则与变量名相同(由字母、数字和下划线组成,不能以数字开头)。
- 参数列表(可选): 定义函数接收的输入参数。每个参数由参数类型和参数名组成,多个参数之间用逗号分隔。参数就像函数的“原材料”,函数根据这些参数进行计算或操作。如果函数不需要接收任何参数,则参数列表可以为空,写成
()
。 - 函数体: 包含函数要执行的代码块,用一对花括号
{}
包裹起来。函数体内部可以包含任何有效的 C++ 语句,包括变量声明、赋值、控制流语句(如if
、for
、while
)等。 return
语句(可选): 用于从函数中返回一个值给调用者。return
语句后跟要返回的值,返回值的类型必须与函数定义的返回值类型一致。对于返回值类型为void
的函数,可以省略return
语句,或者使用不带返回值的return;
来提前结束函数的执行。
- 返回值类型: 指定函数执行完成后返回给调用者的值的类型。可以是任何有效的 C++ 数据类型,例如
函数定义的示例:
c++#include <iostream> #include <string> // 定义一个函数,计算两个整数的乘积 int multiply(int a, int b) { int product = a * b; return product; } // 定义一个函数,打印包含问候语的消息 void greet(std::string name) { std::cout << "你好, " << name << "!" << std::endl; } int main() { int num1 = 5; int num2 = 10; // 调用 multiply 函数 int result = multiply(num1, num2); std::cout << num1 << " 乘以 " << num2 << " 的结果是: " << result << std::endl; // 调用 greet 函数 greet("小明"); return 0; }
- 在上面的例子中,
multiply
函数接收两个int
类型的参数a
和b
,计算它们的乘积,并将结果作为int
类型的值返回。 greet
函数接收一个std::string
类型的参数name
,并在控制台打印一句问候语。由于它不需要返回任何值,所以返回值类型是void
。
- 在上面的例子中,
函数的调用:启动代码模块的执行 (约 25 分钟)
函数调用的语法:
c++函数名(实际参数列表);
- 函数名: 要调用的函数的名称,必须与函数定义时的名称完全一致。
- 实际参数列表: 传递给函数的实际值或表达式,用于替换函数定义中的形式参数。实际参数的数量、类型和顺序必须与函数定义中的形式参数列表匹配。如果被调用的函数没有参数,则调用时也需要使用空括号
()
。
参数传递:连接调用者和被调用者
- 形式参数 (形参): 在函数定义中声明的参数,它们是函数内部使用的占位符,代表函数期望接收的输入值。
- 实际参数 (实参): 在函数调用时传递给函数的具体值或表达式。
- C++ 中常用的参数传递方式有两种:
值传递 (Pass by Value): 在值传递中,实际参数的值会被复制一份传递给函数的形式参数。这意味着函数内部对形参的修改不会影响到外部的实参。这是最常用的传递方式,适用于函数不需要修改原始数据的情况。在上面的
multiply
例子中,num1
和num2
就是通过值传递给multiply
函数的。引用传递 (Pass by Reference): 在引用传递中,形式参数成为实际参数的别名。对形参的任何修改都会直接影响到外部的实参。引用传递使用
&
符号在形式参数类型前声明。这在需要在函数内部修改原始数据时非常有用,可以避免不必要的内存拷贝,提高效率。c++#include <iostream> void increment(int& num) { num++; // 对形参 num 的修改会影响到实参 } int main() { int count = 5; increment(count); std::cout << "Count 的值是: " << count << std::endl; // 输出 6 return 0; }
常量引用传递 (Pass by Constant Reference): 结合了引用传递的效率和值传递的安全性。形式参数成为实际参数的别名,但通过
const
关键字限制了在函数内部修改形参的值。这常用于传递大型对象,既避免了拷贝开销,又保证了原始数据的安全。c++#include <iostream> #include <string> void printLength(const std::string& str) { std::cout << "字符串的长度是: " << str.length() << std::endl; // str[0] = 'A'; // 错误!不能修改常量引用 } int main() { std::string message = "Hello"; printLength(message); return 0; }
函数调用的示例 (接上例):
c++int num1 = 5; int num2 = 10; // 调用 multiply 函数,将 num1 和 num2 的值传递给它 int result = multiply(num1, num2); // 调用 greet 函数,将字符串 "小明" 传递给它 greet("小明");
函数的应用:实践代码组织的艺术 (约 15 分钟)
- 函数的应用场景:
- 编写可复用代码: 将常用的功能封装成函数,可以在程序的多个地方调用,避免重复编写相同的代码,提高开发效率和代码质量。例如,计算阶乘、排序数组、验证用户输入等功能都可以封装成函数。
- 模块化编程: 将大型程序分解成多个小型、独立的函数,每个函数负责完成一个特定的任务。这种模块化的设计方法可以降低程序的复杂性,提高代码的可读性、可维护性和可测试性。
- 简化程序逻辑: 通过合理地组织函数,可以将复杂的程序逻辑分解成更小的、更容易理解的步骤,使代码更加清晰易懂。例如,一个处理用户订单的程序可以分解为获取用户输入、验证输入、计算总价、生成订单等多个函数。
- 函数的应用场景:
总结:
本节课我们学习了 C++ 中自定义数据类型(结构体、枚举和联合体)以及函数的基本概念和使用方法。掌握这些知识对于编写结构良好、功能完善的 C++ 程序至关重要。在接下来的学习中,我们将继续深入探讨函数的更多高级特性,以及如何利用它们构建更复杂的软件系统。
作业
编写一个函数,用于判断一个年份是否为闰年。
参考实现如下:
#include <iostream>
// 函数用于判断一个年份是否为闰年
bool isLeapYear(int year) {
// 闰年的判断规则:
// 1. 能被 4 整除但不能被 100 整除
// 2. 能被 400 整除
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
int main() {
int year;
std::cout << "请输入一个年份: ";
std::cin >> year;
if (isLeapYear(year)) {
std::cout << year << " 年是闰年。" << std::endl;
} else {
std::cout << year << " 年不是闰年。" << std::endl;
}
return 0;
}
代码解释:
包含头文件:
#include <iostream>
包含了输入输出流的库,用于与用户进行交互。函数定义:
bool isLeapYear(int year)
: 定义了一个名为isLeapYear
的函数,它接收一个整数类型的参数year
(表示年份),并返回一个布尔类型的值 (true
表示是闰年,false
表示不是闰年)。
闰年判断逻辑:
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
: 这是判断闰年的核心逻辑。year % 4 == 0
: 判断年份是否能被 4 整除。year % 100 != 0
: 判断年份是否不能被 100 整除。year % 400 == 0
: 判断年份是否能被 400 整除。&&
(逻辑与): 表示同时满足两个条件。||
(逻辑或): 表示满足其中一个条件。- 因此,这个表达式实现了闰年的判断规则:能被 4 整除但不能被 100 整除,或者能被 400 整除。
main
函数:int main()
: 程序的入口点。int year;
: 声明一个整型变量year
来存储用户输入的年份。std::cout << "请输入一个年份: ";
: 提示用户输入年份。std::cin >> year;
: 从用户输入读取年份并存储到year
变量中。if (isLeapYear(year)) { ... } else { ... }
: 调用isLeapYear
函数判断输入的年份是否为闰年,并根据返回结果输出相应的提示信息。
如何使用:
- 将代码复制到 C++ 的集成开发环境 (IDE) 中,例如 Visual Studio Code, Code::Blocks, Dev-C++ 等。
- 编译并运行代码。
- 程序会提示你输入一个年份,然后会判断该年份是否为闰年并输出结果。
示例运行:
请输入一个年份: 2023
2023 年不是闰年。
请输入一个年份: 2024
2024 年是闰年。
请输入一个年份: 1900
1900 年不是闰年。
请输入一个年份: 2000
2000 年是闰年。