入出力
前半6回の連載では、C言語の文法に関する解説を行ってきました。後半6回の連載では、C言語を使って、コンピュータによって解決できる問題の数々を見ていきます。今回は、コンピュータが行う仕事において、もっとも基本的な要素の1つである「入出力」についてです。
入出力
コンピュータが、そもそも計算機であることを考慮しつつ、コンピュータの基本構成図(Fig.1)を眺めて見ます。実際に各種演算を行う能力を持っているのは「CPU」で、計算する仕事内容や、計算の途中結果、最終的な結果などを保持するのが「メモリ」になります。そうすると、コンピュータの本質的な仕事である計算は、「CPU」と「メモリ」があれば行えると考えられます。前回、コンピュータが必ず備えている3つの構成要素として、「CPU」、「メモリ」、「I/O」があると書きましたが、「I/O」は何故必要なのでしょう?
このことを考えるために、例え話をしたいと思います。
会社やクラスやサークルなどで、あなたが中心になって何らかの用事(仕事)を行わなければなりません。量がハンパじゃないので、自分でやるだけではなく、他の人にも分担してもらうことにしました。「中岡君、この箱の中の領収書の合計金額出しといて」と頼んだとします。中岡君は、自分の席に箱を持っていき、ガサガサと作業を始めました。数時間後。あなたは自分の作業が一段落ついたところで、中岡君に頼んだ用事を思い出します。「そういや、遅いなあ」と視線をめぐらすと、中岡君が居眠りしているではありませんか。「だーっ、寝てるんじゃねえ」スケジュールの遅れで気がたっているあなたは、中岡君を叩き起こします。中岡君は眠気まなこをこすりながら「あ、もうできてます」と、しれっと言ってのけるのでした。(注:この例え話はフィクションであり、実在する人物とは関係ありません)
中岡君は、依頼された仕事を確かにやったのですが、依頼主であるあなたに報告をしませんでした。この例え話は、仕事というものは、作業自体をやるだけでは不完全で、作業結果を依頼主に伝えなければ、やってないのと同じであることを示しています。
また、実際のビジネスの世界で、まぬけなことに多々ある事例として、言った、言わないで、もめることがあります。
上司「先週頼んどいた書類はまだか」
部下「え、聞いてませんけど……」
上司「火曜日の会議のときのアレ」
部下「いや、あれは、しばらく様子を見るって話でしたよね」
上司「念のため前倒しで今日までにって言ったよな」
部下「聞いてません」
上司「いや、言ったって」
部下「何時何分何秒に?」
上司「きさま、なんだその態度は!」
(以下、略)
作業の依頼などは、会議や電話での口約束では怖いので、e-mailのように履歴の残る伝達手段を用いるべきです。この例え話は、作業を依頼しても、作業者に依頼が伝わらなければ、作業結果は得られないことを示しています。
入出力をつかさどる「I/O」は、先にも述べたように、コンピュータの本質的な仕事である計算には、直接関係がありません。しかし、「I/O」が依頼された作業内容を「メモリ」に用意したり、作業結果を外部に伝達したりといった役割を果たさなければ、いくら「CPU」と「メモリ」で計算を行っても、コンピュータが仕事をしたとは見なされないのです。高速な「CPU」と大容量「メモリ」があっても、「I/O」が接触不良では、元も子もありません。あなたの「I/O」は大丈夫ですか?Fig.1:コンピュータのしくみ
List.1:list1.c
#include <stdio.h> int main() { printf("Hello,World.\n"); return 0; }
C言語と入出力
C言語を用いて記述し、コンパイルして得られるソフトウェア(実行形式)は、何らかの仕事を行うことを目的としています。したがって、ソフトウェアが仕事をするのに必要な情報(入力)と、その結果を示す何か(出力)を持つことになります。
List.1に示されるような簡単なプログラムにおいても、実行の結果、画面に表示される文字列や、main()関数がその戻り値としてOSに返す終了情報が出力になっています。
C言語で記述したプログラムにおいて、入力や出力がどのようになっているのか、見ていくことにしましょう。Table.1:出力に関する標準関数
標準 に 出力 出 力printf() 書式 指定 文字列 出 力 puts() 文字列 出 力putc() 文字 出 力ファイルに 出力 fwrite() バイナリデータ 出力 fprintf() 書式 指定 文字列 出 力fputs() 文字列 出 力putc() 文字 力 出List.2:list2.c
#include <stdio.h> int main() { char str[] = "string\n", bin[] = {0x88, 0xAC, 0x99, 0x21}; FILE *fp; /* 標準出力へ出力 */ printf("this string writed printf().\n"); puts("this string writed puts()."); putc('A', stdout); /* テキストファイルへ出力 */ fp = fopen("test.txt", "wt"); fwrite(str, sizeof(str) - 1, 1, fp); fprintf(fp, "this string writed fprintf().\n"); fputs("this string writed fputs().", fp); putc('A', fp); fclose(fp); /* バイナリファイルへ出力 */ fp = fopen("test.bin", "wb"); fwrite(str, sizeof(str), 1, fp); fwrite(bin, sizeof(bin), 1, fp); fclose(fp); return 0; }
Fig.2:Windowsでの実行例
Fig.3:Linuxでの実行例
出力
C言語では、CPUやメモリを使って行う計算の機能を、その文法でサポートします。しかし、入出力に関する機能は、通常、何らかの関数を通じて提供されています。標準出力に文字列を書き出すprintf()も、標準関数の一種ですね。
プログラムから何らかの出力を行う標準関数には、Table.1に挙げたような関数があります。ANSI-Cでは、プログラムからの出力先として、文字を出力できる標準出力と、各種データを保存するファイルの、2種類の出力先をサポートしています。画面へのグラフィックの出力や、音楽、音声の出力はサポートしていません。
List.2は、標準出力と2種類のファイルに出力を行う例になっています。コンパイルして実行すると、画面上への文字列の表示と、test.txt、test.binの2つのファイルの作成を行います。実行例がFig.2、Fig.3になります。test.txtはテキストエディタで、test.binはバイナリエディタで、それぞれ内容を確認して下さい。
C言語では、ファイルを次のように扱います。
1:fopen()によってファイルを開く。
2:各種関数を用いて、読み書きを行う。
3:fclose()によってファイルを閉じる。演習:各行に英字A〜Zが1文字ずつ書かれたテキストファイルを出力するプログラムを作成せよ。
List.3:list3.c
#include <stdio.h> int main(int argc, char **argv) { int i; printf("argc = %d.\n", argc); for(i = 0; i < argc; i++) printf("argv[%d] = %s.\n", i, argv[i]); return 0; }
Fig.4:Windowsでの実行例
Fig.5:Linuxでの実行例
List.4:list4.c
#include <ctype.h> #include <stdio.h> #include <string.h> int main(int argc, char **argv) { int i, j; char argv0[128]; printf("argc = %d.\n", argc); for(i = 0; i < argc; i++) { /* argv[0]のコマンド名のみ別処理 */ if(i == 0) { /* 文字列内に「\」が含まれている場合はDOSのパスになっている */ if(strchr(argv[0], '\\') != NULL) /* 文字列内の最後の「\」の後にコマンド名自体がある */ strcpy(argv0, strrchr(argv[0], '\\') + 1); /* 文字列内に「/」が含まれている場合はUNIXのパスになっている */ else if(strchr(argv[0], '/') != NULL) /* 文字列内の最後の「/」の後にコマンド名自体がある */ strcpy(argv0, strrchr(argv[0], '/') + 1); /* 文字列内に「\」「/」が含まれていない場合は */ /* argv[0]自体がコマンド名になっている */ else strcpy(argv0, argv[0]); /* コマンド名に「.」が含まれている場合は拡張子が付いている */ if(strchr(argv0, '.') != NULL) /* 最初の「.」以下を文字列から削除する */ *strchr(argv0, '.') = '\0'; /* コマンド名を小文字にする */ for(j = 0; j < strlen(argv0); j++) argv0[j] = tolower(argv0[j]); /* この時点でargv0が求めるプラットフォーム依存を排したコマンド名となる */ printf("argv[%d] = %s.\n", i, argv0); } else printf("argv[%d] = %s.\n", i, argv[i]); } return 0; }
Fig.6:Windowsでの実行例
Fig.7:Linuxでの実行例
コマンドライン引数
ANSI-Cでは、場合に応じて使い分けられるように、複数の入力手段が用意されています。
コマンドライン引数は、プログラム起動時に、コマンドラインからプログラムに与えられる引数を、main()の引数として、情報を伝達します。
コマンドライン引数を利用する例が、List.3になります。実行例がFig.4、Fig.5になります。
List.3のmain()は2つの引数を持っています。argcは、argument count、すなわちコマンドライン引数の個数で、argvは、argument vector、引数の文字列を表すchar型2次元配列、すなわちコマンドライン引数文字列のリストになります。C言語上でコマンドライン引数を考える場合は、コマンド(プログラム名)文字列自体が、1つ目の引数として扱われます。したがって、コマンドのみでも引数の個数は1になるわけです。
また、Fig.4とFig.5を比較して分かるように、argv[0]の表現がプラットフォーム依存なので、argv[0]を利用する場合には注意が必要です。List.4では、argv[0]から、パス情報と拡張子を削除し、英字を小文字に変換し、プラットフォームに依存せずプログラム名を取得できるようになっています。実行例、Fig.6、Fig.7において、プラットフォームに依存せず同じ文字列が得られていることを確認して下さい。演習:Windows(MS-DOS)のコマンドである「echo」はFig.8のような動作を行う。このような動作を行うプログラムを作成せよ。
Table.2:入力に関する標準関数
標準 入力 から 入力gets()+sscanf() 書式 指定 文字列 入力 gets() 文字列 入力getc() 文字 入力ファイルから 入力 fread() バイナリデータ 入力 fgets()+sscanf() 書式 指定 文字列 入力fgets() 文字列 入力fgetc() 文字 入力List.5:list5.c
#include <ctype.h> #include <stdio.h> #include <string.h> int main() { char train_id[64] = "", start_station[64] = "", goal_station[64] = "", str[64]; int month = 0, day = 0, seat = 0; puts("何月何日の切符をお求めですか?"); puts("xx/xxの形式で入力して下さい。入力後にはリターンキーを押して下さい。"); gets(str); sscanf(str, "%d/%d", &month, &day); printf("%d/%dの切符をお求めですね。\n\n", month, day); printf("列車番号を入力して下さい。入力後にはリターンキーを押して下さい。\n"); gets(train_id); printf("%sの切符をお求めですね。\n\n", train_id); puts("どこから、どこまでの切符をお求めですか?"); puts("xx-xxの形式で入力して下さい。入力後にはリターンキーを押して下さい。"); gets(start_station); if(strchr(start_station, '-') != NULL) { strcpy(goal_station, strchr(start_station, '-') + 1); *strchr(start_station, '-') = '\0'; } printf("%sから%sの切符をお求めですね。\n\n", start_station, goal_station); puts("席の種類はどうしますか?"); puts("1:自由席"); puts("2:指定席"); puts("3:グリーン席"); puts("席の番号を入力して下さい。"); seat = getc(stdin); switch(seat) { case '1': puts("自由席をお求めですね。\n"); break; case '2': puts("指定席をお求めですね。\n"); break; case '3': puts("グリーン席をお求めですね。\n"); break; default: puts("グリーン席にしておきますね。\n"); } return 0; }
Fig.9:Windowsでの実行例
Fig.10:Linuxでの実行例
対話形式のプログラム
券売機でも、みどりの窓口でも、新幹線の切符を買うときには、「何月何日?」「列車番号は?」「どこから?」「どこまで?」「席の種類は?」という問いに次々と答えなければなりません。このプロセスのように、順番に出てくる質問に順次答えるという形式を、対話形式と呼んでいます。皆さんが普段使っているソフトウェアにも、対話形式で入力を促すものが多いと思います。最近の市販ソフトに多く採用されている「ウィザード」と呼ばれるユーザーインターフェイスも対話形式に他なりません。どちらかというと「自ら入力作業を行おうとしても、入力の仕方が分からない」という初心者向けのユーザーインターフェイスで、入力してもらうのを待つのではなく、ソフトウェアから積極的に必要な入力を要求することで、とにかくソフトウェアを使ってもらうためにと採用されることが多いようです。
ANSI-Cでは、標準入力と標準出力(通常はコンソール入出力)を用いた対話形式プログラムを作成できます。標準入力から何らかの入力を受け付ける標準関数には、Table.2に挙げたような関数があります。例えば、先程の新幹線の切符の例をプログラムにしてみたのが、List.5になります。コンパイルして実行した例がFig.9、Fig.10になります。
ANSI-Cには、List.5で用いた標準入力からの入力取得手法の他に、scanf()という、gets()+sscanf()と同じ働きを行う標準関数があります。gets()+sscanf()を使わずに、せっかくあるscanf()を使えばよさそうなものですが、scanf()は特にWindows(MS-DOS)上で、プログラマの意図した挙動を示さない場合があるので、あまり使うことを勧められないのです。List.5ではscanf()を使用していないので、Windows(MS-DOS)上で実行しても、UNIX(Linux)上で実行しても、同じ動作が確認できます。演習:身長(cm)と体重(kg)を対話形式で入力させ、肥満度を示すカウブ指数を求め表示せよ。なお、カウブ指数は、体重(kg)を身長(m)の2乗で割った値である。また、Table.3を参考に、肥満度に関してメッセージを添えるようにせよ。
Table.3:カウブ指数と肥満状態
カウブ 指数 19 未満やせています カウブ 指数 21 未満やせぎみです カウブ 指数 25 未満普通 ですカウブ 指数 27 未満ふとりぎみです カウブ 指数 27 以上ふとっています List.6:list6.txt
8/13 hikari101 tokyo-kyoto 2
Fig.11:Windowsでの実行例
List.7:list7.txt
何月何日の切符をお求めですか? xx/xxの形式で入力して下さい。入力後にはリターンキーを押して下さい。 8/13の切符をお求めですね。 列車番号を入力して下さい。入力後にはリターンキーを押して下さい。 hikari101の切符をお求めですね。 どこから、どこまでの切符をお求めですか? xx-xxの形式で入力して下さい。入力後にはリターンキーを押して下さい。 tokyoからkyotoの切符をお求めですね。 席の種類はどうしますか? 1:自由席 2:指定席 3:グリーン席 席の番号を入力して下さい。 指定席をお求めですね。
リダイレクト
一般的な仕様のコンソール(DOS窓や各種shell)上では、標準入力をコンソールからファイルに変更したり、標準出力をコンソールからファイルに変更したりすることができます。この機能のことを「リダイレクト」と呼びます。リダイレクトは、C言語とは関係なく、プラットフォームが提供する機能ですが、この機能を使うことで、ファイル操作を行うプログラムを簡単に作ることができます。
例えば、List.5のプログラムに対しての入力部分だけを抽出して作成した、List.6のようなテキストファイル(list6.txt)を用意します。ここでリダイレクトを使って、プログラムへコンソールから入力していたのを、list6.txtからの入力に変更して実行してみます。
> list5 < list6.txt
実行例がFig.11になります。キーボードから入力を行わずとも、ファイルの内容を入力だと解釈して、プログラムが動作することが分かります。また、
> list5 < list6.txt > list7.txt
を実行して得られるlist7.txtの内容がList.7ですが、リダイレクトを使えば、プログラムの出力をファイルに保存できることが分かります。
標準入出力を利用するプログラムを作成する場合には、コンソール入出力と、リダイレクトによるファイル入出力の両方を考慮すると、非常に利便性に富んだプログラムを実現することができます。特に、ファイルの変換を行うような「ツール」と呼ばれる種類のプログラムを作成する場合、ツールに与える情報がコマンドライン引数では少し多すぎるような時に考慮するとよいでしょう。演習:先程のカウブ指数を求めるプログラムについて、リダイレクトによる運用を考慮し、見直してみよ。どのような点に着目すればよいのだろう?
List.8:list8.c
#include <stdio.h> #define BUFFER_SIZE 64 int main(int argc, char **argv) { char buffer[BUFFER_SIZE]; int i; FILE *read_fp, *write_fp; if(argc != 3) { puts("引数の数が不正です。以下の書式を守って下さい。"); puts("list8 dest_file src_file"); return 1; } read_fp = fopen(argv[2], "rb"); if(read_fp == NULL) { puts("コピー元ファイルが開けません。"); return 1; } write_fp = fopen(argv[1], "wb"); if(write_fp == NULL) { puts("コピー先ファイルが開けません。"); fclose(read_fp); return 1; } while((i = fread(buffer, 1, BUFFER_SIZE, read_fp)) == BUFFER_SIZE) fwrite(buffer, 1, BUFFER_SIZE, write_fp); fwrite(buffer, 1, i, write_fp); fclose(read_fp); fclose(write_fp); return 0; }
Fig.12:Windowsでの実行例
List.9:list9.c
#include <stdio.h> #define BUFFER_SIZE 128 int main(int argc, char **argv) { char buffer[BUFFER_SIZE]; int i, j; FILE *read_fp, *write_fp; if(argc != 3) { puts("引数の数が不正です。以下の書式を守って下さい。"); puts("list9 dest_file src_file"); return 1; } read_fp = fopen(argv[2], "rt"); if(read_fp == NULL) { puts("コピー元ファイルが開けません。"); return 1; } write_fp = fopen(argv[1], "wt"); if(write_fp == NULL) { puts("コピー先ファイルが開けません。"); fclose(read_fp); return 1; } /* テキストファイルの行数を調べる */ for(i = 0; fgets(buffer, BUFFER_SIZE, read_fp) != NULL; i++); /* 後の行から処理 */ for(; i > 0; i--) { /* iに対応する行を読み込む */ fseek(read_fp, 0, SEEK_SET); for(j = 0; j != i; j++) fgets(buffer, BUFFER_SIZE, read_fp); /* 読み込んだ行を書き込む */ fputs(buffer, write_fp); } fclose(read_fp); fclose(write_fp); return 0; }
Fig.13:Windowsでの実行例
ファイルからの入力
リダイレクトを使えば、標準入出力を用いるように書かれたプログラムで、ファイルを扱うことが可能になります。しかし、ファイルに対して、もっと複雑な処理を行うためには、リダイレクトの利用では不十分です。例えば、ファイルの一部だけを読み書きしたい場合や、バイナリファイルを扱う場合などには不便ですね。
ANSI-Cでは、さきほどの出力を扱うプログラムの例で見たように、標準関数にファイルの入出力を扱う関数群が用意されています。ファイルからの入力に関しては、Table.2に挙げたような関数を利用できます。
List.8は、ファイルをコピーするプログラムの例です。実行例がFig.12になります。このプログラムでは、コピー元ファイルをバイナリ読み込みモードで、コピー先ファイルをバイナリ書き込みモードで開き、BUFFER_SIZE分コピー元から読み込んではコピー先に書き込むという作業を、読み込むデータがなくなるまで繰り返しています。ファイルをバイナリファイルとして扱っている理由は、テキストファイルはバイナリファイルの部分集合だからです。
List.9は、テキストファイルを読み込んで、行の順番を逆順にするプログラムです。実行例がFig.13になります。プログラムの動作手順(アルゴリズム)があまり精練されていませんが、内容を理解し易いプログラムになっていると思います。ファイルを開いて用意するところまではList.8と同様ですが、テキストファイルを行単位で扱うfgets()、fputs()を使って、少しだけ複雑な処理を行っています。このプログラムでは、実際には読み込んだ内容を使わないfgets()が多いので、その部分がムダになっています。こうしたムダは、大きなファイルを扱う場合などに、致命的な動作速度の遅さをもたらすことが多くあります。実務的なプログラムを作成するためには、こうした点に目を向けることも重要です。演習:List.9を、ftell()を用いて、ムダなfgets()の実行を行わないように改良せよ。また、1000行のテキストファイルを処理する場合、改良前と改良後で、fgets()の実行回数がどのようになるか検証せよ。
List.10:list10.c
#include <ctype.h> #include <stdio.h> #include <string.h> #define STR1 "何月何日の切符をお求めですか?" #define STR2 "xx/xxの形式で入力して下さい。入力後にはリターンキーを押して下さい。" #define STR3 "%d/%d" #define STR4 "%d/%dの切符をお求めですね。\n\n" #define STR5 "列車番号を入力して下さい。入力後にはリターンキーを押して下さい。\n" #define STR6 "%sの切符をお求めですね。\n\n" #define STR7 "どこから、どこまでの切符をお求めですか?" #define STR8 "xx-xxの形式で入力して下さい。入力後にはリターンキーを押して下さい。" #define STR9 "%sから%sの切符をお求めですね。\n\n" #define STR10 "席の種類はどうしますか?" #define STR11 "1:自由席" #define STR12 "2:指定席" #define STR13 "3:グリーン席" #define STR14 "席の番号を入力して下さい。" #define STR15 "自由席をお求めですね。\n" #define STR16 "指定席をお求めですね。\n" #define STR17 "グリーン席をお求めですね。\n" #define STR18 "グリーン席にしておきますね。\n" #define CHAR1 '-' #define CHAR2 '1' #define CHAR3 '2' #define CHAR4 '3' int main() { char train_id[64] = "", start_station[64] = "", goal_station[64] = "", str[64]; int month = 0, day = 0, seat = 0; puts(STR1); puts(STR2); gets(str); sscanf(str, STR3, &month, &day); printf(STR4, month, day); printf(STR5); gets(train_id); printf(STR6, train_id); puts(STR7); puts(STR8); gets(start_station); if(strchr(start_station, CHAR1) != NULL) { strcpy(goal_station, strchr(start_station, CHAR1) + 1); *strchr(start_station, CHAR1) = '\0'; } printf(STR9, start_station, goal_station); puts(STR10); puts(STR11); puts(STR12); puts(STR13); puts(STR14); seat = getc(stdin); switch(seat) { case CHAR2: puts(STR15); break; case CHAR3: puts(STR16); break; case CHAR4: puts(STR17); break; default: puts(STR18); } return 0; }
List.11:list11.txt
何月何日の切符をお求めですか? xx/xxの形式で入力して下さい。入力後にはリターンキーを押して下さい。 %d/%d %d/%dの切符をお求めですね。 列車番号を入力して下さい。入力後にはリターンキーを押して下さい。 %sの切符をお求めですね。 どこから、どこまでの切符をお求めですか? xx-xxの形式で入力して下さい。入力後にはリターンキーを押して下さい。 %sから%sの切符をお求めですね。 席の種類はどうしますか? 1:自由席 2:指定席 3:グリーン席 席の番号を入力して下さい。 自由席をお求めですね。 指定席をお求めですね。 グリーン席をお求めですね。 グリーン席にしておきますね。 - 1 2 3
List.12:list12.c
#include <ctype.h> #include <stdio.h> #include <string.h> int main(int argc, char **argv) { char train_id[64] = "", start_station[64] = "", goal_station[64] = "", str[64]; int month = 0, day = 0, seat = 0, i; char strings[18][80], chars[4]; FILE *fp; if(argc != 2 || (fp = fopen(argv[1], "rt")) == NULL) return 1; for(i = 0; i < 18; i++) fgets(strings[i], 80, fp); for(i = 0; i < 4; i++) { fgets(str, 64, fp); chars[i] = str[0]; } fclose(fp); printf(strings[0]); printf(strings[1]); gets(str); sscanf(str, strings[2], &month, &day); printf(strings[3], month, day); printf(strings[4]); gets(train_id); printf(strings[5], train_id); printf(strings[6]); printf(strings[7]); gets(start_station); if(strchr(start_station, chars[0]) != NULL) { strcpy(goal_station, strchr(start_station, chars[0]) + 1); *strchr(start_station, chars[0]) = '\0'; } printf(strings[8], start_station, goal_station); printf(strings[9]); printf(strings[10]); printf(strings[11]); printf(strings[12]); printf(strings[13]); seat = getc(stdin); if(seat == chars[1]) printf(strings[14]); else if(seat == chars[2]) printf(strings[15]); else if(seat == chars[3]) printf(strings[16]); else printf(strings[17]); return 0; }
暗黙の入力
List.1の実行結果と、演習で触れた「echo」の実行例を比較してみましょう。両方とも「Hello,World.」と出力を行うことができますが、List.1では文字列が直接プログラム内に埋め込まれているのに対し、「echo」では文字列が入力としてプログラムに取り込まれます。このことは、プログラム内で使うために直接、値が記述されている数値や文字列は、プログラムに対する入力だと見なせることを示しています。
例えば、List.5に埋め込まれている数値や文字列を、#defineを用いて書き換えてみると、List.10が得られます。ここで、#defineの内容を基にList.11のようなテキストファイルを作り、これを読み込んで動作するようなList.12を作成することができます。このように、プログラム内で使うために直接、値が記述されている数値や文字列を、プログラムに対する入力に書き換えることが可能です。
以前、プログラムを入出力装置と見なせると解説したことがありました。List.1のようなプログラムは入力を持たないのだと考えてしまいがちですが、実はプログラムに対する入力を、実行時ではなく、作成時に与えているのだと解釈することができます。したがって、入力を持たないプログラムは存在しないといえます。
プログラムの作成時に埋め込んでしまう値を入力だと考え、List.12のように扱うことが可能だと分かっていれば、その知識を生かしたソフトウェアの設計を行うことができます。例えば、List.11の日本語メッセージを、英語やその他の言語に翻訳したファイルを用意すれば、プログラムの変更なしに多国語対応が可能になります。通常、プログラムの扱う値は、固定値に思えるものでも場合によって変更される可能性があります。したがって、そういった値を変更し易いしくみを用意しておいてやることが重要なのです。List.13:list13.c
#include <stdio.h> #define BUFFER_SIZE 64 int main(int argc, char **argv) { char buffer[BUFFER_SIZE]; int i, j; FILE *read_fp, *write_fp; if(argc != 3) { puts("引数の数が不正です。以下の書式を守って下さい。"); puts("list13 dest_file src_file"); return 1; } read_fp = fopen(argv[2], "rb"); if(read_fp == NULL) { puts("コピー元ファイルが開けません。"); return 1; } write_fp = fopen(argv[1], "wb"); if(write_fp == NULL) { puts("コピー先ファイルが開けません。"); fclose(read_fp); return 1; } while((i = fread(buffer, 1, BUFFER_SIZE, read_fp)) == BUFFER_SIZE) { for(j = 0; j < BUFFER_SIZE; j++) buffer[j] = ~buffer[j]; fwrite(buffer, 1, BUFFER_SIZE, write_fp); } for(j = 0; j < i; j++) buffer[j] = ~buffer[j]; fwrite(buffer, 1, i, write_fp); fclose(read_fp); fclose(write_fp); return 0; }
Fig.14:Windowsでの実行例
ファイル暗号化プログラムの作成
さまざまな入出力を実現する手法を見てきましたが、最後にそれらを使って、少し実用的なプログラムを作成してみましょう。ファイルの中身を何らかの規則によって変換し、完全に元に戻すことが可能でありながら、普通には読めなくするという、ファイル暗号化プログラムを作成してみます。
あるファイルを暗号化して別のファイルにするプログラムを作成しようというわけですが、List.8やList.9を参考に考えると、ファイルをコピーする過程で、コピーするデータを暗号化しながらコピー先ファイルを作れば、暗号化したファイルが作れそうです。そこで、プログラムの基本的な構成は、List.8の構成を採用することにします。
次に、データを暗号化する手法を考えなければなりませんが、暗号化とはそもそも何なのでしょうか? 1つ目の特徴として、暗号化を行うと、意味のあったデータが、意味の無いように見えるデータに変換されることが挙げられます。例えば、ファイル内の全てのデータに、でたらめに数値を加え、新しいファイルを作れば、作られたファイルは意味が無いようなファイルになります。2つ目の特徴として、暗号化したファイルを基の内容に戻すことができないと、暗号とは言えません。一時的に意味の無いファイルに変換するだけで、いざファイル内のデータを使う時には、意味のあるデータを取り出せなければならないのです。
暗号化の特徴を持った、データ変換方法は、それこそいくらでも考えられますが、もっとも単純な方法として、全てのデータをビット反転させる方法を挙げることができます。ビット反転されたデータは、基のデータとは異なる値になりますが、再度ビット反転させることで、簡単に基のデータに戻すことができます。実際にプログラムを作成するとList.13となります。実行例Fig.14を参考に、さまざまなファイルが暗号化でき、再度プログラムにかけると基に戻ることを確かめて下さい。
List.13によって暗号化されたファイルは、List.13さえ持っていれば簡単に基のファイルに戻すことができます。そこで今度は、ある特定のカギ(暗証番号)がなければ基に戻せないようにしてみましょう。ここでは、そのしくみを実現するために、ビット演算の排他的論理和の性質を利用します。排他的論理和には、ある値a,bがあった場合に
a == a^b^b
が成り立つという性質があります。ここでaをデータ、bをカギと考えれば、cを暗号化されたデータとすると、
c = a^b
で暗号化でき、
c^b
で基に戻せることが分かります。実際にプログラムを作成するとList.14となります。実行例Fig.15を参考に、さまざまなファイルが暗号化でき、再度プログラムにかけると基に戻るが、カギを間違うと基に戻せないことを確かめて下さい。
暗号化を行う場合に重要なのは、正しく基に戻せることです。それさえ守れればよいので、暗号化する手法はいくらでも考えられますが、実際には、暗号化の手法が見破られないように考慮することも必要になります。最近の暗号化手法では、手法が公開されているが、カギを見つけるのが困難というものが主流です。しかし、あまり重要でない、自分の作ったプログラムでしか使用しないのであれば、単純な手法を用いていても、手法を公開しなければ、十分役に立つと思います。List.14:list14.c
#include <stdlib.h> #include <stdio.h> #define BUFFER_SIZE 64 int main(int argc, char **argv) { char buffer[BUFFER_SIZE]; int i, j, key; FILE *read_fp, *write_fp; if(argc != 4) { puts("引数の数が不正です。以下の書式を守って下さい。"); puts("list14 key dest_file src_file"); return 1; } key = atoi(argv[1]); read_fp = fopen(argv[3], "rb"); if(read_fp == NULL) { puts("コピー元ファイルが開けません。"); return 1; } write_fp = fopen(argv[2], "wb"); if(write_fp == NULL) { puts("コピー先ファイルが開けません。"); fclose(read_fp); return 1; } while((i = fread(buffer, 1, BUFFER_SIZE, read_fp)) == BUFFER_SIZE) { for(j = 0; j < BUFFER_SIZE; j++) buffer[j] = buffer[j]^key; fwrite(buffer, 1, BUFFER_SIZE, write_fp); } for(j = 0; j < i; j++) buffer[j] = buffer[j]^key; fwrite(buffer, 1, i, write_fp); fclose(read_fp); fclose(write_fp); return 0; }
Fig.15:Windowsでの実行例
まとめ
今回は、C言語(ANSI-C)に用意されている入出力の機能を中心に、入出力を行うプログラムの作成について解説しました。入力されたデータを加工して出力するのが、プログラムの仕事の本質ですから、今回の内容は、ジャンルを問わず、プログラミング時に必要となるでしょう。既に十分C言語を使いこなしている人にとっても、示唆となる部分があったのではないでしょうか?
ファイル暗号化プログラムの作成で見たように、さまざまなプログラムを作成するためには、その内部の処理手順を自分であれこれ考えなければなりません。来月からは、そういった「アルゴリズム」について考えていきたいと思います。それでは。
コラム
市販品のようなソフトウェアを作りたい
本文中に述べた、出力先のサポート状況を考えると、ANSI-Cの範囲内では、現在市販されているようなゲームや、GUIを持ったアプリケーションが作成できないことが分かります。市販品のようなソフトウェアを作成するためには、それぞれのプラットフォーム上で、画面や音源やその他の周辺機器と入出力を行うための方法を知らなければならないのです。
最近ではC言語がもっともポピュラーなプログラミング言語なので、ほとんどのプラットフォーム上で、周辺機器と入出力を行う、C言語から利用できる関数群が用意されています。そうした関数群をAPI(Application Programming Interface)と呼んでいます。
必要なAPIを知らなければ、市販品のようなソフトウェアは作れませんが、APIを知っていることとプログラミングの能力は別です。現在のさまざまなプラットフォームにおけるAPIは、プラットフォーム内部の構成を色濃く反映したものが多く、APIを利用してプログラミングを行う場合の使い易さが、あまり考慮されていません。このためAPIに関する知識が大量に必要となり、結果的に本来のプログラミング能力が二の次にされるようなAPI知識偏重の風潮が形成されているように思われます。
API知識偏重によってソフトウェアの生産性と品質が犠牲になっているため、最近では、従来のプラットフォームのAPIと、プログラマの間に、使い易さを考慮したライブラリを置いて橋渡しをさせ、APIの提供する機能を簡単に利用できるようにしようとする動きが、少しずつでてきました。こうした動きによって、作りたいものが作り易くなっていくことが期待されます。
特定のプラットフォーム上でのAPIマスターになるのも1つの方向性ですが、プログラマ全員がAPIマスターである必要もありません。「ANSI-Cは修めたけど、その先が…」と迷っている人は、何のエキスパートになりたいのかハッキリさせるべきです。テキストファイルとバイナリファイル
一般的なOS上では、コンピュータ上でさまざまなデータを扱うために、「ファイルシステム」が用意されています。ファイルシステムは、コンピュータ上で、ファイルと呼ばれるデータの塊を、複数個区別して使用できるようにしてくれています。コンピュータ上でデータを加工するのが主な仕事であるプログラムも、このファイルシステムを利用して仕事を行うことが多くなります。
ファイルには、その中身の解釈の仕方によって、2つの種類があります。中に書いてあるデータを、文章を表現したデータと解釈するのがテキストファイル、数値の集合体であると解釈するのがバイナリファイルです。例えば、html形式やCのソースファイルはテキストファイル、コンパイル後に得られる実行形式や、bmp、jpegなどの画像データファイル、wave、auといった音声データファイルはバイナリファイルです。
テキストファイルは、Windowsのメモ帳、vi、emacsといったテキストエディタで閲覧、作成、変更が可能です。逆に言えば、テキストエディタで正しく見ることのできるファイルがテキストファイルだとも言えます。文字を数値化したものが並んでいるデータを収めたファイルで、文字を数値化したものに該当しないデータは含まれません。
バイナリファイルは、ファイル内のデータを全て数値(バイナリ)データとして解釈します。したがって、テキストファイルとは異なり、文字を数値化したデータ以外も含まれ得ます。このため、ほとんどのバイナリファイルは、テキストエディタで中身を見ようとしても、意味のわからない文字の羅列しか見ることができません。バイナリファイルは、ほとんどの場合、特定のプログラムにとってしか意味のない内容になっています。
バイナリファイルを出力するプログラムを作成する場合は、出力している内容が意図したとおりか確認するために、バイナリエディタというツールを使用します。フリーウェアのバイナリエディタが、いくつかありますので、Vector(http://www.vector.co.jp/)などのサイトで入手することができます。改行の憂鬱
標準関数として利用することができる、printf()、puts()、fprintf()、fputs()、scanf()、gets()、fscanf()、fgets()、といった、行を考慮した文字列の入出力を行う関数群を使っていると、まれに意図したものと異なる挙動を示す場合があります。
例えば、標準出力に文字列を出力するputs()と、そのファイル出力版であるfputs()を比較してみると、ほとんどの処理系で、puts()による出力には自動的に改行が付加されるのに対し、fputs()による出力には改行が付加されません。
標準関数の中には、int型のデータ長が厳密に定義されていないのと同様に、その挙動の細部が厳密に定義されていないものが存在します。テキストファイルを扱うプログラムを作成するときには、関数の挙動を勝手に解釈するのではなく、十分にテストを行い、自分の解釈を実際の関数の挙動に合わせるようにして下さい。OSが変わったり、コンパイラが変わったりすることで、挙動が変わる可能性がありますので、その点にも注意が必要です。
また、OSによって、テキストファイル内の改行表現が異なるため、UNIX上で作成されたテキストファイルを、そのままWindows上に持ってくると、メモ帳上では正しく改行が行われない、といった問題もあります。
こうした問題に対処するためには「何らかのファイル入出力を行うプログラムを作成する場合は、基本的にバイナリファイルを扱うようにする」と考えた方が良いでしょう。人間がテキストエディタで作成したファイルを入力される場合と、出力ファイルがテキストエディタで見れる形でないと困る場合のみ、テキストファイルを扱うプログラムにすると良いでしょう。CD-ROM内のソースをUNIX(Linux)で使う場合の注意
今回のソースリストのように、漢字が使用されているテキストファイルを、プラットフォームの異なるコンピュータ間で利用する場合には、少し注意が必要です。テキストファイルにおける漢字の表現は、プラットフォームに依存するので、Windows(MS-DOS)で作成された漢字入りテキストファイルは、そのままの状態ではUNIX(Linux)で読むことができないのです。
Windows(MS-DOS)上で使われている日本語文字コードを「Shift JIS」、UNIX(Linux)上で使われている日本語文字コードを「EUC」といいます。CD-ROM内のソースはWindows上で作成されたテキストファイルなので、このままではUNIX(Linux)上で正しく読むことができませんし、コンパイルして日本語対応コンソール上で実行しても、正しく漢字の表示が行えないのです。
この問題に対処する為には、UNIX(Linux)上に用意されている「nkf」などの漢字コード変換ツールを使います。例えば、
$ nkf -e list.c > new_list.c
を実行すると、list.c内の漢字をUNIX(Linux)上で扱える形式に変換し、new_list.cを作成します。
CD-ROM内のソースをWindows(MS-DOS)以外で使う場合や、InetからDLしたテキストファイルを使う場合には、文字コードの差異に気をつけるようにしましょう。前回の演習問題の答え
まず1問目の問題ですが、22行目までで、ipに対する最新の代入操作が、20行目のfor文内にある「ip = &j」になります。従って、ipと同じ値を示す表現は「&j」ということになります。また、このfor文が終了した時点でのjの値は5ですので、22行目の時点での*ipの値は、その時点でのjの値である「5」になります。
2問目は、CD-ROMに収録されているlist3a.cを見てください。
3問目は、CD-ROMに収録されているlist4a.cを見てください。構造体へのポインタから、構造体のメンバーを取り出すためには「.」ではなく「->」を使用します。main()内において、retはli_params構造体へのポインタなので、retの指し示す構造体内部のarray配列にアクセスする為には、「ret->array[0]」を使用します。
4問目は、CD-ROMに収録されているlist5a.cを見てください。動作の流れ上は、どうしてもfree()を3回実行したいけれども、free()は1行しか書けないというわけです。ここで、ループと組み合わせた記述を考えれば、1行しか書いていないことでも、複数回実行できることに気付けば、free()をループで3回実行するような記述を考えることが出来ます。このアイデアに従って、liner_interpolation()の呼び出し方に工夫を加えています。今回出てきた標準関数一覧
#include <stdio.h>
FILE *fopen(char *filename, char *mode);
int fclose(FILE *fp);
fopen()、fclose()は、ファイル入出力を行う場合に、アクセス対象となるファイルを開いたり閉じたりするのに用います。fopen()の戻り値であるFILE構造体へのポインタが、以降、開かれたファイルを表します。このため、以降の読み書きには、その対象であるファイルを示すFILE構造体へのポインタが必要になっています。fopen()が、ファイルを開くのに失敗した場合は、戻り値がNULLになります。読み書きし終えたら、fclose()でファイルを閉じます。
fopen()のmode文字列は、ファイルの開き方を規定します。読み込みモードで開く場合は「r」、書き込みモードでファイルを作成する場合は「w」、テキストファイルとして扱う場合は「t」、バイナリファイルとして扱う場合は「b」といった文字を用いて、文字列を作ります。
#include <stdio.h>
size_t fwrite(void *buffer, size_t size, size_t n, FILE *fp);
size_t fread(void *buffer, size_t size, size_t n, FILE *fp);
fwrite()は、buffer内のデータを、size×nバイト、ファイルfpに書き込みます。また、fread()は、ファイルfpから、bufferに、size×nバイトのデータを読み込みます。これらの関数においては、データの量を、単位がsizeバイトのものをn個というように考えます。戻り値は、正しく読み書きできた個数になります。したがって、正常に処理が行われた場合は、戻り値とnが一致します。
#include <stdio.h>
int puts(char *string);
int fputs(char *string, FILE *fp);
int putc(int ch, FILE *fp);
これらの関数は、文字列もしくは文字を出力します。puts()は文字列stringを標準出力に、fputs()は文字列stringをファイルfpに、putc()は文字chをファイルfpに出力します。成功すると、puts()、fputs()は負でない数を、putc()は出力したchを返します。失敗した場合は定義済みの値EOFを返します。
C言語では、標準出力も、FILE *stdoutで示されるファイルとして扱われているため、標準出力にputc()を適用する場合には、fpにstdoutを指定します。stdoutはプログラムの起動時に自動的に開かれ、終了時に自動的に閉じられます。
また一般的に、puts()は自動的に改行し、fputs()は自動的には改行しません。
#include <stdio.h>
char *gets(char *buffer);
char *fgets(char *buffer, int max_count, FILE *fp);
int getc(FILE *fp);
これらの関数は、文字列もしくは文字の入力を受け付けます。gets()は標準入力から文字列をbufferに、fgets()はファイルfpから1行分の文字列をbufferに、getc()はファイルfpから1文字を読み込みます。成功すると、gets()、fgets()はbufferを、getc()は読み込んだ1文字を返します。失敗した場合は、gets()、fgets()はNULLを、getc()はEOFを返します。
C言語では、標準入力も、FILE *stdinで示されるファイルとして扱われているため、標準出力にgetc()を適用する場合には、fpにstdinを指定します。stdinはプログラムの起動時に自動的に開かれ、終了時に自動的に閉じられます。
また一般的に、gets()が得る文字列は入力文字列確定時の改行を含まず、fgets()が得る文字列はファイル内の改行を含みます。
#include <stdio.h>
int fseek(FILE *fp, long offset, int position);
long int ftell(FILE *fp);
ファイル内の読み書きを行うと、ファイル内の参照位置が読み書きした分ずつ移動します。このため、読み書きを繰り返すだけで、ファイル内のすべての要素に、先頭から順番にアクセスできるのです。しかし、場合によっては、任意のファイル内の位置に、参照位置を移動させたい場合があります。
fseek()は、ファイル内の参照位置を任意に移動させることができ、この機能によってファイル内の要素へのランダムアクセスが可能になっています。fseek()は、fpの参照位置を、positionからoffsetバイト移動したところに設定します。positionは定義済みの値を用いて、SEEK_SETでファイルの先頭、SEEK_CURで現在の位置、SEEL_ENDでファイルの終端を指定できます。
ftell()は、現在のファイル内の参照位置を、ファイルの先頭からのバイト数で返します。このftell()とfseek()を組み合わせれば、ファイル内のさまざまな位置を記憶しておき、必要なときに必要な場所へ、参照位置を移動させることができます。
#include <stdio.h>
int sscanf(char *buffer, char *format[, ...]);
sscanf()はbuffer内の文字列から、書式付入力を行います。書式の表現に関しては、printf()に準じています。printf()と異なり注意しなければならないのは、変数への入力を行う場合に、その変数がどこにあるか分からなければならないので、変数へのポインタを引数に与えなければならないという点です。List.5の14、15行目を見て考えてみてください。
#include <string.h>
char *strchr(char *string, int ch);
char *strrchr(char *string, int ch);
文字列string内を調べて、文字chがあるか探します。なかった場合はNULLを返し、あった場合はその位置をポインタで返します。strchr()はstringを前から、strrchr()はstringを後から探します。
#include <ctype.h>
int tolower(int ch);
文字chを小文字に変換します。変換できない文字はそのまま返すので、実際にはA〜Zがa〜zに変換されるだけです。
#include <stdlib.h>
int atoi(char *string);
文字列を整数に変換します。キーボードやテキストファイルから数値を入力しようとしても、それらの入力は文字でしか受け取れません。このため、文字列から数値に変換する関数が重宝されます。戻り値は変換された数値になります。