Seaside Laboratory

Posts

signed char との付き合い方

「char が signed になるか unsigned になるかは環境に依存するので気をつけよう」という話はよく聞くが、具体的にどう対処するのか書かれていないことが多い。

標準関数は EOF (-1) の関係で文字を char ではなく int として扱うので、fgetc で取得した文字を isdigit で検査するといった処理なら難なく動いてしまう。ただ、文字列となると char の配列を使わざるを得ないので、int を使うことで回避できていた問題と向き合わなければいけなくなる。

例えば、isdigit を自作しようとした場合、以下のような実装をすることがある。

bool myisdigit( int c )
{
    // ASCII コード基準の判定用テーブル
    // '0' ~ '9' の位置に true を設定 (長いので省略)
    static const bool digits[256] = { false, false, false, ... };

    return digits[c];
}

char が signed だった場合、値の範囲は -128 ~ 127 になるので、文字によっては判定テーブルの範囲外にアクセスしてしまう。インデックス値に 128 を足せば範囲を 0 ~ 255 に変換できるが、char が unsigned だった場合は 128 ~ 383 になってしまい都合が悪い。

標準関数が文字を int で表現しているのであれば、一度 char を int に代入すればうまく動くのではと勘違いするかもしれないが、既に負の値になっている char を int に代入したところで状況は変わらない。

困ったときは先人の知恵ということで、ネット上に公開されている C 標準ライブラリのソースを見てみることにした。

long atol(const char *nptr) {
   const unsigned char *p = (const unsigned char *) nptr;

   // 長いので省略
}

解決策は unsigned char* にキャストするというシンプルな方法。このやり方が優れている点はポインタをキャストすることで全要素を一発で変換していること。ポインタを使わないで同じことをやろうとすると、同サイズのバッファを作成した後に全ての文字をキャストしながらコピーすることになり、プログラムの手間と実行コストが高くなってしまう。

ここまで長々と書いたが、もし ASCII 文字しか処理しないのであれば、ASCII は 7 ビットの文字コードなので char が signed だろうが unsigned だろうが問題なく動く。ただ、ファイルパスといった身近な文字列に非 ASCII 文字が含まれているので、非 ASCII 文字列を渡した途端にクラッシュ、なんてことにならないようキャストしておくのがよさそう。