| 网站首页 | 业界新闻 | 小组 | 威客 | 人才 | 下载频道 | 博客 | 代码贴 | 在线编程 | 编程论坛
欢迎加入我们,一同切磋技术
用户名:   
 
密 码:  
共有 4543 人关注过本帖, 6 人收藏
标题:【解剖麻雀】通过一道小型课题解答一些常见问题
取消只看楼主 加入收藏
TonyDeng
Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20
等 级:贵宾
威 望:304
帖 子:25859
专家分:48889
注 册:2011-6-22
结帖率:100%
收藏(6)
已结贴  问题点数:100 回复次数:15 
【解剖麻雀】通过一道小型课题解答一些常见问题
这道课题,是论坛上一个学生的作业,比较典型,通过这道题目,实际上很多编程中常见的问题,都会遇见,所以不妨在这里集中讲解一下。

下面是题目:
不超过 100 位同学的信息存放在 ASCII 文件StudentInfo.txt 中;不超过10 个学院的信息存放在CodeInfo.txt 中,性
别代码存放在 SexInfo.txt 中, 均为代码和其对应的名称。
要求:
  1. 定义至少包含以上学生信息和学院代码的结构体类型和指针(或数组)变量。
  2. 函数实现从文件中输入信息到定义的数据中
  3. 函数实现排序(1): 学生平均成绩的降序排序,并输出所有信息到显示屏。
  4. 函数实现排序(2) :学生姓名的升序排序,并输出所有信息到显示器。
  5. 函数实现查询(1) :根据学号查询学生信息,并输出该生信息,并输出所有信息到显示器

CodeInfo.txt的内容:
程序代码:
1 信息学院                                
2 计算机学院
3 文法学院
4 外国语学院
5 数理学院
6 会金学院
7 化工学院
8 商学院
9 航空学院
10 艺术学院


SexInfo.txt的内容:
01


