フィンローダのあっぱれご意見番

第52回:ちょっとした差

初出: C MAGAZINE 1996年8月号
Updated: 1996-09-11

[1つ前] [1つ後] [一覧] [ホームページ]


K&Rの演習問題に、行末の改行を取り除く、という問題がある。これに関して NIFTY-ServeのC言語フォーラムに質問が出た。数名がコメントしていたと思う。 実は私もコメントしたのだが。ところで、ネットに限らずNewsgroupにおいてもよ く見かけるのが、おそらくゼミか何かの演習問題が解けないので教えてくれとい う類のものである。極端だと問題がそのまま掲載されていて、答えはどうだ、と いう感じだ。面白いことに、常識的反応というのだろうか、この種の質問に対し てはあまりまともにフォローする人はいない。逆に、むしろ「自分で解かないと 為にならない」と説教する人がいるものである。

 でも、考えてみると、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はざっと見 た程度だからよく分からない。


/*--------------------------------------------------------------------*/ /* 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; } /*--------------------------------------------------------------------*/
 よくやってしまう失敗に、配列の先頭を超えてアンダーランしてしまうという のがある。つまり、buf[-1]を見に行ってしまうのである。これは、文字列の長さ が0の時に特に注意が必要である。fgetsで戻ってくる文字列の長さが0ということ は滅多にないのだが、絶対にないわけではない。実際、NIFTY-Serveにアクセスし たログを後で見ると、行の先頭にNULが入っているというとんでもないデータにな っていることがあって、こんなのをC言語の文字列処理にそのままかけたらすごい ことになる。たいていの通信ソフトには、NULを無視するというオプションがある ので、ログを取っても毎行NULが入っているなどということはないと思うが。ログ をある環境下でfgets以外の関数で受け取った文字列だと、さらに要注意である。

 このコードだが、ちょっと気に入らないことが一つある。strlenでまず長さを 求めているということである。strlenの実装がどうなっているかというと、おそ らく指定した文字列の先頭から1文字ずつ調べて、'\0'が現れた所でカウントを止 める。といった所か。ということは、strlenを実行する時に、すでに一通りの文 字を検査しているわけだ。その後で、わざわざ行末から先頭に向かって、もう一 度文字の検査をするのである。これは面白くない。

 そこで、先頭から検査する時に行末のチェックも入れてしまったのがList 3で ある。これで、確かにスキャンが1回で済んだ。しかし、これって、1行の長さが そこそこあれば、なかなか改行コードが出てこないのだから、かえって無駄だら けの処理という感じがする。あまり考えないでstrlenを使った方が効率も良いよ うな気がするのだ。


/*--------------------------------------------------------------------*/ /* 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; } /*--------------------------------------------------------------------*/


 ところで、MIME形式で使われているbase64というEncodingだが、簡単にいえば 6ビットの情報4個で3バイト=24ビットのバイナリ情報を表現するというものであ る。

---- 図 ----

bit対応の図

 コードにすれば、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のように書いた。
/* 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の 速度の方が圧倒的に遅くて無意味なのかもしれない。ま、要は心構えである。ど うでもいいや、という習慣が蓄積すると、ちょっとした差でも、塵もつもって山 となるものだ。


(C) 1996 Phinloda, All rights reserved
無断でこのページへのリンクを貼ることを承諾します。問い合わせは不要です。
内容は予告なく変更することがあります。