宮商定時制計算部

計算部日記180813

今回の更新は、『計算部日記180723』の続きになります。

リスト構造――可変にできるものは全部可変にしてみよう!(承前)
5. いろいろ実験(続き)
成績処理プログラム――評定平均値
次は、学校らしく、成績処理のプログラムを書いてみましょう。
前の日記で《データベースもどき》と書いたのは、実はこの課題のことを指していたのです。
完成形はこのページの下の方にありますが、ご覧になれば分かるように、別にリストを使わなくてもできそうなものです。
しかし、それを言っていたら、部活ではいつまで経ってもリスト構造を書く機会が訪れそうにないので、あえてリスト構造を多用して書いています。
ただし、何クラスもダミーデータを作るのは大変なので、ここでは単独クラスのみ、本校定時制商業科4年生クラスの4年分の成績を扱うことにします。
現在の商業科4年生のカリキュラムに基づいて、架空の生徒・成績のcsvファイルを作成します。
★本校定時制では、名簿は女子が先となっていますので、ここでもそれに合わせます。
当然のことながら、このファイルは、生徒名・成績とも「完全に」架空のものです。
この手のダミーデータを用意するとき、「自然な(=いかにも現実にありそうな)」データを作り上げるのは容易なことではありません。
それでもここでは、ランダム関数で作った数値を適当にばらまくようなことはせず、一人一人、一科目ずつ「あり得そうな評価」を手動で入力し、極端に不自然にならないよう努めました。
これは、想定生徒数が10人という少なさだからこそできることで、全日制のように一クラスに40人もいるという想定だったらとてもできません。
★下の方の成績一覧表完成画像を見て、「1年生ではオール5だったのに2年生以降は平凡な成績になっている生徒や、1年生の数学が赤点スレスレなのに2年生の数学ではいきなり5になる生徒がいる。こういうのは不自然ではないのか?」という指摘をなさる方がいらっしゃるかもしれません。
しかし、前者は「2年生から家計のためのアルバイトを始めて忙しくなった生徒」、後者は「学力は申し分ないのに、何らかの事情で試験を受けないことがある生徒」という設定で考えたもので、現場の人間から見ればそれほど不自然なものではないのです。
何でもそうですが、「事実は小説よりも奇なり」で、現実は想像を超えるような結果になることが多いものです。
//seitomeibo.csv。左から「生徒コード・氏名」。
1001,岡 胡桃
1002,川合 睦美
1003,武井 康代
1004,西村 瑠璃
1005,藤川 小梅
1006,森本 双葉
1007,渡部 泉
1008,井本 秀樹
1009,小沼 正人
1010,重田 年昭

//kamokuhyo.csv。左から「科目コード・科目名・履修学年」。
20101,国語総合,1
20301,現代社会,1
20401,数学Ⅰ,1
(中略)
21108,財務会計Ⅰ,4
21109,経済活動と法,4
21110,課題研究,4

//seisekihyo.csv。左から「科目コード・生徒コード・評定」。
20101,1001,3
20101,1002,3
20101,1003,5
20101,1004,4
20101,1005,3
(中略)
21110,1006,4
21110,1007,5
21110,1008,2
21110,1009,5
21110,1010,2
完全版は下記の場所にありますので、よろしければご確認ください(別ウィンドウが開きます)。
成績処理プログラム用ダミーデータ
まず、評定平均値の一覧表を作ってみましょう。
せっかくですから、これもリスト構造にしてから表示するようにします。
表示するのは「生徒コード・氏名・評定平均」の3項目としますが、生徒コードと氏名はseitomeibo.csvから作ったリストにリンクしておけばそちらから取り出せるので、メンバは、nextのほかは「名簿リストへのリンク・評定平均」の2項目だけとします。
★評定平均値とは、ある生徒が在学中に取った成績(5段階評価)すべての平均値で、小数第二位を四捨五入して小数第一位まで出します。
この値がある程度の水準に達していないと、推薦入試を受けることができません。
昔は少々面倒な計算を必要としたものですが、今は単純に「評価の全合計 ÷ 評価数」で算出することになっています。
//#include、#define、リスト用構造体定義等。

//-----------------------------------------------------------------------------
//評定平均リスト用構造体定義。
struct Hyotei {
    struct Hyotei *next;
    struct ListNode *to_st_node;    //生徒名簿へのポインタ。
    double hyotei;                  //評定平均。
};

//=============================================================================
//プロトタイプ宣言追加。
struct ListNode **Loop_smList(const char ** const file_name, 
                              const int file_num);
double RoundingSecond(double d);
struct Hyotei *GradeAverage(struct ListNode **headps);

//=============================================================================
//FileOpen関数・ColumnsCount関数・ListCreate関数・smList関数・
//BufferExtension関数・ListFree関数・ListDisplay関数・smList関数。

//=============================================================================
/*複数のリストを作ってその先頭ポインタを配列に格納する関数。*/
//csvファイルからリストを作るsmList関数をループする。
//失敗ならリスト先頭ポインタ格納配列へのポインタにNULLを代入して返す。
struct ListNode **Loop_smList(const char ** const file_name, 
                              const int file_num) {
    struct ListNode **headps;       //リスト先頭ポインタ格納配列へのポインタ。
    int i, j;
        //リスト先頭ポインタ配列の領域を確保する。
    headps = malloc(sizeof(struct ListNode *) * (size_t)file_num);
    if (headps == NULL) {
        puts("リストの先頭ポインタ格納領域が確保できませんでした。");
        return headps;
    }
    
    for (i = 0; i < file_num; i++) {
        headps[i] = smList(file_name[i]);
        if (headps[i] == NULL) {      //リストが作れないものがあれば……
            for (j = 0; j < i; j++) {   //すでに生成したリストを解放する。
                ListFree(headps[j]);
            }
            free(headps);               //リスト先頭ポインタ格納配列も解放。
            headps = NULL;              //そのポインタにNULLを代入して返す。
            break;
        }
    }
    return headps;
}

//=============================================================================
/*小数第二位で四捨五入する関数。*/
double RoundingSecond(double d) {
    int i;
    
    if (d == 0.0) {return 0.0;}    //万一0を受け取った場合は0を返す。
    d = d * 10.0 + 0.5;    //小数第二位で四捨五入して小数第一位まで出すが、
    i = (int)d;            //math.hの関数は使わないことにする。
    d = i / 10.0;          //10倍して0.5を足し、小数を切り捨てて1/10にする。
    
    return d;
}

//=============================================================================
/*評定平均リストを作ってその先頭ポインタを返す関数。*/
struct Hyotei *GradeAverage(struct ListNode **headps) {
    struct ListNode *seitomeibo_head_p, *kamokuhyo_head_p, 
                    *seisekihyo_head_p, *p, *q, *r;
    int gokei = 0, kosu = 0;
    double hyotei;
    struct Hyotei *hyotei_p, *hyotei_head_p = NULL, 
                  *hyotei_pre_p = NULL, *hyotei_next_p;
    
    seitomeibo_head_p = headps[0];  //配列のままでは分かりにくいので、
    kamokuhyo_head_p = headps[1];   //先頭ノードを名前付きの変数にコピーする。
    seisekihyo_head_p = headps[2];
    
    p = seitomeibo_head_p;
    while (p != NULL) {        //生徒名簿リストをループ。
        q = kamokuhyo_head_p;
        while (q != NULL) {      //その中で科目表リストをループ。
            r = seisekihyo_head_p;
            while (r != NULL) {    //その中で成績表リストをループ。
                if ((strcmp(p->member[0], r->member[1]) == 0)
                    && (strcmp(q->member[0], r->member[0]) == 0)) {
                    gokei += (int)strtol(r->member[2], NULL, 10);
                    kosu++;
                }    //↑生徒コードが一致したら評定を足していき、個数も求める。
                r = r->next;
            }
            q = q->next;
        }
        if (gokei * kosu != 0) {     //四捨五入関数を呼ぶ(ゼロ除算回避付き)。
            hyotei = RoundingSecond((double)gokei / kosu);
        } else {
            hyotei = 0.0;
        }
        hyotei_p = malloc(sizeof(struct Hyotei));    //以下、評定リストを作る。
        if (hyotei_p == NULL) {                      //ノードが作れなければ……
            puts("評定平均格納構造体が作れませんでした。");
            hyotei_p = hyotei_head_p;                  //作成済みリストを解放。
            while (hyotei_p != NULL) {
                hyotei_next_p = hyotei_p->next;
                free(hyotei_p);
                hyotei_p = hyotei_next_p;
            }
            hyotei_head_p = NULL;                      //先頭ノードをNULLとして
            break;                                     //ループから抜ける。
        }
        hyotei_p->next = NULL;        //各メンバに値を入れる。nextにはNULL。
        hyotei_p->to_st_node = p;     //to_st_nodeには名簿ノードへのポインタ。
        hyotei_p->hyotei = hyotei;    //hyoteiには評定平均値。
        if (hyotei_head_p == NULL) {  //以下はリスト構築処理。
            hyotei_head_p = hyotei_p;
        } else {
            hyotei_pre_p->next = hyotei_p;
        }
        hyotei_pre_p = hyotei_p;
        gokei = 0; kosu = 0;
        p = p->next;
    }
    return hyotei_head_p;
}

//=============================================================================
int main(void) {
    const char *file_name[] = {
        "seitomeibo.csv", "kamokuhyo.csv", "seisekihyo.csv"
    };
    int file_num, i;
    struct ListNode **headps;
    struct Hyotei *hyotei_p, *hyotei_head_p, *hyotei_next_p;
    
    setvbuf(stdout, NULL, _IONBF, 0);
    
    file_num = sizeof(file_name) / sizeof(char *);
    
        //複数のリストを作る関数を呼ぶ。失敗ならNULLが返る。
    headps = Loop_smList(file_name, file_num);
    if (headps == NULL) {exit(1);}
    
    hyotei_head_p = GradeAverage(headps);    //評定平均リストを作る関数を呼ぶ。
                                             //引数はリストの先頭ポインタ集。
    if (hyotei_head_p != NULL) {    //先頭ノードがNULLでなければ……
        hyotei_p = hyotei_head_p;       //リストができたということなので、
        while (hyotei_p != NULL) {      //中身を表示して確認する。
            printf("%s %s %.1f\n",      //生徒コードと氏名は名簿リストを参照。
                hyotei_p->to_st_node->member[0],    //生徒コード。
                hyotei_p->to_st_node->member[1],    //氏名。
                hyotei_p->hyotei);                  //評定平均。
            hyotei_p = hyotei_p->next;
        }
        hyotei_p = hyotei_head_p;       //評定平均リストの解放。
        while (hyotei_p != NULL) {
            hyotei_next_p = hyotei_p->next;
            free(hyotei_p);
            hyotei_p = hyotei_next_p;
        }
    }
    
    for (i = 0; i < file_num; i++) {    //csvファイルから作ったリストの解放。
        ListFree(headps[i]);
    }
    
    free(headps);
    return 0;
}
1001 岡 胡桃 3.4
1002 川合 睦美 3.9
1003 武井 康代 4.9
1004 西村 瑠璃 3.9
1005 藤川 小梅 3.3
1006 森本 双葉 3.9
1007 渡部 泉 3.5
1008 井本 秀樹 2.1
1009 小沼 正人 4.2
1010 重田 年昭 3.0
mallocを使うと解放処理が必要になるので、どうしてもコードが長くなりますね。
でもまあ、結果は正しく出たようです(手計算と一致しました)。


……さて、この実行結果を見て、おそらくほとんどの人が思うのが、「これって成績順に並べられないの?」ということではないでしょうか。
それには構造体リストをソートする必要があるわけですが、その方法が意外と参考書に載っていません(少なくとも、部の予算で購入した数冊の参考書にはありませんでした)。
ということは、構造体リストのソートというものは、実用レベルでは必要とされないということなのでしょうか?
実用的なのかどうかはよく分かりませんが、プログラムの練習には十分なりそうなので、とにかくやってみることにしました。
構造体リストをソートする(その1)
配列のソート処理が「値」を入れ替えていくのに対し、リストの場合は「次のノードへのポインタ」、すなわちnextメンバを書き換えていくことでソートを実現します。
要するに、配列では「入れ物はそのままで、中身を入れ替えていく」のに対し、リストでは「中身と入れ物はそのままで、次に来るべき入れ物の指定を入れ替えていく」という違いがあるわけです。
このように書くと両者で大した違いはないように見えますが、実際は、リストのソートの方がはるかに面倒というか複雑というか、負荷がかかります。
構造体リストは、入れ物どうしを「ポインタ」で、言い換えれば「矢印のようなもの」で繋いでいます。
そのような、矢印の付いた紐でお互いが繋がった入れ物の連なりにおいて、入れ物を繋ぐ紐を入れ替えていく場面を想像してみてください。
入れ替えたい二つの入れ物の矢印紐だけでなく、それらの「前に」位置する入れ物の矢印紐も入れ換えなければならないことに気づくでしょう。
そのうえ、次のような場合は条件分岐が必要になります。
  1. 先行ノードがない場合(=交換対象ノードの一方が先頭ノードの場合)。
  2. 交換するノードどうしが隣接している場合。
図にすると下のような感じになるでしょうか。
交換対象ノードを「p」と「q」、それぞれの先行ノードを「pp」と「qp」、それぞれの後続ノードを「pn」「qn」としています。
ノード交換
……うんざりするような面倒さですね。
コードを書く前に、交換のパターンを整理しておかないとこんがらがりそうです。
矢印、すなわちnextを書き換えるノードは、次のようになると考えられます。
【標準パターン】p前・p・q前・q
【p先頭パターン】p・q前・q
……「p前」が存在しないので、その書き換えは不要。
【隣接パターン】p前・p・q
……「q前」は「p」なので、pを書き換えれば「q前」の書き換えは不要。
これらを整理すると、
となります。
また、これらのノードの矢印が新たに向く先は、
【矢印方向・標準パターン】p前《→q》・p《→q後》・q前《→p》・q《→p後》
【矢印方向・p先頭パターン】p《→q後》・q前《→p》・q《→p後》
【矢印方向・隣接パターン】p前《→q》・p《→q後》・q《→p》
となっています。
例外は【矢印方向・隣接パターン】の「qの矢印方向」で、この場合のみ「q《→p》」とする必要があります。
以上のパターン整理により、次のような交換手順のコードを書けばうまく動くと思われます。
  1. 【標準パターン】【矢印方向・標準パターン】を基準とする。
  2. pが先頭ノード以外の場合は、「p前」の書き換えを実行。
  3. pとqが隣接していない場合は、「q前」の書き換えを実行。
  4. pとqが隣接している場合は、「q」のnextポインタを「p」とする。
