C言語編 第46章 マルチバイト文字

先頭へ戻る

この章の概要

この章の概要です。

ASCIIコード

第42章で少しだけ触れたように、 文字は、文字コードという数値で表現されます。

つまり、ある数値とある文字とを対応付けるルールを決めておき、数値表現として記憶しておくという手を取ります。 実際、char型の値は、整数値に過ぎず、それを文字情報であるとみなすことで初めて、文字として扱えます。 例えば、以下のような printf関数の呼び分けが分かりやすい例かも知れません。

#include <stdio.h>

int main(void)
{
    char c = 'a';

    printf( "%c\n", c );  /* char型の値を文字として出力 */
    printf( "%d\n", c );  /* char型の値を整数値として出力 */
}

実行結果:

a
97

'a' という表記は、文字を表現したものです。 しかし、実際にメモリ上に記憶されているのは「97」という整数値です。

'a' が「97」であるという対応関係は、 ASCIIコード(アスキーコード)という文字コード体系で決められているルールです。 文字コード体系というのは、文字と文字コードの対応関係のことですが、 これのことを指して文字コードと呼ぶケースも多く、多少の混乱が見られます(当サイトでもあまり明確に区別しません)。

ASCIIコードでの文字と数値表現の対応関係の表は至る所にあります(⇒Wikipedia)。

ASCIIコードは、1文字を 7ビットの整数値で表現します。 7ビットというと、「27 = 128」ですから、わずか 128通りの文字しか表現できません。 元々、アメリカ発祥の文字コードであるため、アルファベットと数字と、ごくわずかな記号類が表現できればよかったため、 これでも十分だったのですが、日本語の表現は絶望的です。

ほとんどの環境では 1バイトが 8ビットなので、 余っている 1ビットを使えるように拡張した、ISO/IEC 8859 という文字コードも使われています。

実のところ、C言語での文字表現に ASCIIコードを使うというルールはありません。 特に日本語環境においては、ひらがな、カタカナ、漢字といった膨大な種類の文字を表現しなければならないため、 ASCIIコードではどうやっても足りないので、より表現力がある文字コードを使います。

といっても、C言語は ASCIIコードを基本として構築されています。
例えば、char型の大きさは 1バイトと定められていますから、 ASCIIコードなら問題ありませんが、より大きな文字コード体系では収まりません。
また、strlen関数のように、文字列を相手にする標準ライブラリ関数は、char型で表現された文字を相手にしているので、1文字が1バイトに収まっていることを前提として実装されています。

そこで、日本語環境では、ASCIIコードと互換性を持ちつつ、より多くの文字を表現できるような文字コード体系を使います。 つまり、ASCIIコードで表現可能な文字に関しては、ASCIIコードとまったく同じ数値で表現するようにルール付けされた文字コード体系を使います。 こうすると少なくとも、ASCIIコードで表現できる文字しか使わないのであれば、どこにも問題を起こさずに済みます。
このような文字コードとして具体的には、Shift_JISや、UTF-8 が使われることが多いです。 どの文字コードが使われているのかは、開発環境のドキュメントを読むなりして、きちんと把握しておかねばなりません。

Shift_JIS と表記するときは、きちんと「Shift_JIS」と書きましょう。 たまに、文字コードの名前を文字列として指定しなければならない場面に出会うことがありますが、 「Shift-JIS」とか「Shift_jis」といったように不正確な表記は受け付けないはずです。 「UTF-8」も同様です。

ここで、本格的に文字コードに関することを考えるととてつもなく厄介になるので、この章の内容は大幅に単純化していることを断っておきます。 C言語編は基本的に VisualStudio をベースとしており、Shift_JIS を前提に説明しています

