Dokusyo-nissi Bessitu 2005-11-09 φ(-_-) ■[lang]はじめての C さて、次のステップはどうしようか ? いまのぼくに理解できそうな本がないか、本屋にいって捜してみた。それがコレ、 作ってわかるCプログラミング C についてはまったくの初心者で他の言語もよく知らないが、 i = i + 1 が意味す ることぐらいは何となく想像できる程度の人で、 フムフム ... ぴったりですネ。 サポートページはコチラ→ http://www.gihyo.co.jp/book/2001/402231/download/ index.html 2005-11-13 φ(-_-) ■[lang]はじめての C C programming note*1 インクリメント演算子は、整数型の変数に適用したときは「値を 1つ増やす」働き をしたが、ポインタ変数に適用すると「指していたものの次を指すようにする」働 きをする。(p73) main 関数の仮引数の 1つ argv はポインタ関数なので、次のような実行文では argv[0] がスキップされることになる。 ++argv; (vector) char *argv[] argv・---->・--> command name ・--> 1st parameter ・--> 2nd parameter 0 ++argv; argv・ ・--> command name ---->・--> 1st parameter ・--> 2nd parameter 0 これは、ポインタ変数が実際にどう使われているか、ということですね。 *1:「作ってわかる Cプログラミング」 2005-11-14 φ(-_-) ■[lang]はじめての C C programming note*1 少し戻って、次のような「標準入力の内容をそのまま標準出力に出力する」プログラム がでてくる。(p16) /* typ1.c */ #include main() { int c; while ((c = getchar()) != EOF) putchar(c); } これを標準入力ではなく、ファイルから読み込ませるには、 標準入力の代わりに、起動時のコマンドラインの第1パラメータで指定された名前の ファイルを入力とする。 その入力用ファイルをオープンできないときにはエラーメッセージを表示して終了 する。また指定されたパラメータの数が 1つでなかったとき (0, または 2つ以上の とき) も、エラーメッセージだけで終了する (p42) ようにする。 /* typ2.c */ #include main(int argc, char *argv[]) { FILE *fp; int c; if (argc != 2) { fprintf(stderr, "parameter error\n"); exit (1); } if ((fp = fopen(argv[1], "r")) == NULL) { fprintf(stderr, "can't open %s\n", argv[1]); exit (1); } while ((c = fgetc(fp)) != EOF) fputc(c, stdout); fclose(fp); return 0; } そういえば、これまでつくったプログラムは全部標準入力させてたような ... ところで、 return というキーワードは、そこで関数の処理を終了して呼び出し元に戻る働きを する。 return の終ろに値が書いてある場合は、それがその関数の戻り値 return value として設定される。 なぜ exit() を使わず return で値を返すようにしたかというと、main() が実は int型の値を返す関数だからだ。このように return するようにしておけば、コンパ イラも「main() 関数に return文がない」という警告メッセージを出さなくなる。 (p84) これは知らなかった。 *1:「作ってわかる Cプログラミング」 2005-11-15 φ(-_-) ■[lang]はじめての C C programming note*1 今度は、ポインタ配列の argv が使われてるプログラム。(p64-65) これは UNIX のコマ ンド cat とほぼ同じ働きをする。つまりこういうことが可能、 $./cat1 file1 file2 file3 > outfile コードはこちら、 /* cat1.c */ #include main(int argc, char *argv[]) { FILE *fp; int c; --argc; /* argv[0] の分を 減らす */ ++argv; /* ← ここで 使われてる */ if (argc < 1) { /* パラメータが ないと */ fprintf(stderr, "parameter error\n"); exit (1); } while (argc--) { /* パラメータの順に file を 渡っていく */ if ((fp = fopen(*argv, "r")) == NULL) { fprintf(stderr, "can't open %s\n", *argv); exit (1); } while ((c = fgetc(fp)) != EOF) fputc(c, stdout); fclose(fp); argv++; /* 次の ポインタ配列へ */ } return 0; } while の中にも1つ while があり、そこの部分は ptr2 プログラムに似ている。その詳 しいことはどんどん後まわしにしといて、標準エラー出力の仕組みから、 標準エラー出力 standard error は stdio.h で定義されている名前で、特別なスト リームの一種だ。どういうストリームかというと、名前から予想されるとおり「エ ラーメッセージの出力」に使われる。 標準エラー出力は、通常はユーザが見ている画面に出力されている。リダイレクシ ョン '>' によって標準出力がファイルなどにつなぎ替えられたときも、標準エラー 出力は切り替えられずにそのまま画面に出力される。 この性質を利用して、データの出力にエラーメッセージが混じってしまうのを防ぐ ことが可能だ。(p66) フムフム ... 次は cat プログラムをいじって、複数の関数に分けてみる ...んだけど、cat1 にはイ ンクリメント演算子がいくつも使われてるので、その説明から、 インクリメント increment 演算子は変数の値を 1つ分増加させるもので i++ というのは、 i = i + 1 という式とほとんど同じである。つまり整数型の変数に適用した場合は「値を 1つ 増やす」という働きになる。 逆に、変数の値を 1つ分減らすデクリメント演算子は "--" だ。 実はインクリメント演算子とデクリメント演算子には、 i++ というように使う使い方のほかに ++i という使い方がある。どちらも「i の値を 1 増やす」という機能には変わりはない が、その式の値に違いがある。 前者は式の値を評価してからインクリメントが行なわれるが、後者はインクリメン トしてからその式の値を評価する。 「式の値を評価する」とは、「その結果が式の値となる」といったほうがわかりや すいかもしれない。 たとえば、i の値が 10 だったとき、 j = i++; では i は 11 になるが、j には増加 (インクリメント) する前の 10 が代入される 。 それに対して、 j = ++i; では、i はやはり 11 になるが、j にも増加後の 11 という数値が代入される。 (p54-55) この cat1 プログラムには両方使われてますね。 *1:「作ってわかる Cプログラミング」 2005-11-16 φ(-_-) ■[lang]はじめての C C programming note*1 複数の関数に分けるというのは、よく使われる - 基本になる - 処理を切り離して別の 関数にしたてておく。それを main() 関数で呼び出す、ということ。 cat1 を改良したプログラムでは 2つの関数が使われている。 do_one() は、ファイル 1つ分の処理をする関数だ。すでに (入力用に) オープンさ れているストリームを引数としてとる。 fopen() 関数が返してきたストリームへの ポインタ (もしくは stdin) を引数として指定してやれば、それを入力として処理 をする (そのまま標準出力へコピーする)。(p76) cant() は、エラーメッセージを出力して終了する - exit() する - だけの関数だ 。 一般に関数は処理が終了するとその呼び出し元へ戻るのが通常だが、この cant() のようにその関数の中で exit() を呼び出すと、プログラムはそこで終了してしま う。 これをやってしまうとプログラムの流れを把握しづらくなるので、あまりやたらと 行なうべきではない。 exit() 関数を呼び出すのは、main() のほかには、異常終了 のための処理を行なう関数ぐらいに限定しておくべきだ。(p77) プログラムは次のとおり、 /* cat2.c */ #include void do_one(); /* プロトタイプ宣言 */ void cant(); main(int argc, char *argv[]) { FILE *fp; --argc; ++argv; /* argv[0] を スキップ */ if (argc < 1) { fprintf(stderr, "parameter error\n"); exit (1); } while (argc--) { if ((fp = fopen(*argv, "r")) == NULL) cant(*argv); /* no return */ do_one(fp); fclose(fp); argv++; } return 0; } void do_one(FILE *fp) { int c; while ((c = fgetc(fp)) != EOF) fputc(c, stdout); } void cant(char *name) { fprintf(stderr, "can't open %s\n", name); exit (1); } 前回同様、各関数の引数など詳しいことは、どんどんとばしています。 *1:「作ってわかる Cプログラミング」 2005-11-18 φ(-_-) ■[lang]はじめての C C programming note*1 関数 do_one() の引数の説明は、外部変数の解説のところででてきます。 「名前の適用範囲 scope」というものを説明しよう。 main() 関数内で FILE *fp; と宣言されている fp という変数は他の関数の中では使えない。これは main() の 関数定義のブロック ( { } で囲まれた範囲 ) 内で宣言されているので、その範囲 内だけで適用する宣言となっているわけだ。 それに対して、関数の外側で宣言されているものを外部宣言 external declaration と呼ぶ。 変数を外部宣言すると、その変数は 1つの関数内だけでなく、ファイル中でその宣 言以降に現れるすべての関数で適用する名前となり、どの関数の中でも参照できる ようになる。外部宣言された変数を外部変数 external variable という。 (p106-107) 注意することとして、 変数をやたらと外部宣言してはいけない。不用意な変更や知らないうちに参照して いるような事態を避けるために、変数はなるべく必要な関数内だけで「見える」よ うに宣言すべきだ。 (ある外部変数が) へたに「どの関数でも (関数の外でも) 適用する名前」になって いると、1つの変数についての変更を行なうたびに、そのファイルをすべて読んでそ の変数の使われ方をチェックし、影響がないかどうかを調べなければいけない。そ れはいかにも効率が悪いし、プログラムの信頼性も落ちる。(p107-108) で、do_one() ではなぜ引数渡しを行なうのか、というと、 ある変数を複数の関数で使用する場合、その変数を外部宣言する方法以外に、「引 数で渡す」という方法も考えられる。 do_one() における引数 fp などがそうだ。 この 2つの方法のうちどちらが適しているかは、場合による。 do_one() の場合、呼び出す側の関数で値の設定や変更をするが、渡された側の関数 では値の参照しかしていないので、この方法が使える。 呼び出された側でも値の変更の必要があるなら、引数で渡す方法は使えない。 C で は、呼び出された関数内で仮引数を変更しても、呼び出し側で実引数として与えら れた変数には影響を与えないからである。(p108) ついでに、cant() 関数の引数についても、 つまり、C では呼ばれた関数が呼び出した関数の変数を直接改変することはできな いからだ。値による呼び出し call by value ともいう。 呼び出された関数側で、呼び出した関数側の変数の内容を変更したい場合は、引数 には変数のアドレス (変数へのポインタ) を渡す必要がある。もちろん、呼ばれる 関数側でもその引数がポインタ型であると宣言していなければならないし、そのポ インタを使って間接的に値を参照するわけだ。 ただし、配列の場合は事情が異なる。配列の名前を関数の引数として使うと、その 場合はその配列の先頭アドレスが渡される。つまり、呼ばれた関数側ではポインタ としてうけとるわけだ。 このことが理解できれば cant() 関数の引数についても理解できることだろう。 (p109-110) それで cant() の引数が *argv となってるのか ... *1:「作ってわかる Cプログラミング」 2005-11-19 φ(-_-) ■[lang]はじめての C C programming note*1 cat2 をさらに改良して「標準入力」と「コマンドラインでファイル名を指定して順にそ れらのファイルを入力元とする」両方のスタイルが使えるようにします。このとき、関 数を分けたことが役にたってきます。 このような入力元の指定方法は、UNIX ではおなじみのスタイルだ。 cat コマンド や sort コマンドなど、何か入力データを加工して出力するような「他のコマンド と組み合わせて使う小さくて気のきいた道具」のようなコマンドでは、このやり方 はよく使われる。(p82) このスタイルを実現するためのプログラム上でのしかけとは、 if (argc == 0) do_one(stdin); /* 標準入力でも 使える */ else { while (argc--) { if ((fp = fopen(*argv, "r")) == NULL) cant(*argv); do_one(fp); fclose(fp); argv++; } } この if文で、標準入力を処理するケース (パラメータが指定されなかった場合) と 、ファイル名が指定されてそれを処理するケースを場合分けしている。(p82) プログラムは次のとおり、 /* cat3.c */ #include #include /* 実は exit() に 必要 */ void do_one(); void cant(); main(int argc, char **argv) /* *argv[] と **argv は 同じ ポインタ配列の表記 */ { FILE *fp; --argc; ++argv; if (argc == 0) /* パラメータが 0 */ do_one(stdin); /* 標準入力を 処理 */ else { while (argc--) { if ((fp = fopen(*argv, "r")) == NULL) cant(*argv); do_one(fp); fclose(fp); argv++; } } return 0; } void do_one(FILE *fp) { int c; while ((c = fgetc(fp)) != EOF) fputc(c, stdout); } void cant(char *name) { fprintf(stderr, "can't open %s\n", name); exit(1); } このプログラム cat3.c は、コマンドラインでパラメータをひとつも指定しなかっ たときには、typ.c と同じ動作をする (リダイレクションの記号とその後ろのファ イル名はパラメータのうちに数えない)。 またパラメータの指定があるときには、cat2.c とまったく同じ動作をする。(p81) 入力時の切り替えをコマンド sort で実装するやり方も後のほうででてきます。 *1:「作ってわかる Cプログラミング」 2005-11-20 φ(-_-) ■[lang]はじめての C C programming note*1 次は、小文字を大文字に変えるプログラム。 最初の ASCII 依存プログラムでは、大文字と小文字のコード番号がちょうど 32 だけ離 れているのを利用しています。その部分のコードは、 while ((c = getc(fp)) != EOF) { if ('a' <= c && c <= 'z') fputc(c - 'a' + 'A', stdout); /* ASCII 依存 */ else fputc(c, stdout); } となり、それ以外のところは cat3 プログラムそのままです。 その応用で、逆に大文字を小文字に変えるには、上の if文の中身を、 if ('A' <= c && c <= 'Z') fputc(c + 'a' - 'A', stdout); に変更すれば OK ですね。 これを、入力ファイルに日本語が混じっている場合を想定して、改良していきます。 コンピュータの記憶装置というのは「小さなスイッチのようなものの集まり」だと 述べた。その「オンかオフか」だけを表現できる最小単位のスイッチ、これをビッ ト bit と呼ぶ。 このビットの 1つ1つを扱っていたのでは処理が繁雑すぎるので、まとめていくつか を扱う必要がある。通常は 8つのビットを 1組みにして 1バイト Byte という単位 で扱う。アドレス (番地) も、通常はこのバイト単位でつけてある。 いままでのプログラムの説明において「1文字ずつ読む」のような表現をしてきた。 ここでいう 1文字とは、char型の変数に入る 1つ分のデータという意味だった。 char型は 1バイトであると考えてよい。(p94) 日本字の扱いについて、 「文字」のコードとして ASCII の文字コードだけを考える場合は、この 1バイトで 表現できる範囲内に収まるので、1文字 = 1バイトと考えてもよかった。 ところが日本語で使われる文字は、とうてい 256種類では足りない。 JIS では X0208 という規格で「情報交換用符号」を定めている。この規格では漢字 6355文字 および漢字以外の 524文字 (ひらがな、カタカナなど)、合計 6879文字にコード (番号) をつけてある。この文字コード体系では、たとえば「愛」の文字は「16区 6 点」 (16-6) と表現する。区は 1 〜 94、点も 1 〜 94 の範囲なので、この方法で も最大 8836文字しか表現できないが、通常の日本語文章の表記に用いるにはこれで 足りるとされている。 この区の番号および点の番号ならば、それぞれ 94通りなので、どちらも 1バイトで 表現し得る。つまり「区、点」のペアで、2バイトを使えば、この漢字コードが表現 できるわけだ。(p96) その問題点として、 さて、実際には ASCII もこの JIS X0208 もいっしょに (混在して) 使われる。そ のままの文字コードの値で、ただ混在させたのではうまくいかない。 たとえば「圭」の字は JIS X0208 では 23-29 なのだが、この区と点の値にそれぞ れ 32 だけ「ゲタをはかせて」大きい値として使うので、つまり 55 と 61 の 2バ イトになる。 しかし、これでは ASCII の 55 (数字の 7) と 61 (=記号) が連続したものと区別 ができない。(p97) では、実際にどう処理するかというと、たとえば EUC_jp コードだと、 ASCII は (コード番号) 0 〜 127 の部分を使うようにする。 JIS X0208 は 2バイ トをそれぞれ 128 だけ大きい値へシフトさせて (161 〜 254)、区別がつくように する。(p97) その処理はというと、 (この) 方法においては、「日本語文字は 2バイト、ASCII は 1バイト」と、そのま ま単純にバイト数が計算できるし、ふつうに fgetc() で処理してもむずかしくはな い。 fgetc() を 2回呼び出せば、日本語文字 1文字分が読めるわけだ。日本語文字か ASCII かどうかを判断するためには、とりあえず 1バイト読んでみて、それが日本 語コードの 1バイト目かどうかをチェックする。(p97) その部分のコードは次のとおり、 while ((c = getc(fp)) != EOF) if (iskanji(c)) { /* 日本語文字かを チェック */ fputc(c, stdout); fputc(fgetc(fp), stdout); /* 2回 読み込む */ } else if (islower(c)) /* 小文字の場合 */ fputc(toupper(c), stdout); /* 大文字に変換 */ else fputc(c, stdout); } (islower() と toupper() については後まわしに) まず、最初の if文にある iskanji() 関数で、入力が日本語文字かどうかをチェックし ています。この関数はマクロ (#define) で定義しておきます。 #define iskanji(c) (0xa1 <= ((c) & 0xff) && ((c) & 0xff) <= 0xfe) (このマクロについては) 解説はしないが、自分で読み解いてみるつもりがない人は 、この時点では「おまじない」だと思ってみてもよい。(p100) つもりがない ? フッ、挑発してますね ... (-_-) では、わかる範囲で、 このマクロの iskanji() に続く ( ) の中は、この条件が満たされれば、真を返す、と いうことですね。 この ( ) の式を単純化すると、 (0xa1 <= (c') && (c') <= 0xfe) となります。0x で始まるのは 16進数なので、これを 10進数にすると、 a -> 10, f -> 15, e ->14 0xa1 -> 1 + 10 x 16 = 161 0xfe -> 14 + 15 x 16 = 254 だから、上の式は、 (161 <= (c') && (c') <= 254) と同じでつまり、c が日本語文字コード番号の範囲 (161 〜 254) にあるかをチェック している。 あと、((c) & 0xff) は、ビット演算子 & を使ったマスク処理と呼ばれるものです。 OS内部ではふつうシステムが扱いやすいように、メモリ配置を 8ビットずつ区切ってそ れを逆転させています。これを little endian と呼びます。 このことは通常、プログラムを書くときには意識しなくてもかまいません。 ところが、日本語文字コードの場合には、big endian といってこの左右が逆になってい ます。 それで、上のプログラムだと、初めの 1バイトをチェックするには、それが日本語文字 だとすると、2バイトのうちの右側の 1バイトを最初にもってきて読む必要があります。 ((c) & 0xff) と書くことで、そのことが実現できる、ということですね。 (シフト JIS の場合には、も少しだけマクロが複雑になります) まあ、初心者なのでこの程度の理解ですけど*2 ... *1:「作ってわかる Cプログラミング」 *2:まちがってる ? 2005-11-21 φ(-_-) ■[lang]はじめての C C programming note*1 toupper() と islower() については、K&R 2nd の「7.8 雑関数」(p203) にその説明が 載っています。これらの関数を使用するにはヘッダファイルの ctype.h をインクルード しておきます。 同じような働きをするものも含めて、 toupper(c) c を大文字に変換 tolower(c) c を小文字に変換 islower(c) c が小文字なら 0 以外、そうでなければ 0 (を返す) isupper(c) c が大文字なら 0 以外、そうでなければ 0 isalpha(c) c がアルファベットなら 0 以外、そうでなければ 0 isdigit(c) c が数字 (0 〜 9) なら 0 以外、そうでなければ 0 isspace(c) c がブランク、タブ、改行、復帰、垂直タブなら 0 以外、そうでなけ れば 0 (小文字を大文字に変換するのに) toupper ライブラリ関数を使えば (特定の、例え ば ASCIIコード体系等に依存せず) その Cコンパイラが想定している環境での文字 コード体系に合わせてうまくやるようになっているはずだ。 自分で一からつくるよりも、同じことをするライブラリ関数があるなら、そのライ ブラリ関数を使うようにすること。 誤りの入り込む余地がその分少なくなるわけだし、プログラムも単純になる。より シンプルなプログラムほど信頼性を増すのだ。(p100) 大文字を小文字に変換するのなら、少しコードをいじって、 while ((c = fgetc(fp)) != EOF) { if (iskanji(c)) { fputc(c, stdout); fputc(fgetc(fp), stdout); } else if (isupper(c)) fputc(tolower(c), stdout); else fputc(c, stdout); } とすれば、OK。 (追記) 気づいてるかもしれないけど、linux/gcc の環境では、この iskanji という関 数をわざわざ使わなくても問題ありません。 インストールの際に EUC_jp ロケールを選択しておけば、ライブラリ関数*2が先のマク ロと同じ操作を (黙って) してくれてるようです。 *1:「作ってわかる Cプログラミング」 *2:fgetc や fputc 2005-11-23 φ(-_-) ■[lang]はじめての C C programming note*1 「小文字を大文字に変換する」コードをプログラム cat3 に組み込んでいきます。 この 2つを 1つのプログラムにまとめて、入力元のファイルはコマンドで複数指定 でき、かつオプションスイッチをつけた場合には大文字変換機能が有効になるよう なプログラムをつくることにしよう。(p103) そのため、今回は flag という小さなパーツを用意します。 まず、値を 2つしかもたない変数 c_flag を外部宣言しておきます。値は 1 と 0 の2つ なので、これも前もってマクロで定義します。 #define YES 1 #define NO 0 int c_flag = NO; "YESか NOか" もしくは "ONか OFFか" のように 2つの値しかとる可能性のない変数 だということを強調するため #define で定義した値を使うようにする。(p105) つぎに、コマンドラインをチェックし、オプションがついた場合には必要な処理をする コードを追加します。 if (argc > 0) if (strcmp(*argv, "-c") == 0) { c_flag = YES; /* flag を 1 に */ --argc; /* コマンドラインから "-c" を スキップ */ ++argv; } } このやり方では ・ファイル名は -c という名前であってはいけない。 ・オプション指定はどのファイル名よりも前になければいけない。 という制限付きだ。しかし、その制限はそれほど不便ではないだろう。 strcmp() という標準ライブラリ関数が使われている。これは 2つの文字列が等しい かどうかを検査する関数だ。等しければ 0 を返し、等しくなければ 0 以外の値を 返す。 (ここでは) *argc が "-c" という長さ 2文字の文字列と等しいかどうかを比べてい る。(p105) なお、strcmp() にはヘッダファイル string.h が必要です。 そして、do_one() に文字列変換の機能を組み込んでいきます。ここで変数 c_flag が役 にたってきます。 void do_one(FILE *fp) { int c; while ((c = fgetc(fp)) != EOF) { if (iskanji(c)) { fputc(c, stdout); fputc(fget(fp), stdout); } else if (islower(c)) { if (c_flag == YES) /* オプションが あるなら */ fputc(toupper(c), stdout); /* 小文字を 大文字に 変換 */ else fputc(c, stdout); } else fputc(c, stdout); /* ここは 文字以外の 記号 */ } } では、「大文字を小文字に変換する」オプションも付け加えたいときには、どうしたら いいのか ? まず、追加するオプションの flag を外部変数として宣言します。 int s_flag = NO; 次に、while のループを使ってオプションを続けて指定してもプログラムが動くように しておきます。 char *s; /* コマンドラインの パラメータ */ while (--argc > 0 && **++argv == '-') { for (s = *argv + 1; *s != '\0'; s++) { switch (*s) { case 'c': c_flag = YES; break; case 's': s_flag = YES: break; } } } この switch文を含む while のループでは、コマンドラインのパラメータを 1つ目 から順に調べ、'-' で始まるパラメータを見つけたら、それはファイル名でなくオ プション指定だと解釈し、中の for文のところへいく。 '-' で始まるパラメータでなく普通のファイル名を意味するパラメータに出会った 時点で、この while文の実行は終了する。(p117) (上のコードの) **++argv == '-' という部分が目についたのではないだろうか ? 一見複雑そうに見えるが何のことは ない。 2つに分けて順を追って考えれば納得できるだろう。 1) ++argv .......... argv が次の要素を指すようにする。 2) **argv == '-' ... argv が指す先のポインタが指す内容が '-' かどうか。 **argv がむずかしければ、*(*argv) だといえばわかるだろうか。(p117) また argv++ ではなくて ++argv なのは、最初の 1つ (argv[0]) はコマンド名なの で、argv[1] 以降のパラメータを対象にしたいからだ。 つまりは、コマンドラインの各パラメータの 1文字目を「'-' かどうか ?」と順番 に検査するための記述だ。 これはよく使われる書き方なので、この while文ごと覚えてしまったほうがよいか もしれない。(p114) この while文はとてもうまくつくられてますね。 cat3 プログラムだと、main() 関数の始めのほうでコマンドラインからコマンド名をス キップさせるため、 --argc; ++argv; と書いておかないとダメだったのを、その部分を条件文にとり入れて、プログラム自体 を短くしています。 それと、while文を抜けた時点では既にコマンドラインからオプション分のパラメータは 除かれていて、後に続く式や関数をいじる必要がありません。 if文やこの switch文のように、条件によって実行する制御の流れを変えるものを、 C では Selection Statement という。 if文は条件が「0 である (偽) か、それ以 外 (真) か」で 2通りに分岐するが、switch文はケースラベル case label を使っ て3つ以上の場合分けをプログラム上で表現できる*2。 case ラベルは switch文のブロック部分に複数使われる。 case ラベルの一般形は case 定数式: というように、定数式とその後ろにコロン ( : ) を伴う。定数式は整数値である必 要がある。 switch というキーワードの後ろにカッコでくくって書いてある「条件 式」の値が、どの case ラベルの整数値と一致するかによって 3つ以上への場合分 け分岐が行なわれる。 プログラムは次のとおり、 /* upper3r.c */ #include #include #include #include #define iskanji(c) (0xa1 <= ((c) & 0xff) && ((c) & 0xff) <= 0xfe) #define YES 1 #define NO 0 void do_one(); void cant(); int c_flag = NO; int s_flag = NO; main(int argc, char **argv) { FILE *fp; char *s; while (--argc > 0 && **++argv == '-') { for (s = *argv + 1; *s != '\0'; s++) { switch (*s) { case 'c': c_flag = YES; break; case 's': s_flag = YES; break; } } } if (argc == 0) do_one(stdin); else { while (argc--) { if ((fp = fopen(*argv, "r")) == NULL) cant(*argv); do_one(fp); fclose(fp); argv++; } } return 0; } void do_one(FILE *fp) { int c; while ((c = fgetc(fp)) != EOF) { if (iskanji(c)) { fputc(c, stdout); fputc(fgetc(fp), stdout); } else if (islower(c)) { if (c_flag == YES && s_flag == NO) fputc(toupper(c), stdout); else fputc(c, stdout); } else if (isupper(c)) { if (c_flag == NO && s_flag == YES) fputc(tolower(c), stdout); else fputc(c, stdout); } else fputc(c, stdout); } } void cant(char *name) { fprintf(stderr, "can't open %s\n", name); exit(1); } (両方のオプションを一度に使うと、2つとも無効になります) *1:「作ってわかる Cプログラミング」 *2:今回は 2つだけ 2005-11-25 φ(-_-) ■[lang]はじめての C C programming note*1 基本に戻って、ちょっと復習。 プログラム upper3r には、次のような while文が含まれています。 while (argc--) { if ((fp = fopen(*argv, "r")) == NULL) cant(*argv); do_one(fp); fclose(fp); argv++; } この while文から抜け出るのはどの時点だろうか ? それは、while の後の ( ) の中の値が 0 になったときです。 C においては if文や while文の条件式での真は 0 以外の数値、偽は 0 ということ を考えればわかるだろう。(p343) main() 関数の仮引数 argc と argv はそれぞれ、 argc -> 関数の引数 (パラメータ) の数 + 1 argv -> それぞれの引数へのポインタ配列 ですから、たとえば次のようにパラメータを指定すると、 $./upper3r upper3r.c このとき argc は 2 という整数値、argv[0], argv[1] がコマンド upper3r とコマンド ラインの upper3r.c とを指すことになります。*2 argc-- は "argc = argc - 1" と同じですから、while文の後の ( ) の中身は、このル ーティンを 1回実行するごとに 1つ減ることになります。 そして "argc == 0" となった時点で、while文を抜け出ます。 argv のほうは逆に次のパラメータへ移動させないといけないので、argv[0] → argv[1] と配列の添字 subscript を 1つずつ増やしていきます。 そのために実行文の最後に、 argv++; が加えられているわけです。 また、islower() を使っているところでは、if文の ( ) の中の式の値が 0 になると、 同じようにこの if文は終了しています。 if (islower(c)) { if (c_flag == YES) fputc(toupper(c), stdout); else fputc(c, stdout); } ライブラリ関数 islower() は c が大文字だと 0 を返すので、当然 if文の後の ( ) の 値は 0 となり、次に続く文は実行されず、結果的にこの if文がスキップされることに なる、ということですね。 *1:「作ってわかる Cプログラミング」 *2:実際は 1つずれますが 2005-11-27 φ(-_-) ■[lang]はじめての C C programming note*1 今回はさらにオプションスイッチを追加し、"-n" オプションが指定されたときに行 番号を出力するように機能追加して、cat4.c とした。(p111) オプションを追加するやり方は同じで、関数 do_one() に少し変更を加えて、新しい関 数 cat() を作成します。 main() と同じくここでも flag を使っていますが、その仕組みがとても巧妙です。 void cat(FILE *fp) { int line_no; /* 行番号 */ int c; int flag; flag = YES; line_no = 0; while ((c = fgetc(fp)) != EOF) { if (n_flag == YES && flag == YES) fprintf(stdout, "%6d ", ++line_no); /* c が 改行記号の ときのみ flag = YES */ /* それ以外の 文字は flag = NO に 変換 */ flag = (c == '\n') ? YES : NO; if (iskanji(c)) { fputc(c, stdout); fputc(fgetc(fp), stdout); } else if ((c_flag == YES && s_flag == NO) && islower(c)) fputc(toupper(c), stdout); else if ((c_flag == NO && s_flag == YES) && isupper(c)) fputc(tolower(c), stdout); else fputc(c, stdout); } } cat() 関数では、line_no と flag という変数が追加されている。 line_no のほうは行数を数えるために使われている。この変数は、関数 cat() が呼 び出されるたびに毎回 while文の直前で 0 に初期化されるので、出力される行番号 はファイルごとにリセットされる (各ファイルの最初の行で 1 に戻る) ことになる 。 fprintf() の中で line_no++ ではなくて ++line_no になっていることにも注目す ること。変数をインクリメントした後の値を使うので (入力ファイルの) 1行目の行 番号は 0 にはならず 1 になる。(p115) コマンドラインにファイル名を続けて指定すると、各ファイルごとに 1 から行番号がつ いていく、ということですね。 flag には「直前の文字が改行 '\n' だった場合」かもしくは「ファイルの先頭」で YES に規定される。 つまり行番号を出力すべき「行の先頭」のタイミングを表す変数だ。(p115) ここのところです、 if (n_flag == YES && flag == YES) fprintf(stdout, "%6d ", ++line_no); flag = (c == '\n') ? YES : NO; 1行目の if文では「もし -n オプションが指定されているときで、かつ直前の文字 が改行コードだったときには」という条件にあてはまる場合に、その文字を出力す る前に行番号を出している。(p116) 次は、3行目の条件演算子について、 これは条件演算子 conditional operator というもので、 式1 ? 式2 : 式3 という形式で使われる。 3つの総演算数をとるため 3項演算子 ternary operator とも呼ばれる。 条件演算子は、式1 がまず評価され、それが真 true だった場合には、式2 が評価 されてその値がこの式全体の値となり、そうでなくて偽 false の場合は式3 が評価 されてそれが値となる ... という働きのものだ。 つまりこの cat4.c で使われている例では、変数 c の値が改行コードだった場合に は変数 flag には YES が代入され、そうでないときには NO が代入されるわけだ。 この if文を見てもわかるように、この部分ではループのくり返し中での次回に備え て「今回の文字が改行だったかどうか」を記録している。(p118) では、 このアルゴリズムに欠点はないだろうか ? たしかに (入力したファイルの) 3行目の前に行番号をつけるには、直前 (2行目の 終わり) は改行コードだろうし、その事情は 4行目でも 5行目でも同じだ。最終行 においてもその直前の行の末尾は改行に違いない。 では 1行目についてはどうだろうか ? 実はそれも心配いらない。そのために while ループの直前で flag の値は YES に設定してあるのだ。(p118-119) あと変更のあったところでは、オプションの flag をチェックする if文で、条件式をち ょっといじってます。 if ((c_flag == YES && s_flag == NO) && islower(c)) fputc(toupper(c), stdout); upper3r.c にあるコードと比べると、行数が短くなったことがわかります。 && 演算子は左から結合していくので、式の左側にある ( ) の中を評価して、それから 右の islower() を評価する、ということですね。 (この内側の ( ) は略してもいいの かもしれませんが ...) プログラムは次のとおり、 /* cat4r.c */ #include #include #include #include #define iskanji(c) (0xa1 <= ((c) & 0xff) && ((c) & 0xff) <= 0xfe) #define YES 1 #define NO 0 void cat(); void cant(); int c_flag = NO; int s_flag = NO; int n_flag = NO; main(int argc, char **argv) { FILE *fp; char *s; while (--argc > 0 && **++argv == '-') { for (s = *argv + 1; *s != '\0'; s++) { switch (*s) { case 'c': c_flag = YES; break; case 's': s_flag = YES; break; case 'n': n_flag = YES; break; } } } if (argc == 0) cat(stdin); else { while (argc--) { if ((fp = fopen(*argv, "r")) == NULL) cant(*argv); cat(fp); fclose(fp); argv++; } } return 0; } void cat(FILE *fp) { int line_no; int c; int flag; flag = YES; line_no = 0; while ((c = fgetc(fp)) != EOF) { if (n_flag == YES && flag == YES) fprintf(stdout, "%6d ", ++line_no); flag = (c == '\n') ? YES : NO; if (iskanji(c)) { fputc(c, stdout); fputc(fgetc(fp), stdout); } else if ((c_flag == YES && s_flag == NO) && islower(c)) fputc(toupper(c), stdout); else if ((c_flag == NO && s_flag == YES) && isupper(c)) fputc(tolower(c), stdout); else fputc(c, stdout); } } void cant(char *name) { fprintf(stderr, "can't open %s\n", name); exit(1); } *1:「作ってわかる Cプログラミング」 2005-12-03 φ(-_-) ■[lang]はじめての C C programming note*1 プログラム cat3 には、入力した文字をそのまま標準出力する関数 do_one() が含まれ ています。 void do_one(FILE *fp) { int c; while ((c = fgetc(fp)) != EOF) fputc(c, stdout); } do_one() を、その入出力を配列を使って行単位で扱う関数 cat() につくり直します。 #define MAX_SIZE (80 + 1 + 1) /* plus '\n' + '\0' */ void cat(FILE *fp) { char buf[MAX_SIZE]; while (fgets(buf, MAX_SIZE, fp) != NULL) fputs(buf, stdout); } 出入力用の標準ライブラリ関数 fgetc() と fputc() とが、それぞれ fgets()、fputs() に替わっています。 まず、文字列の扱い、 C には「文字列 string のためのデータ型」は準備されていない。ではどのように して文字列を扱うのかというと、文字型 char type の配列 array として扱うこと になる。(p122) こんなかんじ、ですか→ char array[n] or char *array たとえば "hello" という 5文字分の長さの文字列が s という名前の配列に入って いるようすは次のようになっている。 s[0] s[1] s[2] s[3] s[4] s[5] ... s[n] h e l l o \0 文字列の終わりを示すために、文字列の最後の文字のすぐ後ろに '\0' というコー ドが入っている。 これはナル文字 null character と呼ばれ、C では文字列の終端を表すシルシとし て使われる。 文字列の終わりにこの '\0' が必要なので、文字列用に使う配列の要素の数は、文 字列 + 1 になる。(p122-123) 次は行 line の定義、 UNIX のファイルは、ただの一次元のバイト列だ。それ以外の何の構造ももっていな い。 行 line という概念は、その一次元のバイト列に「改行」のコードが入っているこ とだけで実現されている。 改行のコードは '\n' で表わされる。(p123) fputs() はちょっと置いといて、次は入力用の標準ライブラリ関数 fgets() とその引数 、 fgets() は 3つの引数をとる。最後の - 3つ目の - 引数でそれが取り扱うストリー ムを指定する。 1つ目の引数で、1行分のデータ格納場所の先頭アドレスを指定する。 配列の名前を (添字を指定せずに) 書いた場合は、その1つ目の要素へのポインタ、 つまり先頭のアドレスという意味になる。 この格納場所は、呼び出す側で配列を宣言するなどして確保しておく必要がある。 (p124) 残りの引数、 第2引数だが、ここにはこの格納場所のサイズ (Byte 単位) を指定する。 fgets() は、指定されたストリームから MAX_SIZE - 1 文字になるまで、または改 行文字が現れるまで (いずれか先に到達したほうまで) 読み込み、それを第1引数で 指定された場所に入れる。(p125) 読み込んだ行の処理は、 読み込まれた最後の文字の後には、続けて 1つのナル文字が書かれる。 たとえば fgets() が "Dill" という文字列と改行コードからなる 1行を d という 配列に読み込んだときの状態は、 d[0] d[1] d[2] d[3] d[4] d[5] ... D i l l \n \0 のようになっている。 改行コード '\n' も入ったあとにナル文字 '\0' が入っているわけだ。(P125) 今度はマクロの NULL のほう、 fgets() は通常は 1番目の引数と同じ値をそのまま関数の値として渡すが、ファイ ルの末尾などストリームの終端に達すると NULL を返す。 これはナルポインタ null pointer と呼ばれ「どこも指していないポインタ」を意 味する。 fopen() がエラーのとき返すのもこれである。 少しわかりにくくなってきたので、途中だけど、K&R 2nd から fgets() のコードを書き 写してみます。(p201) /* fgets: get at most n chars from iop */ char *fgets(char *s, int n, FILE *iop) { register int c; register char *cs; cs = s; while (--n > 0 && (c = getc(iop)) != EOF) if ((*cs++ = c) == '\n') break; *cs = '\0'; return (c == EOF && cs == s) ? NULL : s; } 細かいところは置いといて、getc() が使われていますね。 while文が終了するのは、改行文字に到達したか、またはこの getc() が EOF - end of file ファイル終了のサイン - を返したときです。 また while から抜け出すときには、どちらも文字列の後に '\0' が追加されています。 そして fgets() の関数としての戻り値は char型のポインタなので、ファイルの終端に なると null pointer を返すわけです。 では、なぜ NULL を戻り値として設定するのか、 (たとえば) fopen() が正常にオープン処理を行なったときに返すのは「ストリーム へのポインタ」だ。そこで、エラーを示す値はそれと区別できる必要がある (たま たまポインタがその - ストリームへのポインタの - 値になっては困るわけだ。 NULL は「どのアドレスも指していないことが保証された値」であり、ポインタが取 り得るどの (正常な) 値とも区別することができるので、ポインタ (アドレス) を 返す関数では、この fgets() や fopen() に限らず、エラー時の値として NULL を 返すのが一般的である。(p125) また、プログラム作成のときの注意点として、 ポインタの値を 0 と比較したりポインタに 0 を代入すると、それはコンパイラに よって自動的に null pointer の値として読み替えられるが、(それだと) 混乱をま ねくので、0 とは書かず NULL という (マクロとして定義された) 名前を必ず使う ようにすべきだ。(p127) あと、残っている fputs() ですが、やはり K&R にあるコードを写すと、 /* fputs: put string s on file iop */ int fputs(char *s, FILE *iop) { int c; while (c = *s++) putc(c, iop); return ferror(iop) ? EOF : 0; } 予想どおり、ここでは出力関数 putc() が使われていました。 *1:「作ってわかる Cプログラミング」 2005-12-05 φ(-_-) ■[lang]はじめての C C programming note*1 行単位の入出力が可能になったので、次はメニューカードのようにそれぞれの行を中央 揃えにするプログラムをつくっていきます。 それから、エディタ画面に print したときに中央揃えになることと、フォントの幅が全 て同じだと仮定しておきます。 その部分のコードが、 #include void center(char buf[], int w) { int off; int n; int i; n = strlen(buf) - 1; /* -1 for '\n' */ if (w < n) return; off = (w - n + 1) / 2; for (i = n + 1; i >= 0; i--) buf[i + off] = buf[i]; while (off--) buf[off] = ' '; } n が入力した 1行の長さ、w が print 画面の最大幅になります。 strlen() という関数が新しく使われている。 これは、引数で与えられた文字列の文字数を返す (末尾のナル文字は数えない)。 文字列を扱うときには、とてもよく使う標準ライブラリ関数だ。 fgets() のところでも述べたように、配列の名前を添字なしで書くと「その配列の 先頭の要素へのアドレス」の意味になるので、ここではそのことを利用して、buf という配列の先頭アドレスを (引数として) 渡している。(p132) off は最大幅から入力した文字列の長さを引き、半分にしたものです。これを文字列の 前に空白として当てはめていきます。 文字列の後ろは、改行 '\n' で次の行に飛ぶので、今回はコードで考える必要はありま せん。 方法としては、まず入力したもとの格納領域 buf[] を、1文字ずつずらしながら、off分 だけ拡張します。 for (i = n + 1; i >= 0; i--) buf[i + off] = buf[i]; 当然、格納領域 buf[0] から buf[off] までの配列は不要になります。 この空いた配列に buf[off] から順に空白コードの ' ' を入れていきます。 while (off--) buf[off] = ' '; 以上の操作は、実際には入力と出力の間で行ないますから、先につくった関数 carte() にも変更が加えられます。 #define MAX_SIZE (80 + 1 + 1) #define WIDTH 80 /* width of editor screen */ void carte(FILE *fp) { char buf[MAX_SIZE]; while (fgets(buf, MAX_SIZE, fp) != NULL) { center(buf, WIDTH); fputs(buf, stdout); } } *1:「作ってわかる Cプログラミング」 2005-12-08 φ(-_-) ■[lang]はじめての C C programming note*1 center() は、タブやスペースのような空白が文字列の前後に含まれていると、うまく中 央揃いにはなりません。そこで少しコードを追加していきます。 まず、文字コードが空白か否かをチェックするマクロをつくっておきます。このマクロ は 2つの関数で条件判定に使われるので、main() の前に定義します。 #define ISBLANK(c) ((c) == ' ' || (c) == '\t') はじめに、文字列の前の空白をとり除くための関数をつくります。 void remove_pre_blank(char *buf) { int n; char *p; char *q; for (p = buf; ISBLANK(*p); p++) ; q = buf; n = strlen(p) + 1; while (n--) *q++ = *p++; } (さっそく、null statement が現れた) この null statement がどんな働きをするのかというと、 remove_pre_blank() という関数は、最初の「透明ではない文字」の直前までの空白 やタブをとり除く処理を行なっている。 空の for文*2があるが、ここで p という変数が「透明ではない文字」の最初を指す ことになるようにしているのがわかるだろう。 for文の初期化の部分で buf の先頭を指すようにし、条件判定のところで ISBLANK というマクロを使って「透明な文字である間」くり返すようにし、再初期化の文で ポインタを 1つずつずらすことによってそれを行なっている。(p139) ここでいう初期化、条件判定、再初期化というのは for のカッコの中にある 3つの文、 1. くり返しの前に 1回だけ実行する (初期化) 2. くり返しの条件 3. 毎回の末尾に 1回ずつ実行する (再初期化) のこと。 マクロの ISBLANK は *p が空白やタブだと 0 以外を返すので (条件文が真) その度に ポインタの位置を 1つずつずらすことになります。 ここでの p はポインタであって配列ではない、と考えると理解しやすいと思います。 そして for文を抜けた時点で、p は「最初の空白やタブではない」配列の先頭を指すこ とになります。 そうしてその位置から「文字列の最後まで」の長さ (配列の個数) を strlen() で 求め、 これですね、 n = strlen(p) + 1; /* plus '\0' */ (次に) この個数分だけ前にずらすことによって「前にあった空白やタブを」とり除 くことを実現している。(p138) こちらです、 q = buf; while (n--) *q++ = *p++; *q は buf の先頭の文字を表わしています。しかし、この場合は少し意味が違ってきま す。 *q は q というポインタが指す内容の値を意味するが、ここは代入文の左辺なので 、(つまり) 指す場所そのものだ。 一方、右辺は *p で、同様に p というポインタが指す内容の値なので、p が指す場 所の内容が q が指す場所 (この場合は配列の先頭) に代入されることになる。 (p151) また、ここでのインクリメントの意味ですが、 ++ は、対象の後ろについているスタイルを使っている。つまりその対象 (ポインタ ) を参照し終わってからインクリメントが行なわれる。つまり、 *q = *p; q++; p++; というのと結果は同じになる。(p151) つまりこの while文では、 p というアドレスから始まる文字列を n文字分だけ q というアドレス以降にコピー しているのだ。「なるべく意味がわかりやすく書く」という原則からはずれている ように見えるが、これはよくある書き方であって、 ... そのまま憶えてしまっても よい。(p145) 別の言い方をすると、 (すなわち) ループをくり返すことにより ... 「連なっているものを、ポインタを ずらしながらどんどんコピーしていく」ということが可能になる。(p151) ということです。 (これは) ただ慣用句だというだけでなく、C の文法的にもとても合理的な書き方な のだ。 いずれ C という言語のコンセプトが理解できてくれば、どれだけ合理的な書き方な のかが実感できるようになるだろう。(p145) K&R 2nd にも、でてきたような気が ... *1:「作ってわかる Cプログラミング」 *2:null statement 2005-12-10 φ(-_-) ■[lang]はじめての C C programming note*1 次は、文字列の後ろ側にある空白をとり除く関数、 void remove_pos_blank(char *buf) { char *q; if (*buf == '\0') return; q = buf + strlen(buf) - 2; /* minus 2 ('\n' + '\0') */ while (ISBLANK(*q)) --q; *(q + 1) = '\n'; *(q + 2) = '\0'; } remove_pos_blank() は、その行のうちの最後の「透明ではない文字」より後ろにあ る空白やタブをとり除いている。 これらは、文字列の「後ろから」順に見ている。文字列の末尾はナル文字であり、 その直前は改行コードのはずなので、文字列の長さから 2 だけ引いた位置から開始 している。そこから前方向に、空の while文で順に「透明な文字の間」くり返して 見ている。 そこで、「透明でない文字」が最初に見つかった位置の後ろに改行を、そのもう1つ 後ろにナル文字を新たに書き込むことによって、後ろにあった透明な文字 (タブや 空白) をとり除いているわけだ。(p139) remove_pre_blank() と同様この関数も、文字列を配列としてでなく、ポインタとして引 数にとっています。つまり、この buf は文字列の先頭のアドレスなわけです。 まず、if文でポインタの指す値がナル文字でないことをチェックしています。 そして、ポインタ q を文字列の末尾に移動させ、そこから '\n' と '\0' を除いた位置 にさらに q を動かしています。 q = buf + strlen(buf) - 2; この文の書き方ははじめてですね。 q = buf で、q に文字列の 1つ目のアドレスをコピーします。 つぎに、strlen(buf) により文字列内の char の個数を求め、それを加えていきます*2 。 strlen() は、文字列の末尾のナル文字分は除いて計算するので、 buf + strlen(buf) としても、文字列でのナル文字分が重なることはありません。 つまり、右辺で文字列のデータ数が加算されることで、結果としてポインタ q が文字列 の末尾に移動したことになるわけです。 最後に 2 を引くことで、p は改行コードやナル文字でないことが確認されます。 次の空の while文では、デクリメントを対象の前につけることで、q は空白やナル文字 を除いたその「1つ前の」位置へと移動します。 ここまできてようやく、この文字列の後ろの空白分を除くことで短くすることができる ようになります。 それがこちら、 *(q + 1) = '\n'; *(q + 2) = '\0'; 1行目は、改行コードを加えて行のスタイルを整える、というだけです。 ポイントは、2行目のナル文字を加えるところです。 ナル文字は「文字列の最後」を表わすコードですから、ここに '\0' を挿入することで 、この文字列のそこから後は見えなくなります。 つまり、この remove_pos_blank() を呼び出す側 - ここでは carte() ですが - からは 、新しく挿入した '\0' までが文字列として参照されるので、結果的には空白やタブが 削除されたのと同じになるわけですね。 *(q + 1) というのは、q の 1つ次の場所の内容だ。 1つとは、その対象の型の大き さの 1つ分のことだ。この場合は char なので 1Byte だが、型によってサイズは異 なる。(p154) 将来使うかもしれないので、別の書き方も、 (ここで) カッコが必要なのは '*' のほうが '+' よりも結合の優先順序が高いため だ。 (例えば) *q + 1 と書いてしまっては q の指す場所に 1 を足したものなので 、まるっきり別の意味になってしまう。 カッコを書くのはメンドウだが、実はカッコが必要ない書き方として、上記の 2行 は、 q[1] = '\n'; q[2] = '\0'; のようにも書ける。これは配列の要素の参照の書き方とまったく同じである。 (p154) carte() も少し手直ししておきます。 void carte(FILE *fp) { char buf[MAX_SIZE]; while (fgets(buf, MAX_SIZE, fp) != NULL) { remove_pre_blank(buf); remove_pos_blank(buf); center(buf, WIDTH); fputs(buf, stdout); } } ここで、remove_pre_blank() や remove_pos_blank() の仮引数がポインタなのに、なぜ carte() での実引数が配列の名前 - buf - になっているのか、というと、 呼び出された関数側で、呼び出した関数側の変数の内容を変更したい場合 - ここで は文字列 - は、引数には変数のアドレス (変数へのポインタ) を渡す必要がある。 もちろん、呼ばれる関数側でもその引数がポインタ型であると宣言しておかなけれ ばならないし、そのポインタを使って間接的に値を変更するわけだ。 ただし、配列の場合は事情が異なる。配列の名前を関数の引数として使うと、その 場合はその配列の先頭アドレスが渡される。つまり呼ばれた関数側ではポインタと して受け取るわけだ。(p110) なぜなら、 ・配列の名前だけを書いた場合はその1つ目の要素のアドレスの意味になる。 ・ C の関数の引数は値渡し pass by value なので、関数内の仮引数は、呼び出し 側の実引数のをコピーしたローカルな変数にすぎない。すなわち、定数を渡しても 、(渡された側の) 関数定義内では (その定数の値でほどよく初期化された) 変数と なる。 という 2つの事実により、「関数の引数に配列の名前を渡すと、関数内ではポイン タとして使える」ということが導きだされるのである。(p153) 考えてみると、文字列の後ろにタブやスペースがある状況というのはよくわからない。 タイピングミスとか ? でも [ENTER] と [SPACE], [TAB] のボタンの位置があんなに離れてるのに、ですか ? しかし、プログラマというのは、この「見えない文字」が気になってその削除のためだ けに時間をさくこともけっこうあるらしい (これ、「ハッカーと画家」( isbn:4274065979) に確か書いてあったような ...) *1:「作ってわかる Cプログラミング」 *2:ポインタ演算 2005-12-14 φ(-_-) ■[lang]はじめての C C programming note*1 rmposblank() ではうまく動いてくれてますが、errata によると、そこで使われる関数 に一部不具合があるとのことです。 関数 remove_pos_blank(char *) において、元のコード (のまま) では、「空白も しくはタブの連続のみからなる行」 (つまり 1行すべてが空白またはタブである場 合) において、次の部分でポインタ q が buf[0] より前を指してしまうという問題 があります。 while (ISBLANK(*q)) --q; これを回避するためにコードを修正しました。 修正されたコードがこちらです、 void remove_pre_blank(char *buf) { char *q; if (*buf == '\0') return; /* ポインタ q を '\n' の位置まで 移動させる */ q = buf + strlen(buf) - 1; while (q > buf && ISBLANK(*(q - 1))) --q; *q = '\n'; *(q + 1) = '\0'; } 文字列の先頭でスペースまたはタブを入力して、それを取り消さずに改行すると、確か に「見えない文字だけの」行ができてしまいます ... あり得ますね。 *1:「作ってわかる C プログラミング」 2005-12-18 φ(-_-) ■[lang]はじめての C C programming note*1 今度は、1行に入る文字数を、プログラムの中で定義するかわりに、 コマンドラインのパラメータとして $./ce3r -w30 のように指定して実行するだけで、コンパイルしてプログラムをつくり直すことな く、既定値の 80 文字の代わりに 30 文字の幅で、中央揃えするというように実行 時に値を指定できるよう (p140) 改良していきます。このとき、 -w とその後の数字 (この例では 30) の間には空白を空けずに続けてタイプ することとします←コードが複雑にならないので。 まずはじめは、オプション -w に続く文字がすべて数字からなっているかどうか、チェ ックする関数、 #include #define YES 1 #define NO 0 int alldigit(char *s) { if (*s == '\0') return NO; for ( ; *s != '\0'; s++) { if (!isdigit(*s)) return NO; } return YES; } まず、if文で文字列の先頭が '\0' かどうかをチェックしています。この関数は int型 の戻り値をもつので、'\0' であれば NO つまり 0 を返します。 for文では、文字列を順に見ていって、標準ライブラリ関数 isdigit() を使い、それが 数字か否かをチェックしています。 ここでは、否定演算子 '!' が使われていて、1つでも数字以外の文字が見つかれば 0 を 返し、そうでなければ YES つまり 1 を返します。 オプション処理は main() 関数のはじめで実行されます。 その前に、もう1つ、まちがって入力したとき、その関数の簡単な使い方を標準エラー出 力で表示し、プログラムを終了させる関数をつくっておきます。 void usage(void) { fprintf(stderr, "Usage: cc3r [-wNN] [file ... ]\n"); exit(1); } 次が、オプションの処理、 #include int width = WIDTH; while (--argc > 0 && **++argv == '-') { for (s = *argv + 1; *s != '\0'; s++) { switch(*s) { case 'w': if (alldigit(s + 1) == NO) usage(); /* no return */ width = atoi(s + 1); while (*(++s + 1)) ; break; default: usage(); } } } if (width <= 0 || WIDTH < width) usage(); 最後の if文ですが、これは入力したオプションの数値が 0 以下、または既定値よりも 大きいと、usage() を呼び出してプログラムを終了させるための処置です。 以前つくったオプション解析コードとは、switch文の内容が少しだけ違います、 switch(*s) { case 'w': if (alldigit(s + 1) == NO) { usage(); width = atoi(s + 1); while (*(++s + 1)) ; break; default: usage(); } まず、alldigit() で s + 1 つまり -w オプションに続く文字が数字かどうかチェック し、数字以外なら usage() を呼び出し、関数を終了させます。 そして、文字がすべて数字であれば、その文字を標準ライブラリ関数 atoi() で int型 へと変換し、変数 width に代入します。 もし、オプション -w の後ろのコマンドライン上に文字列があれば、while ループの null statement を使ってスキップさせておき、最後に break でこの switch文を抜けま す。 while(*(++s + 1)) ; この switch文では、例外処理に default ラベルを使い、-w 以外のオプションでは、 usage() を呼び出して関数を終了させるようにしています。 ここまでのコードを組み立てていけば、このプログラムは一応完成しますが、もう少し だけ、より一般的なプログラムに直していきます。 *1:「作ってわかる Cプログラミング」 2005-12-20 φ(-_-) ■[lang]はじめての C C programming note*1 関数 carte() をより一般的なかたちに改良していきます。 int carte(FILE *fp) { char buf[MAX_SIZE]; while (fgets(buf, MAX_SIZE, fp) != NULL) { remove_pre_blank(buf); remove_pos_blank(buf); center(buf, width); if (fputs(buf, stdout) == EOF) { fprintf(stderr, "ce3r: write error\n"); return -1; } } return 0; } carte() 関数の中で fputs() が書くのに失敗したら、stdout にエラーメッセージ を出すだけでなく、return で carte() 関数から戻っている。そのファイルの残り の行は処理しないわけだ。 (つまり) 書くことができない事態なのだから、処理はそこで打ち切りたい。他の入 力ファイルを処理し始めてもきっとムダになるからだ。 fputs() は、エラーが起きると EOF を返し、そうでなければ 0 を返します (K&R 2nd p201)。 (以前つくったものと) 異なる点として、carte() 関数が "void" ではなく "int" と定義されている点があげられる。これは carte() 関数が値を返すようにするため だ。 さて、関数が値を返すのに return 文を使う。 void 型の (値を返さない) 関数で は、return 文はただ "return" と書いてあって、その時点でその関数を終了するた めの文になる。 一方、それ以外の型の (値を返す) 関数では、return の後ろに戻り値を指定するの だ。この carte() 関数では、エラーがあったときに "-1" を、正常にファイルを処 理し終わったら "0" を返すようになっている。(p149) 当然、carte() を呼び出す側の main() 関数のコードにも変更が加えられます。 main(int argc, char **argv) { FILE *fp; int r; ...... if (argc == 0) { if (carte(stdin) != 0) return 1; } else { while (argc--) { if ((fp = fopen(*argv, "r")) == NULL) cant(*argv); /* no return */ r = carte(fp); fclose(fp); if (r != 0) return 1; argv++; } } return 0; } main() の中で carte() を呼び出している箇所では、この関数が返してくる値を見 てエラーがあったかどうかを判断している。もしエラーがあったら、その後のファ イルの処理も行なわないで*2 main() 関数を return 1 で抜け出している。(p149) *1:「作ってわかる Cプログラミング」 *2:必要なくなるので 2005-12-21 φ(-_-) ■[lang]はじめての C C programming note*1 あとは、それぞれの関数を組み立てていきます。今回は 1つのファイルに納めましたが 、長くなるようなら、別々にコンパイルするという方法もあります。 /* ce3r.c */ #include #include #include #include #define MAX_SIZE (80 + 1 + 1) /* plus '\n' + '\0' */ #define WIDTH 80 #define YES 1 #define NO 0 #define ISBLANK(c) ((c) == ' ' || (c) == '\t') int carte(); void center(); void remove_pre_blank(); void remove_pos_blank(); int alldigit(); void usage(); void cant(); int width = WIDTH; main(int argc, char **argv) { FILE *fp; char *s; int r; /* オプション指定の 解析 */ while (--argc > 0 && **++argv == '-') { for (s = *argv + 1; *s != '\0'; s++) { switch (*s) { case 'w': if (alldigit(s + 1) == NO) usage(); /* no return */ width = atoi(s + 1); while (*(++s + 1)) ; break; default: usage(); /* no return */ } } } if (width <= 0 || WIDTH < width) usage(); /* no return */ if (argc == 0) { if(carte(stdin) != 0) return 1; } else { while (argc--) { if ((fp = fopen(*argv, "r")) == NULL) cant(*argv); /* no return */ r = carte(fp); fclose(fp); if (r != 0) return 1; argv++; } } return 0; } int carte(FILE *fp) { char buf[MAX_SIZE]; while (fgets(buf, MAX_SIZE, fp) != NULL) { remove_pre_blank(buf); remove_pos_blank(buf); center(buf, width); if (fputs(buf, stdout) == EOF) { fprintf(stderr, "ce3r: write error\n"); return -1; } } return 0; } void center(char buf[], int w) { int n; int off; int i; n = strlen(buf) - 1; /* -1 for '\n' */ if (w < n) return; off = (w - n + 1) / 2; for (i = n + 1; i >= 0; i--) buf[i + off] = buf[i]; while (off--) buf[off] = ' '; } void remove_pre_blank(char *buf) { int n; char *p; char *q; for (p = buf; ISBLANK(*p); p++) ; q = buf; n = strlen(p) + 1; while (n--) *q++ = *p++; } void remove_pos_blank(char *buf) { char *q; if (*buf == '\0') return; q = buf + strlen(buf) - 1; /* -1 for '\n' */ while (q > buf && ISBLANK(*(q - 1))) --q; *q = '\n'; *(q + 1) = '\0'; } int alldigit(char *s) { if (*s == '\0') return NO; for ( ; *s != '\0'; s++) { if (!isdigit(*s)) return NO; } return YES; } void usage(void) { fprintf(stderr, "Usage: ce3r [-wNN] {file ... ]\n"); exit(1); } void cant(char *name) { fprintf(stderr, "can't open %s\n", name); exit(1); } コンパイルが終わったら、さっそく使ってみます。その前に、menu.txt を作成、 * menu * curried rice stew plain omelet beer coffee dairy lunch まず、幅を 30 にして中央揃えに、 $ ./ce3r -w30 menu.txt 以前つくった cat4r と組み合わせてみます、 $ ./ce3r -w30 menu.txt | ./cat4r -n 行番号がプラスされました。大文字にするなら、 $ ./ce3r -w30 menu.txt | ./cat4r -c -n 中央揃えする関数 center() は、次のように書くこともできます、 void center(char buf[], int w) { int n; int off; n = strlen[buf] - 1; if (w < n) return; off = (w - n + 1) / 2; n += 2; /* +2 for '\n' + '\0' */ while (n--) buf[n + off] = buf[n]; while (off--) buf[off] = ' '; } ここのところが替えてありますね↓ n += 2; while (n--) buf[n + off] = buf[n]; *1:「作ってわかる Cプログラミング」