★なお、pとqの間にノードが一つしかない場合は、その間のノードは「p後」でありかつ「q前」でもある、ということになりますが、これはそのままで大丈夫でしょう。
「同じノード」に「二つの名前」が付いていても、どのノードかは特定できるからです。
これがもし「二つのノード」に「同じ名前」が付いていたとしたら、どちらのノードかが特定できず、混乱が生じます。
それでは書いてみます。
アルゴリズムは、単純な「選択ソート」とします。
ここでは降順としますので、二重ループ用の変数p・qのほか、最大値(を持つノード)を記憶するための変数mを宣言します。
リストの先頭ポインタを引数で渡しますが、ソートの結果、先頭ポインタが入れ替わる可能性が大きいので、先頭ポインタをさらにポインタで渡して中身の書き換えに対応できるようにします。
//=============================================================================
//プロトタイプ宣言追加。
void GASort(struct Hyotei **hyotei_head_p);

//=============================================================================
/*評定平均リストを降順ソートする関数。*/
void GASort(struct Hyotei **hyotei_head_p) {
    struct Hyotei *p, *p_pre_node, *p_next_node,    //外側ループ用変数。
                  *q, *q_pre_node,                  //内側ループ用変数。
                  *m, *m_pre_node = NULL, *m_next_node = NULL;
                               //↑mはmax、すなわち最大値を持つノード用変数。
    p = *hyotei_head_p;        //最初のpはリストの先頭。
    p_pre_node = NULL;         //p前。最初は存在しないのでNULLを代入しておく。
    while (p->next != NULL) {  //外側ループ。
        p_next_node = p->next;   //p次を記憶。
        m = p;                   //mの初期ノードはpとする。
        q = p->next;             //qはpの次のノードからスタート。
        q_pre_node = p;          //q前を記憶。最初はp。
        while (q != NULL) {    //内側ループ  ↓実数では比較しない。整数で比較。
            if ((int)(m->hyotei * 10) < (int)(q->hyotei * 10)) {
                m = q;           //もしmよりqの方が大きければ、mをqに更新。
                m_pre_node = q_pre_node;  //同じく、m前を更新。
                m_next_node = q->next;    //同じく、m次を更新。
            }
            q_pre_node = q;      //次のq前は今のq。次の内側ループの準備。
            q = q->next;         //内側ループを進める。
        }                      //内側ループ終了。mが最大値ノードとなっている。
        if (p != m) {          //pがmのままなら交換は必要ない。
            if (p_pre_node != NULL) { //pがリストの先頭でなければ……
                p_pre_node->next = m;   //p前のnextをmに。
            }
            p->next = m_next_node;    //pのnextをm次に。
            if (p_next_node != m) {   //pとmが隣接していなければ……
                m_pre_node->next = p;   //m前のnextをpに。
                m->next = p_next_node;  //mのnextをp次に。
            } else {                  //pとmが隣接していれば……
                m->next = p;            //mのnextをpに。
            }
            p = m;    //交換で最初のpの位置にmが来ているので、pの位置を戻す。
        }    //↓pがリストの先頭の場合、リスト先頭ポインタを書き換える。
        if (p_pre_node == NULL) {*hyotei_head_p = p;}
        p_pre_node = p;   //次のp前は今のp。次の外側ループの準備。
        p = p->next;      //外側ループを進める。
    }
}

//=============================================================================
int main(void) {
    const char *file_name[] = {
        "seitomeibo.csv", "kamokuhyo.csv", "seisekihyo.csv"
    };
    int file_num, i;
    struct ListNode **headps;
    struct Hyotei *hyotei_p, *hyotei_head_p, *hyotei_next_p;
    
    setvbuf(stdout, NULL, _IONBF, 0);
    
    file_num = sizeof(file_name) / sizeof(char *);
    
    headps = Loop_smList(file_name, file_num);
    if (headps == NULL) {exit(1);}
    
    hyotei_head_p = GradeAverage(headps);    //評定平均リストを作る関数を呼ぶ。
    
    if (hyotei_head_p != NULL) {
        GASort(&hyotei_head_p);              //ソート関数を呼ぶ。
        hyotei_p = hyotei_head_p;            //表示と解放。
        while (hyotei_p != NULL) {
            printf("%s %s %.1f\n", 
                hyotei_p->to_st_node->member[0], 
                hyotei_p->to_st_node->member[1], 
                hyotei_p->hyotei);
            hyotei_p = hyotei_p->next;
        }
        hyotei_p = hyotei_head_p;
        while (hyotei_p != NULL) {
            hyotei_next_p = hyotei_p->next;
            free(hyotei_p);
            hyotei_p = hyotei_next_p;
        }
    }
    
    for (i = 0; i < file_num; i++) {
        ListFree(headps[i]);
    }
    
    free(headps);
    return 0;
}
1003 武井 康代 4.9
1009 小沼 正人 4.2
1004 西村 瑠璃 3.9
1006 森本 双葉 3.9
1002 川合 睦美 3.9
1007 渡部 泉 3.5
1001 岡 胡桃 3.4
1005 藤川 小梅 3.3
1010 重田 年昭 3.0
1008 井本 秀樹 2.1
……ソートは成功しているけれど、同じ評価の生徒三人が順不同になってしまっていますね。
「選択ソート」は安定ソートではないので、同じ値が複数あるデータの一つがかなり上の方にあって、それがうんと下の方のデータと交換されたりすると、このような結果になりがちです。
同じ評価の場合は、生徒コードが若い方が上に来るようにしたいところです。
このソート結果とは直接の関係はないのですが、実は、川合さんと西村さんと森本さんは、評定平均値は同じ3.9ではあるけれど、評価合計は順に136・138・137と、わずかに差があります。
評価合計を科目数35で割って小数第二位を四捨五入すると、評定合計136~138では同じ3.9という平均値になってしまうわけですね。
「差があるのなら、ソートするときはそれを反映させるべきだ。そのために、ソート対象は、評定平均ではなく評定合計にするべきだ」と考える向きもあるかもしれません。
しかし、本校定時制のように全員が卒業までまったく同じ科目を履修するのならそれでいいけれど、選択科目の関係で生徒それぞれの履修科目数がバラバラになることもよくあるので(むしろその方が多いくらいでしょう)、そういう学校ではやはり平均値で順位を出すしかありません。
平均値でできるだけ厳密に値の大小を決めるためには、例えば小数第十位くらいまで算出して比較すれば、かなり精度の高い順位が得られると思われます。
とは言え、生徒によって履修科目が異なるとなると、いくら高い精度の平均値順位を出したところで、共通性・公平性を欠いているのだからあまり意味はない、と言われてしまうかもしれません。
スポーツでも、リュージュなどを除いてほとんどの競技が100分の1秒までしか計測しませんし、それで同タイムなら、1000分の1秒まで計測していたら付いたかもしれない決着も同着で処理されることになるのですから、もともと公平性が高くない評定平均値のランキングなど、小数第一位程度で出せば十分だとも言えそうです。
そのためには、ノードを交換する場合の条件に、「評定平均が同じで、かつ生徒コードが逆順になっている場合」を加えればよいでしょう。
これを書き加えるとなると、if文が長く、Excelの長い関数並みにカッコが何重にも重なって読みにくくなりそうなので、わざと改行を多くして、インデントを付けて書いてみます。
//=============================================================================
/*評定平均リストを降順ソートする関数(改良版)。*/
void GASort(struct Hyotei **hyotei_head_p) {
    struct Hyotei *p, *p_pre_node, *p_next_node,
                  *q, *q_pre_node, 
                  *m, *m_pre_node = NULL, *m_next_node = NULL;
    
    p = *hyotei_head_p;
    p_pre_node = NULL;
    while (p->next != NULL) {
        p_next_node = p->next;
        m = p;
        q = p->next;
        q_pre_node = p;
        while (q != NULL) {
            if (        //↓もしmよりqの方が大きければ……
                   ((int)(m->hyotei * 10) < (int)(q->hyotei * 10)) 
                   ||   //または……
                   (        //↓mとqが等しく、
                       ((int)(m->hyotei * 10) == (int)(q->hyotei * 10)) 
                       &&   //↓かつ、mの生徒コードの方が大きければ……
                       (strcmp(m->to_st_node->member[0], 
                               q->to_st_node->member[0]) > 0)
                   )
               ) {      //↓最大値ノードを入れ替える。
                m = q;
                m_pre_node = q_pre_node;
                m_next_node = q->next;
            }
            q_pre_node = q;
            q = q->next;
        }
        if (p != m) {
            if (p_pre_node != NULL) {
                p_pre_node->next = m;
            }
            p->next = m_next_node;
            if (p_next_node != m) {
                m_pre_node->next = p;
                m->next = p_next_node;
            } else {
                m->next = p;
            }
            p = m;
        }
        if (p_pre_node == NULL) {*hyotei_head_p = p;}
        p_pre_node = p;
        p = p->next;
    }
}
1003 武井 康代 4.9
1009 小沼 正人 4.2
1002 川合 睦美 3.9
1004 西村 瑠璃 3.9
1006 森本 双葉 3.9
1007 渡部 泉 3.5
1001 岡 胡桃 3.4
1005 藤川 小梅 3.3
1010 重田 年昭 3.0
1008 井本 秀樹 2.1
今度は、3.9の三人がきちんと番号順に並びました。
なお、上の22~23行目、生徒コードの比較において、生徒コードは数字なのにstrcmp関数を使っているのは、メンバto_st_nodeの先にある、csvファイルから作ったリストのノードは、next以外のメンバがすべて文字列だからです。
★もちろん「昇順」でソートすることもできます。
17行目の不等号を逆にするだけで「昇順」になります。
構造体リストをソートする(その2)
これで一応、構造体リストのソートは実現できました。
しかし、この方法はややこしく、また各ノードのnextを書き換えてしまうため、リストの元の順番が失われてしまうという欠点を持っています。
そこで、部活では、もう少し楽にできる別の方法がないものかどうかを考えてみることになりました。
その結果、
「要するに、入れ物の矢印を交換しようとするから大変になるので、それよりも、入れ物はそのままにしておいて、外部から順番を指示すればよいのではないか?」
という結論に達しました。
それを実現するための方法の一つが、ソート用のリストを用意して、そちらの方のポインタを組み替えていくというものです(下図)。
外部から指し示すだけで、元のリストには触りませんから、元の順番は破壊されません。
リスト図
この方法なら、交換するポインタが一度に二つだけで済み、しかも交換対象ノードが先頭を含むか隣接しているかで処理を分岐させる必要がありませんので、その分の負荷は下がります。
しかし、新しいリストを作らなければならないこと、ポインタが増えて間接的な参照が多くなったことなどで、コードを書く上での全体的な労力は、結局あまり変わらなかったような気がします。
以下はそのコードと実行結果です。
//ソートのためのリスト用構造体定義。
struct SortList {
    struct SortList *next;
    struct Hyotei *nodelink;
};

//=============================================================================
//プロトタイプ宣言書き換え。
struct SortList *GASort(struct Hyotei *hyotei_head_p);

//=============================================================================
/*ソート用リストを作って評定平均リストを降順ソートする関数。*/
//ソート用リストの先頭ポインタを返す。
struct SortList *GASort(struct Hyotei *hyotei_head_p) {
    struct SortList *sp, *sort_head_p = NULL, *pre_sp = NULL, *next_sp, 
                    *sq, *m;
    struct Hyotei *p, *tmp;
    
    p = hyotei_head_p;    //評定平均値リストをたどり、それぞれにソート用の
    while (p != NULL) {   //構造体を作ってリンクさせる
        sp = malloc(sizeof(struct SortList));    //ソート用構造体生成。
        if (sp == NULL) {                        //生成失敗時の処理。
            puts("ソート用構造体リストが作れませんでした。");
            sp = sort_head_p;
            while (sp != NULL) {
                next_sp = sp->next;
                free(sp);
                sp = next_sp;
            }
            sort_head_p = NULL;
            return sort_head_p;
        }
        sp->next = NULL;   //ソート用構造体のメンバに値を入れる。nextにNULL。
        sp->nodelink = p;  //nodelinkに評定平均値ノードへのポインタ。
        if (sort_head_p == NULL) {  //これが先頭なら……
            sort_head_p = sp;         //当ノードを先頭とする。
        } else {                    //先頭でなければ……
            pre_sp->next = sp;        //前ノードのnextに当ノードを代入。
        }
        pre_sp = sp;       //次のループの準備。次は前ノードが当ノードとなる。
        p = p->next;
    }
    
    sp = sort_head_p;      //ソート用リストをたどり、ソートを実行する。
    while (sp->next != NULL) {  //外側ループ。
        m = sp;                   //開始ノードを最大値ノードとしておく。
        sq = sp->next;
        while (sq != NULL) {    //内側ループ。
            if (                  //↓mとsqを交替するかどうかを比較。
                ((int)( m->nodelink->hyotei * 10) <  //←昇順なら不等号を逆に。
                 (int)(sq->nodelink->hyotei * 10)) 
                ||
                (
                    ((int)( m->nodelink->hyotei * 10) == 
                     (int)(sq->nodelink->hyotei * 10))
                    &&
                    (strcmp( m->nodelink->to_st_node->member[0], 
                            sq->nodelink->to_st_node->member[0]) > 0)
                )                 //↑生徒コードは3回リンクをたどる必要がある。
               ) {
                m = sq;           //交替ならmに。
            }
            sq = sq->next;
        }
        if (sp != m) {          //spがmのままなら交換は必要ない。
            tmp = sp->nodelink;   //spのリンクとmのリンクを単純交換。
            sp->nodelink = m->nodelink;
            m->nodelink = tmp;
        }
        sp = sp->next;
    }
    return sort_head_p;    //新しく作ったソート用リストの先頭ポインタを返す。
}