StudentInfo.txt的内容:
程序代码:
140510 洪旅玻 0 5 25 43 64 96 89 38 16 28 48 93
140207 汪桓矶 1 2 98 68 67 62 84 60 79 86 63 86
140908 钮达浚 1 9 72 69 70 68 72 83 78 66 84 99
140501 邱嶙解 1 5 66 96 76 63 72 79 100 99 96 95
140202 叶建林 1 2 71 97 76 92 76 94 84 63 64 87
140401 巴隆九 1 4 60 89 94 70 86 61 95 75 87 64
140901 俞法复 1 9 68 97 87 64 76 73 87 97 83 92
140609 唐寒丛 0 6 90 61 70 94 75 79 93 67 82 91
140110 班慷刚 1 1 53 81 89 64 46 96 76 2 22 82
140607 鄂宽佼 1 6 63 94 97 60 70 77 73 62 96 75
140605 施俭倍 1 6 68 77 95 97 72 79 67 76 63 79
140910 水奔横 0 9 95 89 48 76 100 81 12 1 85 88
140109 水昌瀑 1 1 99 18 21 74 82 25 32 85 24 92
140703 潘弼宽 1 7 60 80 79 87 73 77 61 92 91 82
140310 邢端地 1 3 91 3 40 71 60 10 67 60 27 73
140909 周桓环 1 9 19 30 21 47 20 99 65 22 3 61
140603 柳钽钩 1 6 86 99 82 66 76 88 83 62 79 71
140206 宓强进 1 2 79 87 81 96 66 83 67 75 75 95
140105 毕镔百 1 1 94 60 92 97 96 62 64 87 89 87
140601 麻俚历 1 6 65 77 64 78 92 95 72 70 84 93
140406 黎辽利 1 4 76 74 93 71 74 61 82 99 96 81
140902 嵇灿处 1 9 91 96 85 82 69 83 79 74 82 94
140504 诸秉栊 1 5 71 80 60 94 68 87 93 72 90 90
140101 荣国宏 1 1 69 80 84 88 69 69 92 74 95 89
140103 钱复阜 1 1 66 82 76 66 65 91 95 79 65 67
140205 雷留狄 1 2 84 80 92 90 84 83 97 69 60 68
140010 吴祷举 0 0 100 98 93 95 88 77 99 72 85 99
140404 解琥俚 1 4 77 95 88 100 82 99 91 74 74 71
140005 裴桓价 1 0 63 65 79 68 74 63 78 76 87 65
140307 苏胞按 1 3 64 84 69 88 87 83 66 71 72 69
140405 阮笃璀 1 4 79 93 90 61 81 88 91 73 75 78
140805 司径晋 1 8 82 85 95 68 89 70 96 84 88 90
140608 乔恳火 1 6 80 75 91 66 74 96 75 61 94 61
140705 褚磊并 1 7 87 69 93 83 61 86 87 62 80 62
140802 董得管 1 8 88 74 92 99 94 88 68 63 70 75
140604 祁浜复 1 6 64 80 71 91 99 93 95 63 74 98
140801 俞筹浩 1 8 75 79 70 90 100 73 68 65 73 97
140903 张积京 1 9 78 65 77 82 98 64 77 64 94 94
140502 伍煅居 1 5 99 93 88 72 66 84 65 70 64 63
140106 童俭棰 1 1 92 60 65 87 82 95 70 80 66 65
140602 范斐畴 1 6 89 82 88 79 90 100 71 60 75 85
140806 齐晁笃 1 8 91 91 88 85 93 88 64 67 97 76
140905 嵇鸿连 1 9 88 98 61 66 97 65 97 88 66 92
140204 龚贵归 1 2 85 91 87 73 100 94 93 98 96 65
140807 米大淋 1 8 67 70 86 66 70 99 80 66 71 82
140104 陈慷沥 1 1 70 65 83 95 96 73 83 83 67 85
140906 贲琮看 1 9 83 61 81 74 91 68 87 97 60 68
140402 苏嘉瀚 1 4 73 77 85 82 95 94 77 89 94 76
140209 酆谅卢 0 2 24 56 1 62 17 17 11 15 13 44
140409 甄宽观 0 4 74 33 91 38 25 51 14 10 86 60
140506 班曝临 0 5 77 99 67 84 61 82 74 70 86 87
140001 杭登陵 1 0 65 62 76 75 97 82 79 64 65 68
140810 王理磷 1 8 89 45 27 60 88 34 89 34 50 0
140809 花陵阜 0 8 82 99 77 96 82 93 82 88 66 75
140610 单材洪 1 6 78 46 36 67 76 75 3 35 81 57
140706 裘顾昂 1 7 86 79 71 64 62 100 71 96 100 90
140308 焦科皓 0 3 64 77 69 83 63 71 89 93 72 100
140002 范藩尖 1 0 63 63 61 66 93 73 61 88 82 66
140007 山按率 0 0 95 74 89 75 90 82 97 94 67 61
140309 米赋皎 0 3 83 34 44 13 86 36 95 86 70 57
140904 邓进灏 1 9 61 92 63 65 76 97 90 63 86 76
140305 马绸岗 1 3 79 78 80 95 95 63 76 87 98 81
140201 郭琛矿 1 2 65 99 64 87 100 61 88 99 92 100
140503 和策近 1 5 69 90 80 78 86 65 95 94 97 93
140704 许焕广 1 7 89 86 62 76 64 100 80 79 96 73
140403 邬贵键 1 4 68 69 76 71 72 93 65 70 89 85
140009 芮瀚火 1 0 80 97 89 76 97 71 94 73 84 65
140210 蔡峦彻 0 2 27 75 80 77 76 25 82 100 16 1
140107 周律枫 0 1 43 69 58 98 72 30 49 66 87 67
140304 仇晖介 1 3 81 100 67 87 94 81 95 91 70 74
140302 郝矶里 1 3 67 83 74 85 91 82 73 98 83 91
140407 柏洹博 0 4 86 97 97 80 95 74 83 81 87 86
140507 祝奎斑 1 5 74 100 89 100 70 91 91 82 96 84
140303 马觉杠 1 3 69 94 73 82 68 63 90 97 94 97
140410 雷留君 0 4 7 63 14 82 85 89 5 53 23 72
140301 巴廖甘 1 3 74 72 82 76 78 73 65 76 68 90
140707 邹荆辟 1 7 88 80 72 78 62 66 92 97 65 74
140004 米川杰 1 0 97 76 61 73 77 62 79 86 90 100
140306 孙炮恒 0 3 91 84 95 77 72 99 72 80 79 82
140108 屈霭季 1 1 48 96 6 10 35 54 0 27 47 39
140710 计家诀 1 7 92 89 91 96 95 93 89 61 66 97
140208 戴财利 1 2 78 43 77 21 100 29 16 20 40 82
140708 赖钧瀚 1 7 95 68 96 85 79 68 76 82 79 98
140907 成技檗 1 9 71 69 63 83 100 75 97 100 89 84
140003 博朗百 1 0 83 97 75 86 91 77 77 78 73 61
140509 胥勃雕 1 5 9 37 35 5 27 93 94 73 3 60
140701 荣君锤 1 7 87 60 79 75 71 66 68 96 100 78