プログラミング言語、コンパイラ、OS、ソースファイル自体、入出力するデータなど、それぞれの立場に文字コードが関わっているので、 すべてが同じ文字コードを使っているのなら単純に済みますが、混在していたらとても面倒です。 例えば、文字を Shift_JIS で表現されているとして扱うコンパイラを使って、UTF-8 のファイルを読み込むプログラムを作るにはどうすればよいでしょう?  ソースファイル自体を Shift_JIS で書きつつ、UTF-8 の文字列リテラルが登場するソースコードをどう書けばよいでしょう?  そういった問題が現実にはたくさんあります。

マルチバイト文字

Shift_JIS や UTF-8 といった文字コードでは、1文字を表現するための大きさが可変となっています。 これは、ASCIIコードで表現できる範囲は同じ数値を使いつつ(つまり 1バイトで表現)、 他の文字を 2バイト以上の数値で表現するといったことをするためです。 このような文字コードは、マルチバイト文字(多バイト文字)と呼ばれます。
言葉のイメージに反するようですが、ASCIIコード自体もマルチバイト文字に分類します。

C言語で char型を使って表現する文字は、マルチバイト文字です。 それが具体的に、ASCIIコードなのか、Shift_JIS なのか、UTF-8 なのか、それとも他の何かなのかは、コンパイラによって異なります。

難しいのは、char型が 1バイトなのに、2バイト以上で表現される文字を含む可能性があるということです。 これは、ASCIIコードを使っている環境では起こり得ない厄介さです。
ASCIIコード以外を使う環境であっても、ASCIIコードで表現可能な文字しか使っていなければ、特に問題は起こりません。 そのため、入門書などでは日本語を使った解説は避けられることがありますが、 C言語編も終盤に差し掛かってきていますので、そろそろ踏み込んでいきましょう。

分かりやすい現実的な問題を確認してみましょう。 次のプログラムは何を出力するでしょうか?

#include <stdio.h>
#include <string.h>

int main(void)
{
    const char str[] = "日本語を使うテスト";

    printf( "%u\n", strlen(str) );

    return 0;
}

文字列リテラルは、char型の配列ですから、マルチバイト文字の文字列です。 Shift_JIS を使う環境ならば、"日本語を使うテスト" は、Shift_JIS で表現された "日本語を使うテスト" です。

日本語を扱う我々の感覚では、"日本語を使うテスト" という文字列は9文字であると考えますが、 実行結果は次のようになります。

実行結果:

18

Shift_JIS では、すべての文字は 1バイトまたは 2バイトで表現されます。 日本語の文字の多くが 2バイトで表現されるため、"日本語を使うテスト" という文字列は 18バイトを必要とします。
一方、strlen関数は、1バイトが 1文字を表現しているという前提のもとで実装されているため、18 という結果を返してしまいます。

このように、1文字が 1バイトでない文字を含んでいると、 char型を扱う標準ライブラリ関数が意図通りに動作しない可能性がありますから、何とか解決策を探さないといけません。

マルチバイト文字を扱う標準ライブラリ関数

では、1文字が 1バイトでない文字を含んでいても文字数をカウントできる方法を見ていきましょう。

文字列の文字数を調べるには、mblen関数を使います。mblen関数は、stdlib.h に以下のように宣言されています。

int mblen(const char* s, size_t n);

機能性を高めた mbrlen関数もあります。こちらは wchar.h に宣言されています。

mblen関数自体が、マルチバイト文字列の文字数を返してくれる訳ではありません。 この関数は、1文字のマルチバイト文字が、何バイト使っているのかを返します。

第1引数にマルチバイト文字を指すポインタを渡します。

第2引数には、マルチバイト文字が最大で何バイト使う可能性があるのかを指定します。ここには、MB_CUR_MAXで表される値以下の値を指定します。MB_CUR_MAX は、stdlib.h で定義されているマクロで、現在のロケール(後述します)において、マルチバイト文字1文字が最大で何バイトで表現されるを表します。

