電 子 計 算 機 基 礎 論 実 習 計 算 機 基 礎 論 3 B 金沢大学理学部数学教室 1996年06月26日(水)  72.はじめに きょうは,C言語における構造体,ファイル操作,関数の再帰呼び出しについて入門的 な事項を学びます。くわしいことは参考書を読んで勉強してください。  73.構造体 構造体(structure)とは,いくつかの変数を一つにまとめて作った新しい型の変数のこ とです。 == complex.c ================================================================== #include struct complex { /* 新しい型 struct complex を定義 */ double re; double im; }; /* ←このセミコロンを忘れやすいので注意 */ typedef struct complex Complex; /* それを以下 Complex と呼ぶ */ void print(Complex z); /* 複素数を出力 */ Complex add(Complex z1, Complex z2); /* 複素数の足し算 */ void print(Complex z) { printf("(%f + %f i)", z.re, z.im); } Complex add(Complex z1, Complex z2) { Complex tmp; tmp.re = z1.re + z2.re; tmp.im = z1.im + z2.im; return tmp; } int main() { Complex z1 = { 3.0, 7.0 }; /* 構造体の初期化 */ Complex z2 = { 4.0, -2.0 }; print(z1); printf(" + "); print(z2); printf(" = "); print(add(z1, z2)); putchar('\n'); return 0; } =============================================================================== - 58 - ここでは,サンプルとして複素数を扱うプログラムを考えてみましょう。 「複素数 z の実部は変数 rez, 虚部は imz, 複素数 w の実部は rew, 虚部は imw ...」のようにしてプログラムを書くと,どの変数とどの変数の組が一つの複素数を表わ すかを間違えたりする可能性がでてきます。  ※ re は real part, im は imaginary part のつもり。 そこで,二つの数を組み合わせて一つの複素数型変数としてしまいましょう。これが構 造体です。 「struct complex {」で始まる部分が,その新しい型の定義です。「struct」は構造体 を表わすキーワードなので,いつでもこう書きます。そのあとの complex は構造体名で す。中カッコの中は,この新しい型が二つの double 型変数からなっていて,一つめが re, 二つめが im と呼ばれることを示しています。珍しいことに,「}」のあとにセミコ ロンがつきます。忘れやすいのでよく注意してください。 こう定義をしておくと,以下では「struct complex」が「int」と同じような型名として 使えます。しかし,いちいち struct と打つのはめんどうなので,「Complex」を 「struct complex」の代わりに使えるようにしましょう。それがその次の typedef の行 です。以後は,「Complex」を「int」と同じような型名として使えます。 変数名は小文字で,定数名は大文字で書くのが習慣でした。型名は大文字で始めて二文 字目からは小文字にするのが習慣のようです。 「void print(Complex z);」「Complex add(Complex z1, Complex z2);」はプロトタイ プ宣言です。「Complex」を従来の「int」「double」のようなものだと思えば,目新し いものではないでしょう。 その次に,それらの関数の定義がきます。そこにあるように,Complex 型変数 z の re 成分は z.re で表わされます。 main 関数の最初の部分では構造体の初期化をしています。「int n = 1;」にあたるもの です。  ※ WS の CC でコンパイルするときは z1 と z2 の宣言と初期化を main の外へ出す    こと。そうでないとコンパイルしてくれない。 この例は簡単すぎて構造体を定義する必然性が感じられないと思いますが,複雑なプロ グラムでは構造体が効果を発揮します。  74.ファイル操作 プログラムの中からファイルを読み書きすることができます。それには,  1.ファイルをオープン  2.ファイルを読み書き  3.ファイルをクローズ という3つの手順を順に行ないます。 最初の「ファイルをオープン」では,ファイル名を指定して fopen 関数を呼び出し, 「ファイルポインタ」と呼ばれるものを返り値として受け取ります。次の「ファイルを 読み書き」では,そのファイルポインタを指定してデータを読み出したり書き込んだり します。最後の「ファイルをクローズ」を行なうと一連の操作を終了します。  ※ 「ファイルポインタ」については説明しない。  ※ プログラムが正常に終了するときにはオープンされたファイルはすべて自動的に    クローズされるので,万が一クローズを忘れても心配はいらない。 - 59 - == mycopy1.c ================================================================== #include int main() { FILE *in, *out; int c; in = fopen("test.txt", "rt"); /* ファイルを読み出し用にオープン */ if (in == NULL) { fprintf(stderr, "test.txt をオープンできません.\n"); return 1; } out = fopen("test.$$$", "wt"); /* ファイルを書き込み用にオープン */ if (out == NULL) { fprintf(stderr, "test.$$$ をオープンできません.\n"); fclose(in); return 1; } while ((c = fgetc(in)) != EOF) { /* ファイルから1バイト読み出す */ if (fputc(c, out) == EOF) { /* ファイル に 1バイト書き込む */ fprintf(stderr, "書き込みエラーです.\n"); fclose(in); fclose(out); return 1; } } fclose(in); fclose(out); /* ファイルをクローズ */ return 0; } =============================================================================== これは test.txt を text.$$$ にコピーするプログラムです。 「FILE *in, *out;」がファイルポインタ in と out の宣言です。「*」はこのように つけるものだと思っておいてください。「FILE」は型名ですがこれ以上説明しません。 「in = fopen("test.txt", "rt");」はファイル test.txt を読み出し用にオープンしま す。第2引数の "rt" は「読み出し用に,テキストファイルとして」という意味です。 次の if 文ではファイルがオープンできたかどうかチェックしています。例えばそのフ ァイルが存在しなかったなどの理由でオープンできなかった場合は fopen() から NULL が返っています。NULL の説明はしません。 「out = fopen("test.$$$", "wt");」はファイル test.$$$ を書き込み用にオープンし ます。第2引数の "wt" は「書き込み用に,テキストファイルとして」の意味です。前 から test.$$$ が存在した場合は,その内容は捨てられてしまいます。 次の if 文ではファイルがオープンできたかどうかチェックしています。ファイルが書 き込み禁止だったなどの理由でオープンできなかった場合は fopen() から NULL が返っ ています。その場合,最初のファイルがオープンされていますので,「fclose(in);」で それをクローズしてから終了しています。 その次の while 文では,in から1バイトずつ取り出しては out に書き込んでいます。 「fgetc(in)」は in から1バイト取り出します。ファイルが終了している場合は EOF を返します。「fputc(c, out)」は,ファイルに文字 c を書き込みます。ディスクがい っぱいなどの理由で書き込みに失敗すると EOF を返します。その場合は二つのファイル をクローズしてから終了しています。 最後は,二つのファイルをクローズして終了しています。 - 60 - == mycopy2.c ================================================================== #include char buf[128+1]; int main() { FILE *in; FILE *out; if ((in = fopen("test.txt", "rt")) == NULL) { /* 読み出し用にオープン */ fprintf(stderr, "test.txt をオープンできません.\n"); return 1; } if ((out = fopen("test.$$$", "wt")) == NULL) { /* 書き込み用にオープン */ fprintf(stderr, "test.$$$ をオープンできません.\n"); fclose(in); return 1; } while (fgets(buf, 128+1, in) != NULL) { /* 一行読み出す */ if (fputs(buf, out) == EOF) { /* 一行書き込む */ fprintf(stderr, "書き込みエラーです.\n"); fclose(in); fclose(out); return 1; } } fclose(in); fclose(out); /* ファイルをクローズ */ return 0; } =============================================================================== これも同じく test.txt を text.$$$ にコピーするプログラムです。 前と違うのは,fgets() で一行分まとめて読み込み,fputs() で一行分まとめて書き込 んでいる点です。 「fgets(buf, 128+1, in)」は in から一行を配列 buf に読み込みます。ただし高々 128 文字しか読み込まれないので,配列からはみ出る心配がありません。fgets() は gets() と違って '\n' も配列に加え,その後ろに '\0' をつけます。「128+1」と書い たのはこの '\0' の分を別にして 128 文字,と強調するためです。fgets() はファイ ルの終わりでは NULL を返します。 「fputs(buf, out)」は out に配列 buf を書き出します。fputs() は puts() と違って 最後に '\n' をつけ加えませんが,このプログラムでは buf には '\n' まで読み込まれ ているので全体としては in から読み込んだものと out へ書き出したものは全く同じに なります。fputs() はエラーのときは EOF を返します。  ※ これらのプログラムではエラー処理に関する部分が大半を占めている。実用的な    プログラムを書く場合はこういうことがよくあるので,忍耐が必要である。  ※ WS では「cat test.$$$」としてもダメで「cat test.\$\$\$」とする必要がある    ので,「test.$$$」は別の名前に代えたほうがいいかも知れない。  ※ コマンドライン引数でファイル名を指定したい場合は, int main(int argc, char *argv[]) { /* ... */ in = fopen(argv[1], "rt"); out = fopen(argv[2], "wt"); /* ... */ }    のように書くことになる。 - 61 -  75.関数の再帰呼び出し 関数の中で自分自身を呼び出すことができます。そのことを再帰呼び出しといいます。 == fpower.c =================================================================== #include /* lcc fpower.c -lmathlib としてコンパイル */ #include /* pow */ double power(double d, int n); main() { int i; for (i = 3; i < 30000; i = i + 1483) { printf("%f %f\n", power(1.0001, i), pow(1.0001, i)); } } double power(double d, int n) { /* d の n 乗を返す。n は非負に限る。*/ double tmp; if (n == 0) { return 1; } else if (n % 2 == 0) { tmp = power(d, n / 2); /* n が偶数のとき */ return tmp * tmp; } else { tmp = power(d, n / 2); /* n が奇数のとき */ return tmp * tmp * d; } } =============================================================================== power() は自作のベキ乗関数です。n が正の偶数のときは自分自身を呼び出して n/2 乗 を求め,それを二乗して返しています。  ※ return power(d, n / 2) * power(d, n / 2) とすると power を2回呼び出すの    で時間のムダになる。 n が奇数のときは (n-1)/2 乗を二乗したものに底 d をかけて返しています。C言語で は「/」は小数点以下切り捨てですから,n / 2 で (n - 1) / 2 が求められます。 この方法では power を呼び出す回数は log(n) のオーダーで済みますので,単純に n 回ループを回わす方法と比べると n が大きい場合に所要時間に差が出てきます。 main 関数では自作関数ともともとついている pow() とを比べて検算をしています。こ こでは指数 i がぐんぐん大きくなるよう i を 1483 ずつ増やしているので,power() を単純にループを回わすものに書き換えて比べてみると速度の差が実感できます。  ※ 1483 に特別な意味はない。:-) == gcd.c ====================================================================== int gcd(int x, int y) { if (y == 0) { return x; } else { return gcd(y, x % y); } } =============================================================================== これは非負の整数 x, y の最大公約数を求める関数を再帰を使って書いたみたもので す。少し考えると,ユークリッドの互除法のアルゴリズムを用いていることがわかるで しょう。 - 62 -  76.画面制御エスケープシーケンス CTRL+[ で始まる決められたシーケンスを画面に出力することによって,画面を制御する ことができます。ただし,これらのシーケンスは機種に依存するので注意が必要です。 ここでは実習室のパソコン(FMR)の場合を説明します。WS では結果が端末に依存する のであまり使いやすくありません。  ※ CTRL+[ は,edias では「PF5」「CTRL+[」または「PF5」「ESC」で入力する。    画面では ^[ と表示されるので以下そのように表記する。ちなみに emacs では    CTRL+Q, ESC で入力できる。 ^[[m 普通表示 ^[[6m アンダーライン ^[[30m 黒(見えない) ^[[7;33m 反転・黄 ^[[31m 赤 ^[[5m 点滅 ^[[32m 緑 ^[[33m 黄 ^[[4;10H 4行目の10カラム目へカーソル移動 ^[[34m 青 ^[[0K カーソル以降行末まで消去 ^[[35m マゼンタ ^[[2J 画面全体を消去 ^[[36m シアン (^[[40;0m^[[2J だという説もあり) 次のようなファイルを作って type してみましょう。 == escape ===================================================================== ^[[2J ^[[4;10Hhello, world ^[[8;20H^[[7;33mhello, world ^[[m =============================================================================== printf の "..." の中でこれらを使うには,「^[」ではなく「\033」とします。 == escape.c =================================================================== #include main() { printf("\033[2J"); printf("\033[4;10Hhello, world"); printf("\033[%d;%dH\033[7;33mhello, world", 8, 20); printf("\033[m"); } =============================================================================== %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% %% %% この実習は4年生と2年生合同で行なっていますので,両学年の授業期間の共 %% %% 通部分をとって7月10日(水)で終わりにします。ただしそれ以降も9月い %% %% っぱいは水曜2限は実習室を予約してあります。私もなるべくこの時間は空け %% %% ておきます。課題の締め切りはこれから決めます。 %% %% %% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% %% %% MS-Windows でメニューに「セッション(S)」(実際は S の下にアンダーライン)%% %% とあったら,「ALT+S」がショートカットキーです。つまり,ALT+S を押すとそ %% %% こを選んだのと同じ効果が得られます。キーボードに慣れていればマウスを使 %% %% より速いかも。 %% %% %% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 岩瀬順一 - 63 -