📘 第九章 · 用户自定义数据类型

谭浩强《C程序设计》第五版 — 交互式教学演示

📋第九章知识体系总览(谭浩强 P.223-279)

  • 9.1 typedef — 类型重定义
  • 9.2 结构体类型
  • 9.3 结构体数组
  • 9.4 结构体指针
  • 9.5 用指针处理链表
  • 9.6 共用体类型
  • 9.7 枚举类型
  • 9.8 类型定义符typedef
💡 本章重点:结构体是C语言中组织复杂数据的核心工具。掌握结构体的定义成员访问嵌套以及链表操作是开发复杂程序的基础。

🗺️本章学习路线图

① typedef
类型重定义
② 结构体
定义与使用
③ 结构体数组
批量数据
④ 结构体指针
p->member
⑤ 链表
动态数据结构
⑥ 共用体
内存复用

📚谭浩强教材配套说明

教材章节对应:

📌 9.1 定义结构体类型(P.224-232):结构体声明,定义变量,引用成员

📌 9.2 结构体数组(P.233-243):数组与结构体结合,排序查找

📌 9.3 结构体指针(P.244-251):指向结构体的指针,p->member语法

📌 9.4 用指针处理链表(P.252-266):链表概念,创建/插入/删除/遍历

📌 9.5 共用体(P.267-273):共用体概念,与结构体区别

📌 9.6 枚举类型(P.274-279):枚举定义,使用优点

📌 9.7 用typedef定义类型(P.280-283):类型重定义,简化代码

🔤9.7 typedef — 类型重定义(谭浩强 P.280-283)

typedef 的作用:为已有类型名创建一个新的别名,使代码更简洁、更易读。

📝基本语法

// 原类型写法 struct Point { int x; int y; }; struct Point p1; // 每次都要写struct // typedef 简化后 typedef struct Point { int x; int y; } Point; // Point 是类型名,不是变量名 Point p1; // 直接用 Point 定义变量
typedef int INTEGER; // int 的别名 INTEGER a = 10; // 等价于 int a = 10;
✅ 作用:简化冗长的类型名,提高代码可读性

⚖️typedef vs #define

对比项 typedef #define
处理阶段 编译时处理 预处理阶段(文本替换)
类型检查 ✅ 有类型检查 ❌ 无类型检查
复杂类型 ✅ 可处理指针、数组、结构体 ❌ 只能文本替换
示例 typedef int* IP; #define IP int*
IP a, b; int *a, *b; int *a, b;(b是int)

💡实际应用场景

// 1. 简化结构体定义 typedef struct Student { char name[20]; int age; float score; } Student; // 2. 定义函数指针类型 typedef int (*CompareFunc)(void*, void*); // 3. 简化复杂数组类型 typedef char NameArr[20]; NameArr names[100]; // char names[100][20] // 4. 简化标准类型 typedef unsigned char BYTE; typedef unsigned int UINT;

📦9.1 结构体基础(谭浩强 P.224-232)

结构体的特点:
📌 结构体是一种复合数据类型,可以将不同类型的数据组合成一个整体
📌 结构体的各个成员独立存储,内存布局连续
📌 与数组不同,结构体可以包含不同类型的成员

🖊️定义结构体的三种方式

方式一:先定义类型,再定义变量(最常用)