戻り値は、第1引数で指定した文字が使っているバイト数です。
第1引数で指定したポインタが、2バイト以上で表現されるマルチバイト文字の途中のバイトを指しているときや、 第2引数で指定した値よりも多くのバイト数を使う文字を指しているときには、-1 を返します。

ロケールという言葉が登場しました。 ロケールとは、文化や言語などの慣習のことです。 標準ライブラリ関数の中には、ロケールの違いによる影響を受けるものがあります。

ロケールには、いくつかの設定項目があります。 その中の1つに LC_CTYPE という項目があり、この項目の設定は、マルチバイト文字を扱う関数に影響を与えます (ほかの関数にもいくつか影響を与えます)。 設定を変更するには、

標準には、Cロケールというロケールだけが定義されており、ロケールに関して特に気にしなければ、 デフォルトでCロケールであるとみなされます。 ロケールは、setlocale関数を使います。setlocale関数は、locale.h に以下のように宣言されています。

char* setlocale(int category, const char* locale);

第1引数に変更したい項目名(LC_CTYPE など)を、第2引数に設定値を指定します。
第2引数を "C" としたときが、Cロケールを意味します。 また、"" を指定すると、環境が定義する基本設定(ネイティブロケール)を使うことを意味します。 そのほか、コンパイラで定義されている各種設定値が使用できる可能性があります。

戻り値は、成功した場合は変更前の設定値が返され、失敗した場合はヌルポインタが返されます。第2引数に "C" や "" 以外を指定する場合は、失敗する可能性がおおいにありますから、必ずエラーチェックを行うようにした方がいいです。

では、実際にマルチバイト文字列の文字数をカウントするプログラムを作成してみます。

#include <stdio.h>
#include <stdlib.h>
#include <locale.h>

int main(void)
{
    const char str[] = "日本語を使うテスト";
    int char_count;
    int i;

    /* LC_CTYPE をネイティブロケールに変更 */
    if( setlocale( LC_CTYPE, "" ) == NULL ){
        fputs( "ロケールの設定に失敗しました。\n", stderr );
        return EXIT_FAILURE;
    }

    i = 0;
    char_count = 0;
    while( str[i] != '\0' ){
        int res = mblen( &str[i], MB_CUR_MAX );
        if( res < 0 ){
            fputs( "不正な文字を含んでいます。\n", stderr );
            return EXIT_FAILURE;
        }

        i += res;
        char_count++;
    }

    printf( "length: %d\n", char_count );

    return 0;
}

実行結果

length: 9

ロケールのデフォルトは "C"ロケールです。 LC_CTYPE が "C"ロケールになっている場合、マルチバイト文字を扱う関数は ASCIIコードであるものとして動作します。 今回、Shift_JIS のような、環境の標準設定に従って動作してほしいので、まず、ネイティブロケールに変更しています。

mblen関数の第1引数へは、マルチバイト文字の1文字を指すポインタを渡さねばなりません。 そのためには、単純に添字をインクリメントしていってはうまくいきません。 変数str は char型であり、Shift_JIS の 1文字は 1バイトではないからです。 添字は +1 を繰り返すのではなく、1文字のバイト数ずつ進ませる必要があります。

そこで、mblen関数の戻り値を変数 i に加算することを繰り返して、1文字のバイト数ずつ進ませるようにしています。 同時に、文字数をカウントする変数 char_count の方をインクリメントして、文字数を数えます。

0x5c問題

Shift_JIS には、0x5c問題と呼ばれている有名な問題点があります。 ここで、0x5c は 16進数の 5c のことで、Shift_JIS において 0x5c というバイトが登場すると、厄介事が起こるということです。

Shift_JIS は、1バイトと 2バイトの文字が混在しています。 これは文字列を先頭から解析していったとき、"特定の範囲" の値を持ったバイトが登場したら、 その後続の 1バイトと組み合わせて、2バイトで 1文字であるとみなすことで実現されています
その "特定の範囲" 以外の値を持ったバイトが登場したときには、 そのバイト単体で 1文字を意味しているとみなされます

