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 という文字コードも使われています。

元々、アメリカ発祥の文字コードであるため、アルファベットと数字と、ごくわずかな記号類が表現できればよいので、これで十分だったのですが、日本語では、ひらがな、カタカナ、漢字といった膨大な種類の文字を表現しなければならないため、まったく足りません。そのため、日本語環境では、1文字の大きさをもっと大きくした、より表現力がある文字コードを使います。

しかし、C言語は ASCIIコードを基本として構築されているといえます。例えば、基本文字集合(第8章)に含まれている文字は、ASCIIコードに含まれているものだけで構成されています。

また、strlen関数のように、文字列を相手にする標準ライブラリ関数は、char型を使っているので、1文字が1バイトで表現できることを前提として実装されています。

マルチバイト文字

日本語環境においては、ASCIIコードの表現力では、必要な文字の大半が表現しきれないので、ほかの文字コードを使います。普通、ASCIIコードと互換性を持ちつつ、より多くの文字を表現できるような文字コード体系を使います。つまり、ASCIIコードで表現可能な文字に関しては、ASCIIコードとまったく同じ数値で表現するようにルール付けされた文字コード体系を使います。

このような文字コードとして、Shift_JISや、UTF-8 が使われることが多いです。どの文字コードが使われているのかは、開発環境のドキュメントを読むなりして、きちんと把握しておかねばなりません。

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

本格的に文字コードに関することを考えると、とても長くなってしまうので、この章の内容は大幅に単純化していることを断っておきます。当サイトは基本的に VisualStudio をベースとしているので、ここで使われている Shift_JIS を前提に説明しています

正確には、Shift_JIS を拡張した CP932 というコードです。

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

Shift_JIS や UTF-8 といった文字コードでは、1文字を表現するための大きさが一定ではありません。このような文字コードは、マルチバイト文字(多バイト文字)と呼ばれます。言葉のイメージに反するようですが、ASCIIコード自体もマルチバイト文字に分類します。

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

1文字の大きさが一定しない文字コードを使う場合、char型の変数に文字が収まらない可能性があることに注意しなければなりません。これは、ASCIIコードを使っている環境では起こり得ない厄介さです。ASCIIコードとの互換性を持つ文字コードなのであれば、ASCIIコードの範疇の文字だけを使っている限りは、特に問題は起こりません。

1文字の大きさが一定でない文字コードを使っている場合に起こる、現実的な問題を確認してみましょう。次のプログラムは何を出力するでしょうか?

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

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

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

    return 0;
}

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

実行結果:

18

Shift_JIS では、文字を 1バイトまたは 2バイトで表現します。日本語の文字の多くが 2バイトで表現されるため、"日本語を使うテスト" という文字列は 18バイトを必要とします。

一方、strlen関数は、1バイトが 1文字を表現しているという前提のもとで実装されているため、18 という結果を返します。我々が期待している結果が「文字数」であるのに対して、strlen関数は「バイト数」を返してしまう訳です。

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

C11 (UTF-8 の文字列リテラル)

C11 の文字列リテラルには、u8プリフィックスを付加することができ、UTF-8 を使うことを指示できます。

u8プリフィックスを付加しても、型としては変わらず char の配列型です。

通常の文字列リテラルが、デフォルトで UTF-8 を使う処理系では、u8プリフィックスを付加しても、特に変化はありません。

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

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

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

    return 0;
}

実行結果:

27

なお、文字リテラルには u8プリフィックスを付加することはできません

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

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

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

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

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

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

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

第2引数には、引数s が指す文字列をどれだけ調べるかを指定します。ここには、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文字を指すポインタを渡さねばなりません。そのためには、単純に添字をインクリメントしていってはうまくいきません。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( "日本語を表示するテスト" );  /* 「表」の後ろで問題が起こるかも知れない */

内部的には UTF-8 のような、同種の問題が起こらない文字コードを使うなど、内部的な対策がなされている環境もあり、一概にあらゆる Shift_JIS 使用環境で問題が起こるとは限りません。

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/7/3 「C11 (UTF-8 の文字列リテラル)」の項を追加。
その他、全体的に見直し修正。

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

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

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

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

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



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

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

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

Programming Place Plus のトップページへ


はてなブックマーク Pocket に保存 Twitter でツイート Twitter をフォロー
Facebook でシェア Google+ で共有 LINE で送る rss1.0 取得ボタン RSS
管理者情報 プライバシーポリシー