今までに学習した事に基づき、 起動時に与えられたファイルに含まれる文字の出現回数を数えるプログラムを書いてみる。 ただし、起動時にファイルが一つも与えられなければ、画面に質問を表示し、ユーザからの 指示をうけるものとする。
/* count.c */ /* * 起動時に与えられたファイルに含まれる文字を調べ、 * それぞれの文字ごとに現れた数を表示する */ #include <stdio.h> #include <strings.h> int parse_file(char *); int main(int argc, char *argv[]) { FILE *fp; char filename[80]; int type; argc --; /* argc, argvには、実行ファイル名の情報も含まれているので * 1 減らすことで、引数の数となる。 */ switch (argc) { case 0: printf("どのような処理をしますか ? (番号で選択してください)\n"); printf("1: 標準入力の処理\n"); printf("2: ファイル名を指定する\n"); scanf("%d", &type); /* 標準入力から整数を読み込む */ if (type == 1) { parse_file(NULL); } else { printf("ファイル名 -> "); scanf("%s", filename); /* 標準入力から文字列を読み込む */ parse_file(filename); } break; default: while (argc) { argv++; parse_file(*argv); argc--; } } return 0; } int parse_file(char *name) { int ch, i; int counter[256]; /* char 型は 0-255 の 256 通りの可能性がある */ int total = 0; FILE *fp; if (name != NULL) { if(NULL == (fp = fopen(name, "r"))) { fprintf(stderr, "ファイル %s を開くことができません。\n", name); return -1; } } else { fp = stdin; } while(EOF != (ch = fgetc(fp))) counter[ch]++; /* 該当する配列要素の中身をインクリメントする */ fclose(fp); for(i = 0; i < 256; ++i) { if (counter[i] > 0) { total += counter[i]; printf("%#04x: %5d\n", i, counter[i]); } } printf("\ntotal: %5d\n", total); return 0; }
scanf
関数
キーボードから整数を読み込んだり、ファイル名を読み込んだりするのに、新しく
scanf関数を用いている。
scanf関数は、printf
関数と同様な書式を用いて、
与えられた内容を変換して変数に格納する。scanf
の書式は次の通りである。
#include <stdio.h> int scanf(書式文字列, 格納先変数のポインタ, ...);
上の例では、最初の scanf は、入力を "%d"、すなわち10進数としての整数として
変換し、int 型の変数 type に格納している。
格納先は、アドレスで指定する必要があるため、通常変数である type に格納するためには
"&"演算子を適用して変数のアドレスを求めている。
2番目のファイル名を入力している scanf
では、変換方法を "%s"、
すなわち文字列を、格納先には配列 filename を指定している。
scanf
は入力が標準入力に固定されているが、FILE 型のポインタで示される
ストリームから入力を行う関数として fscanf
がある。fscanf は
#include <stdio.h> int fscanf(ストリーム, 書式文字列, 格納先変数のポインタ, ...);
という形式で使用する。
scanf関数は、書式で指定された内容と異なる入力が与えられた場合、 変換を行わないなど期待にそぐわないの結果を与えたることもある上、 格納先の変数の大きさの確認を行わない(特に文字列の場合)ので、 使用には十分注意を払う必要がある。
複合代入演算子とは、プログラム中で良く現れるa = a + b;
等のような
ある変数に、その変数と何らかの式との計算結果を代入する場合に用いられる演算子である。
例としてあげると、
複合代入演算子の使用例 | 用いない同等表現 |
---|---|
a += b |
a = a + b |
a -= b |
a = a - b |
a *= b |
a = a * b |
a /= b |
a = a / b |
a %= b |
a = a % b |
などがある。a = a ※ b
が a ※= b
という形で表現されていることが分かる。
複合代入演算子を使った方がプログラムの可読性が向上する場合が多い。
main 関数と parse_file という実際に数え上げる関数からなるこのプログラムで、
新しく出てきたのは、上で説明したscanf
関数と、+= という
複合代入演算子だけである。 読んで、内容が理解できるだろうか?
さて、プログラムを読んでみて理解できただろうか? 実は、このプログラムをコンパイルして動かしても、正しい答えを出さない。 どこが間違っているのであろうか?
答は、「配列が初期化されていない」である。 C では、メモリ上に変数の領域を確保する際に、その内容のリセットは行われず、 その時点で記憶されていた内容がそのまま残る。 従って、出現回数を数えるのであれば、あらかじめ配列の内容を 0 にリセットしておく必要がある。
配列 count
の全ての要素を 0 に初期化する場合、宣言と同時に行うのであれば
int count[256] = {0};
の用に行うことができる。
{...}
は初期化子と呼ばれ、先頭から順に配列に値を設定するのに用いられる。
初期化に用いる要素数が配列のよう素数より少ない場合は、残りに 0 が代入される。
しかし、宣言と異なるタイミングではこの手法は使えない。そのような場合には
for (i = 0; i < 256; ++i) count[i] = 0;
のように、配列の要素を一つづつ 0 に初期化するか、bzero(3)
や
memset(3)
を使って、
bzero(code, sizeof(code)); memset(code, 0, sizeof(code));
などのように行う必要がある。
これらの関数を使う場合には適切なヘッダファイル(string.h または strings.h)を読み込んで置く必要がある。
sizeof
は引数に与えられた変数のメモリ上で占める領域の大きさをバイト単位で返す演算子である。
count.c
を書き直し、その出現した割合を視覚的に
捉えられるように「*」を使って簡易的にグラフ表示を行うようにせよ。
ただし、画面サイズの幅は 80と仮定して良い。
./count2 text_file ... (略) .... 5a: 1|* 61: 1693|************************* 62: 465|******* 63: 824|************ 64: 796|************ 65: 3345|************************************************* 66: 727|*********** 67: 306|***** 68: 1130|***************** 69: 1832|*************************** 6a: 48|* ... (略) ....
ある授業の受講者に関する情報を取り扱うようなプログラムを考えるとしよう。 取り扱う情報としては、「学籍番号」、「姓」、「名」、 「講義への出席回数」、「テストの点数」を考える。 課題11では、このような情報をプログラム上でうまく取り扱う方法について取りあげる。
一番原始的な方法としては、
/* 一人目 */ char gakuseki_bango_01[10]; char family_name_01[40]; char given_name_01[40]; int shusseki_01; int mark_01; /* 二人目 */ char gakuseki_bango_02[10]; char family_name_02[40]; char given_name_02[40]; int shusseki_02; int mark_02;
のように、すべてについて個別に変数を用意する方法が考えられる。 しかし、これではプログラムの可読性も低下するし、そもそも手間がかかって仕方がない。
このような場合には、配列を使えば良かったのであった。 そこで、配列を使って宣言すると次のようになる。 たとえば受講生が最大30人であるという事があらかじめ判っているとして、 配列を使って宣言を行うと、次のようになる。
char gakuseki_bango[30][10]; char family_name[30][40]; char given_name[30][40]; int shusseki[30]; int mark[30];
しかし、これでも、ある受講生の情報が様々な配列にバラバラに格納されることになり、 扱いやすいとは言いがたい。たとえば、一人一人の学生の情報を入力するような 関数を考えた場合、引数として5つの変数を渡さなければならない。
このような複数の型をまとめて扱いたいような場合、 C 言語では、複数のデータをひとまとめにして扱う「構造体(structure)」という データ型を使用することができる。
構造体を使用するためには、最初にどのようなデータを含む構造体なのかを宣言する必要がある。
宣言は struct
を用いて
struct student_info { char gakuseki_bango[10]; char family_name[40]; char given_name[40]; int shusseki; int mark; };
のように行う。student_info
はタグと呼ばれ、構造体の型についた名前のようなものである。
(宣言と同時に変数も宣言してしまうような場合には、タグは省略することも可能。)
{} でくくられた中に宣言された変数は「メンバ」と呼ばれる。
この宣言により、student_info
という構造体の構成が定義されたので、
これ以降 student_info
「型」の変数は、次のような宣言で使用することができる。
struct student_info info; struct student_info data[40]; struct student_info *pdata;
data
は、構造体の配列の宣言の例であり、pdata
は構造体のポインタの宣言の例である。
構造体の宣言と変数の宣言を同時にすることもできる。
struct student_info { char gakuseki_bango[10]; char family_name[40]; char given_name[40]; int shusseki; int mark; } info, data[40], *pdata;
構造体の初期化の際には、次のようにして値をセットすることができる。
struct student_info info = { "08913765", "大垣内", "多徳", 13, 65 };
しかし、初期化以外の場面ではこの手法は取れず、値を設定するためには、 メンバーごとに行う必要がある。ただし、同じ型の構造体変数からの代入は可能である。
構造体の各メンバーへのアクセスは.
演算子を用いる。
各メンバーはその型に可能な行為はすべて許される。
strcpy(info.gakuseki_bango, "08913765");
strcpy(info.family_name, "大垣内");
strcpy(info.given_name, "多徳");
info.shusseki = 13;
info.mark = 65;
構造体が、ポインタ変数であった場合、"*" 演算子よりも、"."演算子の優先順位が高いため、
(*pdata).shusseki
のように必ず "()" が必要である。
いちいち "()" をつけるのは煩雑なので、その短縮形として padta->shusseki
という
形式を使うことができる。
strcpy
関数は、第二引数で与えられる文字列(char 型のポインタであった)の
内容を、第一引数で与えられるポインタにコピーする。
コピー先の大きさに関するチェックは行われないので、注意すること。
特に文字列には、終端文字が含まれる為に「目に見える」長さより1バイト多く必要である事に留意する。
コピーする文字列の長さを制限するstrncpy
などもある。
ある講義の受講生のデータとして次のような書式の ~tacha/kadai11/data
を用意した。
このファイルは一行が一人の学生を表しており、それぞれ、学籍番号、姓、名、出席回数、テストの点数が書かれているものとする。
101219 赤松 将明 12 67 129529 小泉 恒三郎 9 77 132979 細田 章生 9 45 191646 永田 大和 10 70 198622 江田 利勝 10 98 217691 逢沢 正志 13 19 233260 武部 圭朗 9 23 241497 杉田 康弘 9 28 289740 上野 忠彦 13 99 290975 長浜 教嚴 11 58 337877 嘉数 正忠 10 80 348560 小沢 茂樹 11 29 351722 塩崎 孝弘 11 56 387218 土屋 英一郎 14 83 390102 大串 健嗣 11 92 458131 原口 宜伸 12 26 495343 福島 律夫 10 39 559234 伴野 政賢 8 94 651989 市村 章宏 11 76 672924 篠田 匠 10 53
このファイルを読み込み、テストの点数に加えて出席回数1回につき 2点与えたものを総合得点として計算し、 各人の学籍番号、姓名、総合得点を表示するプログラムを作成せよ。
総合得点の最高点、最低点を求め、該当する受講生の情報を表示するように改良せよ。
学籍番号、姓名、総合得点を総合得点の高い順に表示できるように改良せよ。 整列の手法が不明な場合にはバブルソートを用いよ。
9日目 | 表紙 | 11日目 |