//=============================================================================
int main(void) {
    const char *file_name[] = {
        "seitomeibo.csv", "kamokuhyo.csv", "seisekihyo.csv"
    };
    int file_num, i;
    struct ListNode **headps;
    struct Hyotei *hyotei_p, *hyotei_head_p, *hyotei_next_p;
    struct SortList *sort_head_p = NULL, *sp, *next_sp;
    
    setvbuf(stdout, NULL, _IONBF, 0);
    
    file_num = sizeof(file_name) / sizeof(char *);
    
    headps = Loop_smList(file_name, file_num);
    if (headps == NULL) {exit(1);}
    
    hyotei_head_p = GradeAverage(headps);
    if (hyotei_head_p != NULL) {
        sort_head_p = GASort(hyotei_head_p);    //ソート用関数を呼ぶ。
    }
    
    if (sort_head_p != NULL) {    //確認表示とリストの解放。
        puts("ソート順");          //まずソートした方。
        sp = sort_head_p;         //↓必要な値を取り出すのに
        while (sp != NULL) {      //最大3回リンクをたどらねばならない。
            printf("%s %s %.1f\n", 
                sp->nodelink->to_st_node->member[0], 
                sp->nodelink->to_st_node->member[1], 
                sp->nodelink->hyotei);
            sp = sp->next;
        }
        sp = sort_head_p;
        while (sp != NULL) {
            next_sp = sp->next;
            free(sp);
            sp = next_sp;
        }
    }
    
    if (hyotei_head_p != NULL) {    //次に元の評定平均値リスト。
        puts("\n元の順");
        hyotei_p = hyotei_head_p;   //こちらには触っていないので、
        while (hyotei_p != NULL) {  //元の順番はそのまま残っている。
            printf("%s %s %.1f\n", 
                hyotei_p->to_st_node->member[0], 
                hyotei_p->to_st_node->member[1], 
                hyotei_p->hyotei);
            hyotei_p = hyotei_p->next;
        }
        hyotei_p = hyotei_head_p;
        while (hyotei_p != NULL) {
            hyotei_next_p = hyotei_p->next;
            free(hyotei_p);
            hyotei_p = hyotei_next_p;
        }
    }
    
    for (i = 0; i < file_num; i++) {
        ListFree(headps[i]);
    }
    
    free(headps);
    return 0;
}
ソート順
1003 武井 康代 4.9
1009 小沼 正人 4.2
1002 川合 睦美 3.9
1004 西村 瑠璃 3.9
1006 森本 双葉 3.9
1007 渡部 泉 3.5
1001 岡 胡桃 3.4
1005 藤川 小梅 3.3
1010 重田 年昭 3.0
1008 井本 秀樹 2.1

元の順
1001 岡 胡桃 3.4
1002 川合 睦美 3.9
1003 武井 康代 4.9
1004 西村 瑠璃 3.9
1005 藤川 小梅 3.3
1006 森本 双葉 3.9
1007 渡部 泉 3.5
1008 井本 秀樹 2.1
1009 小沼 正人 4.2
1010 重田 年昭 3.0
ソートは正しく処理され、元の順番も消えていません。
この方法は成功としてよいでしょう。
構造体リストをソートする(その3)
さて、ソート用の構造体の並びがリストで実現できるなら、同じことが配列でも可能になるはずです。
ソート用配列図
リストでは当然構造体を使いましたが、配列では構造体を使う必要はなく、「ソート対象リストのノードへのポインタ」の配列で済むはずです。
であるならば、ソート対象リストのノード数さえつきとめれば、ソート用の外部配列の生成はmalloc一行で書けてしまいます。
今度はこれで書いてみましょう。
//ソートのためのリスト用構造体定義は削除。

//=============================================================================
//プロトタイプ宣言書き換え。
struct Hyotei **GASort(struct Hyotei *hyotei_head_p);

//=============================================================================
/*ソート用配列を作って評定平均リストを降順ソートする関数。*/
//ソート用配列の先頭ポインタを返す。
struct Hyotei **GASort(struct Hyotei *hyotei_head_p) {
    struct Hyotei *hp, **sa, *tmp;
    int node_num = 0, i = 0, j, m;
    
    hp = hyotei_head_p;        //評定平均値ノードの数を調べる。
    while (hp != NULL) {
        node_num++;
        hp = hp->next;
    }
        //↓ソート用配列を作る。その際、一つ多く作って番兵を置く。
    sa = malloc(sizeof(struct ListNode *) * (size_t)(node_num + 1));
    if (sa == NULL) {
        puts("ソート用構造体配列が作れませんでした。");
        return sa;
    }
    sa[node_num] = NULL;       //番兵はNULL。
    
    hp = hyotei_head_p;        //配列に評定平均値ノードへのポインタを代入。
    while (hp != NULL) {
        sa[i++] = hp;
        hp = hp->next;
    }
    
    for (i = 0; i < node_num - 1; i++) {     //ソート処理。
        m = i;
        for (j = i + 1; j < node_num; j++) {
            if (
                ((int)(sa[m]->hyotei * 10) < 
                 (int)(sa[j]->hyotei * 10)) 
                ||
                (
                    ((int)(sa[m]->hyotei * 10) == 
                     (int)(sa[j]->hyotei * 10))
                    &&
                    (strcmp(sa[m]->to_st_node->member[0], 
                            sa[j]->to_st_node->member[0]) > 0)
                )
               ) {
                m = j;           //最大値の更新は添字を入れ替えるだけで済む。
            }
        }
        if (i != m) {
            tmp = sa[i];
            sa[i] = sa[m];
            sa[m] = tmp;
        }
    }
    return sa;
}

//=============================================================================
int main(void) {
    const char *file_name[] = {
        "seitomeibo.csv", "kamokuhyo.csv", "seisekihyo.csv"
    };
    int file_num, i;
    struct ListNode **headps;
    struct Hyotei *hyotei_p, *hyotei_head_p, *hyotei_next_p, **sa = NULL;
    
    setvbuf(stdout, NULL, _IONBF, 0);
    
    file_num = sizeof(file_name) / sizeof(char *);
    
    headps = Loop_smList(file_name, file_num);
    if (headps == NULL) {exit(1);}
    
    hyotei_head_p = GradeAverage(headps);
    if (hyotei_head_p != NULL) {
        sa = GASort(hyotei_head_p);        //ソート関数を呼ぶ。
    }
    
    if (sa != NULL) {      //以下、確認表示と解放。番兵があるので、
        puts("ソート順");   //GASort関数から要素数を返してもらう必要はない。
        i = 0;
        while (sa[i] != NULL) {
            printf("%s %s %.1f\n", 
                sa[i]->to_st_node->member[0], 
                sa[i]->to_st_node->member[1], 
                sa[i]->hyotei);
            i++;
        }
        free(sa);
    }
    
    if (hyotei_head_p != NULL) {
        puts("\n元の順");
        hyotei_p = hyotei_head_p;
        while (hyotei_p != NULL) {
            printf("%s %s %.1f\n", 
                hyotei_p->to_st_node->member[0], 
                hyotei_p->to_st_node->member[1], 
                hyotei_p->hyotei);
            hyotei_p = hyotei_p->next;
        }
        hyotei_p = hyotei_head_p;
        while (hyotei_p != NULL) {
            hyotei_next_p = hyotei_p->next;
            free(hyotei_p);
            hyotei_p = hyotei_next_p;
        }
    }
    
    for (i = 0; i < file_num; i++) {
        ListFree(headps[i]);
    }
    
    free(headps);
    return 0;
}
ソート順
1003 武井 康代 4.9
1009 小沼 正人 4.2
1002 川合 睦美 3.9
1004 西村 瑠璃 3.9
1006 森本 双葉 3.9
1007 渡部 泉 3.5
1001 岡 胡桃 3.4
1005 藤川 小梅 3.3
1010 重田 年昭 3.0
1008 井本 秀樹 2.1

元の順
1001 岡 胡桃 3.4
1002 川合 睦美 3.9
1003 武井 康代 4.9
1004 西村 瑠璃 3.9
1005 藤川 小梅 3.3
1006 森本 双葉 3.9
1007 渡部 泉 3.5
1008 井本 秀樹 2.1
1009 小沼 正人 4.2
1010 重田 年昭 3.0
大丈夫そうです。
単純比較で、この方がソート用リストを使う方法よりもコードが10行くらい短くなっていますね。
リストだと、ノードを一つ一つ作って、次のノードへのポインタを繋いでいかなければなりませんが、配列ならその必要はありません。
外部から順番を指示するオブジェクトは、それ自身の入れ物を入れ替える必要はない、つまり順番は固定なのですから、リストよりも配列の方が有利であると言えるでしょう。
そういうわけで、構造体リストのソート処理には、この最後のものを採用したいと思います。
まあ今回の目的は「リスト構造の練習」なのですから、一つ前の外部リストを使う方法の方が目的にかなってはいるのですけれどもね。
★これはあくまでも部活動日記であって、技術的な情報公開の場ではありませんから、部活での失敗談も書いておきたいと思います。
実は、この外部配列を使うソートのコードを書いていたとき、顧問も部員もうまくプログラムが動かず、しばらく(二、三日くらい?)悩んでしまったのでした。
顧問の手元にはそのときの失敗コードが残っていないので、今となっては何につっかえていたのかが判然としなくなってしまいましたが、あまりにもうまくいかないので、顧問は「構造体リストをソートする(その2)」の外部リストを使う方法の方に「逃走した」という経緯があります(笑)。
つまり、部活では、外部配列を使うソートの方が先に設計されたが、外部リストを使うソートの方が先に書き上げられ、後から前者が完成した、という流れになります。
で、顧問は、リストを使うソートの方がすんなりうまくいったものですから、まだ配列を使うソートをあきらめずに作成中だった部員に対して、「これってさあ、外部リストを使う方法じゃないとうまくいかないんじゃないかなあ?」などと誤った助言をしてしまったりもしました。
おまけに、顧問は選択ソートのやり方を間違えていたことも後に判明しました。
降順で、より大きな値が出てきたら「片っ端から」交換していたという……(〃ノωノ)……上のコードで言えば、mを使わずにソートしていたということです。
それに対して、部員A君の方は、その後配列を使うソートを、顧問と違って「正しい」選択ソートによって何とか成功させました(素晴らしい!……ただし、上のコードはA君が書いたものと同一ではありません)。
今、上のコードを眺めてみると、むしろリストを作るよりも簡単なくらいなのに、いったい何につっかえていたものやら、何とも不思議な思いにとらわれます。
コンパイラはエラーも警告も吐かないのに、実行するとセグメンテーション違反が出てしまうという非常に厄介な状況で、かつまだデバッガの使い方がよく分かっていなかったため(今でも分かっていませんが)、結局原因はつきとめられずに終わってしまいました。


成績処理プログラム――成績一覧表
次は成績一覧表を作ってみましょう。
学年ごと、計4枚作成します。
ここでは表示のみとしますが、できあがった表はcsvファイルとして出力するという前提で、項目間はカンマ区切りとします。
正直に告白すると、処理結果をカンマ区切り表示としたのは、どの環境でもきれいに整形表示されるprintfのコードが、顧問も部員も書けなかったためです。
全角文字が入ると、(Shift-JISはともかくとして)ユニコードではどうにもうまくいきません。
ユニコード全角文字で縦線を揃えるには、何かコツのようなものがあるのでしょうか?
//=============================================================================
//プロトタイプ宣言追加。
void GradeTable(struct ListNode **headps);

//=============================================================================
/*成績一覧表を作る関数。*/
void GradeTable(struct ListNode **headps) {
    struct ListNode *seitomeibo_head, *kamokuhyo_head, 
                    *seisekihyo_head, *p, *q, *r;
    char *gakunen[4] = {"1", "2", "3", "4"};       //定時制は4学年。
    int i;
    
    seitomeibo_head = headps[0];
    kamokuhyo_head = headps[1];
    seisekihyo_head = headps[2];
    
    for (i = 0; i < 4; i++) {
        if (i != 0) {printf("\n");}
        printf("第%s学年成績一覧表\n", gakunen[i]);
        printf("番号,氏名");
        p = kamokuhyo_head;         //「番号,氏名」の右に科目名を列挙。
        while (p != NULL) {
            if (strcmp(p->member[2], gakunen[i]) == 0) {
                printf(",%s", p->member[1]);
            }
            p = p->next;
        }
        printf("\n");
        
        p = seitomeibo_head;        //三重ループ。三つのリストを走査。
        while (p != NULL) {           //↓左端に生徒コードと氏名を表示。
            printf("%s,%s", p->member[0], p->member[1]);
            q = kamokuhyo_head;
            while (q != NULL) {       //↓生徒コードと科目コードと学年が
                r = seisekihyo_head;  //一致したら、評価を表示。
                while (r != NULL) {
                    if((strcmp(p->member[0], r->member[1]) == 0) &&
                        (strcmp(q->member[0], r->member[0]) == 0) &&
                        (strcmp(q->member[2], gakunen[i]) == 0)) {
                        printf(",%s", r->member[2]);
                        break;
                    }
                    r = r->next;
                }
                q = q->next;
            }
            p = p->next;
            printf("\n");
        }
    }
}

