でも、考えてみると、K&Rの演習問題について質問するのと、ゼミの演習問題に ついて質問するのと、一体どこが違うのだろう、という疑問が沸いてくる。一つ の可能性としては、ゼミの演習問題を解いてもらうという態度の裏に、実は単位 が欲しいだけの話であって、C言語を身に付ける気は全くない、という場合もある かもしれない。そういうのは、少しずるい感じが確かにする。しかし、本当にそ の人が分からなくて困っているのかもしれない。
C言語フォーラムで出ていた質問は、配列Aの要素の和を求める関数SUMを書け、 というものだった。関数名が大文字というのがまずどうかしている。さすがに、 この程度の問題だと「解いて」と改まって言われると、何かワナがあるんじゃな いか、と思ってしまうほど単純すぎる。例えば、実は要素の数が5兆個あるんです、 とか、DBL_MAXに近い数が2つばかりあるとか、本当は一つもないんです、とか。 何かの漫画じゃないけど、printf("求めました\n"); だけで正解になるとか。も しこれが何かの授業に出た問題だとすれば、もしかすると、よほど分かりにくい 授業だったのではないか、と想像してしまうのだ。
で、関数SUMの求め方を真面目に書いてみたのだが、これはまた難問である。答 えのコードを示すだけなら5分もかからないが、自力で解決できない人が理解でき るような解答を書くというは並大抵の難しさではないのだ。
*話を戻すと、行末の改行を取り除くといっても、具体的なやり方はいくらでも ありそうだ。このような単純な問題を解く時に、プログラマーの個性が出てくる ものである。次のコードは、通信ソフト「魔王」の中で使っているものだ。関数 呼び出し側には、先頭の空白をスキップしたポインタを戻している。行末といっ ても、この関数は、C言語でいうところの1行ではなく、先頭からスキャンして最 初に改行コードが現れるまでを1行とみなしている。先頭から見ているのは、多分、 文字列終了の'\0'が現れるまでの間に複数のnewlineが含まれる場合を想定したの である。通信ソフトの場合、改行コードは何が来るか分からないから'\n'ではな くて、0x0a、0x0dの両方見ている。
/*-------------------------------------------------------------------------*/ /* List 1 */ char *remove_crlf(char *s) { char *t; while (*s == ' ' || *s == '\t') s++; t = s; while (*t) { if (*t == 0x0a || *t == 0x0d) { *t = 0; break; } t++; }return s; }
WWWで公開しているPhinloda's pageは、おかげさまで6月10日現在15,000アクセ ス程度になっていて、オープン当時の様子がウソのようだ。このページに会社か らアクセスしてしまって、あわててページを閉じたとかいう話があるのだが、べ つに慌てることはないのではないかと思うぞ。もしかすると皆さんがこれを読ん でいる頃はオープニング画像が衣がえ(衣ってあったか…?)しているかもしれない が、春〜夏にかけて出ていた女の子が持っているのはC入門という思わせぶりな本 である。
さて、この画像データだが、MIME encodeされた電子メールでやって来る。今で もなおDOSを使う場合が多いので、DOSでMIMEをdecodeするフリーソフトはないか と探したことがある。あるらしいのだが、なぜか見つからない(^^;)。というわけ で、えーい、作った方が早い、って感じになって半日で作ったプログラムの中の コードがこれである。こちらは、strlenで長さを求めて後ろに飛んでから、逆戻 りしている。改行だけでなく、空白、タブも削っているのは、なぜかしらないが、
> Content-Description: PDLLFRON.JPG_
こんな所に空白を付けるメールが来るからついでにカットしたわけだ (_の所は実際は空白)。この空白って必要なのだろうか。RFCはざっと見 た程度だからよく分からない。
よくやってしまう失敗に、配列の先頭を超えてアンダーランしてしまうという のがある。つまり、buf[-1]を見に行ってしまうのである。これは、文字列の長さ が0の時に特に注意が必要である。fgetsで戻ってくる文字列の長さが0ということ は滅多にないのだが、絶対にないわけではない。実際、NIFTY-Serveにアクセスし たログを後で見ると、行の先頭にNULが入っているというとんでもないデータにな っていることがあって、こんなのをC言語の文字列処理にそのままかけたらすごい ことになる。たいていの通信ソフトには、NULを無視するというオプションがある ので、ログを取っても毎行NULが入っているなどということはないと思うが。ログ をある環境下でfgets以外の関数で受け取った文字列だと、さらに要注意である。
/*--------------------------------------------------------------------*/ /* List 2 */ static void remove_newline(void) { int len; len = (int) strlen(buf); while (len-- > 0) { if (buf[len] != 0x0a && buf[len] != 0x0d && buf[len] != ' ' && buf[len] != '\t') break; buf[len] = '\0'; } return; } /*--------------------------------------------------------------------*/
このコードだが、ちょっと気に入らないことが一つある。strlenでまず長さを 求めているということである。strlenの実装がどうなっているかというと、おそ らく指定した文字列の先頭から1文字ずつ調べて、'\0'が現れた所でカウントを止 める。といった所か。ということは、strlenを実行する時に、すでに一通りの文 字を検査しているわけだ。その後で、わざわざ行末から先頭に向かって、もう一 度文字の検査をするのである。これは面白くない。
そこで、先頭から検査する時に行末のチェックも入れてしまったのがList 3で ある。これで、確かにスキャンが1回で済んだ。しかし、これって、1行の長さが そこそこあれば、なかなか改行コードが出てこないのだから、かえって無駄だら けの処理という感じがする。あまり考えないでstrlenを使った方が効率も良いよ うな気がするのだ。
ところで、MIME形式で使われているbase64というEncodingだが、簡単にいえば 6ビットの情報4個で3バイト=24ビットのバイナリ情報を表現するというものであ る。
/*--------------------------------------------------------------------*/ /* List 3 */ static void remove_newline(void) { char c, *s, *t;s = buf; t = NULL; while (c = *s) { if (c == 0x0a || c == 0x0d || c == ' ' || c == '\t') { if (t == NULL) t = s; } else { t = NULL; } s++; }
if (t != NULL) *t = '\0'; return; } /*--------------------------------------------------------------------*/
---- 図 ----
コードにすれば、List 4のような単純な処理である。base64のデータが入った 配列cからデコードした結果をnに格納する。
/* List 4 */ b_to_n(char *n, int c[4]) { n[0] = (c[0] << 2) | ((c[1] >> 4) & 3); n[1] = (c[1] << 4) | ((c[2] >> 2) & 0xf); n[2] = (c[2] << 6) | c[3]; }
/* List 5 */ b_to_n(char *n, int c[4]) { n[0] = t0[c[0]] | t1[c[1]]; n[1] = t2[c[1]] | t3[c[2]]; n[2] = t4[c[2]] | c[3]; }
しかし、これで本当に処理が速くなったかというと、定かではない。実はI/Oの 速度の方が圧倒的に遅くて無意味なのかもしれない。ま、要は心構えである。ど うでもいいや、という習慣が蓄積すると、ちょっとした差でも、塵もつもって山 となるものだ。