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

先頭へ戻る

このエントリーをはてなブックマークに追加

この章の概要

この章の概要です。

シングルバイト文字

C言語において、文字を表現するためのデータ型は char型ですが、この型は規格上、必ず 1Byte の大きさを持つことになっています

第42章でバイナリデータを取り扱ったとき、少しだけ触れたように、 文字というデータは、文字コードという整数値で表現されます

C言語で使われている文字コードは、ASCIIコードと呼ばれるもので、これは 7bit の範囲を使って 1つの文字を表現しています。 7bit の文字コードならば、1Byte の char型で表現可能でしょう。

ASCIIコードは、必ず決まった大きさ(7bit) を持っていることから、 シングルバイト文字というタイプの文字コード体系に分類されます。

さて、元々アメリカ発祥のC言語においては、ASCIIコードでも十分に自国の文字を表現できたのでしょうが、日本語ではそうはいきません。 7bit では 128種類の文字しか表現できないのですから、ひらがなとカタカナだけで、ほとんど使い切ってしまいます。 何より、漢字という膨大な数の文字を持つ我々としては、何らかの別手段が必要です。
そこで、次の項で取り上げるマルチバイト文字が登場します。

マルチバイト文字

シングルバイト文字のように、特定のサイズで固定されている文字コード体系に対して、 マルチバイト文字は、1文字を表現するためのサイズが可変であるものを指します。

C言語で日本語を扱う方法は、環境によっても異なります。 ここでは、Windows環境でのスタンダードである Shift_JIS を例とします。
ちなみに、「Shift」と「JIS」の間の区切りは必ず「_」です。 どこかにこの単語を書くときは、常に正確に「Shift_JIS」と記述することを徹底して下さい。

HTML のドキュメント等、使用する文字コードを指定する場面というのは幾つかあります。 このとき、正確に表記しないと正しく指定を受け付けないことがあります。

他に、EUC-JP や UTF-8 といったマルチバイト文字を使用する環境もあります。 Shift_JIS の事情がそのまま当てはまる訳ではありませんが、1文字を表現するために異なるバイト数が混在している点は共通しています。

Shift_JIS は、代表的なマルチバイト文字体系の文字コードです。 Shift_JIS では、1文字を 1Byte あるいは 2Byte で表現します

仕組みを詳細には解説しませんが、要するに、1Byte で表現できる文字と、2Byte で表現する文字とを混在させているのが Shift_JIS の特徴です。 漢字も含め、全角文字は 2Byte で表現されています。
このサイズの混在という複雑さと引き換えに(?)、Shift_JIS を使えば、約11000文字を扱えます。

サイズが混在していることは、プログラミング上の扱いも難しくしてしまいます。 例えば、文字列全体のサイズが分かっていても、そこに含まれる文字数を知ることが簡単にはできません。
文字列全体のサイズが 10Byte のとき、1文字が 1Byte で固定されていれば、文字数が 10文字であることは確定していますが、 マルチバイト文字ではそうはいきません。 そのため、マルチバイト文字では原則、先頭から全ての文字を辿っていき、どのような文字で構成されているかを調べる処理が必要になります。

標準関数のマルチバイト文字サポート

これまでの章では、特に気にすることなく日本語を扱ってきました。 実際、次のプログラムは正しく動作します。

#include <stdio.h>

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

	puts( str );

	return 0;
}

実行結果

日本語を使うテスト

日本語の表現のために Shift_JIS を使っている場合、"日本語を使うテスト" の部分は Shift_JIS で表現されています。 それをC言語のプログラムとしては、1文字が 1Byte である前提として取り扱います。
前述したように、Shift_JIS においては、全角文字を 2Byte で表現していますから、"日本語を使うテスト" は全体で 18Byte の領域を取っています。 strlen関数(⇒リファレンス)を使って文字数を調べてみると…

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

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

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

	return 0;
}

実行結果

日本語を使うテスト
length: 18

このように 18 が出力されます。 strlen関数は、あくまでも 1文字が 1Byte の前提で文字数をカウントするのでこのような結果になります。 日本語としての文字数は 9文字であっても関係ありません。