//=============================================================================
int main(void) {
    const char *file_name[] = {
        "seitomeibo.csv", "kamokuhyo.csv", "seisekihyo.csv"
    };
    int file_num, i;
    struct ListNode **headps;
    
    setvbuf(stdout, NULL, _IONBF, 0);
    
    file_num = sizeof(file_name) / sizeof(char *);
    
    headps = Loop_smList(file_name, file_num);
    if (headps == NULL) {exit(1);}
    
    GradeTable(headps);        //一覧表を作る関数を呼ぶ。
    
    for (i = 0; i < file_num; i++) {
        ListFree(headps[i]);
    }
    
    free(headps);
    return 0;
}
第1学年成績一覧表
番号,氏名,国語総合,現代社会,数学Ⅰ,科学と人間生活,体育,保健,コミュニケーション英語基礎,ビジネス基礎,簿記
1001,岡 胡桃,3,4,3,4,3,4,4,4,3
1002,川合 睦美,3,5,2,2,5,4,4,5,4
1003,武井 康代,5,5,5,5,4,5,5,5,5
1004,西村 瑠璃,4,3,4,4,4,3,4,4,4
1005,藤川 小梅,3,3,3,3,4,3,3,3,3
1006,森本 双葉,5,5,5,5,5,5,5,5,5
1007,渡部 泉,5,4,2,4,3,5,2,4,4
1008,井本 秀樹,2,2,2,2,3,2,2,2,2
1009,小沼 正人,3,3,3,3,4,4,4,5,4
1010,重田 年昭,2,4,3,3,4,4,3,2,4

第2学年成績一覧表
番号,氏名,国語総合,地理A,数学Ⅰ,体育,保健,音楽Ⅰ,コミュニケーション英語基礎,簿記,ビジネス実務
1001,岡 胡桃,3,3,3,3,3,4,3,4,4
1002,川合 睦美,5,4,2,4,3,4,5,4,4
1003,武井 康代,5,5,5,4,5,5,5,5,5
1004,西村 瑠璃,4,4,4,4,4,4,4,4,4
1005,藤川 小梅,3,4,3,3,3,4,3,4,3
1006,森本 双葉,4,4,3,4,3,4,3,4,4
1007,渡部 泉,3,4,5,3,2,3,2,3,4
1008,井本 秀樹,2,2,2,3,2,2,2,2,2
1009,小沼 正人,4,4,4,5,4,4,5,4,4
1010,重田 年昭,3,3,3,4,2,3,2,4,3

第3学年成績一覧表
番号,氏名,現代文A,世界史A,化学基礎,体育,音楽Ⅰ,コミュニケーション英語Ⅰ,ビジネス実務,情報処理,財務会計Ⅰ
1001,岡 胡桃,4,3,4,3,3,4,4,3,3
1002,川合 睦美,4,4,3,4,4,4,4,4,4
1003,武井 康代,5,5,5,4,5,5,5,5,5
1004,西村 瑠璃,4,4,4,4,4,4,4,4,4
1005,藤川 小梅,4,3,3,3,4,3,3,3,4
1006,森本 双葉,3,3,3,3,3,3,3,3,3
1007,渡部 泉,2,3,3,3,3,5,4,4,3
1008,井本 秀樹,2,2,2,3,2,2,2,2,2
1009,小沼 正人,4,4,4,5,4,4,5,4,4
1010,重田 年昭,3,3,2,4,3,3,3,2,3

第4学年成績一覧表
番号,氏名,国語表現,政治経済,体育,コミュニケーション英語Ⅰ,家庭基礎,財務会計Ⅰ,経済活動と法,課題研究
1001,岡 胡桃,3,4,3,3,4,3,3,3
1002,川合 睦美,4,4,4,4,4,4,4,4
1003,武井 康代,5,5,4,5,5,5,5,5
1004,西村 瑠璃,4,4,4,4,4,4,4,4
1005,藤川 小梅,4,3,3,3,4,4,3,3
1006,森本 双葉,4,4,4,4,4,4,4,4
1007,渡部 泉,4,5,3,3,3,4,4,5
1008,井本 秀樹,2,2,4,2,2,2,2,2
1009,小沼 正人,5,5,5,5,4,4,4,5
1010,重田 年昭,2,2,3,4,4,2,3,2
……一応正しく表示されたようですが、何となくもの足りないですね。
右端に、それぞれの生徒の「評定合計」と「評定平均」くらいは入っていてほしいところです(できれば「ランキング」や、最下行に「科目ごとの平均値」も)。
それに、このままでは、おそらく「評価が抜けている場合」が正しく処理されないと思われます。
何らかの理由で評価が入っていないケース、例えば、上の4学年の表で、1001番・岡さんの「経済活動と法」(右から二番目)の評価が入っていなかった場合は、
1001,岡 胡桃,4,4,3,3,4,3,,3
というように、右から二番目はカンマだけというか、評価がなくてもカンマは表示されなければなりません。
試しに、csvファイルの該当行(331行目)を削除して実行してみると……
第4学年成績一覧表
番号,氏名,国語表現,政治経済,体育,コミュニケーション英語Ⅰ,家庭基礎,財務会計Ⅰ,経済活動と法,課題研究
1001,岡 胡桃,4,4,3,3,4,3,3        //7科目しかない。上の科目名とズレが生じた。
1002,川合 睦美,4,4,4,4,4,4,4,4    //ここから下は全員8科目ある。
1003,武井 康代,5,5,4,5,5,5,5,5
1004,西村 瑠璃,4,4,4,4,4,4,4,4
1005,藤川 小梅,4,3,3,3,4,4,3,3
1006,森本 双葉,3,4,4,4,4,4,4,4
1007,渡部 泉,2,5,3,3,3,4,4,5
1008,井本 秀樹,2,2,4,2,2,2,2,2
1009,小沼 正人,4,5,5,5,4,4,4,5
1010,重田 年昭,3,2,3,4,4,2,3,2
予想どおりダメでした。
評価が抜けていると、それよりも右の科目が左に「移動した」形になり、その結果、上の科目名と位置がズレてしまいます。
この手のデータベースでは、「生徒別履修科目登録テーブル」のようなものがあって、集計時にはそれをほかのテーブルと結合することにより、漏れのない必要十分な数のデータを取得することが多いと思います。
ここでその方法を採用することももちろんできますが、上の科目名に対応するデータがない科目をカンマだけで表示するくらいのことならば、フラグを使えば簡単に実現できると思われます。
例えば、このように。
//=============================================================================
/*成績一覧表を作る関数(評定のない科目をカンマのみにする機能付き)。*/
void GradeTable(struct ListNode **headps) {
    struct ListNode *seitomeibo_head, *kamokuhyo_head, 
                    *seisekihyo_head, *p, *q, *r;
    char *gakunen[4] = {"1", "2", "3", "4"};
    int i, flg = 0;
    
    seitomeibo_head = headps[0];
    kamokuhyo_head = headps[1];
    seisekihyo_head = headps[2];
    
    for (i = 0; i < 4; i++) {
        if (i != 0) {printf("\n");}
        printf("第%s学年成績一覧表\n", gakunen[i]);
        printf("番号,氏名");
        p = kamokuhyo_head;
        while (p != NULL) {
            if (strcmp(p->member[2], gakunen[i]) == 0) {
                printf(",%s", p->member[1]);
            }
            p = p->next;
        }
        printf("\n");
        
        p = seitomeibo_head;
        while (p != NULL) {
            printf("%s,%s", p->member[0], p->member[1]);
            q = kamokuhyo_head;
            while (q != NULL) {
                r = seisekihyo_head;
                while (r != NULL) {
                    if((strcmp(p->member[0], r->member[1]) == 0) &&
                        (strcmp(q->member[0], r->member[0]) == 0) &&
                        (strcmp(q->member[2], gakunen[i]) == 0)) {
                        printf(",%s", r->member[2]);
                        flg = 1;    //成績があった場合はフラグを立てる。
                        break;
                    }
                    r = r->next;
                }       //↓フラグが立っていなければカンマのみ表示。
                if ((flg == 0) && (strcmp(q->member[2], gakunen[i]) == 0)) {
                    printf(",");
                }
                flg = 0;            //次のループに進む前にフラグを倒す。
                q = q->next;
            }
            p = p->next;
            printf("\n");
        }
    }
}
カンマのみの表示になるべきは、「現在調べている生徒の評価がなく」、かつ「現在調べている学年対象」の科目です。
これで、例えばさっきの岡さんの「経済活動と法」を一時的に削除してみると……
第4学年成績一覧表
番号,氏名,国語表現,政治経済,体育,コミュニケーション英語Ⅰ,家庭基礎,財務会計Ⅰ,経済活動と法,課題研究
1001,岡 胡桃,3,4,3,3,4,3,,3        //成績がない科目がカンマのみになった。
1002,川合 睦美,4,4,4,4,4,4,4,4
1003,武井 康代,5,5,4,5,5,5,5,5
1004,西村 瑠璃,4,4,4,4,4,4,4,4
1005,藤川 小梅,4,3,3,3,4,4,3,3
1006,森本 双葉,4,4,4,4,4,4,4,4
1007,渡部 泉,4,5,3,3,3,4,4,5
1008,井本 秀樹,2,2,4,2,2,2,2,2
1009,小沼 正人,5,5,5,5,4,4,4,5
1010,重田 年昭,2,2,3,4,4,2,3,2
ちゃんとカンマのみになりました。
次に、2年生の体育(科目コード20602)を全員削除してみます。
第2学年成績一覧表
番号,氏名,国語総合,地理A,数学Ⅰ,体育,保健,音楽Ⅰ,コミュニケーション英語基礎,簿記,ビジネス実務
1001,岡 胡桃,3,3,3,,3,4,3,4,4        //↓体育が全員カンマのみになった。
1002,川合 睦美,5,4,2,,3,4,5,4,4
1003,武井 康代,5,5,5,,5,5,5,5,5
1004,西村 瑠璃,4,4,4,,4,4,4,4,4
1005,藤川 小梅,3,4,3,,3,4,3,4,3
1006,森本 双葉,4,4,3,,3,4,3,4,4
1007,渡部 泉,3,4,5,,2,3,2,3,4
1008,井本 秀樹,2,2,2,,2,2,2,2,2
1009,小沼 正人,4,4,4,,4,4,5,4,4
1010,重田 年昭,3,3,3,,2,3,2,4,3
体育(左から4番目)の成績が全員正しくカンマのみになりました。
先へ進みましょう。
各生徒・各科目の評価のほかに成績一覧表に加えたい項目は、各生徒の「評定合計」「科目数」「評定平均」「ランキング」、各科目の「合計」「平均」などです。
これらは実際の成績一覧表にはたいてい入っているので、ここでも入れておくべきでしょう。
まず各生徒のデータですが、「評定合計」「科目数」「評定平均」は書き出しながらカウントあるいは計算できるので、今の書き出しループ内にその処理を書き加えて、それぞれの生徒成績の右端にそれらを追加するような処理が可能です。
問題は「ランキング」です。
こればかりは「全員の評定合計」あるいは「全員の評定平均」が出てからでないと割り出せないので、「評定合計」「科目数」「評定平均」と同様の処理は不可能です。
成績を書き出す前に求めておく必要があるでしょう。
次に各科目の「合計」「平均」について。
これらは各学年の表の最下行に「付け加える」形になるので、表を書き出しながらでもできないことはないでしょう。
例えば、1年生の一覧表なら9科目分の科目別構造体配列を用意しておいて、生徒の成績を書き出しつつ、その科目別構造体には「合計」「データの個数」を入れていき、最後に「平均値」を計算して、全科目分を一気に書き出す、というような方法でいけると思います。
しかし、今の書き出しループ内にこの処理を差し込むのはいかにも煩雑になりそうですし、各科目の合計と平均を出すくらいの処理なら別のループを回して計算しても大した負荷にはならないと思われるので、これも上の「ランキング」同様、今の書き出しループの外で事前に求めてから組み込むことにします。
では、まず生徒成績計算結果リストを作る関数から作ってみましょう。
ランキングは別の関数で求めることにします。
評定平均リストのソートで採用した外部配列を使う手法を、ここでも応用します。
上に書いた理由から、評定合計ではなく評定平均でのランキングとします。
また、定時制高校の学年を表す「4」はマジックナンバーなので、これと学年の文字列配列はマクロの方に移します。
#define GRADE_NUMBER 4
#define GRADE_ARRAY "1", "2", "3", "4"

//-----------------------------------------------------------------------------
//生徒成績計算結果リスト用構造体定義。
struct StudentCalc {
    struct StudentCalc *next;
    struct ListNode *to_st_node;	//対応する生徒ノードへのポインタ。
    int total[GRADE_NUMBER];        //評価合計。
    int sj_num[GRADE_NUMBER];       //科目数。
    double ave[GRADE_NUMBER];       //評価平均。
    int rank[GRADE_NUMBER];         //評価平均のランキング。
};

//=============================================================================
//プロトタイプ宣言追加。
struct StudentCalc *CalcGradeList(struct ListNode **headps);
void CGLValue(struct ListNode **headps, struct StudentCalc *scp_head_p);
int RankStudentCalc(struct StudentCalc *scp_head_p);

//=============================================================================
/*生徒成績計算結果リストを作る関数。*/
struct StudentCalc *CalcGradeList(struct ListNode **headps) {
    struct StudentCalc *scp_head_p = NULL, *scp, *pre_scp = NULL, *next_scp;
    struct ListNode *seitomeibo_head_p, *p;
    int is_rank;
    
    seitomeibo_head_p = headps[0];
    
    p = seitomeibo_head_p;
    while (p != NULL) {            //生徒数分のノードを作るためのループ。
        scp = malloc(sizeof(struct StudentCalc));
        if (scp == NULL) {
            puts("生徒別成績集計リストが作れませんでした。");
            scp = scp_head_p;
            while (scp != NULL) {
                next_scp = scp->next;
                free(scp);
                scp = next_scp;
            }
            scp_head_p = NULL;     //リストが作れなければNULLを返す。
            return scp_head_p;
        }
        scp->next = NULL;          //先にnextと生徒ノードへのポインタを埋める。
        scp->to_st_node = p;
        if (scp_head_p == NULL) {
            scp_head_p = scp;
        } else {
            pre_scp->next = scp;
        }
        pre_scp = scp;
        p = p->next;
    }
        //ランキング以外を、当リストのノードに格納する関数を呼ぶ。
    CGLValue(headps, scp_head_p);
        //ランキングを求め、当リストのノードに格納する関数を呼ぶ。
    is_rank = RankStudentCalc(scp_head_p);
    if (is_rank != 0) {            //ランキングのリストが作れなければ
        scp_head_p = NULL;         //NULLを返す。
    }
    return scp_head_p;
}

