Dokusyo-nissi Bessitu 2004-12-27 φ(-_-) ■ [lang]プログラミング言語 C ・次に示す関数 trim() は、文字列の末尾から余分な空白とタブと改行文字をとり 除き、そのどれにもあたらない最も右側の文字が見つかったところで break を用い てループを抜け出すプログラムである。*1 1. int trim(char s[]) 2. { 3. int n; 4. for (n = strlen(s) - 1; n >= 0; n--) { 5. if (s[n] != ' ' && s[n] != '\t' && s[n] != '\n') { 6. break; 7. } 8. } 9. s[n + 1] = '\0'; 10. /* 文字列の終わりを示すためのヌル文字を追加しておく */ 11. return n; 12. } ・ここで strlen() は文字列の長さを求めるために使われている。この forループ では、行の最後の文字から開始して、空白・タブ・改行文字ではない最初の文字を さがして逆方向にスキャンしていく。そしてそうした文字が見つかったとき、ある いは n が負になったとき (つまり文字列全体をスキャンし終わったとき) にループ は終了する。 ・たとえ行に空白文字しかないときでも、この関数が正常に動作することを確かめ てほしい。(p78-79) 1. /* trim_str.c */ 2. #include 3. #include 4. int trim(char str[]); 5. main() 6. { 7. char str_1[] = "abcdef\n\t\t"; 8. char str_2[] = " "; /* 空白文字の連続でも OK */ 9. trim(str_1); 10. trim(str_2); 11. printf("%s", str_1); 12. printf("< end of str_1\n"); 13. printf("%s", str_2); 14. printf("< end of str_2\n"); 15. return 0; 16. } 17. int trim(char s[]) 18. { 19. int n; 20. for (n = strlen(s) - 1; n >= 0; n--) { 21. if (s[n] != ' ' && s[n] != '\t' && s[n] != '\n') { 22. break; 23. } 24. } 25. s[n + 1] = '\0'; 26. return n; 27. } $./trim_str として確認。 *1:←でもコレ、なんの役にたつんだろう ? 2005-01-15 φ(-_-) ■ [lang]プログラミング言語 C C プログラムは変数あるいは関数のいずれかである外部オブジェクトの集合で構成 されている。 外部的とは、主に関数内で定義される引数と (自動)変数を記述する内部的という考 え方と対照的に使用される。 (第1章で述べたように) 外部変数はすべての関数の外側で (前もって) 定義され、 多くの関数で利用することが可能となる。 C では関数内で別の関数を定義することは許されていないので関数自身は常に外部 的である(といえる)。 特に指定のない限り外部変数および (外部)関数は、その同じ名前で参照すると、別 にコンパイルされた関数からであってもやはり同じ変数または関数を参照できると いう性質をもっている。この性質を標準規格では外部リンクと呼んでいる。 もし 2つの関数がデータを共用しそしてどちらからも相手の関数を呼び出さない場 合、引数で (データを) 受渡しするより外部変数を使ったほうがベンリである。( p90) 演算子 + - * / が使える電卓プログラムを書いてみよう。 実装をやさしくするためこのプログラムでは (普通の) 挿入記法を使わず、逆ポー ランド法を使用することにする。 逆ポーランド法はポケット電卓や Forth、Postscript のような言語で使われている 方式である。 逆ポーランド法では各演算子は被演算数の後に置く。(つまり) 次のような挿入記法 の式、 (1 - 2) * (4 + 5) は 1 2 - 4 5 + * のように表す。カッコは不要である。 これの実装はきわめて簡単である。(←そんなことはなかったゾ) まず各演算数をスタック上にプッシュする。(次に) 演算子が来ると要求される個数 の被演算数がポップされ、それに演算子が適用されてその結果が (再び) スタック にプッシュされる。 上の例では 1 と 2 がプッシュされ (- 演算子によって) その差 -1 が代わってプ ッシュされる。 次に 4 と 5 がプッシュされ (+ 演算子によって) その和 9 が入れ替わりにプッシ ュされる。 さらに -1 と 9 の積 -9 が (* 演算子により計算され、最終的に) 入れ替わる。 スタック上のこの数字が入力行の終了時にポップされてプリントされる。 エラーの検出等を加えていくと操作が長くなるので、プログラム全般にわたってコ ードを並べていくより、それぞれの操作を別個の関数にするほうがいい。 また入力から次の演算子や被演算数を取り出す別の関数も必要になる。 このプログラムの設計上でたいせつなのは、スタックをどこに置くか、である。つ まりどのルーティンでそれに直接アクセスするかである。 (以上を考慮に入れて) スタックとそれに関連する情報は外部変数とし、main() 関 数ではなく push() 関数と pop() 関数でアクセスできるようにする。(p90-100) 1. /* main.c */ 2. #include 3. #include 4. /* 関数 atof() に必要、atof() は文字列を数字に変換 */ 5. #include "calc.h" 6. #define MAX_OP 100 7. /* 逆ポーランド電卓プログラム */ 8. main() 9. { 10. int type; 11. double tmp; 12. /* 2項の左右の数を区別するために導入する仮変数 */ 13. char str[MAX_OP]; 14. while ((type = get_op(str)) != EOF) { 15. switch(type) { 16. case NUMBER: 17. /* type が数字であれば */ 18. push(atof(str)); 19. break; 20. /* 数字を積んでいく */ 21. case '+': 22. push(pop() + pop()); 23. break; 24. case '-': 25. tmp = pop(); 26. push(pop() - tmp); 27. break; 28. case '*': 29. push(pop() * pop()); 30. break; 31. case '/'; 32. tmp = pop(); 33. if (tmp != 0.0) 34. push(pop() / tmp); 35. else 36. printf("[警告 !] 0 で割っちゃダメ\n"); 37. break; 38. case '\n': 39. printf("ans : %.8g\n", pop()); 40. /* 引数 g は有効桁数 */ 41. break; 42. default: 43. printf("[警告 !] %s ってゆー演算子、知りませんヨ\n", str); 44. break; 45. } 46. } 47. return 0; 48. } 1. /* stack.c */ 2. #include 3. #include "calc.h" 4. #define MAX_VAL 100 5. int stk_p = 0; /* スタックポインタの初期化 */ 6. double value[MAX_VAL]; 7. void push(double fig) 8. { 9. if (stk_p < MAX_VAL) 10. value[stk_p++] = fig; 11. else 12. printf("[警告 !] スタックがいっぱいで %g が push できません\n", fig); 13. } 14. double pop(void) 15. { 16. if (stk_p > 0) 17. return value[--stk_p]; 18. else { 19. printf("[警告 !] スタックは空ですネ\n"); 20. return 0.0 21. } 22. } 1. /* get_op */ 2. #include 3. #include 4. /* 関数 isdigit() に必要、isdigit() は 10進法かどうかを判別 */ 5. #include "calc.h" 6. int get_op(char str[]) 7. { 8. int i, c; 9. while ((str[0] = c = get_ch()) == ' ' || c == '\t') 10. ; 11. /* 空白とタブをスキップ */ 12. str[1] = '\0'; 13. if (!isdigit(c) && c != ".") 14. return c; 15. /* c が数字でなければそれを返す */ 16. i = 0; 17. if(isdigit(c)) 18. while (isdigit(str[++i] = c = get_ch())) 19. ; 20. /* 整数部分の数字を集める */ 21. if (c == '.') 22. while (isdigit(str[++i] = c = get_ch())) 23. ; 24. /* 小数点以下の数字を集める */ 25. str[i] = '\0'; 26. if (c != EOF) 27. unget_ch(); 28. /* この部分は関数 get_ch() のコメントを参照 */ 29. return NUMBER; 30. /* input したのが数字であれば返り値は '0' になる */ 31. /* main() の switch文はこの結果を判断材料とする */ 32. } 1. /* get_ch.c */ 2. #include 3. #define BUF_SIZE 100 4. char buf[BUF_SIZE]; 5. int buf_pt = 0; /* インデックス変数 */ 6. int get_ch(void) 7. { 8. return (buf_pt > 0 ? buf[--buf_pt] : getchar(); 9. } 10. void unget_ch(int c) 11. { 12. if (buf_pt >= BUF_SIZE) 13. printf("[警告 !] バッファからあふれてますヨ\n"); 14. else 15. buf[buf_pt++] = c; 16. } 17. /* 18. * プログラムでは数でない文字を 1つ余分に読み込む必要がある 19. * そのため unget_ch() でその文字を記憶しておき、get_ch() が 20. * 呼び出される毎に 1文字分を戻しておく 21. */ 1. /* calc.h */ 2. #define NUMBER '0' 3. /* '0' は NUMBER が数であることの表記 */ 4. void push(double); 5. double pop(void); 6. int get_op(char[]); 7. int get_ch(void); 8. void unget_ch(int); コンパイルしてみます。まずオブジェクトファイルの作成。 $cc -c main.c stack.c get_op.c get_ch.c コンパイルエラーがあればここで訂正。 次に実行ファイルをつくる。 $cc -o rpn_calc main.o stack.o get_op.o get_ch.o 最後に確認。 $./rpn_calc 改行して待ち受け画面になるので 1 2 - 4 5 + * ← Enter コードがまちがってなければ ans : -9 とプリントされる . . はず。 (プログラムの終了は Ctrl + d) 2005-01-26 φ(-_-) ■ [lang]プログラミング言語 C C は関数を再帰的 (recursion) に使用することができる。つまり関数は (その内部 で) 関数自身を直接または間接に呼び出してもよい。 数字を文字列に変換する関数 print_d() を考えてみよう。 数字の場合、逆順生成という性質をもつ。すなわち低い桁の数字が高い桁の数字よ り先に得られる。 (そのため出力時には) 逆順にプリントさせなければならない。 この問題には 2つの解がある。一つは生成した順に配列に格納し (do-while文を使 って) 逆順にプリントする方法である。 もう一つの方法が再帰を使う解法で、逆順生成に対処するため、関数内部で関数自 身を呼び出すようにする。 1. /* print_d.c */ 2. #include 3. /* 数字 n を 10進法でプリントする */ 4. void print_d(int n) 5. { 6. if (n < 0) { 7. putchar('-'); 8. n = -n; 9. } 10. /* 数字 n が負の場合はじめに正の整数に変換しておく */ 11. if (n / 10) 12. print_d(n / 10); 13. /* 数字が 2桁以上のとき再帰関数を呼び出す */ 14. /* このとき int型の除算なので小数点以下が切捨てとなる */ 15. putchar(n % 10 + '0'); 16. /* 高い桁から順にプリント */ 17. } 関数が自分自身を再帰的に呼び出すと、それぞれの呼び出しごとにすべての自動変 数が一新され、以前とはまったく独立したセットになる。 例えば print_d(123) では最初の print_d() では n = 123 である。 第2レベルの print_d() では '12' が渡り、つぎにそのうちの '1' が第3レベルの print_d() に渡る。 第3レベルの print_d() は '1' をプリントしてから第2レベルに戻る。第2レベルの print_d() で '2' がプリントされ、最初のレベルへと戻る。そして '3' がプリン トされ (この関数は) 終了となるわけである。 再帰を使ったもう一つのよい例が quicksort すなわち C.A.R.Hoare が 1962年に考 えだしたソートアルゴリズムである。 ある配列が与えられたとき、まず 1つの要素 (下のコードでは int n) を選んだら 、その要素より小さいパートと、等しいかより大きいパートとの 2つのサブセット に分割する。 次に同じプロセスを再帰的に 2つのサブセットに適用していく。 そしてサブセットが 2より少ない要素をもつようになればこのソートは必要なくな り再帰は終了する。 1. /* q_sort.c */ 2. void q_sort(int v[], int left, int right) 3. { 4. int i, last; 5. void swap (int v[], int i, int j); 6. if (left >= right) 7. return; 8. /* 配列が 2 より少ない要素になったときは */ 9. /* 以下のプログラムは実行されない */ 10. swap (v, left, (left + right) / 2); 11. last = left; 12. /* 分割要素をサブセットの始め = v[0] に移動させる */ 13. for (i = left + 1; i <= right; i++) /* 分割 */ 14. if (v[i] < v[left]) 15. swap (v, ++last, i); 16. /* for文はここまで */ 17. swap (v, left, last); /* 分割要素を回復 */ 18. q_sort(v, left, last - 1); 19. q_sort(v, last + 1, right); 20. /* 再帰関数を使い左右それぞれのサブセットをソートしていく */ 21. } 1. /* swap.c */ 2. /* スワップ関数 */ 3. void swap (int v[], int i, int j) 4. { 5. int tmp; 6. tmp = v[i]; 7. v[i] = v[j]; 8. v[j] = tmp; 9. } 再帰を使う場合、処理中の値のスタックを保持しなければならない。そのためメモ リの節約にはならないしまた速度も (使用しないときと比べ) 速いとはいえない。 しかし再帰を使ったプログラムでは (コード自体が) よりコンパクトになり、使わ ないプログラムに比べてずっと書きやすくまた理解するのが容易となることが多い 。 再帰は tree のように再帰的に定義されるデータ構造には特に適した書き方である 。(p105-107) 2005-01-31 φ(-_-) ■ [lang]プログラミング言語 C 文字列を入力すると逆順にして出力する関数 reverse() を再帰を用いて作成せよ。 1. /* reverse.c */ 2. #include 3. void rev_str(char); 4. main() 5. { 6. char s; 7. printf("Input any word and push [enter] [ctrl + d]\n"); 8. rev_str(s); 9. printf("\n"); 10. return 0; 11. } 12. void rev_str(char c) 13. { 14. if ((c = getchar()) != EOF) 15. rev_str(c); 16. putchar(c); 17. } 2005-02-03 φ(-_-) ■ [lang]プログラミング言語 C (C のプリプロセッサで) 最もよく使われる 2つの機能は、コンパイル中にファイル の内容を読み込むための #include と、token (= 文字の綴り) を任意の文字列に置 き換えるための #define である。 #include を使えば全てのソースファイルに同じ定義と変数宣言を入れることができ るので、悪質なバグが紛れ込みにくくなることが (ある程度) 保証されることにな る。 もちろん、読み込むファイルの内容を変更した場合には、それを適用する全てのフ ァイルはコンパイルし直さなければならない。 マクロは次の形式で定義される。 #define NAME text #define 中の NAME は変数名と同じ形をもち、置き換える text のほうは任意 (の 文字列等) である。 通常、この text の部分は行の最後に置く。 (また) 定義が長くなる場合には、行の最後にバックスラッシュ (= '\') を置くと 、次の行へ続けることができる。 #define で定義された NAME の有効範囲は、定義の書かれた行からソースファイル の最後までとなる。 (マクロは) 引用符で囲まれた文字列には適用されない。(また、NAME の文字がある 文字列の一部である場合にも適用されることはない) 例えば YES がマクロで定義された NAME だとして、 printf("YES") や YESMAN ではマクロによる置き換えは実行されない。 NAME および text (の中身) は任意 (= 自由) である。 例えば #define FOREVER for ( ; ; ) の場合には、無限ループを表す新しい NAME、FOREVER が定義されている。(for ( ; ; ) の部分が text) (仮)引数つきのマクロの定義も可能である。 #define MAX(A, B) ((A) > (B) ? (A) : (B)) このとき x = MAX(p + q, r + s); は (マクロによって) 次のように置き換えられる。 x = ((p + q) > (r + s) ? (p + q) : (r + s)); 関数の場合と違い、データ型が異なっていても、別の NAME で定義する必要はない 。このマクロではどんなデータ型であっても適用が可能となる。 (ただしこのように仮引数を使うと) 式が二重に評価されるので、インクリメント演 算子や入出力の場合には副作用がでてよくない (ときがある)。 (また) カッコを用いる場合、どのような順序で評価されていくかを、注意深く考え ておく必要がある。 実用例として、ヘッダファイル では、getchar と putchar はマクロと して定義されている。(なぜなら) 文字を処理するたびに関数呼び出しを行なうとい う実行時のオーバーヘッドを避ける (ためにである)。 (他にも) の中の関数群は普通、マクロとして定義されている。 NAME は #undef を用いて (コードの途中で) 未定義にすることもできる。 (こんなことはやらないと思うが) getchar を未定義にして、同じ名前で別の関数 getchar を使いたい場合には、 #undef getchar int getchar(void) { ... } というふうにすればいい。(p107-110) 2005-02-13 φ(-_-) ■ [lang]プログラミング言語 C (前に説明したようにマクロは) 引用符で囲まれた文字列には適用されない。 しかし置き換えるトークン列のパラメータ (←text) の直前に # がついていれば、 対応する引数が文字列引用符 ( " ) で囲まれる。 このとき文字列中にある " または \ という文字の前には (自動的に) \ 文字が挿 入される。(←特殊文字として認識されないように) デバック用に使われる関数 printf() を、文字列と組み合わせてプリントマクロと して考えてみよう。 #define D_PRINT(expr) printf(#expr " = %g\n", expr) 引数に x/y を挿入して、 D_PRINT(x/y); としたとき、このマクロは次のように展開される。 printf("x/y" " = %g\n", x/y); (それぞれの引用符で囲まれた) 文字列は結合されるので、これは下の文と同じであ る。 printf("x/y = %g\n", x/y); 演算子 ## を用いると、マクロの展開中に実引数を結びつけることができる。 define PASTE(front, back) front ## back PASTE(name, 1); として展開すると、 text 中の各パラメータ (← front と back) は実引数 (← name と 1) に置き換え られる。 また、## とその両側にあるスペース部分は (自動的に) 除かれるので、結果として 、 name1 という文字記号が得られることになる。 ## を用いる場合、実引数にカッコで囲まれたトークンが入るときには、マクロを2 つのレベルに分ける必要がある。 define CAT(x, y) x ## y define X_CAT(x, y) CAT(x, y) X_CAT((1, 2), 3); として展開させると、X_CAT 自体には ## が含まれないので、その結果、 123 という文字記号が得られる。(p110-111, p289-291) こんなかんじ、ですか ? 1. /* love.c */ 2. #include 3. #define LOVE(person) printf("I love %s.\n", #person) 4. main() 5. { 6. LOVE(you); 7. return 0; 8. } 「叫ぶ ! C プログラマ」(isbn:4881663763) によれば、## は実際には、こんなふうに 使用されるらしい。(p128) 1. /* country.c */ 2. #include 3. #define NUM_CO(number) printf("country : %s\n", country ## mumber) 4. main() 5. { 6. char *country01 = "U.S.A."; 7. char *country81 = "Japan"; 8. NUM_CO(81); 9. return 0; 10. } 2005-02-21 φ(-_-) ■ [lang]プログラミング言語 C (#define で取り込む) 前処理そのものを制御するには、その間に評価を行なう (# で始まる) 条件文を使うことで可能となる。この時評価される条件の値に応じて、 コンパイル時にコードが選択され取り込まれる。 まず #if 行で式を評価する。この式は定数整数式なので sizeof 演算子、型変換 (キャスト)、enum 定数を含んではならない。 この式の値が 0 以外 (= 真) である場合にのみ、後続する #endif 行までのコード が取り込まれる。条件式に #elif と #else とが含まれる場合も、同様にそれぞれ の値によって評価が行なわれる。 例えば、あるヘッダファイルを「確実に一度だけ」取り込むためには、そのヘッダ ファイルの内容を次のように条件式で囲むとよい。 #if !defined(HEADER) #define HEADER /* header.h ファイルの内容をここに置く */ #endif defined 演算子は、(カッコの中の) NAME が定義されていれば 1、そうでなければ 0 を返す。 最初に header.h ファイルを取り込んだ時点で、HEADER という NAME が定義される 。 次に、同じファイルを取り込もうとすると、すでにこの NAME は定義されている (ので defined 演算子の値は 1 となりさらに ! 演算子により最終的には 0 が返る ことになる)。(その結果) #endif 行までのコードはスキップされる。 この方法で系統的に (条件文を) 記述すると、各 OS の システム毎に依存するヘッダ を (自動的に) 選択させることができる。こうすれば、ユーザはシステムへの依存 性をいちいち気にかける必要性がなくなる。 次の一連のテストでは SYSTEM という NAME を使って、どのヘッダファイルを選択 するかを決定している。 #if SYSTEM == SYSV #define HEADER "sysv.h" #elif SYSTEM == BSD #define HEADER "bsd.h" #elif SYSTEM == MSDOS #define HEADER "msdos.h" #else #define HEADER "default.h" #endif #define HEADER #ifdef および #ifndef を使った条件文では、NAME が定義済みかどうかをテストす ることが可能となる。 例えば、#ifndef 文を用いると、最初の例を (defined 演算子を使わずに) 次のよう に書くことができる。 #ifndef HEADER #define HEADER /* header.h ファイルの内容をここに置く */ #endif (p111-112)