搜索更多相关主题的帖子: 结构体 显示屏 信息 学院 
2014-12-28 15:38
TonyDeng
Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20
等 级:贵宾
威 望:304
帖 子:25859
专家分:48889
注 册:2011-6-22
收藏
得分:0 
首先审题,阅读要求的5点内容,第3、4、5很明显,我们需要一个菜单,让程序分别输出对应的结果。所以,先做一个菜单,让程序跑起来,这是基本的框架。要做一个简单的菜单,只要把各个选择逐行输出到屏幕,然后通过键盘接收选择就可以了,下面是一个实现:

先写一个菜单数组:
程序代码:
const char* menu[] =                        // 主菜单
{
    "0.结束程序",
    "1.按平均成绩降序排列的学生表",
    "2.按姓名升序排列的学生表",
    "3.按学号查询学生信息",
    NULL
};

这是一个指针数组,menu是数组名,[]表示不定数目,具体的数目由编译器在编译时计算出来。char*是指针声明,表明menu数组的每个元素均是一个指向char类型的指针。const是“常量”的意思,放在前面,表示menu数组每个元素(指针)的内容不可修改,因为菜单的提示信息的确是不需要修改的,所以按照逻辑需求,事先向编译器通告:在往后的编译过程中,如果你发现我的代码有试图修改这些数据的行为,不能允许,向我发出警告。后面是数组初始化,对不定长的数组,都需要这样在定义时初始化,这些数据是常量字符串,编译后是存储在常量数据区的,真的不能修改,这与事先声明const的意图一致——其实编译器不需要我们显式写出const也是按const编译的,不过我们要提醒自己和读者到底在写什么,所以明确写出来,这不是多余的代码,而是为着清晰性和可读性而故意写的。最后的NULL指针,是我们仿照字符串的构造方式,表明数组在这里结束,它相当于字符串中的'\0'字符,这是为不用在外部宣告数组尺寸而设计的方案。

下面是菜单实现的代码:
程序代码:
// 显示菜单
size_t ShowMenu(const char* menu[])
{
    size_t index;

    putchar('\n');
    for (index = 0; menu[index] != NULL; ++index)
    {
        printf_s("%s\n", menu[index]);
    }
    printf_s("\n请选择: ");

    return index;
}

// 菜单选择
int MenuChoice(const char* menu[])
{
    int choice, count;

    do
    {
        count = ShowMenu(menu);
        _flushall();
    } while ((scanf_s("%d", &choice) != 1) || (choice < 0) || (choice > count));

    return choice;
}

留意代码中的for()循环,结束条件是menu[index] != NULL,这就是查询数组数据,到发现元素的指针是空指针时,就结束循环了——回忆一下我们是怎么处理字符串的,两相对照,可以互相加深理解。_flushall()是非标准库函数,与具体的实现环境有关,在这里,是MS-C在DOS/Windows下的实现方案(凡是以下划线开头的函数,都是这样,这是微软的命名约定),这个函数的作用是清除程序当前打开的所有流(包括输入流和输出流)的缓冲区,用于排除scanf()输入了不期望的数据而堵塞流系统的问题(亦即很多人常说的吸收残留数据,不过那些方案没有一个是真正可以应付所有出错情形的,这个是最彻底的解决方案)。scanf()函数的返回值是成功读取的数据项数目,由于只有一个%d,即希望读入1个整数,所以只要返回值不是1,就必定是读入出错,而只有读入成功,||运算才会依次检查后面choice的范围是否在允许范围内,否则重新要求用户输入选择。在运行测试时,我们可以用各种各样的输入来攻击这个函数,用负值、用字符按键、输入一大串之后再按回车、不输入按回车等等,看看程序是否能够受得住,以及画面是否难看,再斟酌将来需要怎样的变动。

这个菜单的方案,是返回菜单的序数,用户输入的其实也是序数,所以安排0-结束项在第一行,因为它的序数就是零。如果需要用任意的按键响应选择,那么需要另外写一个实现方案,用到结构体。揣摩一下我拆分函数的意图,当我们真的需要变动菜单方案,在主函数main()中的调用代码,是不需要太多修改的,甚至连修改都不需要,改一下数据结构和菜单的实现代码即可,这样,就是所谓的“接口”(interface,接口的意思,亦称界面)——写程序的关键是接口直观和相对固定

主程序我们这样写:
程序代码:
#include <stdio.h>
#include <stdlib.h>