//=============================================================================
//生徒成績計算結果リストの各ノードに値を格納する関数。
void CGLValue(struct ListNode **headps, struct StudentCalc *scp_head_p) {
    struct StudentCalc *scp;
    struct ListNode *kamokuhyo_head_p, *seisekihyo_head_p, *p, *q;
    const char *gakunen[GRADE_NUMBER] = {GRADE_ARRAY};
    int i, gokei = 0, kosu = 0;
    double heikin;
    
    kamokuhyo_head_p = headps[1];
    seisekihyo_head_p = headps[2];
    
    for (i = 0; i < GRADE_NUMBER; i++) {  //学年数分ループを回す。
        scp = scp_head_p;        //生徒リストの代わりに、上で作った
        while (scp != NULL) {    //生徒成績計算結果リストをループに組み込む。
            p = kamokuhyo_head_p;
            while (p != NULL) {
                q = seisekihyo_head_p;
                while (q != NULL) {
                    if ((strcmp(scp->to_st_node->member[0], 
                                q->member[1]) == 0) &&
                        (strcmp(p->member[0], q->member[0]) == 0) &&
                        (strcmp(p->member[2], gakunen[i]) == 0)) {
                        gokei += (int)strtol(q->member[2], NULL, 10);
                        kosu++;
                        break;
                    }
                    q = q->next;
                }
                p = p->next;
            }
            if (gokei * kosu != 0) {
                heikin = RoundingSecond((double)gokei / kosu);
            } else {
                heikin = 0.0;
            }
            scp->total[i] = gokei;
            scp->sj_num[i] = kosu;
            scp->ave[i] = heikin;
            scp->rank[i] = 0;    //ランキングは別関数で埋める。ここでは仮の値。
            gokei = 0; kosu = 0;
            
            scp = scp->next;
        }
    }
}

//=============================================================================
/*各生徒の学年ごとのランキングを求めて生徒成績計算結果構造体に書き込む関数。*/
//ソート用配列でソートしてからランキングを出す。
int RankStudentCalc(struct StudentCalc *scp_head_p) {
    int node_num = 0, i = 0, j, m, g;
    struct StudentCalc *scp, **sca, *tmp;
    
    scp = scp_head_p;        //生徒成績計算結果リストのノード数を求める。
    while (scp != NULL) {
        node_num++;
        scp = scp->next;
    }
        //ランキングのための外部配列を作る。
    sca = malloc(sizeof(struct StudentCalc *) * (size_t)(node_num + 1));
    if (sca == NULL) {
        puts("ソート用配列が作れませんでした。");
        return 1;
    }
    sca[node_num] = NULL;    //番兵をNULLとする。
    
    scp = scp_head_p;        //外部配列に生徒成績計算結果リストのノードへの
    while (scp != NULL) {    //ポインタを代入。
        sca[i++] = scp;
        scp = scp->next;
    }
    
    for (g = 0; g < GRADE_NUMBER; g++) {        //学年数分ループを回す。
        for (i = 0; i < node_num - 1; i++) {    //以下、外部配列によるソート。
            m = i;
            for (j = i + 1; j < node_num; j++) {
                if ((int)(sca[m]->ave[g] * 10) < (int)(sca[j]->ave[g] * 10)) {
                    m = j;
                }
            }
            if (i != m) {
                tmp = sca[i];
                sca[i] = sca[m];
                sca[m] = tmp;
            }
        }
        sca[0]->rank[g] = 1;      //すでに降順になっているので、最初は1位。
        i = 1;
        while (sca[i] != NULL) {  //平均が同点なら同順位、違えばループ順 + 1。
            if ((int)(sca[i - 1]->ave[g] * 10) == (int)(sca[i]->ave[g] * 10)) {
                sca[i]->rank[g] = sca[i - 1]->rank[g];
            } else {
                sca[i]->rank[g] = i + 1;
            }
            i++;
        }
    }
    free(sca);    //ランキングが入ればソート用配列は不要になるので解放する。
    return 0;
}

//=============================================================================
int main(void) {
    const char *file_name[] = {
        "seitomeibo.csv", "kamokuhyo.csv", "seisekihyo.csv"
    };
    int file_num, i;
    struct ListNode **headps;
    struct StudentCalc *scp_head_p, *scp, *scp_next_p;
     
    setvbuf(stdout, NULL, _IONBF, 0);
     
    file_num = sizeof(file_name) / sizeof(char *);
     
    headps = Loop_smList(file_name, file_num);
    if (headps == NULL) {exit(1);}
        //生徒成績計算結果リストを作る関数を作る関数を呼ぶ。
    scp_head_p = CalcGradeList(headps);
    
    printf("\n");        //以下、それを確認表示してから解放する処理。
    scp = scp_head_p;
    while (scp != NULL) {
        printf("%s %s %d/%d/%d/%d %.1f/%.1f/%.1f/%.1f %d/%d/%d/%d\n", 
            scp->to_st_node->member[0], 
            scp->to_st_node->member[1], 
            scp->total[0], scp->total[1], scp->total[2], scp->total[3], 
            scp->ave[0], scp->ave[1], scp->ave[2], scp->ave[3], 
            scp->rank[0], scp->rank[1], scp->rank[2], scp->rank[3]
            );
        scp = scp->next;
    }
    scp = scp_head_p;
    while (scp != NULL) {
        scp_next_p = scp->next;
        free(scp);
        scp = scp_next_p;
    }
    
    for (i = 0; i < file_num; i++) {
        ListFree(headps[i]);
    }
     
    free(headps);
    return 0;
}
う~ん、ずいぶん長くなってしまいました。
ともあれ、これを実行してみると……
1001 岡 胡桃 32/30/31/26 3.6/3.3/3.4/3.3 7/6/5/8
1002 川合 睦美 34/35/35/32 3.8/3.9/3.9/4.0 3/4/4/3
1003 武井 康代 44/44/44/39 4.9/4.9/4.9/4.9 2/1/1/1
1004 西村 瑠璃 34/36/36/32 3.8/4.0/4.0/4.0 3/3/3/3
1005 藤川 小梅 28/30/30/27 3.1/3.3/3.3/3.4 9/6/6/7
1006 森本 双葉 45/33/27/32 5.0/3.7/3.0/4.0 1/5/8/3
1007 渡部 泉 33/29/30/31 3.7/3.2/3.3/3.9 5/8/6/6
1008 井本 秀樹 19/19/19/18 2.1/2.1/2.1/2.3 10/10/10/10
1009 小沼 正人 33/38/38/37 3.7/4.2/4.2/4.6 5/2/2/2
1010 重田 年昭 29/27/26/22 3.2/3.0/2.9/2.8 8/9/9/9
大丈夫そうです。
表示されたのは、左から順に、生徒コード・氏名・各学年の評価合計・各学年の評定平均・各学年の評定平均ランキングです。
次に科目成績計算結果リストを作る関数です。
//科目成績計算結果リスト用構造体定義。
struct SubjectCalc {    //こちらは四年間で一度なので、配列を持つメンバはない。
    struct SubjectCalc *next;
    struct ListNode *to_sj_node;    //科目リストへのポインタ。
    int total;    //科目評価合計。
    int cnt;      //評価数。今回は使わないが、アラインメント警告防止になる。
    double ave;   //科目評価平均。
};

//=============================================================================
//プロトタイプ宣言追加。
struct SubjectCalc *SubjectAverageList(struct ListNode **headps);
void SALValue(struct ListNode **headps, struct SubjectCalc *sjcp_head_p);

//=============================================================================
/*科目成績計算結果リストを作る関数。*/
struct SubjectCalc *SubjectAverageList(struct ListNode **headps) {
    struct SubjectCalc *sjcp_head_p = NULL, *sjcp, 
                       *pre_sjcp = NULL, *next_sjcp;
    struct ListNode *kamokuhyo_head_p, *p;
    
    kamokuhyo_head_p = headps[1];
    
    p = kamokuhyo_head_p;
    while (p != NULL) {            //科目数分のノードを作るためのループ。
        sjcp = malloc(sizeof(struct SubjectCalc));
        if (sjcp == NULL) {
            puts("科目平均値リストが作れませんでした。");
            sjcp = sjcp_head_p;
            while (sjcp != NULL) {
                next_sjcp = sjcp->next;
                free(sjcp);
                sjcp = next_sjcp;
            }
            sjcp_head_p = NULL;     //リストが作れなければNULLを返す。
            return sjcp_head_p;
        }
        sjcp->next = NULL;          //先にnextと科目ノードへのポインタを代入。
        sjcp->to_sj_node = p;
        if (sjcp_head_p == NULL) {
            sjcp_head_p = sjcp;
        } else {
            pre_sjcp->next = sjcp;
        }
        pre_sjcp = sjcp;
        p = p->next;
    }
        //各科目の評価合計と平均を、当リストのノードに格納する関数を呼ぶ。
    SALValue(headps, sjcp_head_p);
    
    return sjcp_head_p;
}

//=============================================================================
/*科目成績計算結果リストの各ノードに値を格納する関数。*/
void SALValue(struct ListNode **headps, struct SubjectCalc *sjcp_head_p) {
    struct SubjectCalc *sjcp;
    struct ListNode *seitomeibo_head_p, *seisekihyo_head_p, *p, *q;
    int gokei = 0, kosu = 0;
    double heikin;
    
    seitomeibo_head_p = headps[0];
    seisekihyo_head_p = headps[2];
    
    sjcp = sjcp_head_p;       //科目リストの代わりに、上で作った
    while (sjcp != NULL) {    //科目成績計算結果リストをループに組み込む。
        p = seitomeibo_head_p;
        while (p != NULL) {
            q = seisekihyo_head_p;
            while (q != NULL) {
                if ((strcmp(sjcp->to_sj_node->member[0], q->member[0]) == 0)
                    && (strcmp(p->member[0], q->member[1]) == 0)) {
                    gokei += (int)strtol(q->member[2], NULL, 10);
                    kosu++;
                }
                q = q->next;
            }
            p = p->next;
        }
        if (gokei * kosu != 0) {
            heikin = RoundingSecond((double)gokei / kosu);
        } else {
            heikin = 0.0;
        }
        sjcp->total = gokei;
        sjcp->cnt = kosu;
        sjcp->ave = heikin;
        gokei = 0; kosu = 0;
        
        sjcp = sjcp->next;
    }
}

//=============================================================================
int main(void) {
    const char *file_name[] = {
        "seitomeibo.csv", "kamokuhyo.csv", "seisekihyo.csv"
    };
    int file_num, i;
    struct ListNode **headps;
    struct SubjectCalc *sjcp_head_p, *sjcp, *sjcp_next_p;
     
    setvbuf(stdout, NULL, _IONBF, 0);
     
    file_num = sizeof(file_name) / sizeof(char *);
     
    headps = Loop_smList(file_name, file_num);
    if (headps == NULL) {exit(1);}
        //科目成績計算結果リストを作る関数を作る関数を呼ぶ。
    sjcp_head_p = SubjectAverageList(headps);
    
    sjcp = sjcp_head_p;        //以下、それを表示してから解放する。
    while (sjcp != NULL) {
        printf("%s %s %s年 %d %d %.1f\n", 
            sjcp->to_sj_node->member[0], 
            sjcp->to_sj_node->member[1], 
            sjcp->to_sj_node->member[2], 
            sjcp->total, sjcp->cnt, sjcp->ave
            );
        sjcp = sjcp->next;
    }
    sjcp = sjcp_head_p;
    while (sjcp != NULL) {
        sjcp_next_p = sjcp->next;
        free(sjcp);
        sjcp = sjcp_next_p;
    }
    
    for (i = 0; i < file_num; i++) {
        ListFree(headps[i]);
    }
     
    free(headps);
    return 0;
}
ランキングがない分、生徒成績計算結果リストよりも短くなってはいるけれど、それでも長い……_| ̄|○。
コードの横にも書きましたが、今回のプログラムでは各科目の評価数は表示しませんので、cntメンバはなくてもかまいません(平均算出時の除算は使い捨てのkosu変数を使っている)。
しかし、これをメンバから抜くと、コンパイラの設定によってはアラインメント警告が出ることがあるので、一応入れておくことにします。
これも表示確認してみると……
20101 国語総合 1年 35 10 3.5
20301 現代社会 1年 38 10 3.8
20401 数学Ⅰ 1年 32 10 3.2
20501 科学と人間生活 1年 35 10 3.5
20601 体育 1年 39 10 3.9
20605 保健 1年 39 10 3.9
20801 コミュニケーション英語基礎 1年 36 10 3.6
21101 ビジネス基礎 1年 39 10 3.9
21102 簿記 1年 38 10 3.8
20102 国語総合 2年 36 10 3.6
20201 地理A 2年 37 10 3.7
20402 数学Ⅰ 2年 34 10 3.4
20602 体育 2年 37 10 3.7
20606 保健 2年 31 10 3.1
20701 音楽Ⅰ 2年 37 10 3.7
20802 コミュニケーション英語基礎 2年 34 10 3.4
21103 簿記 2年 38 10 3.8
21104 ビジネス実務 2年 37 10 3.7
20103 現代文A 3年 35 10 3.5
20202 世界史A 3年 34 10 3.4
20502 化学基礎 3年 33 10 3.3
20603 体育 3年 36 10 3.6
20702 音楽Ⅰ 3年 35 10 3.5
20803 コミュニケーション英語Ⅰ 3年 37 10 3.7
21105 ビジネス実務 3年 37 10 3.7
21106 情報処理 3年 34 10 3.4
21107 財務会計Ⅰ 3年 35 10 3.5
20104 国語表現 4年 37 10 3.7
20302 政治経済 4年 38 10 3.8
20604 体育 4年 37 10 3.7
20804 コミュニケーション英語Ⅰ 4年 37 10 3.7
20901 家庭基礎 4年 38 10 3.8
21108 財務会計Ⅰ 4年 36 10 3.6
21109 経済活動と法 4年 36 10 3.6
21110 課題研究 4年 37 10 3.7
正しい結果が出たようです。
表示されたのは、左から順に、科目コード・科目名・履修学年・科目評定合計・科目評定数・科目評定平均です。
では、上の一覧表を作る関数に、これらを組み込んでみましょう。
CalcGradeList関数とSubjectAverageList関数がNULLを返してきた場合は一覧表が作れませんので、その場合は1を返して一覧表作成を中止するようにします。
//=============================================================================
//プロトタイプ宣言変更。
int GradeTable(struct ListNode **headps);

