Skip to content

C++ 第四课:自定义数据类型与函数

课程目标:

  • 掌握自定义数据类型的力量: 深入理解并熟练运用结构体 (struct)、枚举 (enum) 和联合体 (union),为处理复杂数据奠定基础。
  • 理解函数的核心概念: 透彻理解函数作为代码组织和复用的基本单元,认识其在程序设计中的重要性。
  • 精通函数的定义与调用: 掌握定义和调用函数的完整流程,包括返回值、参数传递等关键要素。
  • 运用所学知识解决实际问题: 能够结合自定义数据类型和函数,编写结构清晰、功能完善的 C++ 程序,解决实际编程挑战。

课程内容:

第一部分: 自定义数据类型的魅力

在之前的课程中,我们学习了 C++ 提供的基本数据类型,如 intfloatchar 等。但现实世界的数据往往更加复杂,需要我们自定义数据类型来更好地描述和管理。本部分将介绍三种强大的自定义数据类型:结构体、枚举和联合体。

  1. 结构体 (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;
      }
      • 要使用结构体,首先需要声明结构体类型的变量,就像声明 intfloat 类型的变量一样。
      • 使用点运算符 . 来访问结构体变量的成员。例如,s1.name 表示访问 s1 这个 Student 结构体变量的 name 成员。
    • C++ 中的 struct 与 C 语言中的 struct 的主要区别

      • 成员函数: C++ 中的 struct 不仅可以包含数据成员,还可以包含函数(称为成员函数或方法),用于操作结构体内部的数据。这使得结构体更像一个轻量级的类,能够将数据和行为封装在一起。而 C 语言的 struct 只能包含数据成员。
      • 默认访问权限: 虽然 C++ 中 struct 的默认成员访问权限是 public,与 C 语言相同,但这只是默认行为。C++ 的 struct 同样可以使用 publicprivateprotected 关键字来显式控制成员的访问权限,尽管在实践中,class 更常用于实现具有私有成员的抽象数据类型。
    • structclass 的异同

      • 在 C++ 中,structclass 在功能上几乎完全相同。它们都可以包含数据成员和成员函数,并且都支持继承、多态等面向对象编程的特性。
      • 核心区别在于默认的成员访问权限和继承权限:
        • struct 的默认成员访问权限是 public,默认继承方式是 public 继承。
        • class 的默认成员访问权限是 private,默认继承方式是 private 继承。
      • 最佳实践的约定: 通常,我们使用 struct 来表示一个简单的、由不同数据类型组成的数据集合,强调数据的组合。而 class 则更常用于表示具有复杂行为和需要封装性的抽象数据类型。但这仅仅是一种约定,语法上两者并没有本质区别。
    • 结构体的应用场景

      • 表示具有多个相关属性的实体,例如学生、书籍、商品、坐标点等。
      • 作为函数参数和返回值,传递复杂的数据结构。
      • 构建更复杂的数据结构,如链表、树等。
  2. 枚举 (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,
      };
      • 可以显式地为枚举常量指定整数值。如果只指定部分常量的值,后续常量的值仍然会依次递增。
    • 枚举的应用场景

      • 表示状态码或错误码。
      • 表示选项或模式。
      • 提高代码可读性,使代码更易于理解和维护。
  3. 联合体 (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 函数中会变得难以管理和维护。函数是解决这个问题的重要工具,它可以将代码分割成独立的可重用模块。

  1. 函数的概念:构建代码模块 (约 15 分钟)

    • 什么是函数?
      • 函数是一段独立的代码块,它执行特定的任务,并且可以被程序中的其他部分多次调用。 函数就像一个小型“加工厂”,你给它一些“原材料”(输入),它会按照预定的流程进行处理,然后给你“产品”(输出)。
      • 例如,你可以创建一个函数来计算两个数的和,或者创建一个函数来打印一条欢迎消息。
    • 为什么要使用函数?
      • 避免代码冗余: 如果程序中有多处需要执行相同的操作,可以将这部分代码封装成一个函数,然后在需要的地方调用,避免重复编写相同的代码。
      • 提高代码可读性: 将复杂的程序逻辑分解成多个小型、功能独立的函数,可以使代码结构更清晰,更容易理解...,提高代码的可读性。每个函数专注于完成一个特定的任务,使得代码的逻辑更加模块化,易于理解和维护。想象一下,一个很长的 main 函数包含了所有的逻辑,阅读起来会非常困难。而将不同的任务分配到不同的函数中,就像给一本书加上了章节标题,让读者更容易把握整体结构。
      • 便于代码调试: 当程序出现错误时,如果代码被组织成多个函数,我们可以更容易地定位到出错的函数,并进行独立的测试和调试。这就像排查电路故障,将电路分成不同的模块可以更快地找到问题所在。
      • 提高代码的复用性: 一个设计良好的函数可以在程序的多个地方被调用,甚至可以在不同的程序中复用,大大提高了代码的效率和可维护性。
  2. 函数的定义:如何创建你的代码模块 (约 30 分钟)

    • 函数定义的语法:

      c++
      返回值类型 函数名(参数类型1 参数名1, 参数类型2 参数名2, ...) {
          // 函数体:包含函数要执行的代码
          return 返回值; // 如果返回值类型不是 void
      }
      • 返回值类型: 指定函数执行完成后返回给调用者的值的类型。可以是任何有效的 C++ 数据类型,例如 intfloatstd::string,甚至是自定义的结构体类型。如果函数不返回任何值,则返回值类型应声明为 void
      • 函数名: 函数的标识符,用于在程序中唯一地识别该函数。函数名应具有描述性,能够清晰地表达函数的功能。命名规则与变量名相同(由字母、数字和下划线组成,不能以数字开头)。
      • 参数列表(可选): 定义函数接收的输入参数。每个参数由参数类型和参数名组成,多个参数之间用逗号分隔。参数就像函数的“原材料”,函数根据这些参数进行计算或操作。如果函数不需要接收任何参数,则参数列表可以为空,写成 ()
      • 函数体: 包含函数要执行的代码块,用一对花括号 {} 包裹起来。函数体内部可以包含任何有效的 C++ 语句,包括变量声明、赋值、控制流语句(如 ifforwhile)等。
      • return 语句(可选): 用于从函数中返回一个值给调用者。return 语句后跟要返回的值,返回值的类型必须与函数定义的返回值类型一致。对于返回值类型为 void 的函数,可以省略 return 语句,或者使用不带返回值的 return; 来提前结束函数的执行。
    • 函数定义的示例:

      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 类型的参数 ab,计算它们的乘积,并将结果作为 int 类型的值返回。
      • greet 函数接收一个 std::string 类型的参数 name,并在控制台打印一句问候语。由于它不需要返回任何值,所以返回值类型是 void
  3. 函数的调用:启动代码模块的执行 (约 25 分钟)

    • 函数调用的语法:

      c++
      函数名(实际参数列表);
      • 函数名: 要调用的函数的名称,必须与函数定义时的名称完全一致。
      • 实际参数列表: 传递给函数的实际值或表达式,用于替换函数定义中的形式参数。实际参数的数量、类型和顺序必须与函数定义中的形式参数列表匹配。如果被调用的函数没有参数,则调用时也需要使用空括号 ()
    • 参数传递:连接调用者和被调用者

      • 形式参数 (形参): 在函数定义中声明的参数,它们是函数内部使用的占位符,代表函数期望接收的输入值。
      • 实际参数 (实参): 在函数调用时传递给函数的具体值或表达式。
      • C++ 中常用的参数传递方式有两种:
        • 值传递 (Pass by Value): 在值传递中,实际参数的值会被复制一份传递给函数的形式参数。这意味着函数内部对形参的修改不会影响到外部的实参。这是最常用的传递方式,适用于函数不需要修改原始数据的情况。在上面的 multiply 例子中,num1num2 就是通过值传递给 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("小明");
  4. 函数的应用:实践代码组织的艺术 (约 15 分钟)

    • 函数的应用场景:
      • 编写可复用代码: 将常用的功能封装成函数,可以在程序的多个地方调用,避免重复编写相同的代码,提高开发效率和代码质量。例如,计算阶乘、排序数组、验证用户输入等功能都可以封装成函数。
      • 模块化编程: 将大型程序分解成多个小型、独立的函数,每个函数负责完成一个特定的任务。这种模块化的设计方法可以降低程序的复杂性,提高代码的可读性、可维护性和可测试性。
      • 简化程序逻辑: 通过合理地组织函数,可以将复杂的程序逻辑分解成更小的、更容易理解的步骤,使代码更加清晰易懂。例如,一个处理用户订单的程序可以分解为获取用户输入、验证输入、计算总价、生成订单等多个函数。

总结:

本节课我们学习了 C++ 中自定义数据类型(结构体、枚举和联合体)以及函数的基本概念和使用方法。掌握这些知识对于编写结构良好、功能完善的 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;
}

