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

第51回:推敲

初出: C MAGAZINE 1996年7月号
Updated: 1996-10-01

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


NIFTY-Serve FCに面白い質問が出ていた。16進ダンプを行った時に表示される ような文字列を数値に変換するというものである。 例えば"012345F"から、0x01, 0x23, 0x5Fという数値を取り出したいの である。この質問自体は何という程でもない。ただし質問者はスマートな回答を 要求していたので、あれこれ考えてしまった。このような単純な問題ほど奥が深 いものである。

 FCでは、strtolを使った方法と、1文字ずつ読み込んで数値に変換するというオー ソドックスな方法が紹介されていたが、一文字ずつ変換というのは、原理として はList 1のような処理である。これだけの処理でも、いろんな書き方が出来るの で、プログラマーの個性が出るものだ。なお、実際はEOFの検出とか、範囲外の文 字の処理をしなければならないので、簡単といっても面倒なものである。List 1 は、その点すごくでたらめかもしれない。


List 1: 1文字 ASCII→16進変換

int ascii_to_hex()
{
    int c;

    c = getchar();
    c -= '0';
    if (c > 9)
        c -= ('A' - '0' + 10);
    return c;
}

 さて、1文字を16進変換できれば、2桁の16進アスキー文字を整数に変換するの は簡単である。List 2でよい。ちなみに、C言語に慣れ始めた人はList 3のように 失敗することがあるというのは言うまでもない。
List 2: 2文字 ASCII→16進変換

    int i;

    i = ascii_to_hex();
    i *= 16;
    i += ascii_to_hex();

List 3: 2文字 ASCII→16進変換(誤り)

    int i;

    i = ascii_to_hex() * 16 + ascii_to_hex();

 実際にプログラムを作るとすれば、1文字を読んで数値化するという処理は、全 体の負荷の中で、かなり頻繁に実行される箇所になるかもしれない。つまり、こ こを考えなしに作ってしまうと、プログラム全体の効率に影響するかもしれない。 という場合には、関数呼び出しを避けてマクロにするとか、環境によってはイン ライン展開を指定するといった工夫が必要になる。コンパイラにおまかせ、とい うのもいいかもしれない。

 本当に速度が必要なら、テーブルを引くというのも一つの手である。8ビットの 文字を引くテーブルは、たかだか256バイト程度である。しかし、List 4のような コードを実際に見ると、何か無駄なことをしているような気がするものだ。


List 4: 1文字 ASCII→16進変換

unsigned char table[256] = {
    255, 255, 255, 255, 255, 255, 255, 255,
    255, 255, 255, 255, 255, 255, 255, 255,
    255, 255, 255, 255, 255, 255, 255, 255,
    255, 255, 255, 255, 255, 255, 255, 255,
    255, 255, 255, 255, 255, 255, 255, 255,
    255, 255, 255, 255, 255, 255, 255, 255,
      0,   1,   2,   3,   4,   5,   6,   7,
      8,   9, 255, 255, 255, 255, 255, 255,
    255,  10,  11,  12,  13,  14,  15, 255,
    255, 255, 255, 255, 255, 255, 255, 255,
        (略)
    255, 255, 255, 255, 255, 255, 255, 255
};

int ascii_to_hex()
{
    int c;

    return (int) table[getchar() & 0xff];
}

なお、List 4もエラーに無頓着だから、このコードを使う時には考えなければな らない。テーブルの、16進に該当しない所に255という値を入れたのは、テーブル を引いた結果が0から15の範囲外の値になった時に区別できるようにしたのである。 だから、もし予期しない所でその値が戻ってきたらプログラムを強制終了してし まえば、一応エラー時にも対応できるのだ。0xffでマスクしているのは、getchar ()がEOFを戻すかもしれないからである。配列のインデックスが-1(EOFは-1と想定 する)になるというのは、ちょっと不気味かもしれないが、もしそこが正しいオブ ジェクトであれば本質的な問題はない。ということは、List 5のように、テーブ ルの先頭にもう一つ変換結果を追加してしまえば、EOFに対応するためのマスクは 不要になる。
List 5: 1文字 ASCII→16進変換