//プロトタイプ宣言追加。
void SubjTotalAve(const int year, const int gokei, const int kosu, 
                  struct SubjectCalc *sjcp_head_p);
void GTFree(struct StudentCalc *scp_head_p, struct SubjectCalc *sjcp_head_p);

//=============================================================================
/*成績一覧表を作る関数。*/
int GradeTable(struct ListNode **headps) {
    struct ListNode *seitomeibo_head_p, *kamokuhyo_head_p, 
                    *seisekihyo_head_p, *p, *q, *r;
    struct StudentCalc *scp_head_p, *scp;
    struct SubjectCalc *sjcp_head_p;
    const char *gakunen[GRADE_NUMBER] = {GRADE_ARRAY};
    int i, flg = 0, gokei = 0, kosu = 0;
    
    seitomeibo_head_p = headps[0];
    kamokuhyo_head_p = headps[1];
    seisekihyo_head_p = headps[2];
        //生徒成績計算結果リストを作る関数を呼ぶ。
    scp_head_p = CalcGradeList(headps);
    if (scp_head_p == NULL) {return 1;}
        //科目成績計算結果リストを作る関数を呼ぶ。
    sjcp_head_p = SubjectAverageList(headps);
    if (sjcp_head_p == NULL) {return 1;}
    
    for (i = 0; i < GRADE_NUMBER; i++) {    //学年数分ループする。
        printf("\n");
        printf("第%s学年成績一覧表\n", gakunen[i]);
        printf("番号,氏名");
        p = kamokuhyo_head_p;    //その学年の科目名を列挙。
        while (p != NULL) {
            if (strcmp(p->member[2], gakunen[i]) == 0) {
                printf(",%s", p->member[1]);
            }
            p = p->next;
        }
        printf(",合計,科目数,平均,平均順位");
        printf("\n");
        
        p = seitomeibo_head_p;
        while (p != NULL) {
            printf("%s,%s", p->member[0], p->member[1]);
            q = kamokuhyo_head_p;
            while (q != NULL) {
                r = seisekihyo_head_p;
                while (r != NULL) {
                    if((strcmp(p->member[0], r->member[1]) == 0) &&
                        (strcmp(q->member[0], r->member[0]) == 0) &&
                        (strcmp(q->member[2], gakunen[i]) == 0)) {
                        printf(",%s", r->member[2]);
                        flg = 1;   //↓学年の評価総計。
                        gokei += (int)strtol(r->member[2], NULL, 10);
                        kosu++;    //学年の全評価平均のための評価総数。
                        break;
                    }
                    r = r->next;
                }
                if ((flg == 0) && (strcmp(q->member[2], gakunen[i]) == 0)) {
                    printf(",");
                }
                flg = 0;
                q = q->next;
            }
            scp = scp_head_p;    //右端に評定合計・科目数・平均・順位を入れる。
            while (scp != NULL) {
                if (strcmp(p->member[0], scp->to_st_node->member[0]) == 0) {
                    printf(",%d,%d,%.1f,%d", 
                    scp->total[i], scp->sj_num[i], scp->ave[i], scp->rank[i]);
                }
                scp = scp->next;
            }
            p = p->next;
            printf("\n");
        }   //↓科目合計と科目平均を記入する関数を呼ぶ。
        SubjTotalAve(i, gokei, kosu, sjcp_head_p);
        gokei = 0; kosu = 0;
    }  //↓生徒成績計算結果リストと科目成績計算結果リストを解放する関数を呼ぶ。
    GTFree(scp_head_p, sjcp_head_p);
    return 0;
}

//=============================================================================
/*成績一覧表に科目合計と科目平均を記入する関数。*/
void SubjTotalAve(const int year, const int gokei, const int kosu, 
                  struct SubjectCalc *sjcp_head_p) {
    int i;
    struct SubjectCalc *sjcp;
    const char *gakunen[GRADE_NUMBER] = {GRADE_ARRAY};
    double heikin;
    
    for (i = 0; i < 2; i++) {    //合計と平均の二行を表示するので二回。
        if (i == 0) {
            printf(",科目合計");
        } else {
            printf(",科目平均");
        }
        sjcp = sjcp_head_p;      //学年が合致したらその科目の値を表示。
        while (sjcp != NULL) {
            if (strcmp(sjcp->to_sj_node->member[2], gakunen[year]) == 0) {
                if (i == 0) {
                    printf(",%d", sjcp->total);
                } else {
                    printf(",%.1f", sjcp->ave);
                }
            }
            sjcp = sjcp->next;
        }
        if (i == 0) {            //右端に評価総計と全評価平均を求めて表示する。
            printf(",%d\n", gokei);
        } else {
            if (gokei * kosu != 0) {
                heikin = RoundingSecond((double)gokei / kosu);
            } else {
                heikin = 0.0;
            }
            printf(",%.1f\n", heikin);
        }
    }
}

//=============================================================================
/*生徒成績計算結果リストと科目成績計算結果リストを解放する関数。*/
void GTFree(struct StudentCalc *scp_head_p, struct SubjectCalc *sjcp_head_p) {
    struct StudentCalc *scp, *scp_next;
    struct SubjectCalc *sjcp, *sjcp_next;
    
    scp = scp_head_p;            //生徒成績計算結果リストの解放。
    while (scp != NULL) {
        scp_next = scp->next;
        free(scp);
        scp = scp_next;
    }
    sjcp = sjcp_head_p;          //科目成績計算結果リストの解放。
    while (sjcp != NULL) {
        sjcp_next = sjcp->next;
        free(sjcp);
        sjcp = sjcp_next;
    }
}

//=============================================================================
int main(void) {
    const char *file_name[] = {
        "seitomeibo.csv", "kamokuhyo.csv", "seisekihyo.csv"
    };
    int file_num, i, is_gt;
    struct ListNode **headps;
     
    setvbuf(stdout, NULL, _IONBF, 0);
     
    file_num = sizeof(file_name) / sizeof(char *);
     
    headps = Loop_smList(file_name, file_num);
    if (headps == NULL) {exit(1);}
    
    is_gt = GradeTable(headps);    //一覧表を作る関数を呼ぶ。
    if (is_gt != 0) {
        puts("一覧表が作れませんでした。");
    }
    
    for (i = 0; i < file_num; i++) {
        ListFree(headps[i]);
    }
     
    free(headps);
    return 0;
}
第1学年成績一覧表
番号,氏名,国語総合,現代社会,数学Ⅰ,科学と人間生活,体育,保健,コミュニケーション英語基礎,ビジネス基礎,簿記,合計,科目数,平均,平均順位
1001,岡 胡桃,3,4,3,4,3,4,4,4,3,32,9,3.6,7
1002,川合 睦美,3,5,2,2,5,4,4,5,4,34,9,3.8,3
1003,武井 康代,5,5,5,5,4,5,5,5,5,44,9,4.9,2
1004,西村 瑠璃,4,3,4,4,4,3,4,4,4,34,9,3.8,3
1005,藤川 小梅,3,3,3,3,4,3,3,3,3,28,9,3.1,9
1006,森本 双葉,5,5,5,5,5,5,5,5,5,45,9,5.0,1
1007,渡部 泉,5,4,2,4,3,5,2,4,4,33,9,3.7,5
1008,井本 秀樹,2,2,2,2,3,2,2,2,2,19,9,2.1,10
1009,小沼 正人,3,3,3,3,4,4,4,5,4,33,9,3.7,5
1010,重田 年昭,2,4,3,3,4,4,3,2,4,29,9,3.2,8
,科目合計,35,38,32,35,39,39,36,39,38,331
,科目平均,3.5,3.8,3.2,3.5,3.9,3.9,3.6,3.9,3.8,3.7

第2学年成績一覧表
番号,氏名,国語総合,地理A,数学Ⅰ,体育,保健,音楽Ⅰ,コミュニケーション英語基礎,簿記,ビジネス実務,合計,科目数,平均,平均順位
1001,岡 胡桃,3,3,3,3,3,4,3,4,4,30,9,3.3,6
1002,川合 睦美,5,4,2,4,3,4,5,4,4,35,9,3.9,4
1003,武井 康代,5,5,5,4,5,5,5,5,5,44,9,4.9,1
1004,西村 瑠璃,4,4,4,4,4,4,4,4,4,36,9,4.0,3
1005,藤川 小梅,3,4,3,3,3,4,3,4,3,30,9,3.3,6
1006,森本 双葉,4,4,3,4,3,4,3,4,4,33,9,3.7,5
1007,渡部 泉,3,4,5,3,2,3,2,3,4,29,9,3.2,8
1008,井本 秀樹,2,2,2,3,2,2,2,2,2,19,9,2.1,10
1009,小沼 正人,4,4,4,5,4,4,5,4,4,38,9,4.2,2
1010,重田 年昭,3,3,3,4,2,3,2,4,3,27,9,3.0,9
,科目合計,36,37,34,37,31,37,34,38,37,321
,科目平均,3.6,3.7,3.4,3.7,3.1,3.7,3.4,3.8,3.7,3.6

第3学年成績一覧表
番号,氏名,現代文A,世界史A,化学基礎,体育,音楽Ⅰ,コミュニケーション英語Ⅰ,ビジネス実務,情報処理,財務会計Ⅰ,合計,科目数,平均,平均順位
1001,岡 胡桃,4,3,4,3,3,4,4,3,3,31,9,3.4,5
1002,川合 睦美,4,4,3,4,4,4,4,4,4,35,9,3.9,4
1003,武井 康代,5,5,5,4,5,5,5,5,5,44,9,4.9,1
1004,西村 瑠璃,4,4,4,4,4,4,4,4,4,36,9,4.0,3
1005,藤川 小梅,4,3,3,3,4,3,3,3,4,30,9,3.3,6
1006,森本 双葉,3,3,3,3,3,3,3,3,3,27,9,3.0,8
1007,渡部 泉,2,3,3,3,3,5,4,4,3,30,9,3.3,6
1008,井本 秀樹,2,2,2,3,2,2,2,2,2,19,9,2.1,10
1009,小沼 正人,4,4,4,5,4,4,5,4,4,38,9,4.2,2
1010,重田 年昭,3,3,2,4,3,3,3,2,3,26,9,2.9,9
,科目合計,35,34,33,36,35,37,37,34,35,316
,科目平均,3.5,3.4,3.3,3.6,3.5,3.7,3.7,3.4,3.5,3.5

第4学年成績一覧表
番号,氏名,国語表現,政治経済,体育,コミュニケーション英語Ⅰ,家庭基礎,財務会計Ⅰ,経済活動と法,課題研究,合計,科目数,平均,平均順位
1001,岡 胡桃,3,4,3,3,4,3,3,3,26,8,3.3,8
1002,川合 睦美,4,4,4,4,4,4,4,4,32,8,4.0,3
1003,武井 康代,5,5,4,5,5,5,5,5,39,8,4.9,1
1004,西村 瑠璃,4,4,4,4,4,4,4,4,32,8,4.0,3
1005,藤川 小梅,4,3,3,3,4,4,3,3,27,8,3.4,7
1006,森本 双葉,4,4,4,4,4,4,4,4,32,8,4.0,3
1007,渡部 泉,4,5,3,3,3,4,4,5,31,8,3.9,6
1008,井本 秀樹,2,2,4,2,2,2,2,2,18,8,2.3,10
1009,小沼 正人,5,5,5,5,4,4,4,5,37,8,4.6,2
1010,重田 年昭,2,2,3,4,4,2,3,2,22,8,2.8,9
,科目合計,37,38,37,37,38,36,36,37,296
,科目平均,3.7,3.8,3.7,3.7,3.8,3.6,3.6,3.7,3.7
これでやっと、欲しかった値がすべて表示されました。
この表示では分かりにくいので、これをコンソール画面からコピーしてテキストエディタに貼り付けて保存し、拡張子をtxtからcsvに変更してExcelで読み込んでみると……
どうやら正しい成績一覧表ができたようです。
この画像では3年生以下が見えませんが、もちろん、4年生までちゃんとあります。
計算して出した値、すなわち各生徒の評価合計や評価平均、その順位、科目ごとの評価合計や評価平均、それらすべての合計と平均など、手計算とすべて一致しましたので、正確な値が計算されていると見てよいでしょう。
最初の予想よりもはるかに面倒なコードを書くことになりましたが、実行すると一瞬で終わります。


それでは最後に、処理をユーザーに選択させる関数を作っておきましょう。
その前に、評定平均リストを表示するためのGADisplay関数を新たに作ります。
その際、表示をすべてカンマ区切りに統一するため、上の確認表示では半角スペース区切りとしましたが、GADisplay関数ではカンマ区切りとします。
引数selectedは、ユーザーが選んだ処理の選択番号です。
さらに、評定平均ソート用配列と評定平均リストを解放するためのGAFree関数も作ります。
//=============================================================================
//プロトタイプ宣言追加。
void GADisplay(struct Hyotei **sa, struct Hyotei *hyotei_head_p, 
               const int selected);