これは要するに、C言語としてはマルチバイト文字をそれほど特別視してくれていないということです。 だからこそ、これまでの章のように気にせず日本語を使えたとも言えますし、strlen関数では日本語としての文字数が調べられないという事態を生むとも言えます。 また、この後の項で取り上げるように、現実的な問題を生むこともあります。


一応、日本語としての文字数のカウントにもチャレンジしておきましょう。 ここでは mblen関数(⇒リファレンス)を使います。

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

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

	setlocale( LC_CTYPE, "" );

	i = 0;
	char_count = 0;
	while( str[i] != '\0' ){
		i += mblen( &str[i], MB_CUR_MAX );
		char_count++;
	}

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

	return 0;
}

実行結果

length: 9

mblen関数は、第1引数にマルチバイト文字のアドレスを渡すと、そこを起点として後続に何文字あるかを返します。 このとき、第2引数に指定したバイト数以上は数えません。
上記のサンプルの場合、第2引数に MB_CUR_MAX(⇒リファレンス) を指定していますが、 これは、現在のロケールにおいて、マルチバイト文字1文字が最大で何バイトであり得るかに置き換わるマクロです。

ロケールという新しい単語が登場しましたがこれは、国や地域、言語といった文化的な設定を表します。 locale.h に宣言されている setlocale関数(⇒リファレンス)を使って設定を変更できます。

mblen関数で日本語を扱うには、先ほどのサンプルプログラムのように、 setlocale関数を使って、LC_CTYPE(⇒リファレンス) という項目を設定しておく必要があります。 MB_CUR_MAXマクロも、ロケールの影響を受けるので、setlocale関数を最初に呼び出しておく必要があります。
setlocale関数の第2引数を "" に指定すると、使用しているシステムにおけるデフォルト設定がなされます。 Windows環境であれば、Shift_JIS になります。

あくまで、日本語版の Windows に限った話をしています。

あとは、mblen関数を使ってマルチバイト文字列を 1文字ずつ調べながら、文字数をカウントしていきます。 mblen関数の第2引数を MB_CUR_MAX にしているので、戻り値は必ずマルチバイト文字1文字分の大きさになります。

0x5c問題

Shift_JIS における有名な問題点は、0x5c問題と呼ばれるものです。 0x5c は 16進数の 5c のことです。

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

問題なのは、この 2Byte目の方に 0x5c が登場する可能性があるという点です。 Shift_JIS としては、「"特定の範囲の値" + 0x5c」の組み合わせによって、何らかの全角文字を表現しているつもりなのですが、 マルチバイト文字列をあくまでも ASCII として扱ってしまうC言語では、"特定の範囲の値" と 0x5c はそれぞれ個別の文字のようにみなしてしまいます。

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

ただしこの問題は、現在の日本語処理環境では内部的に対策がなされていることもあり、特に問題が発生しない環境もあります。 実際、現在の VisualC++ の日本語版でも問題は発生しません。

問題への対策としては、0x5c を含んでいる文字の直後に、意図的に「\」を追加してやることで、「\\」という並びになるようにします。 「\\」は「\」という 1文字(1Byte) に置換されます。 この 1Byte は、前述の「"特定の範囲の値" + 0x5c」における「0x5c」のことです。 こうなれば、直後の文字を巻き込んで誤認識する事態を避けられます。
例えば、「表」という漢字は、0x5c問題が起こる代表例なのですが、これを出力する際に、

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

のようにすると文字が化けてしまいますが、

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

こうすれば正しく出力できます。恰好悪いですが…。


ちなみに、C言語で扱うマルチバイト文字の中に、0x00 が登場しないことは保証されています。 そのため、文字列の末尾を表すヌル文字と誤認識されることはありません。


練習問題

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

問題A Shift_JIS において、以下の実行結果が幾つになるか答えて下さい。

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

int main(void)
{
	printf( "%u\n", sizeof('あ') );
	printf( "%u\n", sizeof("ABCABC") );
	printf( "%u\n", strlen("ABCABC") );

	return 0;
}

問題B ASCII で表現できる文字と、日本語の混在した Shift_JIS の文字列を、逆順にして出力するプログラムを作成して下さい。


解答ページはこちら

参考リンク

更新履歴

'2014/1/25 setlocale関数の LC_CTYPE の指定を修正。説明を追加。

'2010/8/29 新規作成。



前の章へ

次の章へ

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

Programming Place Plus のトップページへ