0x5c問題は、2バイト目の方に 0x5c が登場したときに発生します。 Shift_JIS としては、「"特定の範囲の値" + 0x5c」の組み合わせによって、何らかの1文字を表現しているつもりですが、 ASCIIコードとして扱うようなプログラムでは、"特定の範囲の値" と 0x5c をそれぞれ別個の文字として扱います。

ではどうして 0x5c が問題なのかというと、実は ASCIIコードにおいて 0x5c は「\」だからです。 C言語では、「\」はエスケープ文字の意味があるため、それと誤認識され、この直後の 1バイトをエスケープしてしまう訳です。 その結果、典型的には文字化けを引き起こします。

例えば、「表」や「ソ」という文字が、0x5c問題が起こる代表例な文字です。 意外とよくある文字なので、問題が大きいです。

puts( "日本語を表示するテスト" );  /* 「表」の後ろで問題が起こるかも知れない */

ただし、何らかの対策がなされている場合もあり、特に問題が発生しない環境もあります。 実際、現在の VisualStudio でも問題は発生しません。

0x5c問題への対策としては、0x5c を含んでいる文字の直後に、意図的に「\」を追加してやることです。 すると、「\\」が並んだ状態になるため、「\」という 1文字に置換されます。 置換後の「\ (0x5c)」が、2バイト目の「0x5c」として使われて、意図通りの文字を表現できます。

この対策を講じると、次のように書くことになります。

puts( "日本語を表\示するテスト" );

非常に格好悪いですが、これで対策できます。

文字列リテラルの連結

少々、話が変わりますが、文字列リテラルを続けて記述すると、それぞれが連結して書かれたことになります。

#include <stdio.h>

int main(void)
{
    char str[] = "abcde"
                 "fghij";

    puts( str );

    return 0;
}

実行結果:

abcdefghij

このルールは、うっかり意図せずに連結してしまう可能性を持っています。 この機能を使わないとしても、連結されてしまうのだということは覚えておくべきです。

なお、次のような記述も許可されます。

#include <stdio.h>

#define STR1 "abcde"
#define STR2 "fghij"

int main(void)
{
    char str[] = STR1 STR2;

    puts( str );

    return 0;
}

実行結果:

abcdefghij

マルチバイトの文字列リテラルと、ワイド文字列リテラル(第47章参照)との連結については、C95 の時点では未定義となっています。C99 で結果が規定されました(第47章)。


練習問題

問題① Shift_JIS において、「表」という文字が 0x5c を含んでいることを、バイナリエディタを使って確認して下さい。

問題② マルチバイト文字に Shift_JIS を使う場合、以下の実行結果が幾つになるか答えて下さい。

printf( "%u\n", sizeof('あ') );
printf( "%u\n", sizeof("ABCABC") );
printf( "%u\n", strlen("ABCABC") );

問題③ ASCIIコードで表現できる文字と、できない文字とが混在した Shift_JIS の文字列を、逆順にして出力するプログラムを作成して下さい。


解答ページはこちら

参考リンク

更新履歴

'2018/4/22 解説中で C95 を(C89 に対して)特別扱いしないように修正。そもそもC言語編は C95ベースなので、余計な説明は省く。

'2018/4/20 「NULL」よりも「ヌルポインタ」が適切な箇所について、「ヌルポインタ」に修正。

'2018/4/2 「VisualC++」という表現を「VisualStudio」に統一。

'2018/3/21 全面的に文章を見直し、修正を行った。

'2018/3/2 第27章から「文字列定数の連結」の項を移動してきた。

≪更に古い更新履歴を展開する≫





前の章へ(第45章 コマンドライン)

次の章へ(第47章 ワイド文字)

C言語編のトップページへ

Programming Place Plus のトップページへ


このエントリーをはてなブックマークに追加
rss1.0 取得ボタン RSS