C言語でテストの点数を集計してみよう(総合演習)
ここまでの C言語 レッスンで学んだことを全部つないで、動くものを1つ完成させます。新しい構文は出てきません。構造体・配列・ポインタ・for文・関数・qsort——すでに練習したものだけで組み立てます。
作るのはテストの点数の集計です。教科ごとの平均を出し、合計点で評定を付け、順位順に並べ替えて、一覧にまとめます。
この演習の主題は「配列の長さは自分で持つ」です。C言語には、配列が自分の長さを知っている仕組みがありません。件数は人間が管理する——これがC言語のいちばん大きな性質で、初心者がつまずく場所のほとんどがここに集まります。
だからステップ2では、sizeofが関数の中では効かなくなるという有名な罠を、正面から扱います。知らずに書くと静かに壊れる種類の話です。
このページではコードを実行していません。C言語はコンパイルが必要で、ブラウザの中では走らせられないためです。かわりにお手本と同じコードを書けたかを、その場で判定します(判定はあなたのブラウザの中だけで行われ、どこにも送信されません)。
表示している実行結果は本物です。このページを作るときに、実際にgccでコンパイルして走らせた出力をそのまま載せています。
完成イメージ
最後まで進むと、このコードが完成します。下の実行結果は、このページを作るときに開発機で実際にコードを走らせて得たものです(このページ上では実行していません)。
===== 成績一覧 ===== 高橋 国 91 数 88 理 95 合計 274 評定A 佐藤 国 78 数 92 理 85 合計 255 評定B 伊藤 国 83 数 79 理 68 合計 230 評定B 鈴木 国 64 数 58 理 71 合計 193 評定C 田中 国 55 数 72 理 60 合計 187 評定C ---------------------------------------- 人数 : 5人 国語平均 : 74.2点 数学平均 : 77.8点 理科平均 : 75.8点 合格(220点以上): 3人 首位 : 高橋(274点)
STEP 1成績を構造体の配列で表す
🎯 このステップのゴール: typedef structで型を作り、それを並べた配列を用意する
まずデータの形を決めます。生徒1人が持つのは「名前・国語・数学・理科」の4つです。
typedef struct { ... } Student;——typedefを付けているのが要点です。
これが無いと、使うたびにstruct Student s;と書かなければなりません。typedefで名前を付けておけばStudent s;で済みます。C言語では、構造体にはほぼ必ずtypedefを付けます。
char name[16]にも注意点があります。C言語に「文字列」という型はありません。charの配列があるだけです。
16という数字は自分で決めた上限です。そして末尾の\0ぶんを1文字ぶん使うので、実際に入るのは15文字までです。日本語はUTF-8で1文字3バイトなので、5文字ほどしか入りません。
ここを油断すると、はみ出した分が隣の変数を壊します。C言語で最も多い不具合の原因が、この「配列からのはみ出し」です。
staticを付けているのは「このファイルの外からは見えない」という意味です。関係のないファイルから触られないようにする、小さな安全策です。
&を使わずstudents[0].nameのように書けば、普通に読めます。
人数: 5人 1人目: 佐藤 国78 数92 理85
STEP 2sizeofで件数を求める——ただし関数の外だけ
🎯 このステップのゴール: 配列の長さの求め方と、関数に渡すと効かなくなる理由を確かめる
件数を求めます。sizeof(students) / sizeof(students[0])——「配列全体の大きさ ÷ 1個の大きさ」という定番の書き方です。
この演習ではStudentが28バイト、5人ぶんで140バイト。140 ÷ 28 = 5と求まります。
ここからがC言語でいちばん有名な罠です。この計算を関数の中でやると、必ず間違った答えが出ます。
理由は、Cでは配列を関数に渡せないからです。渡しているように見えて、実際には先頭の要素を指すポインタだけが渡っています。
だから関数の中のsizeof(list)は配列全体ではなく、ポインタの大きさ(8バイト)になります。8 ÷ 28 = 0——件数が0になり、ループが1回も回りません。
コンパイラは何も言いません。文法として正しいからです。実行してみて「何も表示されない」ことに気づくまで、間違いに気づけません。
だからC言語では、配列を渡すときは必ず件数も一緒に渡します。void report(Student *list, int count)——この形が、C言語のあらゆる場面に出てきます。mainのargcとargvも、まったく同じ考え方です。
下のコードで、関数の外と中で同じsizeofがどう変わるかを自分の目で確かめてください。
関数の外でsizeof: 5人 関数の中でsizeof: 0人(間違い) 引数で受け取った件数: 5人(正しい) 先頭は 佐藤 さん
STEP 3ポインタで渡して合計点を出す
🎯 このステップのゴール: アロー演算子で構造体を読み、constで書き換えないと宣言する
1人ぶんの合計点を出す関数を書きます。
int total_of(const Student *s)——構造体そのものではなく、ポインタを受け取っています。
なぜポインタで渡すのか。構造体をそのまま渡すと、28バイトまるごと複製されます。ポインタなら8バイトだけです。構造体が大きくなるほど差が開きます。
constを付けているのが、もう1つの要点です。「この関数は中身を書き換えません」という宣言で、うっかり書き換えるコードを書くとコンパイルエラーになります。
読むだけの関数には必ずconstを付けてください。読む人に意図が伝わりますし、コンパイラが見張ってくれます。
中身を取り出すのがs->japaneseという書き方です。
.と->の使い分けは単純です。実体なら.、ポインタなら->。(*s).japaneseと書いても同じですが、括弧が要るので->のほうが読みやすい——そのために用意された記法です。
評定を決めるgrade_ofはifを並べています。上から順に見て、最初に当たったところでreturnする——elseを書かなくても済むので、階段状に読めます。
戻り値がcharで、'A'とシングルクォートなのに注目してください。C言語ではダブルクォートは文字列、シングルクォートは1文字。混同すると型が合わずに怒られます。
佐藤: 合計255点 評定B 鈴木: 合計193点 評定C 高橋: 合計274点 評定A 田中: 合計187点 評定C 伊藤: 合計230点 評定B
STEP 4平均を出す——整数の割り算に気をつけて
🎯 このステップのゴール: キャストして小数の割り算にし、0で割らないようにする
教科ごとの平均を出します。ここに、C言語で必ず一度は踏む落とし穴があります。
sum / countと書くと——小数点以下が消えます。371 / 5は74.2ではなく74です。
整数どうしの割り算は、整数を返す——C言語の決まりです。しかもコンパイラは何も言いません。「74.2のはずが74と出る」と気づくまで、間違いに気づけません。
直すにはキャストします。(double)sum / count——片方を小数にすれば、もう片方も自動で小数に合わせられます。
書く位置が結果を変えます。(double)(sum / count)と括弧を付けると、先に整数で割ってから小数にするので、74.0になってしまいます。切り捨ててから小数にしても、消えた端数は戻りません。キャストは割り算より先です。
もう1つ、count == 0を先に確かめています。整数の0除算はプログラムがその場で落ちます。
「今のデータでは0にならない」は理由になりません。この関数はあとから別の場所でも使われます。割る前に0を確かめる——習慣にしてください。
表示は%.1fで小数第1位までです。doubleを%dで出すと、まったく無関係な数字が出ます——C言語のprintfは型を確かめてくれないので、書式と型を自分で合わせる必要があります。
国語の合計: 371点 整数のまま割ると: 74点 キャストしてから割ると: 74.2点 数学平均: 77.8点 理科平均: 75.8点
STEP 5qsortで合計点の高い順に並べ替える
🎯 このステップのゴール: 比較関数を書いて渡し、元の配列が変わることを意識する
合計点の高い順に並べ替えます。使うのは標準ライブラリのqsortです。
qsort(list, count, sizeof(Student), compare_by_total)——渡すのは4つ。「並べ替える配列」「件数」「1個の大きさ」「比べ方」です。
3つ目にsizeof(Student)を渡しているのに注目してください。qsortは何の配列を並べ替えているのか知りません。だから「1個が何バイトか」を教えてやる必要があります。
比較関数の引数がconst void *——「型の分からないポインタ」なのも同じ理由です。qsortはどんな型でも扱えるよう作られているので、受け取る側で本来の型に戻します。
const Student *sa = (const Student *)a;——このキャストが「これは実はStudentだ」と教える一行です。
返す値の決まりは覚えるしかありません。「負なら1つ目が先」「0なら同じ」「正なら2つ目が先」。total_of(sb) - total_of(sa)と2つ目から1つ目を引くことで、大きいほうが先に来ます——つまり降順です。引く順を逆にすれば昇順になります。
qsortは元の配列を直接書き換えます。並べ替えた新しい配列が返るのではありません。元の順番はもう戻せません——入力した順を残したいなら、先に複製しておく必要があります。
この演習では並び順にこだわらないのでそのまま並べ替えていますが、実務では「呼んだら元データが変わる」関数に注意してください。
--- 並べ替える前 --- 佐藤 鈴木 高橋 田中 伊藤 --- 合計点の高い順 --- 1位 高橋(274点) 2位 佐藤(255点) 3位 伊藤(230点) 4位 鈴木(193点) 5位 田中(187点)
STEP 6ひとつの関数にまとめて仕上げる
🎯 このステップのゴール: reportにまとめ、桁を揃えて全体像を出す
最後の仕上げです。ここまでの部品をreportという1つの関数にまとめます。
void report(Student *list, int count)——配列と件数を必ず一緒に受け取るという、ステップ2で確かめた形です。
#define PASS_TOTAL 220で合格点を定めます。#defineはコンパイル前に文字を置き換える仕組みで、プログラムが動き出す前にPASS_TOTALが220に書き換わります。
const intとの違いを知っておくと役に立ちます。#defineはただの置き換えで型がありません。const intは型のある本物の変数です。今のCではconst intのほうが安全とされていますが、#defineも広く使われているので、両方読めるようにしておいてください。
桁揃えはprintfの書式です。%-6sは「左揃えで6文字ぶん」、%3dは「右揃えで3桁」。文字は左、数字は右に揃えると読みやすくなります。
ただし日本語では完全には揃いません。C言語の%-6sはバイト数で数えるので、日本語1文字が3バイトぶん消費します。大きくは整うので実用になりますが、厳密に揃えたいなら別の仕組みが要ります。
完成です。studentsに生徒を1人足してみてください。——STUDENT_COUNTは直さなくて構いません。sizeofで求めているので、自動で6人になります。
ここに、件数を5と直接書かなかった意味があります。直接書いていたら、生徒を足すたびに数字を直すことになり、いつか必ず直し忘れます。そしてCでは配列の外を読んでも止まらないので、意味の分からない数字が出るか、運が悪ければプログラムごと落ちます。
PASS_TOTALを200に変えれば、合格者が増えます。1か所直すだけで判定が変わる——定数に切り出しておいた効果です。
===== 成績一覧 ===== 高橋 国 91 数 88 理 95 合計 274 評定A 佐藤 国 78 数 92 理 85 合計 255 評定B 伊藤 国 83 数 79 理 68 合計 230 評定B 鈴木 国 64 数 58 理 71 合計 193 評定C 田中 国 55 数 72 理 60 合計 187 評定C ---------------------------------------- 人数 : 5人 国語平均 : 74.2点 数学平均 : 77.8点 理科平均 : 75.8点 合格(220点以上): 3人 首位 : 高橋(274点)
よくある質問
このページではコードを実行しないのですか?
していません。C言語はコンパイルが必要で、ブラウザの中では走らせられないためです。このページではお手本と同じコードを書けたかどうかを判定する形にしています(判定はあなたのブラウザの中だけで行われ、コードはどこにも送信されません)。
ただし表示している実行結果は本物です。このページを作るときに実際にgccでコンパイルして走らせた出力を、そのまま載せています。
関数の中で sizeof が効かないのはなぜですか
Cでは配列を関数に渡せないからです。渡しているように見えて、実際には先頭の要素を指すポインタだけが渡ります。
だから関数の中のsizeof(list)はポインタの大きさ(多くの環境で8バイト)になります。要素1個が28バイトなら8 / 28 = 0——件数が0になります。
コンパイラは警告を出しません。文法として正しいためです。
解決は1つだけ。件数を引数で一緒に渡すことです。void report(Student *list, int count)という形は、この事情があるからCのいたるところに出てきます。
平均が小数になりません
整数どうしを割っているからです。Cでは371 / 5は74.2ではなく74になります。小数点以下が切り捨てられる決まりです。
(double)sum / countのようにキャストしてから割ります。
括弧の位置に注意してください。(double)(sum / count)と書くと先に整数で割ってから小数にするので、切り捨てられた結果が74.0になるだけです。キャストは割り算より先に効かせます。
malloc は使わなくていいのですか
この演習では要りません。件数が最初から分かっているので、固定長の配列で足ります。
mallocが要るのは、実行してみるまで件数が分からないときです。ファイルから何行読むか分からない、利用者が件数を入力する——そういう場面ではじめて必要になります。
そしてmallocを使うとfreeを書く責任が生まれます。書き忘れればメモリが解放されず、二重に書けばプログラムが落ちます。必要になってから覚えるので十分です。
🚩 次はこのカテゴリへ
この演習で手間だったところ——件数を自分で持つ、qsortに比較関数を渡す、文字列をcharの配列で扱う——は、C++ではどれも楽になります。std::vectorは自分の長さを知っていますし、std::stringははみ出しを心配せずに済みます。Cの不便さを味わったあとだと、C++が何を解決したのかがはっきり分かります。