490 likes | 606 Views
第七章 结构和模块化设计 7.1 结构. 复合数据对象的需求 描述由不同类型的数据元素组成的数据对象 例如: 教学管理系统中的学生数据 包括:姓名、性别、学号、班级 语言支持 C 语言中的结构 Pascal 语言中的记录 C++ 、 Java 语言中的类. 语法结构 struct 结构名 { 类型 成员变量; 。。。 }; /* 学生信息的结构 * / struct Student { char name[32]; char sex; int no; char class[8]; };.
E N D
第七章 结构和模块化设计7.1 结构 • 复合数据对象的需求 • 描述由不同类型的数据元素组成的数据对象 • 例如: • 教学管理系统中的学生数据 • 包括:姓名、性别、学号、班级 • 语言支持 • C 语言中的结构 • Pascal 语言中的记录 • C++ 、Java 语言中的类
语法结构 struct 结构名 { 类型 成员变量; 。。。 }; /* 学生信息的结构 */ struct Student { char name[32]; char sex; int no; char class[8]; }; 结构变量说明 struct Student chen; 或直接说明 struct { char name[32]; char sex; int no; char class[8]; } chen; 结构定义和变量说明
结构变量及相关运算 • 结构变量占用的内存空间 • 大于等于各个元素占用内存空间之和 • 结构变量的运算 • 初始化 如:struct Student chen= { “chen”, ‘F’, 24, “9707” }; • 赋值 • 函数参数 • 结构分量的运算 • 引用: chen.name chen.sex • 运算取决于分量的类型
例7-1:日期的验证 #include <stdio.h> struct date { int year; /* 年 */ unsigned char month; /* 月 */ unsigned char day; /* 日 */ }; main( ) { int days[ ] = {0,31,28,31,30,31,30,31,31,30,31,30,31}; struct date x; do { scanf( “%d%d%d”, &x.year, &x.month, &x.day ); } while( x.day <= 0 || x.day > days[x.month] || x.month <= 0 || x.month > 12 ); /* 出错要求重新输入 */ printf( “It is correct date.\n” ); }
程序读解 • 结构的使用 • 分量使用:各种运算、取地址、作为参数; • 分量类型:指针、其他结构(不允许递归定义) • unsigned char • 无符号字符(1个字节:0-255) • %d 指示读整数,存到该字节 • 数组 days 的使用 • 避免 switch 语句的多分支 • 执行速度高于 switch
例7-2:洗牌和发牌模拟 • 任务: • 考虑扑克牌的 4 个花色和 52 张,完成洗牌过程,通过输出完成发牌过程 • 基本思路 • 设置数据对象保存 52 张牌、4 个花色和牌名信息 • 产生随机数来控制牌的交换,模拟洗牌过程 • 输出所有牌,分为 4 组,模拟发牌过程
数据对象设计 52张牌 cards[52] 4个花色 suit[4] 13个牌号 face[13] 算法 初始化52张牌 洗牌过程 发牌过程 初始化 依次设置每张牌的花色和牌号 洗牌过程 依次处理每张牌(第 i 张) 产生一个随机数(0-51) j 将第 j 张和第 i 张牌交换 算法设计
程序结构设计 • 数据结构 • 采用 struct 描述扑克牌的信息(花色、牌号) • 采用整数数组表示牌号 1, 2, 3, … 13 • 采用字符数组表示花色 H, D, C, S • 程序结构 • 设置 3 个函数 fillDeck, shuffle, deal • 分别负责初始化、洗牌和发牌的完成
程序实现(1/3) 通用函数头文件 #include <stdio.h> #include <stdlib.h> #include <time.h> struct card { int face; char suit; }; main( ) { struct card deck[52]; int face[ ] = {1,2,3,4,5,6,7,8,9,10,11,12,13}; char suit[ ] = { ‘H’, ‘D’, ‘C’, ‘S’ }; srand( time(NULL) ); fillDesk( deck, face, suit ); shuffle( deck ); deal( deck ); } 时间函数头文件 数组初始化 获得当前时间 初始化随机数函数
程序实现(2/3) void fillDeck( struct card deck[ ], int face[ ], char suit[ ] ) { int i; /* 初始化:顺序排列52张牌 */ for( i=0; i<52; i++ ) { deck[ i ].face = face[ i%13 ]; deck[ i ].suit = suit[ i/13 ]; } } 结构分量的引用
程序实现(3/3) void deal( struct card deck[ ] ) { int i; /* 发牌过程 */ for( i=0; i<52; i++ ) printf(“%d of %c%c”, deck[ i ].face, deck[ i ].suit, (i+1)%4? ‘\t’: ‘\n’ ); } void shuffle( struct card deck[ ] ) { int i, j; /* 洗牌过程 */ struct card temp; for( i=0; i<52; i++ ) { j = rand( ) %52; temp = deck[ i ]; deck[ i ] = deck [ j ]; deck[ j ] = temp; } } 随机数生成 制表符 条件表达式
几点程序说明 • 结构数组 • 元素是一个完整的结构 • 结构变量的赋值 temp = desk[ i ] • 整个结构的复制(时间开销大) • 数组初始化 • 可利用初值个数设置数组大小 • 随机数的产生 • int rand( ) 返回随机数 • void srand( unsigned seed ) 初始化随机数生成 • time( ) 获得当前时间(单位:秒) • 需要通用函数和时间函数头文件 stdlib.h, time.h
虚拟机的概念 • 抽象的计算机 • 接受一组指令,按照给定的指令序列执行 • 虚拟机概念在程序设计中的应用 • 使得一个程序模块,按照类似的方式工作: • 以一组函数实现虚拟机的指令系统 • 以函数调用表示指令的执行 • 例如: • 上例中,扑克牌数组以及相关的 3 个函数组成一个虚拟机;3 个函数实现了该虚拟机的 3 个指令。 • 使用者(main函数)仅负责向虚拟机发送指令(函数调用),不必关系其内部实现细节
抽象方法与信息隐蔽 • 抽象方法的运用 • 通过 3 个函数的设置(fillDeck, shuffle, deal),完全隐蔽了扑克牌相关处理的内部计算逻辑; • 虚拟机使用者的视点 • 仅了解 3 个函数的使用方法,好象它们是系统提供的标准函数 • 对软件工程的支持 • 虚拟机的使用者和实现者可以是不同的开发组 • 分别实现大型软件的不同组成部分 • 软件模块接口就是一组函数原型及其说明
7.2 模块化程序设计 • 软件模块 • 数据对象及其相关处理算法 • 程序模块:数据结构及其相关函数 • 例 7-1 中的模块 • 主控模块:main函数 • 扑克牌模块:数组 deck 以及3个函数 • 信息隐蔽 • 模块的使用者无须连接数据对象的细节 如:main 函数编制时,无须了解数组 deck 的详细定义、数据内容和结构组织。
例7-3:简易学生管理系统 • 任务: • 分别输入学生的户籍信息和学籍信息,打印出学生基本信息表(假设学生人数<250人) • 户籍信息:姓名、身份证号码、出生年月日、住址; • 学籍信息:学号、身份证号码、所属学院、专业、班级 • 学生基本信息表:学号、姓名、年龄、所属学院、班级; • 数据对象 • 学生户籍数据、学生学籍数据 • 学生基本信息表可以直接输出(不保存)
算法设计 • 输入户籍数据 (每行输入一个学生的数据,空格分割各个项目) • 输入学籍数据 (每行输入一个学生的数据,空格分割各个项目) • 构造并输出学生基本信息表 (提取户籍和学籍数据,构造并输出学生信息表)
算法的逐步求精 • 1、2 步 • 仅涉及输入输出,忽略算法描述 • 3 构造学生基本信息表 • 3.1 依次从户籍数据中取出一个学生的信息 • 3.2 根据其身份证号码,找出该生的学籍信息 • 3.3 综合该生的户籍信息和学籍信息,构造基本信息记录,填入学生基本信息表 • 3.4 重复 3.1-3.3 的处理,直至处理完所有学生的数据
程序结构设计 • 数据对象 • 户籍数据、学籍数据、学生基本信息表 • 尽可能符合信息的原始结构、采用struct • 采用结构数组来保存数据 • 模块与函数的设计 • 围绕户籍数据,提供输入、依次提取的函数 • 围绕学籍数据,提供输入、查找的函数 • 为学生基本信息,提供构造并输出的函数 • 形成 3 个程序模块
程序结构 主控模块 main( ) 户籍数据模块 InfoAddr 数据接口 InputAddr( … ) 输入数据 学籍数据模块 InfoStudent 学籍数据 InputStudent( … ) 输入数据 GetStudent( … ) 查找学生信息 基本信息表模块 Output( … ) 基本信息表的输出
户籍处理模块的设计 #include <stdio.h> #include <string.h> typedef struct { /* 户籍数据结构 */ char name[ 16 ]; /* 姓名 */ long no; /* 身份证号 */ struct { int year, mon, day; /* 作为分量的结构 */ } birthday; /* 生日 */ char addr[ 128 ]; /* 地址 */ } InfoAddr; int InputAddr( InfoAddr info[ ] ); /* 输入数据到数组info, 返回学生数量 */
学籍处理模块的设计 typedef struct { /* 学籍数据结构 */ char student[ 20 ]; /* 学号 */ long no; /* 身份证号 */ char college[ 32 ]; /* 学院 */ char class[ 10 ]; /* 班级 */ } InfoStudent; int InputStudent( InfoStudent info[ ] ); /* 输入数据到数组info, 返回学生数量 */ InfoStudent *GetStudent( long no, InfoStudent info[ ], int n ); /* 获得身份证号为 no 的学生的学籍数据, n是学生数量 */
主控模块的设计 void Output(int n, InfoAddr addr[ ], InfoStudent info[ ]); /* 构造学生基本信息,按表格输出(n是学生人数) */ main( ) { static InfoAddr addr[ 256 ]; static InfoStudent stu[ 256 ]; int num; /* 人数 */ num = InputAddr( addr ); /* 输入户籍数据 */ if( num != InputStudent( stu ) ) /* 输入学籍数据 */ return; Output( num, addr, stu ); /* 输出基本信息表 */ }
设计说明 • 函数原型设计 • 考虑函数内部功能的实现中,需要用到的所有输入输出信息; • 将输入信息作为参数;输出结果作为返回值,或通过参数返回; • 确保信息处理的局部化 • 实现技术细节 • 对于复杂结构,利用结构,结构分量的结构 • 对于大的数据对象(结构数组),采用静态变量
学生基本信息表的构造和输出 void Output(int n, InfoAddr addr[ ], InfoStudent info[ ]) { int i, age; InfoAddr *q; /* 结构指针 */ InfoStudent *p; for( i=0; i<n; i++ ) { q = &addr[ i ]; /* 取户籍数据 */ age = 2005 – q->birthday.year; /* 计算年龄 */ p = GetStudent( q->no, info, n ); /* 取学籍数据 */ printf( “%8ld %.16s %2d %.32s %.10s\n”, q->no, q->name, age, p->college, p->class ); } }
户籍信息的输入 int InputAddr( InfoAddr info[ ] ) { /* 输入数据到数组info, 返回学生数量 */ int n = 6; char buf[ 256 ]; InfoAddr *p; for( p = info; n == 6; p++ ) { gets(buf); /* 读入一行 */ n = sscanf( buf, “%s%ld%d%d%d%.128s”, p->name, &p->no, &p->birthday.year, &p->birthday.mon, &p->birthday.day, p->addr ); /* 读入到数组元素中 */ } return p – info - 1; }
学籍数据的输入 int InputStudent( InfoStudent info[ ] ) { /* 输入数据到数组info, 返回学生数量 */ int n = 4; char buf[ 256 ]; InfoStudent *p; for( p=info; n == 4; p++ ) { gets( buf ); /* 读入一行 */ n = sscanf( buf, “%s%ld%s%s”, p->student, &p->no, p->college, p->class ); } /* 无足够输入时终止 */ return p – info - 1; }
查找学籍数据的实现 InfoStudent *GetStudent( long no, InfoStudent info[ ], int n ) { /* 获得身份证号为 no 的学生的学籍数据 */ int i; for( i=0; i<n; i++ ) { if( info[ i ]->no == no ) return &info[ i ]; } return NULL; }
设计方法 • 功能分解 • 按照系统功能,逐步求精 • 使用函数进行功能抽象 • 模块化 • 数据抽象 • 围绕数据组织程序模块 • 数据结构 + 相关函数 • 异常处理 • 考虑各种错误的可能性 • 特别是数据输入中的各种错误
格式化输入输出的说明 • 转换说明符(scanf、printf) • 格式: %m.nd • 宽度m:用于整数和实数的输入、字符串的输入 • 精度n:用于小数部分的输出位数、字符串的输出宽度 • 返回值 • scanf: 匹配的数据项个数 • printf: 输出的字符个数 • & 表示取变量的内存单元地址 • 数组名本身是内存单元地址,不用&
其他输入输出 • 字符输入 int getchar( ) • 从标准输入读入一字符,返回ASCII值 gets( char buf[ ] ) • 从标准输入读入一行字符,返回数组名或0 • 来自字符数组的格式化输入和输出 • sscanf( char buf[ ], char fmt[ ], … ) • sprintf( char buf[ ], char fmt[ ], … )
模块化程序的文件组织 • 标准的组织方法 • 将所有结构定义、函数原型放在某个头文件中(*.h) • 将每个模块的函数定义和全局变量放在同一个源程序文件中(*.c) • 每个源程序文件中,添加 #include “头文件名” • 建立工程文件,统一管理所有源程序文件
7.3 动态数据结构与链表 • 数组的应用条件: • 必须预先确定上限 • 无法适应元素个数可变的应用需求 • 解决方案 • 采用动态数据结构 • 根据需求,在运行中申请存储空间 • 常用的动态数据结构 • 链表、树、图等
表头 数据 指针 数据 指针 数据 NULL 链表的概念 • 链表是利用指针将一组数据组成起来的数据结构: • 指针提供了查找数据的手段
学籍信息链表 struct Link { /* 链表节点结构 */ InfoStudent data; /* 学籍信息 */ struct Link *next; /* 指向下一节点 */ }; 使用特征 • 构造时,逐个建立节点和连接关系 • 访问时,从表头逐个查找 • 增加或删除数据时,仅改变指针连接关系
表头置空 读取一行 学籍信息 读成功? 返回表头 添加到 表头 例7-4:一个读入学籍信息构造链表的函数 • 函数原型 • struct Link *InputStudent(void) • 从 stdin 读数据、返回链表表头 • 数据对象 • 表头指针 p • 当前节点指针 q • 读入的学籍信息 x N Y
程序实现 struct Link *InputStudent( void ) { char buf[256]; /* 输入数据构造链表,返回表头指针 */ InfoStudent x; struct Link *p = NULL, *q; while( 1 ) { gets( buf ); /* 读取一行字符 */ if( 4 != sscanf( buf, “%s%ld%s%s”, x.student, &x.no, x.college, x.class ) ) return p; /* 无足够数据则终止 */ q = (struct Link *)malloc(sizeof(struct Link)); if( NULL == q ) return p; q->data = x; q->next = p; p = q; /* 加在表头 */ } }
例7-5:学籍管理系统 • 提供学生信息输入,信息输出和指定信息删除的功能 • 功能分解: • 命令输入(主控) • 信息输入(InputStudent) • 信息输出(OutputStudent) • 指定信息删除
命令输入 C 信息输入 C=1 信息输出 C=2 输入学号n C=3 信息删除 数据对象和主控逻辑 • 数据对象 • 一组学生信息(数量不定) • 采用链表实现(易于维护) • 表头 pHead • 命令 c • 学号 no
函数抽象的运用 • int GetCommand( void ); • 读取用户命令,返回 • struct Link *InputStudent( void ); • 负责信息输入(输入一组学籍数据,组织成链表) • void OutputStudent( struct Link *p ); • 负责信息输出(输出链表中的所有学籍数据) • struct Link *RemoveStudent( struct Link *p, char *n ); • 负责信息删除 • 删除学号 n 指定的学生信息,返回修改后的链表 • void DeleteLink( struct Link *p ); • 释放各个节点占用的空间
主控逻辑的实现 main( ) { struct Link *pHead = NULL; int c; char no[32]; while( 1 ) { c = GetCommand( ); /* 命令输入 */ switch( c ) { case 1: pHead = InputStudent( ); break; /* 信息输入 */ case 2: OutputStudent( pHead ); break; /* 信息输出 */ case 3: scanf( “%s”, no ); pHead = RemoveStudent( pHead, no ); break; /* 信息删除 */ default: DeleteLink( pHead ); return 0; /* 释放链表空间 */ } } }
命令输入的实现 int GetCommand( void ) { int c; do { printf( “Select an function as follow:\n” ); printf( “1 for Input\n” ); printf( “2 for Output\n” ); printf( “3 for Remove\n” ); printf( “0 for Exit\n” ); printf( “Input(0-3): “ ); scanf( “%d”, &c ); } while( c < 0 || c > 3 ); /* 消除非法输入 */ return c; }
信息输出(遍历链表) void OutputStudent( struct Link *p ) { while( p != NULL ) { printf( “\n%s, %ld, %s, %s”, p->data.student, /* 学号 */ p->data.no, /* 身份证号 */ p->data.college, /* 学院 */ p->data.class ); /* 班级 */ p = p->next; /* 指向下一节点 */ } }
链表的处理方法 • 元素的查找方法 • 沿着节点指针,逐个查找 • 比数组效率低 • 元素的删除方法 • 改变元素的指针连接关系 • 比数组效率高 • 元素的插入 • 选择位置,改变指针连接关系 • 比数组效率高
信息删除(链表元素的删除) struct Link *RemoveStudent( struct Link *p, /* 表头指针 */ char *n ) /* 学号 */ { struct Link *q; if( NULL == p ) return NULL; if( 0 == strcmp(p->data.student, n) ) { q = p; /* 头元素=指定学生 */ p = p->next; /* 指向其余元素 */ delete q; /* 释放头元素空间 */ } else p->next = RemoveStudent( p->next, n ); /* 在其余表中删除(递归法) */ return p; /* 修改后的链表 */ }
动态数据结构的释放 void DeleteLink( struct Link *p ) { struct Link *q; while( p != NULL ) { q = p; p = p->next; /* 指向下一节点 */ delete q; /* 释放当前节点 */ } }
动态数据结构的使用 • 应用中的问题 • 面向复杂算法和复杂数据结构 • 动态数据结构的全局性破坏信息隐蔽 • 出现专门用于组织数据的数据指针 • 如何控制复杂性 • 使用抽象手段 • 模块化、函数抽象、局部化 • 仍需要研究新的抽象手段
本章作业 • 阅读第十章 • 完成自我测验练习 10.3 10.4 • 程序设计练习 • 12.7 12.9 • 为时间(小时、分、秒)的保存设计一个结构;编写程序,读入形如 21:32:04 的输入,保存在结构中,检查输入数据的正确性。 • 修改程序例 7-3:在学籍信息中添加入学日期,修改程序中相应的部分。 • 上机题 • 修改例 7-4 中函数 InputStudent,保证链表中各个学生的信息是按照学号升序的顺序组织的 • 连同例 7-5 完成程序测试。