unsigned char table_257[257] = {
    255,
    255, 255, 255, 255, 255, 255, 255, 255,
    255, 255, 255, 255, 255, 255, 255, 255,
    255, 255, 255, 255, 255, 255, 255, 255,
    255, 255, 255, 255, 255, 255, 255, 255,
    255, 255, 255, 255, 255, 255, 255, 255,
    255, 255, 255, 255, 255, 255, 255, 255,
      0,   1,   2,   3,   4,   5,   6,   7,
      8,   9, 255, 255, 255, 255, 255, 255,
    255,  10,  11,  12,  13,  14,  15, 255,
    255, 255, 255, 255, 255, 255, 255, 255,
        (略)
    255, 255, 255, 255, 255, 255, 255, 255
};
unsigned char *table = table_257 + 1;

int ascii_to_hex()
{
    int c;

    return (int) table[getchar()];
}

 EOFの判別を厳密にやる必要はなく、この256バイトがもったいないというので あれば、どうせビットをマスクするのだから、もっとマスクしてしまえばテーブ ルは小さくなる。ただし、こうなると16進以外の文字が現れたら何がどうなるか わかったもんじゃない。
List 6: 1文字 ASCII→16進変換

unsigned char table[32] = {
    255,  10,  11,  12,  13,  14,  15, 255,
    255, 255, 255, 255, 255, 255, 255, 255,
      0,   1,   2,   3,   4,   5,   6,   7,
      8,   9, 255, 255, 255, 255, 255, 255
};

int ascii_to_hex()
{
    int c;

    return (int) table[getchar() & 0x1f];
}

 逆にメモリがあり余っているというのならどうか。unsigned shortでテーブル を作っておいて2バイトずつ読むという手もある。65536文字に対応するテーブル を使うのはあんまりだというなら、範囲をチェックして、16進数値の場合は0x3030 から0x4F4Fまでの7968文字だけを処理すればよい。0x1f1fでマスクしてからテー ブルを引けば、0x0101から0x1919の範囲になるので6170文字で済む。済むって感 覚でもないか。それに、コードを検証するのが大変そうだ。実行時にテーブルを 生成した方が確実かもしれない。  ところで、テーブルを引くのであれば、わざわざascii_to_hex()などどいう関 数を呼び出すのは面白くない。どうせ1アクションの処理なのだから、関数を呼び 出さずに直接参照することにすれば、関数呼び出しのオーバーヘッドはなくなる。 ちょっと見た目が冗長だが、かといってマクロを定義するほどのこともなかろう。
List 7

    int i;

    i = (int) table[getchar() & 0x1f];
    i *= 16;
    i += (int) table[getchar() & 0x1f];

 List 7で、i *= 16;という文を分けたのは、なんとなく、である。多分、List 8 のように書きたくなる人も多いのではないだろうか。私の感覚では、List 7の方 がいい。もちろんプログラムの立場としてはどちらでもいいのである。
List 8

    int i;

    i = (int) table[getchar() & 0x1f] * 16;
    i += (int) table[getchar() & 0x1f];

 さて、最後に、些細ではあるが、毎回16を掛けるというのが気になるかもしれ ない。シフトに置き換えるという話ではない。もちろん、16を掛けるか4つ左シフ トするかを選択することも意味があるが、その程度ならコンパイラにおまかせ、 でなんとかなるだろう。しかし、16進数の1バイト目と2バイト目で違うテーブル を使うことにすれば、16を掛けるオーバーヘッドはなくなる。ただし、少しメモ リを余計に使うことになるが。
List 9: 1文字 ASCII→16進変換

unsigned char table_h[32] = {
    255,  10*16, 11*16, 12*16, 13*16, 14*16, 15*16, 255,
    255,  255,   255,   255,   255,   255,   255,   255,
    0,    1*16,  2*16,  3*16,  4*16,  5*16,  6*16,  7*16,
    8*16, 9*16,  255,   255,   255,   255,   255,   255
};

unsigned char table_l[32] = {
    255,  10,  11,  12,  13,  14,  15, 255,
    255, 255, 255, 255, 255, 255, 255, 255,
      0,   1,   2,   3,   4,   5,   6,   7,
      8,   9, 255, 255, 255, 255, 255, 255
};

    int i;

    i = (int) table_h[getchar() & 0x1f];
    i += (int) table_l[getchar() & 0x1f];

 この他にも、unsigned charではなくintの配列のテーブルでよいのではないか 等、考える価値がある場所はあるのだが、いずれにしても、実際に動かすプログ ラムを作る時には、あれこれ全部吹っ飛んでしまって、考えずに書いてしまうも のである。だからこそ、日頃からあれこれ考えてどうすればよいかを決めておく のが重要だ。いざという時に出てくるのは練習の成果である。暇な時には、どう でもいいようなプログラムをあれこれ考えてみるのも一興かもしれない。

 しかし問題は、そのような暇があるかということである。


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