// 程序主入口
int main(int argc, char* argv[])
{
    int choice;
    while ((choice = MenuChoice(menu)) != 0)
    {
        switch (choice)
        {
            case 1:
                break;
            case 2:
                break;
            case 3:
                break;
            default:
                break;
        }
    }

    return EXIT_SUCCESS;
}

EXIT_SUCCESS是stdlib.h头定义的宏,其实就是0。

下面是运行画面:

图片附件: 游客没有浏览图片的权限,请 登录注册


[ 本帖最后由 TonyDeng 于 2014-12-28 20:04 编辑 ]

授人以渔,不授人以鱼。
2014-12-28 16:11
TonyDeng
Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20
等 级:贵宾
威 望:304
帖 子:25859
专家分:48889
注 册:2011-6-22
收藏
得分:0 
现在处理要求的第1点。逐个处理,先挑一个相对简单的练手,就选CodeInfo.txt,这是一个院系编码表,亦即数据库表的一条记录,它有2个字段,编码和院系名称,所以,我们构造如下数据类型:
程序代码:
// 学院数据结构
struct CollegeItem
{
    int  Code;          // 编码
    char Name[51];      // 名称
};


以上只是一笔记录的数据,而学校有多个院系,那么需要一个数组。按题目叙述,“不超过10个学院”,这是暗示可以用数组处理的,暂时不需要用链表。因此,我们声明程序将使用如下数组:
程序代码:
// 学院数据表
struct CollegeInfo
{
    size_t      Count;
    CollegeItem Data[10];
};
extern CollegeInfo Colleges;

extern是声明,不是定义,它的作用是告诉编译器:你往后将会遇到一个叫Colleges的变量,那么它是一个类型为CollegeInfo的类型的数据,具体的定义在别处给出,这里不给(实际上它的定义在main()函数中给出,存放在test.cpp文件中)。数据结构CollegeInfo其实是一个一维数组,真正的元素是CollegeItem类型的数据(命名为Data的字段),这个数组的元素最大数目是10,但是真正有多少,由Count的值告诉你。这次,不是使用类字符串的结束标志方案了(这是BASIC类语言的字符串构造方案),省去检索元素数目的循环,提高效率。

下面是最终的头文件,命名为School.h:
程序代码:
#pragma once

// 学院数据结构
struct CollegeItem
{
    int  Code;          // 编码
    char Name[51];      // 名称
};

// 学院数据表
struct CollegeInfo
{
    size_t      Count;
    CollegeItem Data[10];
};
extern CollegeInfo Colleges;

// 从磁盘文件载入学院数据
bool LoadColleges(const char* fileName, CollegeInfo* colleges);

// 列出学院明细清单
void ListColleges(const CollegeInfo* colleges);


#pragma once是vs-C/C++的预编译指令,它告诉预编译处理器:这个头文件,只需要包含一次,如果有多个.cpp源代码文件#include这个头,你不要重复包含。这个处理的传统手法,是用一个宏,类似下面这样:
程序代码:
#ifndef __SCHOOL__
#define __SCHOOL__

//头内容

#endif

亦即如果没有发现__SCHOOL__这个宏已定义,那么表明这个头文件是第一次被处理,这样,就可以包含它了,然后定义了__SCHOOL__头,则下次预编译器再读到这个头文件,就会发现__SCHOOL__宏已经存在了,则会忽略处理动作,跳开重复定义数据结构和变量声明等禁忌代码。这个传统手法,在vs-C/C++中用#pragma once代替(once是只处理一次的意思),当然,你也可以用回传统的方案,一样的。