void GAFree(struct Hyotei **sa, struct Hyotei *hyotei_head_p);

//=============================================================================
/*評定平均リストを番号順または成績順に表示する関数。*/
void GADisplay(struct Hyotei **sa, struct Hyotei *hyotei_head_p, 
               const int selected) {
    int i = 0;
    struct Hyotei *hyotei_p;
      //switch文で書く場合は、selected == 3をfall throughすることになる。
      //意図したものなのにGCCで-Wextraを付けるとfall through警告が出てしまう。
      //煩わしいので、switch文はやめてdo~while文とif文の組み合わせとした。
    do {
        if ((selected == 1) || (selected == 3)) {
            puts("評定平均値番号順");
            hyotei_p = hyotei_head_p;
            while (hyotei_p != NULL) {
                printf("%s,%s,%.1f\n", 
                    hyotei_p->to_st_node->member[0], 
                    hyotei_p->to_st_node->member[1], 
                    hyotei_p->hyotei);
                hyotei_p = hyotei_p->next;
            }
            if (selected == 1) {break;}
            else {printf("\n");}
        }
        if ((selected == 2) || (selected == 3)) {
            puts("評定平均値成績降順");
            while (sa[i] != NULL) {
                printf("%s,%s,%.1f\n", 
                    sa[i]->to_st_node->member[0], 
                    sa[i]->to_st_node->member[1], 
                    sa[i]->hyotei);
                i++;
            }
        }
    } while (0);
}

//=============================================================================
/*評定平均ソート用配列と評定平均リストを解放する関数。*/
void GAFree(struct Hyotei **sa, struct Hyotei *hyotei_head_p) {
    struct Hyotei *hyotei_p, *hyotei_next_p;
    
    free(sa);
    
    hyotei_p = hyotei_head_p;
    while (hyotei_p != NULL) {
        hyotei_next_p = hyotei_p->next;
        free(hyotei_p);
        hyotei_p = hyotei_next_p;
    }
}
以下は、処理をユーザーに選択させる関数です。
//=============================================================================
//プロトタイプ宣言追加。
int OperationSelect(void);

//=============================================================================
/*処理をユーザーに選択させる関数。*/
int OperationSelect(void) {
    const char *str_select[] = {"0","1","2","3","4","5","6","7","8","9","a"};
    char str[3], select_lf_add[3];
    int i, select_num, selected = 0, flg = 0;
    
    select_num = sizeof(str_select) / sizeof(char *);
    
    puts("成績処理\n"
         "0:全csvファイル確認表示\n"
         "1:評定平均表表示(番号順)\n"
         "2:評定平均表表示(成績降順)\n"
         "3:評定平均表表示(番号順+成績降順)\n"
         "4:成績一覧表表示(第1学年)\n"
         "5:成績一覧表表示(第2学年)\n"
         "6:成績一覧表表示(第3学年)\n"
         "7:成績一覧表表示(第4学年)\n"
         "8:成績一覧表表示(全学年)\n"
         "9:終了\n"
         "a:csvファイルを読み込み直す");
    
    do {
        printf("番号または記号を入力してからEnterを押してください。\n"
               "入力:");
        fgets(str, sizeof(str), stdin);
        for (i = 0; i < select_num; i++) {
            sprintf(select_lf_add, "%s\n", str_select[i]);
            if (strcmp(str, select_lf_add) == 0) {
                selected = i;
                flg = 1;
                break;
            }
        }
        if (flg == 1) {
            break;
        } else {
            puts("入力値が不正です。");
            if (strchr(str, '\n') == NULL) {  //str中に\nがなければ……
                while(getchar() != '\n') {};    //3文字以上入力され、バッファに
            }                                   //余計な文字が残った状態なので
        }                                       //それらをgetcharでクリアする。
    } while (1);
    
    return selected;
}

//=============================================================================
int main(void) {
    const char *file_name[] = {
        "seitomeibo.csv", "kamokuhyo.csv", "seisekihyo.csv"
    };
    int file_num, i, is_gt, selected;
    struct ListNode **headps;
    struct Hyotei *hyotei_head_p, **sa = NULL;
        
    setvbuf(stdout, NULL, _IONBF, 0);    //バッファをオフにする。
        
    file_num = sizeof(file_name) / sizeof(char *);    //ファイル数を求める。
        //csvファイルから複数の構造体リストを作る関数を呼ぶ。
    headps = Loop_smList(file_name, file_num);
    if (headps == NULL) {exit(1);}
    
    do {
        selected = OperationSelect();
        switch (selected) {
        case 0:    //csvファイルのリストを順に表示する関数を呼ぶ。
            for (i = 0; i < file_num; i++) {
                ListDisplay(headps[i]);
            }
            printf("\n");
            continue;
        case 1:
        case 2:    //評定平均表とその降順表を作る関数を呼び、
        case 3:    //さらに評定平均リストを表示・解放する関数を呼ぶ。
            hyotei_head_p = GradeAverage(headps);
            if (hyotei_head_p != NULL) {
                sa = GASort(hyotei_head_p);
            }
            if ((sa != NULL) && (hyotei_head_p != NULL)) {
                printf("\n");
                GADisplay(sa, hyotei_head_p, selected);
                printf("\n");
                GAFree(sa, hyotei_head_p);
            }
            continue;
        case 4:
        case 5:
        case 6:
        case 7:
        case 8:    //一覧表を作る関数を呼ぶ。
            is_gt = GradeTable(headps, selected);
            if (is_gt != 0) {
                puts("一覧表が作れませんでした。");
                break;
            }
            printf("\n");
            continue;
        case 9:
            puts("終了します。");
            break;
        case 10:   //csvファイルを読み込み直して新しいリストを作る。
            for (i = 0; i < file_num; i++) {  //既存のcsvリストの解放。
                ListFree(headps[i]);
            }
            headps = Loop_smList(file_name, file_num);
            if (headps == NULL) {exit(1);}
            puts("\nファイルを読み込み直しました。\n");
        }
    } while (selected != 9);
        //以下は終了処理。csvリストとリスト先頭ポインタ配列を解放。
    for (i = 0; i < file_num; i++) {
        ListFree(headps[i]);
    }
    free(headps);
    return 0;
}
ご覧のように、成績一覧表の学年を選択できるようにしたので、先ほど変更したGradeTable関数を、さらに次のように変更します。
//=============================================================================
//プロトタイプ宣言変更。
int GradeTable(struct ListNode **headps, const int selected);

