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

下面是题目:
不超过 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
tlliqi
Rank: 19Rank: 19Rank: 19Rank: 19Rank: 19Rank: 19
等 级:贵宾
威 望:204
帖 子:15453
专家分:65956
注 册:2006-4-27
收藏
得分:15 
第1个来学习
2014-12-28 20:28
longwu9t
Rank: 11Rank: 11Rank: 11Rank: 11
等 级:小飞侠
威 望:6
帖 子:732
专家分:2468
注 册:2014-10-9
收藏
得分:15 
好吧 我也来凑个热闹 随便拿点分

Only the Code Tells the Truth             K.I.S.S
2014-12-28 22:25
liu229118351
Rank: 3Rank: 3
等 级:论坛游侠
帖 子:83
专家分:101
注 册:2013-10-23
收藏
得分:15 
排队来学习、!

单曲循环,需要信心+耐心+恒心
2014-12-29 09:19
comewest
Rank: 5Rank: 5
等 级:职业侠客
威 望:1
帖 子:74
专家分:335
注 册:2014-12-3
收藏
得分:15 
目的同上
2014-12-29 14:00
诸葛欧阳
Rank: 19Rank: 19Rank: 19Rank: 19Rank: 19Rank: 19
来 自:流年
等 级:贵宾
威 望:82
帖 子:2790
专家分:14619
注 册:2014-10-16
收藏
得分:15 
多谢楼主教导,向楼主学习。

一片落叶掉进了回忆的流年。
2014-12-29 18:52
快速回复:【解剖麻雀】通过一道小型课题解答一些常见问题
数据加载中...
 
   



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

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