这个头文件,同时声明了两个函数,将会被别的.cpp模块使用,其具体实现代码在School.cpp文件中(绝不是在.h中写实现代码,这是论坛上很多人常犯的错误)。这就是类似printf()的声明可以在stdio.h中被我们看到,但看不到真正的实现代码。头文件.h相当于书籍的目录,内容在书内,把目录页撕下来给你,就是.h文件,内容在.cpp部分中。这是C/C++语言把接口(.h)和实现(.cpp)分开的独特方案,前者公开可见,但后者是隐藏保护作者权益的(C#、Java等不用分开头文件和实现文件),两者合并,叫“头”。完整的是头,单有.h是没用的——printf()的实现代码在.lib或.DLL中,是事先编译好的,供链接和执行用,不可能让我们看到,所以,不要再有诸如到哪里找到某某.h这样的说法,你找到也用不着。同样的,如果我要保护自己的源代码,就会不公开School.cpp,但你可以看到School.h,拥有目录,但不知道具体说什么。


[ 本帖最后由 TonyDeng 于 2014-12-28 20:11 编辑 ]

授人以渔,不授人以鱼。
2014-12-28 17:06
TonyDeng
Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20
等 级:贵宾
威 望:304
帖 子:25859
专家分:48889
注 册:2011-6-22
收藏
得分:0 
下面是School.cpp的代码:
程序代码:
#include <Windows.h>
#include <stdio.h>
#include "MyTools.h"
#include "School.h"

// 从磁盘文件载入学院数据
bool LoadColleges(const char* fileName, CollegeInfo* colleges)
{
    FILE* file;

    if (fopen_s(&file, fileName, "rt") != 0)
    {
        HLOCAL message = GetSystemErrorMessageA(GetLastError());
        if (message != NULL)
        {
            printf_s("文件%s无法打开: %s\n", fileName, message);
            LocalFree(message);
        }
        return false;
    }

    for (size_t index = 0; ; ++index)
    {
        if (fscanf_s(file, "%d", &(colleges->Data[index].Code)) != 1)
        {
            break;
        }
        if (fscanf_s(file, "%s\n", colleges->Data[index].Name, _countof(colleges->Data[index].Name)) != 1)
        {
            break;
        }
        ++(colleges->Count);
    }

    fclose(file);

    return true;
}

// 列出学院明细清单
void ListColleges(const CollegeInfo* colleges)
{
    for (size_t index = 0; index < colleges->Count; ++index)
    {
        printf_s("%d, %s\n", colleges->Data[index].Code, colleges->Data[index].Name);
    }
}


这些代码反而比较简单,联系前面介绍数据结构时的想法,就知道每行代码的意图。由于需要反馈为什么无法打开文件的原因(这种原因千变万化),这里使用了一个Windows API,它的作用是根据错误码返回具体的文字信息,而且这些文字是中文的,故需要包含Windows.h头文件,使用到Kernel32.DLL或需要与Kernel32.LIB链接,那个API我封装在MyTools头中,先不要管它的细节,只用就好了。


[ 本帖最后由 TonyDeng 于 2014-12-28 17:34 编辑 ]

授人以渔,不授人以鱼。
2014-12-28 17:32
TonyDeng
Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20
等 级:贵宾
威 望:304
帖 子:25859
专家分:48889
注 册:2011-6-22
收藏
得分:0 
现在详细讲解一下School.cpp代码中较常见的问题。

fopen_s()函数,是ms-C/C++推荐取代fopen()的版本。通常的fopen()函数,是返回打开文件的句柄(FILE*指针),以NULL表示打开失败,但这种处理方法,无法反馈文件为什么打开失败,因为无处输出错误代码,而C/C++标准又没说fopen()函数执行之后应该提取系统错误码。fopen_s()就是为了修正这个缺陷,它返回的值是error_t类型,是一种错误编码,直接告诉调用者出错的原因,当返回值为零时即是零错误,成功,不是C/C++传统的零为逻辑假,相反,在Windows API中零往往是成功的值,包括语言标准自己规定的main()函数返回值零为成功也是这样)。fopen_s()函数的实现,同时也在执行后对系统错误数据区置值,执行函数后马上调用GetLastError(),所获得的值与fopen_s()的返回值是一样的,所以,这里其实可以不必调用GetLastError(),明白了道理,才可以修改代码。

fopen_s()的参数,是以指针传参的方式返回文件句柄,故调用格式是fopen_s(&file)这样,file是FILE*指针,为了返回这个指针,必须传入指针的指针!查看fopen_s()函数的原型,FILE**就是这个意思,以后看到类似的参数声明,应该知道怎么用了。

根据题目给出的数据,CodeInfo.txt是文件文件,不是二进制数据,所以打开文件的方式应是“读文本”,r是read,t是text。在文本文件中,读取数据就如我们平常的键盘上输入数据一样,实际上C/C++的控制台输入输出流也是以文件的形式实现的,键盘的文件句柄是stdin,屏幕的文件句柄是stdout,从键盘读数据,等于从文件stdin读数据,所以我们看到,代码中读文件的方法,与键盘输入完全一样。但有一个特殊的地方,从键盘读数,使用scanf(),格式控制字符串不需要写回车符'\n',但从文本文件读数,却需要写出'\n',因为文件中确实存在一个\n符让函数去读,否则会出错的。

