情報処理Ⅱ 2008年1月21日(月)
本日学ぶこと 前処理指令 マクロ 問題 サイコロを何度も振って,全ての目が最低1回出るまで,何回振らなければならないか? 最小6回,上限なし(∞回?) 欲しいのは現実的な値 100面のサイコロだったら?
前処理とコンパイル(1) 前処理・コンパイル・アセンブル・リンクの各処理は通常,コンパイラ(ccなど)が一手に引き受ける. ソースファイル (前処理前) ソースファイル (前処理後) 前処理 コンパイル アセンブル オブジェクト ファイル 実行ファイル リンク 前処理・コンパイル・アセンブル・リンクの各処理は通常,コンパイラ(ccなど)が一手に引き受ける. 入p.115 リp.15
前処理とコンパイル(2) 前処理は, 前処理のコマンド(プリプロセッサ,前処理系)は,cpp 狭義には,「コンパイルに先立って行われる処理」であり,したがってコンパイルとは別 広義には,ccでコンパイルすれば自動的に処理してくれる,という意味でコンパイル作業の一部 前処理のコマンド(プリプロセッサ,前処理系)は,cpp Cの前処理以外にも使用可能 入p.113 リp.380
前処理指令 (Preprocessing directive) 「プリプロセッサ 指令」ともいう マクロ定義(#define) オブジェクト形式マクロ ⇒「定数」の定義 関数形式マクロ ⇒「関数もどき」の定義 マクロとは…コンピュータ関連の作業において,複数の機能や意味をまとめて扱えるようにしたもの ソースファイルの取り込み(#include) ⇒12月3日その2参照 条件付きコンパイル(条件付き取り込みともいう.#if ... #endif など) 「マクロとは」の説明は, http://d.hatena.ne.jp/keyword/%A5%DE%A5%AF%A5%ED による. リp.380
要素数は,コンパイル時に評価可能な定数式なので,問題なし オブジェクト形式マクロ(1) 語の置き換えを行う. #define 置換対象 置換内容 #define WORD_SIZE 6 と記述すると,それ以降 int a[WORD_SIZE]; は int a[6]; と同じ意味になる. プログラム修正により変わり得る定数値があるときに,よく用いられる. 配列の上限値,他と区別する値など. うまく使うことで,定数値を変えるときのプログラム修正箇所を少なくできる. 要素数は,コンパイル時に評価可能な定数式なので,問題なし 列挙型のほうがいいかも リpp.384-388
オブジェクト形式マクロ(2) 注意点 前提: #define WORD_SIZE 6+1 単純に置き換える. int a[WORD_SIZE * 2]; は,int a[6+1 * 2]; に置き換えられる(意図した動作ではない). ⇒ #define WORD_SIZE (6+1) とすればよい. 語のみを置き換える. print_WORD_SIZE() のような「語の一部」や,printf("WORD_SIZE"); のような「文字列中の語」は,置き換えない.
オブジェクト形式マクロ(3) 注意点(続き) 予約語も置換可能. #define char signed char は文法上問題ないが,よい書き方ではない. typedef signed char schar; とすべきである. 置換内容のない名前も定義できる. #define DEBUG 末尾にセミコロンをつけない. #define WORD_SIZE 6; は(たいていの場合)間違い.
関数形式マクロ(1) オブジェクト形式マクロとほぼ同じ書式. 置換対象に「(…)」をつける. #define pint(x) printf("%d\n",x) に対して,pint(a+1); は printf("%d\n", a+1); に置き換えられる. 複数の引数をとることもできる.そのときは,置換対象の各引数をカンマで区切る. カッコ内に何も書かなければ,引数なしの関数形式マクロが定義される. リpp.388-390
関数形式マクロ(2) 注意点 単純に置き換える. #define mul(x, y) x*y に対して, z=mul(6+1,2); としたとき,z=14ではなくz=8となる. ⇒ #define mul(x, y) ((x)*(y)) のように, 置換内容には,全体と各引数にカッコをつける. 置換内容の中に,引数を2箇所以上書くことができる. その回数だけ置換される. #define triple(x) ((x)+(x)+(x)) に対してb=triple(++a); と書くと,b=((++a)+(++a)+(++a)); となる.
関数形式マクロ(3) 置換内容の中で「#引数」と書くと,引数を文字列にできる. #define pint(x) printf(#x " = %d\n", x) に対して,pint(a+1); は printf("a+1" " = %d\n", a+1); に置き換えられる. 通常の関数定義では,変数名を 引数にとってその文字列を得る ことはできない. 「文字列リテラルの連結」により,これは printf("a+1 = %d\n", a+1); と同じとなる. リpp.390-392, p.10, p.57
関数か関数形式マクロか 関数…「機能」を正確に表現したいとき マクロ…「機能」を簡便に表現したいとき 例:int square_int(int x) { return x * x; } 引数や戻り値の型に制約される. 関数呼び出しのオーバーヘッドがある. ローカル変数や制御文を活用できる. 実引数が++aなどのときも,その評価は一度だけ. マクロ…「機能」を簡便に表現したいとき 例:#define square_int(x) ((x) * (x)) 引数や評価式に型はない. (狭義の)コンパイル前に展開され,オーバーヘッドは少ない. ローカル変数や制御文は使用しにくい. (マクロ利用側の)引数は,置換内容の回数だけ評価される.
# define pint( x ) printf ( #x " = %d\n" , x ) 前処理指令と空白・コメント ...不可 ...必須 # define pint( x ) printf ( #x " = %d\n" , x ) ...任意 一つの前処理指令は,1行で書かなければならない.ただし, 行末に「\」を置くことで,複数行で書ける. 関数形式マクロの場合,括弧の途中で改行できる. 前処理指令の中でコメント(/* */)を書くと,前処理時に空白文字に置き換えられる. リpp.382-383
サイコロ問題 仕様 ① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩ ⑪ 目の数は6つ.ただし変更の可能性あり. サイコロの目は1~6のいずれかとする. 出た目はその都度出力する. 全ての目が出たら,何回振ったかを出力し,終了する. ① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩ ⑪ allspot.c
サイコロ問題の考え方(1) サイコロの振り方 ライブラリ関数のrandを用いる あらかじめ #include <stdlib.h> int a = rand(); により,aにはint型の値が一つ代入される.この値は,ある範囲の中でどの値も等しい確率で選ばれる(一様乱数). int spot = rand() % 6; で,spot には 0~5 のいずれかが代入される. int spot = rand() % 6 + 1; とすればいい! 擬似乱数のため,何度実行しても同じ目が出る.これを変えるには,ライブラリ関数のsrandを呼び出して,適切な値の種を与えればよい. リpp.504-506, p.118
サイコロ問題の考え方(2) 「全ての目が出る」とは? int spot_counter[SPOT_MAX + 1]; 目ごとに出た回数 特定の目がまだ出ていないかの判定は, if (spot_counter[spot] == 0) 全ての目が出たかを,この配列変数だけで判定することはできるが,非効率 int counter_unfound = SPOT_MAX; まだ出ていない目がいくつあるか 一つずつ減らしていき,0になれば「全ての目が出た」
サイコロ問題で定義したマクロ #define SPOT_MAX 6 目の数を表す,オブジェクト形式マクロ ここ以外で「6」と書かない.これにより,目の数が変わるようなプログラムにも対処しやすい. #define cast_dice(spot_max) (rand() % (spot_max) + 1) サイコロを1回振って,出た目を返す,関数形式マクロ 呼び出し元では int spot = cast_dice(SPOT_MAX); であり,これは int spot = (rand() % (SPOT_MAX) + 1); になる. 「cast_dice(100*5)」のように使うことも可能
条件付きコンパイル(1) #if 定数式 … #endif 定数式が真のときに「…」を残し,そうでなければ「…」を捨てる. 定数式の評価や「…」の取捨は,前処理時(≠実行時)に行われる. リpp.397-398
条件付きコンパイル(2) 「#if 定数式」に代えて,「#ifdef 名前」や 「#ifndef 名前」も利用可能. 「#else」や「#elif 定数式」も記述可能. 条件付きコンパイルは入れ子にできる. 条件付きコンパイル 参考: Cのif文 #if 条件式1 … #elif 条件式2 #else #endif if (条件式1) { … } else if (条件式2) { } else { } リpp.399-402
ヘッダファイルで条件付きコンパイル ある環境の /usr/include/stdio.h より #ifndef _STDIO_H #define _STDIO_H 1 … #endif /* !_STDIO_H */ この記述により,複数のファイルに #include <stdio.h> があっても問題なく動作する. ヘッダファイルの中で,別のヘッダファイルをインクルードすることがある. _STDIO_H を定義して「…」を獲得するのは,1回だけ. 「インクルードガード」と呼ばれる. http://www.geocities.jp/ky_webid/c/060.html
次に学ぶこと ファイル入出力 問題 これまでそのプログラムを何回実行したか,記録できる? ファイルの各行を逆順に出力できる? "wakayama\n" ⇒ "amayakaw\n"
ファイルとは 補助記憶装置に保存する単位となる,データの集まり. プログラムが終了しても,内容が保持されるデータ構造. バイト列とは? (比較)変数の値はメモリ上にあるため,プログラム終了時に破棄される. コンピュータの電源を切って入れ直しても,保持されていることが多い. ストリームと呼ばれるバイト列として,読み書き可能. バイト列とは? char配列で表現できるデータ構造. '\0'(ナル文字,ヌル文字)で終わるものではなく,途中に '\0' があってもいいという点が,文字列と異なる. 入p.188 リp.455
実行回数管理プログラム 仕様 考え方 ./count を実行すると,count.txt というファイルに実行回数が保存される. 入pp.186-187 count.c
ファイルポインタ Cでファイルを操作するには,ファイルポインタを使用する. FILE オブジェクト fp stdio.h で定義されているFILE型のポインタ. 例: FILE *fp; FILE オブジェクト fp リp.457
ファイル操作のライブラリ関数(1) FILE *fopen(char *path, char *mode); ファイルを開き(プログラムから使えるようにし),ファイルポインタを返す. 第1引数はファイル名(「パス名」ともいう). 第2引数が "r" なら,読み込み専用で開く. 第2引数が "w" なら,書き込み専用で開く. リpp.463-464, p.462
ファイル操作のライブラリ関数(2) int fscanf(FILE *stream, const char *format, ...); char *fgets(char *s, int size, FILE *stream); 「size-1バイト」,「改行文字まで」,「ファイルの終わりまで」のうち最小のバイト数を読み込んで,s が指し示す配列領域に格納し,最後に '\0' をつける. int fgetc(FILE *stream); 1バイト読み出して,その値を返す. リpp.467-468, p.462, p.461
ファイル操作のライブラリ関数(3) int fprintf(FILE *stream, const char *format, ...); int fclose(FILE *fp); 開いたファイルを閉じる(プログラムから使えないようにする). リpp.464-465, pp.458-459
ifとfopenの組み合わせ コード例 if ((fp = fopen("count.txt", "w")) == NULL) { printf("failed to open file: count.txt\n"); return 1; } ファイルを開くことができなければ,メッセージを出力して,関数の処理を終える.開ければ,fpにファイル構造体のポインタが代入され,あとのファイル処理で利用できる. カッコの対応に注意. × if (fp=fopen(ファイル名, "w"))==NULL) × if (fp=fopen(ファイル名, "w")==NULL) この例では,「fpへの代入」と「if文」を分けて書いてもよい.しかし「代入」と「while文」を同時に書く(分けると保守性を損なう)ことが多いので,この記述に慣れてほしい.
ファイルから整数値を得る方法 fscanfで整数値を獲得する fgetsで1行読み出して配列に格納し,atoiで文字列を整数値に変換する fgetsで1行読み出して配列に格納し,sscanfで文字列から整数値を獲得する fgetcで1文字ずつ読み出し,整数値を構成する 「非負整数値を一つ読み出す」なら,どれでもよい 実用上の観点(入力情報が変わってもコードを書きやすい)でおすすめは,「fgets+sscanf」と「fgetc (ただし関数にする)」. 各手法のデメリット: fscanf…入力がフォーマット(第2引数で与える文字列)に合っていないとき,読み飛ばせない. fgets+atoi…1行の情報を格納する配列領域が必要になる.1行から獲得したい情報が複数あるとき,処理が難しい. fgets+sscanf…1行の情報を格納する配列領域が必要になる.入力が空白文字(' 'でも'\n'でもよい)で区切られていて一つ一つ読み出すとき,処理が複雑になる. fgetc…1文字を読み出す変数が必要になる.バイト列から整数への変換を自前で作ることになり,バグが入りやすい.
まとめ 前処理指令をうまく使えば,読みやすく保守しやすいプログラムを書くことができる. 前処理は,コンパイルの前に行われる.そのため,前処理指令の書式はCの文法と異なる. ファイル操作により,プログラムの外から情報を受け取ったり,情報を保存したりすることができる.
スケジュール 第13回:1月28日(月) 第14回:1月30日(水) 試験:2月?日(?) ファイル入出力(続き),標準入出力,mallocなど 第14回:1月30日(水) おさらい問題 試験:2月?日(?) 自筆ノート1冊の参照を認める 教科書・書籍,印刷資料等は参照不可