struct Point { // 定义结构体类型 int x; // 成员1 int y; //成员2 }; // 注意:这里有分号! struct Point p1, p2; // 定义结构体变量

方式二:定义类型的同时定义变量

struct Student { char name[20]; int age; float score; } s1, s2; // 定义变量

方式三:直接定义结构体变量(匿名结构体)

struct { int width; int height; } rect; // rect是变量,不是类型名

💻内存模型可视化

选择字段高亮:
💡 struct Student 占用 28 字节 = 20(name) + 4(age) + 4(score)

🔧引用结构体成员

// 定义结构体 struct Student { char name[20]; int age; float score; }; // 定义变量 struct Student stu; // 赋值(使用 . 运算符) stu.age = 18; stu.score = 95.5; // 注意:字符数组不能用=直接赋值,要用strcpy strcpy(stu.name, "张三"); // 读取 printf("%s %d %.1f", stu.name, stu.age, stu.score);
点击"演示成员访问"观察结构体成员的赋值与读取...

🔗结构体的嵌套

// 日期结构体 struct Date { int year; int month; int day; }; // 人员结构体(嵌套Date) struct Person { char name[30]; struct Date birthday; // 嵌套结构体 char gender; }; struct Person p; p.birthday.year = 2000; // 访问嵌套成员 p.birthday.month = 1; p.birthday.day = 1;
⚠️ 注意:结构体不能嵌套自身,但可以嵌套指向自身的指针(链表的基础)

📊9.2 结构体数组(谭浩强 P.233-243)

结构体数组:将结构体类型与数组结合,存储多个结构体变量的数据。
// 定义结构体数组 struct Student { char name[20]; int age; float score; }; struct Student class[5]; // 5个学生的数组 // 赋值 class[0].age = 18; class[0].score = 92.5; strcpy(class[0].name, "张三");

👥学生信息管理演示

序号 姓名 年龄 成绩
点击按钮操作学生数组...

📝内存布局

struct Student { char name[20]; int age; float score; }

每个学生占 28 字节,数组连续存储:
💡 内存布局:每个元素28字节 = 20(name) + 4(age) + 4(score),数组元素连续排列

🔗9.3 结构体指针(谭浩强 P.244-251)

结构体指针:指向结构体变量的指针,通过指针访问成员有两种方式:
📌 p->member (箭头运算符)
📌 (*p).member (先解引用,再点运算符)

➡️箭头运算符 p->member

struct Student { char name[20]; int age; }; struct Student stu = {"李四", 20}; struct Student *p = &stu; // 定义指向stu的指针 // 两种等价写法 p->age; // ✅ 箭头运算符(常用) (*p).age; // ✅ 先解引用再点运算符 stu.age; // ✅ 直接访问
点击按钮观察三种访问方式的结果...

🔄指针遍历结构体数组

struct Student class[3] = { {"甲", 18}, {"乙", 19}, {"丙", 20} }; // 用指针遍历数组 for (struct Student *p = class; p < class + 3; p++) { printf("%s %d\n", p->name, p->age); }

📌p->member 与 (*p).member 的区别

表达式 含义 注意
p->member 通过指针访问成员 简洁,推荐使用
(*p).member 解引用后用点运算符 括号不可省略!
p.member 错误! p是指针,不是结构体

🔗🔗9.4 用指针处理链表(谭浩强 P.252-266)

链表的特点:
📌 链表是一种动态数据结构,可以动态地分配和释放内存
📌 链表节点包含数据域指针域
📌 通过指针将各节点连接起来,不需要连续的内存空间
📌 插入/删除操作高效,不需要移动大量元素

🏗️链表节点结构

// 定义链表节点结构体 typedef struct Node { int data; // 数据域:存储数据 struct Node *next; // 指针域:指向下一个节点 } Node;
💡 节点结构:data(数据)+ next(指针)
最后一个节点的 next = NULL 表示链表结束

🎬链表操作演示

点击按钮操作链表...

📝创建链表代码

// 创建一个包含3个节点的链表 Node* createList() { Node *head, *p1, *p2; p1 = p2 = (Node*)malloc(sizeof(Node)); p1->data = 10; p1->next = NULL; head = p1; p2 = (Node*)malloc(sizeof(Node)); p2->data = 20; p2->next = NULL; p1->next = p2; return head; // 返回头指针 }
⚠️ 重要:链表操作必须使用 malloc() 动态分配内存,free() 释放内存

链表 vs 数组

对比项 链表 数组
存储方式 不连续,通过指针连接 连续内存
插入/删除 ✅ O(1),只需修改指针 ❌ O(n),需移动元素
访问元素 ❌ O(n),需遍历 ✅ O(1),直接下标访问
内存使用 按需分配,有额外指针开销 固定大小

🔀9.5 共用体(谭浩强 P.267-273)

共用体(Union)的特点:
📌 多个成员共享同一段内存
📌 任何时刻只能存储一个成员的值
📌 共用体的大小 = 最大成员的大小
📌 用于节省内存或实现数据类型转换

📝共用体定义

// 共用体定义 union Data { int i; // 占用4字节 float f; // 占用4字节 char ch; // 占用1字节 }; union Data d; d.i = 65; // 存储int,共享内存 printf("%c", d.ch); // 输出 'A'(65的ASCII码)

⚖️结构体 vs 共用体

💡 结论:结构体各成员独立存储,共用体各成员共享同一内存

💡共用体的应用场景

// 1. 节省内存:一个人可能是学生或教师 struct Person { char name[20]; int type; // 0=学生, 1=教师 union { int studentId; char teacherId[10]; } info; }; // 2. 数据类型转换 union Converter { float f; unsigned int bits; // 查看float的二进制位 };

📝9.6 枚举类型(谭浩强 P.274-279)

枚举(Enum)的特点:
📌 枚举将整型常量组织成有意义的名字
📌 枚举常量默认从0开始递增
📌 提高代码可读性和可维护性
📌 编译时检查,减少魔法数字

📝枚举定义

// 定义枚举类型(谭浩强 P.274) enum Weekday { SUNDAY, // 0 MONDAY, // 1 TUESDAY, // 2 WEDNESDAY, // 3 THURSDAY, // 4 FRIDAY, // 5 SATURDAY // 6 }; // 可以指定初始值 enum Color { RED = 1, GREEN = 2, BLUE = 4 }; // 使用枚举 enum Weekday today = MONDAY; printf("%d", today); // 输出 1
SUNDAY(0)
MONDAY(1)
TUESDAY(2)
WEDNESDAY(3)
THURSDAY(4)
FRIDAY(5)
SATURDAY(6)
点击选择星期,当前选中:MONDAY = 1

💡枚举的优点

为什么使用枚举?

📌 可读性好:MONDAY 比数字 1 更易理解
📌 易维护:修改枚举值,所有引用自动更新
📌 类型安全:编译器检查类型匹配
📌 调试方便:调试器显示符号名而非数字
// 不推荐:魔法数字 if (status == 1) { ... } // status是什么? // 推荐:枚举 enum Status { IDLE=1, RUNNING, PAUSED, STOPPED }; if (status == RUNNING) { ... } // 一目了然

🧠随堂测验