读入磁盘数据的时候,只要每读入成功一笔记录,就在数据结构中给数组长度加1,同理,删除数据要减,这种结构伸缩数组是非常方便的,不会丢失数据。这是自己维护的,其换取的优势是数组处理的速度加快,不用再循环搜寻结束标志了,也不需要——看看strlen()的实现代码就知道,那是一个循环,不要以为那不浪费CPU时间,当字符串很长的时候,这种查询消耗积累下来是很可观的,这也是C/C++的字符串处理速度效率那么低的原因(它只是整数的处理效率高而已)。当然,明白了这些方案的各自特点,就可以根据实际情况自己选择了,我这里特意给了各种不同的方案,并加以解释,并不是说非要这样不可。

最后,看看我fscanf_s()分2行读2个数是什么意思?肯定有人说,这样很笨,有更简短的写法。是的,的确有,但我仍然要这样写,为什么呢?这里先卖个关子。


[ 本帖最后由 TonyDeng 于 2014-12-28 20:17 编辑 ]

授人以渔,不授人以鱼。
2014-12-28 18:16
TonyDeng
Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20
等 级:贵宾
威 望:304
帖 子:25859
专家分:48889
注 册:2011-6-22
收藏
得分:0 
以下是引用c语言总虐我在2014-12-30 00:28:57的发言:

我是第一个看到的~~~
Q-Q

这是连载。你已经有现成可运行的全部源代码和项目文件,可以运行对照着看,也可以尝试修改测试。有什么问题,可以在这里问。还有你隐藏了的几个需求,也可以发出来思考一下怎么做,看我是不是已经留下了实现的途径。

授人以渔,不授人以鱼。
2014-12-30 16:29
TonyDeng
Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20
等 级:贵宾
威 望:304
帖 子:25859
专家分:48889
注 册:2011-6-22
收藏
得分:0 
现在继续看学生模块是怎么实现的。这个模块,分两个文件,分别是头(.h)和实现部分(.cpp),具体代码如下:

Student.h
程序代码:
#pragma once

// 学生数据结构
struct StudentItem
{
    char   Code[7];         // 编码
    char   Name[21];        // 姓名
    int    Sex;             // 性别
    int    SchoolCode;      // 学院编码
    double Scores[10];      // 课程成绩
};

// 学生数据表结构
struct StudentInfo
{
    size_t Count;
    StudentItem Data[100];
};
extern StudentInfo Students;

// 从磁盘文件载入性别编码方案
bool LoadSex(const char* fileName);

// 从磁盘文件载入学生数据
bool LoadStudents(const char* fileName, StudentInfo* students);

// 列出学生明细清单
void ListStudents(const StudentInfo* students, const char* outputFileName, bool outputAverage);

// 按姓名排序
void StudentsSortByName(StudentInfo* students);

// 按平均成绩排序
void StudentsSortByScore(StudentInfo* students);

 
Student.cpp
程序代码:
#include <Windows.h>
#include <stdio.h>
#include <string.h>
#include "MyTools.h"
#include "School.h"
#include "Student.h"

// 性别数据结构及数据
struct SexItem
{
    int  Code;          // 编码
    char Name[3];       // 名称
} Sex[2];

extern CollegeInfo Colleges;

// 从磁盘文件载入性别编码方案
bool LoadSex(const char* fileName)
{
    FILE* file;

    if (fopen_s(&file, fileName, "rt") != 0)
    {
        HLOCAL message = GetSystemErrorMessageA(GetLastError());
        if (message != NULL)
        {
            printf_s("文件%s无法打开: %s\n", fileName, message);
            LocalFree(message);
        }
        return false;
    }

    for (size_t index = 0; (index < _countof(Sex)) && (fscanf_s(file, "%d %s\n", &(Sex[index].Code), Sex[index].Name, _countof(Sex[index].Name))) == 2; ++index)
    {
        ;
    }

    fclose(file);

    return true;
};

// 从磁盘文件载入学生数据
bool LoadStudents(const char* fileName, StudentInfo* students)
{
    FILE* file;

    if (fopen_s(&file, fileName, "rt") != 0)
    {
        HLOCAL message = GetSystemErrorMessageA(GetLastError());
        if (message != NULL)
        {
            printf_s("文件%s无法打开: %s\n", fileName, message);
            LocalFree(message);
        }
        return false;
    }

    for (size_t index = 0; ; ++index)
    {
        if (fscanf_s(file, "%s", students->Data[index].Code, _countof(students->Data[index].Code)) != 1)
        {
            break;
        }
        if (fscanf_s(file, "%s", students->Data[index].Name, _countof(students->Data[index].Name)) != 1)
        {
            break;
        }
        if (fscanf_s(file, "%d", &(students->Data[index].Sex)) != 1)
        {
            break;
        }
        if (fscanf_s(file, "%d", &(students->Data[index].SchoolCode)) != 1)
        {
            break;
        }
        bool success = true;
        for (size_t subjectIndex = 0; subjectIndex < _countof(students->Data[index].Scores); ++subjectIndex)
        {
            if (fscanf_s(file, "%lf", &(students->Data[index].Scores[subjectIndex])) != 1)
            {
                success = false;
                break;
            }
        }
        if (!success)
        {
            break;
        }
        ++(students->Count);
    }

    fclose(file);

    return true;
}