//=============================================================================
/*成績一覧表を作る関数(学年選択機能付き)。*/
int GradeTable(struct ListNode **headps, const int selected) {
    struct ListNode *seitomeibo_head_p, *kamokuhyo_head_p, 
                    *seisekihyo_head_p, *p, *q, *r;
    struct StudentCalc *scp_head_p, *scp;
    struct SubjectCalc *sjcp_head_p;
    const char *gakunen[GRADE_NUMBER] = {GRADE_ARRAY};
    int i, bg, ed, flg = 0, gokei = 0, kosu = 0;
    
    switch (selected) {      //bgとedはループの開始番号と終了番号。
    case 8:                  //全学年の一覧表。ループは学年数。
        bg = 0;
        ed = GRADE_NUMBER;
        break;
    default:                 //単独学年一覧表。ループは1回。
        bg = selected - 4;   //選択肢番号から4を引くと当該学年になる。
        ed = bg + 1;
    }
    
    seitomeibo_head_p = headps[0];
    kamokuhyo_head_p = headps[1];
    seisekihyo_head_p = headps[2];
    
    scp_head_p = CalcGradeList(headps);
    if (scp_head_p == NULL) {return 1;}
    
    sjcp_head_p = SubjectAverageList(headps);
    if (sjcp_head_p == NULL) {return 1;}
    
    for (i = bg; i < ed; i++) {
        printf("\n");

    //……以下同じにつき省略。
以上をすべて反映してまとめたものをに下記の場所に書き出しておきますので、本プログラムの全貌を通覧する場合はそちらをご覧ください(別ウィンドウが開きます)。
成績処理プログラム
実行するとこうなります。
成績処理
0:全csvファイル確認表示
1:評定平均表表示(番号順)
2:評定平均表表示(成績降順)
3:評定平均表表示(番号順+成績降順)
4:成績一覧表表示(第1学年)
5:成績一覧表表示(第2学年)
6:成績一覧表表示(第3学年)
7:成績一覧表表示(第4学年)
8:成績一覧表表示(全学年)
9:終了
a:csvファイルを読み込み直す
番号または記号を入力してからEnterを押してください。
入力:
どの番号を選択しても意図どおりの結果が表示されます。
ここでは二つだけ、3と5を選んでみます。
評定平均値番号順
1001,岡 胡桃,3.4
1002,川合 睦美,3.9
1003,武井 康代,4.9
1004,西村 瑠璃,3.9
1005,藤川 小梅,3.3
1006,森本 双葉,3.9
1007,渡部 泉,3.5
1008,井本 秀樹,2.1
1009,小沼 正人,4.2
1010,重田 年昭,3.0

評定平均値成績降順
1003,武井 康代,4.9
1009,小沼 正人,4.2
1002,川合 睦美,3.9
1004,西村 瑠璃,3.9
1006,森本 双葉,3.9
1007,渡部 泉,3.5
1001,岡 胡桃,3.4
1005,藤川 小梅,3.3
1010,重田 年昭,3.0
1008,井本 秀樹,2.1
第2学年成績一覧表
番号,氏名,国語総合,地理A,数学Ⅰ,体育,保健,音楽Ⅰ,コミュニケーション英語基礎,簿記,ビジネス実務,合計,科目数,平均,平均順位
1001,岡 胡桃,3,3,3,3,3,4,3,4,4,30,9,3.3,6
1002,川合 睦美,5,4,2,4,3,4,5,4,4,35,9,3.9,4
1003,武井 康代,5,5,5,4,5,5,5,5,5,44,9,4.9,1
1004,西村 瑠璃,4,4,4,4,4,4,4,4,4,36,9,4.0,3
1005,藤川 小梅,3,4,3,3,3,4,3,4,3,30,9,3.3,6
1006,森本 双葉,4,4,3,4,3,4,3,4,4,33,9,3.7,5
1007,渡部 泉,3,4,5,3,2,3,2,3,4,29,9,3.2,8
1008,井本 秀樹,2,2,2,3,2,2,2,2,2,19,9,2.1,10
1009,小沼 正人,4,4,4,5,4,4,5,4,4,38,9,4.2,2
1010,重田 年昭,3,3,3,4,2,3,2,4,3,27,9,3.0,9
,科目合計,36,37,34,37,31,37,34,38,37,321
,科目平均,3.6,3.7,3.4,3.7,3.1,3.7,3.4,3.8,3.7,3.6
画面が書き換えられたのが分からないくらい一瞬で表示されますので、速度的にも問題ないでしょう(まあファイルが小さいということもありますが)。
さて、今は4年生まで完全なデータが入ったファイルを使っているけれど、データが不完全なファイルを読ませたらどうなるでしょうか。
試してみましょう。
まずは生徒名簿ファイルから、行を適当に削除し、最後に余計な生徒「寿限無くん」(笑)を付け加えてみます。
本プログラムは可変にこだわっているので、彼程度の長さの名前なら何の問題もなく対応できるはずです。
なお、「csvファイルを読み込み直す」というコマンドがありますので、ファイルを書き換えるたびにいちいちプログラムを起動し直すことはせず、コマンド選択肢「a」を選んで試してみることにします。
評定平均値成績降順
1009,小沼 正人,4.2
1002,川合 睦美,3.9
1001,岡 胡桃,3.4
1010,重田 年昭,3.0
1011,寿限無 寿限無 五劫の擦り切れ 海砂利水魚の水行末 雲来末 風来末 食う寝る処に住む処 藪ら柑子の藪柑子 パイポ パイポ パイポのシューリンガン シューリンガンのグーリンダイ グーリンダイのポンポコピーのポンポコナーの長久命の長助,0.0
-------------------------------------------------------------------------------
第4学年成績一覧表
番号,氏名,国語表現,政治経済,体育,コミュニケーション英語Ⅰ,家庭基礎,財務会計Ⅰ,経済活動と法,課題研究,合計,科目数,平均,平均順位
1001,岡 胡桃,3,4,3,3,4,3,3,3,26,8,3.3,3
1002,川合 睦美,4,4,4,4,4,4,4,4,32,8,4.0,2
1009,小沼 正人,5,5,5,5,4,4,4,5,37,8,4.6,1
1010,重田 年昭,2,2,3,4,4,2,3,2,22,8,2.8,4
1011,寿限無 寿限無 五劫の擦り切れ 海砂利水魚の水行末 雲来末 風来末 食う寝る処に住む処 藪ら柑子の藪柑子 パイポ パイポ パイポのシューリンガン シューリンガンのグーリンダイ グーリンダイのポンポコピーのポンポコナーの長久命の長助,,,,,,,,,0,0,0.0,5
,科目合計,14,15,15,16,16,13,14,14,117
,科目平均,3.5,3.8,3.8,4.0,4.0,3.3,3.5,3.5,3.7
大丈夫そうです。
寿限無くんは評定平均0.0、順位は最下位、科目の評定合計と評定平均は四人分で計算されており、成績がまったくない寿限無くんの分が含まれていないようなのは期待どおりです。
次は科目ファイルの4行目から31行目を削除し、最後に余計な科目「21111,●●C言語とC++言語開発,4」を付け加えてみしょう(全部で8科目)。
評定平均値成績降順
1003,武井 康代,5.0
1006,森本 双葉,4.4
1004,西村 瑠璃,3.9
1007,渡部 泉,3.9
1002,川合 睦美,3.7
1009,小沼 正人,3.7
1001,岡 胡桃,3.3
1005,藤川 小梅,3.3
1010,重田 年昭,2.9
1008,井本 秀樹,2.0
-------------------------------------------------------------------------------
第1学年成績一覧表
番号,氏名,国語総合,現代社会,数学Ⅰ,合計,科目数,平均,平均順位
1001,岡 胡桃,3,4,3,10,3,3.3,5
1002,川合 睦美,3,5,2,10,3,3.3,5
1003,武井 康代,5,5,5,15,3,5.0,1
1004,西村 瑠璃,4,3,4,11,3,3.7,3
1005,藤川 小梅,3,3,3,9,3,3.0,7
1006,森本 双葉,5,5,5,15,3,5.0,1
1007,渡部 泉,5,4,2,11,3,3.7,3
1008,井本 秀樹,2,2,2,6,3,2.0,10
1009,小沼 正人,3,3,3,9,3,3.0,7
1010,重田 年昭,2,4,3,9,3,3.0,7
,科目合計,35,38,32,105
,科目平均,3.5,3.8,3.2,3.5

第2学年成績一覧表
番号,氏名,合計,科目数,平均,平均順位
1001,岡 胡桃,0,0,0.0,1
1002,川合 睦美,0,0,0.0,1
1003,武井 康代,0,0,0.0,1
1004,西村 瑠璃,0,0,0.0,1
1005,藤川 小梅,0,0,0.0,1
1006,森本 双葉,0,0,0.0,1
1007,渡部 泉,0,0,0.0,1
1008,井本 秀樹,0,0,0.0,1
1009,小沼 正人,0,0,0.0,1
1010,重田 年昭,0,0,0.0,1
,科目合計,0
,科目平均,0.0

第3学年成績一覧表
番号,氏名,合計,科目数,平均,平均順位
1001,岡 胡桃,0,0,0.0,1
1002,川合 睦美,0,0,0.0,1
1003,武井 康代,0,0,0.0,1
1004,西村 瑠璃,0,0,0.0,1
1005,藤川 小梅,0,0,0.0,1
1006,森本 双葉,0,0,0.0,1
1007,渡部 泉,0,0,0.0,1
1008,井本 秀樹,0,0,0.0,1
1009,小沼 正人,0,0,0.0,1
1010,重田 年昭,0,0,0.0,1
,科目合計,0
,科目平均,0.0

第4学年成績一覧表
番号,氏名,家庭基礎,財務会計Ⅰ,経済活動と法,課題研究,●●C言語とC++言語開発,合計,科目数,平均,平均順位
1001,岡 胡桃,4,3,3,3,,13,4,3.3,8
1002,川合 睦美,4,4,4,4,,16,4,4.0,3
1003,武井 康代,5,5,5,5,,20,4,5.0,1
1004,西村 瑠璃,4,4,4,4,,16,4,4.0,3
1005,藤川 小梅,4,4,3,3,,14,4,3.5,7
1006,森本 双葉,4,4,4,4,,16,4,4.0,3
1007,渡部 泉,3,4,4,5,,16,4,4.0,3
1008,井本 秀樹,2,2,2,2,,8,4,2.0,10
1009,小沼 正人,4,4,4,5,,17,4,4.3,2
1010,重田 年昭,4,2,3,2,,11,4,2.8,9
,科目合計,38,36,36,37,0,147
,科目平均,3.8,3.6,3.6,3.7,0.0,3.7
こちらも大丈夫でしょう。
科目がまったくない2学年と3学年は全員が評価0、順位1位となっています。
4学年の一覧表を見ると、科目名だけで成績のない「●●C言語とC++言語開発」が全員カンマだけになっていることが分かります。
もちろん平均計算からは除外されています。
次は評価ファイルの、161行目から下(2年の「簿記」から下)を削除してみます。
評定平均値成績降順
1003,武井 康代,4.9
1006,森本 双葉,4.4
1004,西村 瑠璃,3.9
1009,小沼 正人,3.9
1002,川合 睦美,3.8
1001,岡 胡桃,3.4
1007,渡部 泉,3.4
1005,藤川 小梅,3.2
1010,重田 年昭,3.1
1008,井本 秀樹,2.1
-------------------------------------------------------------------------------
第1学年成績一覧表
番号,氏名,国語総合,現代社会,数学Ⅰ,科学と人間生活,体育,保健,コミュニケーション英語基礎,ビジネス基礎,簿記,合計,科目数,平均,平均順位
1001,岡 胡桃,3,4,3,4,3,4,4,4,3,32,9,3.6,7
1002,川合 睦美,3,5,2,2,5,4,4,5,4,34,9,3.8,3
1003,武井 康代,5,5,5,5,4,5,5,5,5,44,9,4.9,2
1004,西村 瑠璃,4,3,4,4,4,3,4,4,4,34,9,3.8,3
1005,藤川 小梅,3,3,3,3,4,3,3,3,3,28,9,3.1,9
1006,森本 双葉,5,5,5,5,5,5,5,5,5,45,9,5.0,1
1007,渡部 泉,5,4,2,4,3,5,2,4,4,33,9,3.7,5
1008,井本 秀樹,2,2,2,2,3,2,2,2,2,19,9,2.1,10
1009,小沼 正人,3,3,3,3,4,4,4,5,4,33,9,3.7,5
1010,重田 年昭,2,4,3,3,4,4,3,2,4,29,9,3.2,8
,科目合計,35,38,32,35,39,39,36,39,38,331
,科目平均,3.5,3.8,3.2,3.5,3.9,3.9,3.6,3.9,3.8,3.7

第2学年成績一覧表
番号,氏名,国語総合,地理A,数学Ⅰ,体育,保健,音楽Ⅰ,コミュニケーション英語基礎,簿記,ビジネス実務,合計,科目数,平均,平均順位
1001,岡 胡桃,3,3,3,3,3,4,3,,,22,7,3.1,7
1002,川合 睦美,5,4,2,4,3,4,5,,,27,7,3.9,4
1003,武井 康代,5,5,5,4,5,5,5,,,34,7,4.9,1
1004,西村 瑠璃,4,4,4,4,4,4,4,,,28,7,4.0,3
1005,藤川 小梅,3,4,3,3,3,4,3,,,23,7,3.3,6
1006,森本 双葉,4,4,3,4,3,4,3,,,25,7,3.6,5
1007,渡部 泉,3,4,5,3,2,3,2,,,22,7,3.1,7
1008,井本 秀樹,2,2,2,3,2,2,2,,,15,7,2.1,10
1009,小沼 正人,4,4,4,5,4,4,5,,,30,7,4.3,2
1010,重田 年昭,3,3,3,4,2,3,2,,,20,7,2.9,9
,科目合計,36,37,34,37,31,37,34,0,0,246
,科目平均,3.6,3.7,3.4,3.7,3.1,3.7,3.4,0.0,0.0,3.5

第3学年成績一覧表
番号,氏名,現代文A,世界史A,化学基礎,体育,音楽Ⅰ,コミュニケーション英語Ⅰ,ビジネス実務,情報処理,財務会計Ⅰ,合計,科目数,平均,平均順位
1001,岡 胡桃,,,,,,,,,,0,0,0.0,1
1002,川合 睦美,,,,,,,,,,0,0,0.0,1
1003,武井 康代,,,,,,,,,,0,0,0.0,1
1004,西村 瑠璃,,,,,,,,,,0,0,0.0,1
1005,藤川 小梅,,,,,,,,,,0,0,0.0,1
1006,森本 双葉,,,,,,,,,,0,0,0.0,1
1007,渡部 泉,,,,,,,,,,0,0,0.0,1
1008,井本 秀樹,,,,,,,,,,0,0,0.0,1
1009,小沼 正人,,,,,,,,,,0,0,0.0,1
1010,重田 年昭,,,,,,,,,,0,0,0.0,1
,科目合計,0,0,0,0,0,0,0,0,0,0
,科目平均,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0

第4学年成績一覧表
番号,氏名,国語表現,政治経済,体育,コミュニケーション英語Ⅰ,家庭基礎,財務会計Ⅰ,経済活動と法,課題研究,合計,科目数,平均,平均順位
1001,岡 胡桃,,,,,,,,,0,0,0.0,1
1002,川合 睦美,,,,,,,,,0,0,0.0,1
1003,武井 康代,,,,,,,,,0,0,0.0,1
1004,西村 瑠璃,,,,,,,,,0,0,0.0,1
1005,藤川 小梅,,,,,,,,,0,0,0.0,1
1006,森本 双葉,,,,,,,,,0,0,0.0,1
1007,渡部 泉,,,,,,,,,0,0,0.0,1
1008,井本 秀樹,,,,,,,,,0,0,0.0,1
1009,小沼 正人,,,,,,,,,0,0,0.0,1
1010,重田 年昭,,,,,,,,,0,0,0.0,1
,科目合計,0,0,0,0,0,0,0,0,0
,科目平均,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0
こちらも大丈夫みたいですね。
評価が一つも入っていない3学年と4学年はすべてカンマだけ、途中から評価がなくなる2学年は簿記とビジネス実務がカンマだけになり、9科目中7科目での計算となっています。
次に、生徒を一人だけ、科目を一つだけ、評価をその生徒と科目の一つだけにして実行してみます。
評定平均値成績降順
1006,森本 双葉,4.0
-------------------------------------------------------------------------------
第1学年成績一覧表
番号,氏名,合計,科目数,平均,平均順位
1006,森本 双葉,0,0,0.0,1
,科目合計,0
,科目平均,0.0

第2学年成績一覧表
番号,氏名,合計,科目数,平均,平均順位
1006,森本 双葉,0,0,0.0,1
,科目合計,0
,科目平均,0.0

第3学年成績一覧表
番号,氏名,合計,科目数,平均,平均順位
1006,森本 双葉,0,0,0.0,1
,科目合計,0
,科目平均,0.0

第4学年成績一覧表
番号,氏名,国語表現,合計,科目数,平均,平均順位
1006,森本 双葉,4,4,1,4.0,1
,科目合計,4,4
,科目平均,4.0,4.0
これもOK。
森本さんの4年次科目・国語表現だけを残したところ、ちゃんとそれだけが表示されました。
もしデータが一つもないファイルを読み込んだら、「空ファイルです。処理を中止します。」が表示されてプログラムが終了してしまいますので、これが最小限の表示になります。
では最後に、全然関係のないファイルを読ませてみましょう。
三つのファイルの内容をそれぞれ次のように、まったく適当に書き換えて実行してみます。
seitomeibo.csv
あさぢが露,平安時代末期または鎌倉時代初期成立(『無名草子』中の『浅茅が原の内侍』と同一作品と思われる),物語の背景の一部が謎のまま進み、後半に至って種明かしがされるという、推理小説的な筋書きが特徴。本文は難解だが、じっくり取り組めば読めないほどではない。『とりかへばや』『有明の別れ』以降の物語の中では面白い方。
我が身にたどる姫君,鎌倉時代中期成立(おそらく『風葉和歌集』成立の1271年前後),現存する物語中、間違いなく最も難解な本文を持っている。注釈書なしでは厳しい。巻六は明らかにレズビアンを描いており、これがほかの物語にはない大きな特色となっている。面白いかと尋ねられたら、「つまらなくはない」と答える程度。
夢の通ひ路物語,室町時代中期以降成立(文章の感触が新しく、鎌倉時代の作品とは思えない),二つのストーリーが、ほとんど互いの連絡なく並行していく奇妙な物語。『世界の終りとハードボイルド・ワンダーランド』を五〇〇年ほど先取りしていると言ったら大げさか。かなりの長編だが、本文は難しくない。しかし、全体に生気を欠き、読者を引っ張っていく力は弱い。『住吉物語』の引用がある。2015年のセンターテストで出題された。

kamokuhyo.csv
1966.07.17,ウルトラ作戦第一号,ベムラー,良作
1966.07.24,侵略者を撃て,バルタン星人,傑作
1966.07.31,科特隊出撃せよ,ネロンガ ,傑作
1966.08.07,大爆発五秒前,ラゴン,良作
1966.08.14,ミロガンダの秘密,グリーンモンス,良作

seisekihyo.csv
西部戦線異状なし,1930
第三の男,1949
鬼火,1963
レッド・ツェッペリン狂熱のライヴ,1976
評定平均値成績降順
あさぢが露,平安時代末期または鎌倉時代初期成立(『無名草子』中の『浅茅が原の内侍』と同一作品と思われる),0.0
夢の通ひ路物語,室町時代中期以降成立(文章の感触が新しく、鎌倉時代の作品とは思えない),0.0
我が身にたどる姫君,鎌倉時代中期成立(おそらく『風葉和歌集』成立の1271年前後),0.0
-------------------------------------------------------------------------------
第1学年成績一覧表
番号,氏名,合計,科目数,平均,平均順位
あさぢが露,平安時代末期または鎌倉時代初期成立(『無名草子』中の『浅茅が原の内侍』と同一作品と思われる),0,0,0.0,1
我が身にたどる姫君,鎌倉時代中期成立(おそらく『風葉和歌集』成立の1271年前後),0,0,0.0,1
夢の通ひ路物語,室町時代中期以降成立(文章の感触が新しく、鎌倉時代の作品とは思えない),0,0,0.0,1
,科目合計,0
,科目平均,0.0

(以下略)
評定平均値成績降順の方は、『夢の通ひ路物語』と『我が身にたどる姫君』が入れ替わっていますが、これは題名の文字コードの順でしょう。
ウルトラマンと洋画は全然表に出てこないですね(笑)。
まあそれでも、こんなファイルでも異常終了はしないことが分かりました。
とにかく、不完全なファイルを読ませても、暴走したり異常終了したりしないことが重要です。
不完全なファイルであっても、それに応じた不完全な表か、あるいはコードに仕込んだエラーメッセージが表示されれば、それはプログラムの制御下にあるということですから、とりあえず合格としてよいでしょう。


最初に思い浮かんだイメージよりもはるかに長いコードを書く羽目になってしまいましたが、構造体のリスト構造は「作るのが楽しかった」というのが正直なところです。
まあリスト構造の強みである「挿入・削除」の練習をまったくしなかったはどうなのかという気もするけれど、今後、もしクラスの練習をするような機会があれば、それをリスト構造にしてみて、「挿入・削除」はそのときにでも試してみたいと思っています。