代码解释:

  1. 包含头文件: #include <iostream> 包含了输入输出流的库,用于与用户进行交互。

  2. 函数定义:

    • bool isLeapYear(int year): 定义了一个名为 isLeapYear 的函数,它接收一个整数类型的参数 year (表示年份),并返回一个布尔类型的值 ( true 表示是闰年, false 表示不是闰年)。
  3. 闰年判断逻辑:

    • 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 整除。
  4. main 函数:

    • int main(): 程序的入口点。
    • int year;: 声明一个整型变量 year 来存储用户输入的年份。
    • std::cout << "请输入一个年份: ";: 提示用户输入年份。
    • std::cin >> year;: 从用户输入读取年份并存储到 year 变量中。
    • if (isLeapYear(year)) { ... } else { ... }: 调用 isLeapYear 函数判断输入的年份是否为闰年,并根据返回结果输出相应的提示信息。

如何使用:

  1. 将代码复制到 C++ 的集成开发环境 (IDE) 中,例如 Visual Studio Code, Code::Blocks, Dev-C++ 等。
  2. 编译并运行代码。
  3. 程序会提示你输入一个年份,然后会判断该年份是否为闰年并输出结果。

示例运行:

bash
请输入一个年份: 2023
2023 年不是闰年。

请输入一个年份: 2024
2024 年是闰年。

请输入一个年份: 1900
1900 年不是闰年。

请输入一个年份: 2000
2000 年是闰年。