// 求指定学生的平均成绩
double AverageScore(const StudentItem* student)
{
    double total = 0.0;
    for (size_t index = 0; index < _countof(student->Scores); ++index)
    {
        total += student->Scores[index];
    }

    return total / _countof(student->Scores);
}

// 列出学生明细清单
void ListStudents(const StudentInfo* students, const char* outputFileName, bool outputAverage)
{
    FILE* file = stdout;        // 默认向控制台标准设备输出结果

    if (outputFileName != NULL)
    {
        if (fopen_s(&file, outputFileName, "wt") != 0)
        {
            HLOCAL message = GetSystemErrorMessageA(GetLastError());
            if (message != NULL)
            {
                printf_s("输出文件%s无法建立: %s,改向标准设备输出.\n", outputFileName, message);
                LocalFree(message);
                file = stdout;
            }
        }
    }

    for (size_t index = 0; index < students->Count; ++index)
    {
        fprintf_s(file, "%s, ", students->Data[index].Code);
        fprintf_s(file, "%s, ", students->Data[index].Name);
        fprintf_s(file, "%s, ", Sex[students->Data[index].Sex].Name);
        fprintf_s(file, "%s, ", Colleges.Data[students->Data[index].SchoolCode - 1].Name);     // 注:这里假定学院数据以编码与数组下标挂钩,否则应编写检索函数
        for (size_t subjectIndex = 0; subjectIndex < _countof(students->Data[index].Scores); ++subjectIndex)
        {
            fprintf_s(file, "%.0f ", students->Data[index].Scores[subjectIndex]);
        }
        if (outputAverage)
        {
            fprintf_s(file, "[%6.2f]\n", AverageScore(&(students->Data[index])));
        }
        else
        {
            fputc('\n', file);
        }
    }

    if (file != stdout)
    {
        fclose(file);
    }
}

// 复制一个学生数据
StudentItem CopyStudent(StudentItem* source)
{
    StudentItem target;

    strcpy_s(target.Code, source->Code);
    strcpy_s(target.Name, source->Name);
    target.Sex = source->Sex;
    target.SchoolCode = source->SchoolCode;
    memcpy_s(target.Scores, sizeof(target.Scores), source->Scores, sizeof(source->Scores));

    return target;
}

// 交换两个学生的变量内容
void SwapStudent(StudentItem* s1, StudentItem* s2)
{
    StudentItem temp = CopyStudent(s2);
    *s2 = CopyStudent(s1);
    *s1 = CopyStudent(&temp);
}

// 按姓名排序
void StudentsSortByName(StudentInfo* students)
{
    // 用冒泡法进行排序
    for (size_t i = 0; i < students->Count - 1; ++i)
    {
        for (size_t j = i; j < students->Count; ++j)
        {
            if (strcmp(students->Data[i].Name, students->Data[j].Name) < 0)     // 升序
            {
                SwapStudent(&(students->Data[i]), &(students->Data[j]));
            }
        }
    }
}

// 按平均成绩排序
void StudentsSortByScore(StudentInfo* students)
{
    // 用冒泡法进行排序
    for (size_t i = 0; i < students->Count - 1; ++i)
    {
        for (size_t j = i; j < students->Count; ++j)
        {
            if (AverageScore(&(students->Data[i])) > AverageScore(&(students->Data[j])))    // 降序
            {
                SwapStudent(&(students->Data[i]), &(students->Data[j]));
            }
        }
    }
}




[ 本帖最后由 TonyDeng 于 2014-12-30 17:55 编辑 ]

授人以渔,不授人以鱼。
2014-12-30 17:54
TonyDeng
Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20
等 级:贵宾
威 望:304
帖 子:25859
专家分:48889
注 册:2011-6-22
收藏
得分:0 
Student的这个程序的核心模块,我慢慢分析。数据结构的问题,前面在School模块中已经讲了,两者是完全一样的结构和用法,不再重复。

现在先讲结构体数组的排序问题,正如我们平常做练习那样,排序首要是数据的复制,普通内置数据类型的复制很简单,用=算符就可以了,但结构体与普通的内置数据类型不同(在C中它是一块复合数据区,在C++中则不单是数据区,还包括函数和方法代码段,实际上的C++的类就是从C的结构体发展出来的,原本命名为“带类的C”,C++的关键词class和struct实际上没有区别,只是在默认的访问可见性上相反而已,两者是同义词),所以对这样的数据/代码区,存在隐藏字段,是不能简单用memcpy()这类函数复制的。在C和C++的编程规范中,以及C标准的解释者,都告诫我们不要试图一揽子复制对象,原因也在这里。对象(结构体就是对象的一种)的复制,必须逐个字段复制,就如字符串的复制必须逐个字符拷贝一样!

所以,看看14楼的代码实现,我专门写一个复制结构体的函数,就是逐个字段复制的,这繁琐动作不能省。之所以把这个功能拆分为函数,除了这个功能要大量使用之外,还因为要给面向对象编程思想作埋伏,这种复制函数,在C++中被写成运算符重载函数,代码就是这样的。在面向过程的编程之中,完全可以按面向对象的思想组织代码,就是这种方式。只要你习惯了这种拆分函数的思维,将来迁移到面向对象的语言时,会觉得顺理成章,否则很难扭转过来,从而抵触面向对象思维,殊不知两者其实是可以不冲突的。


授人以渔,不授人以鱼。
2014-12-30 18:46
TonyDeng
Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20
等 级:贵宾
威 望:304
帖 子:25859
专家分:48889
注 册:2011-6-22
收藏
得分:0 
现在谈一谈代码的可读性和自说明的问题。

留意一下14楼的代码,我专门写了一个交换数据的函数SwapStudent()。这也是我们平时写过的东西了,原理都懂得,也都会写,但我极少看见有人为此提炼为一个函数的,都是内嵌在逻辑代码之中,搅乱阅读者的大脑:本来读一个冒泡排序的算法代码,大脑就是要反映一个数据交换的步骤,这下好,一到这里,就跳出另外一个算法,让你思考一番才看出那几行或十几行是干什么的,就算不干扰、不打断原本的思路,这展开了的代码也把函数体拉长,让人未看先怕。把交换数据这一个简单动作提取出来,起一个自我说明干什么的名字,阅读者就不用再看具体如何交换的,除非他怀疑你的交换代码写错了,才有必要去看,否则你假定他没错就可以了,集中注意力于当前代码段的逻辑。swap就是交换的意思,这就是代码的自说明,不用像很多人那样,到处是密密麻麻的注释。基本的英文还是要懂的。

你看我两个冒泡排序的算法,一眼看下去,都是非常典型和一致的,除了比较行不同,其余完全相同。从这里也可以看到某种抽象的东西,看得出,你就可能会想更进一步,排序函数只写一个行不行?你有这种意识,就明白C++的模板和C#的泛型到底是为何而来;搞不明白,就一辈子重复劳动永远敲大同小异的代码。当我们要修改交换算法的时候,只要改swap()函数的具体实现就可以了,逻辑代码是根本不用碰的,想象一下你把那3行代码嵌入到这两个排序代码中,将来要修改得花多少功夫?更烦的是你未必找得全所有用到这些代码的地方!我这里是一层一层的,除了交换函数,还提取了数据实体复制函数,在交换代码中,也是一眼看出是在从事复制动作。事实上,这两个函数,稍微分析一下就知道,必定是经常用的,所以在我写的时候,根本不用想,首先就写出这些性质的函数,不管当前用不用得着,先写了出来,必定有用得着的时候!

授人以渔,不授人以鱼。
2014-12-30 19:20
TonyDeng
Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20
等 级:贵宾
威 望:304
帖 子:25859
专家分:48889
注 册:2011-6-22
收藏
得分:0 
你可以先不理会理论,运行了看效果,然后尝试修改。

授人以渔,不授人以鱼。
2015-01-03 10:00
快速回复:【解剖麻雀】通过一道小型课题解答一些常见问题
数据加载中...
 
   



关于我们 | 广告合作 | 编程中国 | 清除Cookies | TOP | 手机版

编程中国 版权所有,并保留所有权利。
Powered by Discuz, Processed in 0.034046 second(s), 9 queries.
Copyright©2004-2024, BCCN.NET, All Rights Reserved