● Unicodeに対応する

wchar_t型がワイド文字型と呼ばれて、Unicodeを表現するのに使用されるが、しかし、Windowsではこの型は2バイトなのである。Windows以外では違うようなのだが...
2バイトではUnicode本来のこの世のすべての文字に対応しようとしても無理なため、ロケール(地域)を設定して文字の種類を決める必要があるみたいです。

■ ロケールの設定
▼ 直接、ロケールを文字列として設定する、これでもOK
でも、環境により違ってるとかあるので文字化けする事があるかも。
#include <stdio.h>
#include <locale.h>

int main()
{
	_wsetlocale(LC_ALL, L"jpn");
	return 0;
}

▼ 面倒なので現在のロケールを呼び出す
#include <stdio.h>
#include <locale.h>

int main()
{
	wprintf(L"%s",_wsetlocale(LC_CTYPE, L""));
	return 0;
}

▼ 呼び出してそのまま設定、これが一番楽かも
#include <stdio.h>
#include <locale.h>

int main()
{
	_wsetlocale(LC_ALL, _wsetlocale(LC_CTYPE, L""));
	return 0;
}

実際には、文字や文字列を L"" や、L'' で囲むとワイド文字として表現できて、Unicode対応の関数を使用すればよいだけだけどね。
型名は wchar_t 型、char を入れ替えればそのままOK、ただしマルチバイトなので半角全角の文字列の長さなどの結果が違う可能性がある。
VisualStudioでよく見る _T() と _TEXT() は、Unicode対応の場合には自動でL""を付ける動作をするマクロで、
マルチバイトとシングルバイト両方に対応するためにあるっぽいけど、こんなに色々な文字が現れると正直混乱する。

■ Unicode対応の命令
今までの命令を次のように変換すれば動作する。
puts    →  _putws
printf  →  wprintf
sprintf →  swprintf
wchar.hが必要
strlen  →  wcslen
string.hが必要
strcpy  →  wcscpy  
strcat  →  wcscat
strcmp  →  wcscmp
strtok  →  wcstok
strstr  →  wcsstr
wc や ws に str を変換するとUnicode対応命令になるみたい。

▼ まずは、一文字を表示
先頭にLを付ける、L'a'により、ワイド文字に変換されます
#include <stdio.h>
#include <locale.h>

int main()
{
	_wsetlocale(LC_ALL, _wsetlocale(LC_CTYPE, L""));
	wchar_t wc = L'a';
	wprintf(L"%c", wc);
	return 0;
}

▼ 文字列の表示
この場合には定数になるのでconstをつけないとコンパイラに怒られます
#include <stdio.h>
#include <locale.h>

int main()
{
	_wsetlocale(LC_ALL, _wsetlocale(LC_CTYPE, L""));
	const wchar_t* wstr = L"test";
	wprintf(L"%s", wstr);
	return 0;
}

▼ 文字列の表示2
ワイド文字の配列を初期化して代入しているので普通に文字列ですね。
#include <stdio.h>
#include <locale.h>

int main()
{
	_wsetlocale(LC_ALL, _wsetlocale(LC_CTYPE, L""));
	wchar_t wstr[] = L"test";
	wprintf(L"%s", wstr);
	return 0;
}

▼ 文字の長さを取得する
wchar_tのstrlenに相当するのが、wcslen です。
wchar.hをインクルードする必要があります。
#include <stdio.h>
#include <locale.h>
#include <wchar.h>

int main()
{
	_wsetlocale(LC_ALL, _wsetlocale(LC_CTYPE, L""));
	wchar_t wstr[] = L"こんにちはabc";
	wprintf(L"%d",wcslen(wstr));
	return 0;
}
実行結果は8になります。
シングルバイト文字列と関数 strlen だと13文字になるので違いますね。

Unicodeといっても、ただ単にバイト数が増えたnull終端文字列ですので、次のように書き換えても同じ結果が返ります。
#include <stdio.h>
#include <locale.h>

int i;

int main()
{
	_wsetlocale(LC_ALL, _wsetlocale(LC_CTYPE, L""));
	wchar_t wstr[] = L"こんにちはabc";
	while (wstr[i]) {
		i++;
	}
	wprintf(L"%d",i);
	return 0;
}

■ 文字列の変換
絶対に避けては通れないのが、シングルバイト→マルチバイト→シングルバイト の変換ですよね。
この処理はロケールの設定に依存するので間違っているとぐちゃぐちゃな文字列になります。
▼ シングルバイト文字列→マルチバイト文字列に変換
#include <stdio.h>
#include <stdlib.h>
#include <locale.h>

int main()
{
	_wsetlocale(LC_ALL, _wsetlocale(LC_CTYPE, L""));
	wchar_t wstr[128];
	char*str =  "こんにちはabc";
	mbstowcs(wstr, str, sizeof(wstr)/sizeof(wstr[0]));
	wprintf(L"%s",wstr);
	return 0;
}

▼ マルチバイト文字列→シングルバイト文字列
#include <stdio.h>
#include <stdlib.h>
#include <locale.h>

int main()
{
	_wsetlocale(LC_ALL, _wsetlocale(LC_CTYPE, L""));
	wchar_t*wstr = L"こんにちはabc";
	char str[128];
	wcstombs(str, wstr, sizeof(str)/sizeof(str[0]));
	printf("%s",str);
	return 0;
}

■ サロゲートペア
UTF-16の2バイトですべての文字を表現するという夢は実現が無理そうなので対応できない文字を次の2バイトを使って対応しようってシロモノです。
ASCII文字とSHIFT JISみたいなものか。

前半 D800DBFF と後半 DC00DFFF とかややこしい事になるので割愛
0xD8000xDBFFの範囲(= この数も含む)に値が入っていれば次の2バイトとワンセットの一文字としてカウントするようです。
コードで書くと、if(0xD800<=c && 0xDBFF>=c) で判定し、もう一文字分ポインタ(2バイト)を進めればOK


■ 異体字セレクタ
似て異なる文字を表現しようとしても4バイトでは足りないということになって、異体字セレクタなるものが追加されています。
異体字セレクタとはいわゆる枝番、基底文字の次に特定の値の範囲が出現したら枝番が付加されているということです。

IVSでは U+E0100〜U+E01EF が異体字セレクタ(4バイト)
UTF16では21ビットの場合には規則にのっとり符号化する必要があるそうで(16bitではそんなルールは無い)そのままでは16進数として扱えない。

▼21ビットでの符号化のルール
z-zzzz yyyy-yyyy xxxx-xxxx → 1101-10ww-wwyy-yyyy 1101-11yy-xxxx-xxxx
(wwww = zzzzz - 1) wwwwはzzzzzをマイナス1をする必要がある

▼符号化による16進数への変換
E0100を2進数で表すと 0000 1110 0000 0001 0000 0000
01110から-1をすると1101
符号化に従って組み立てると 1101-1011-0100-0000 1101-1101-0000-0000
2進数を16進数に変換すると、 DB40 DD00 になります。

同様にE01EFを2進数で表すと 0000 1110 0000 0001 1110 1111
01110から-1をすると1101
符号化に従って組み立てると 1101-1011-0100-0000 1101-1101-1110-1111
2進数を16進数に変換すると、 DB40 DDEF になります。

▼IVSの判定
16進数の値を見ると先頭の0xDB40は共通なので、文字の次に0xDB40が出現したら次の文字はDD00〜DDEFの範囲を持つ枝番ということになります。
ということは、もう一文字分ポインタ(2バイト)を進めればOKとなります。
よくよく見ると、通常文字では0xDB40はサロゲートペアの範囲に含まれるので、最初に異体字セレクタの判断をしてポインタを進めて、その後、サロゲートペアの判断と文字カウントをすればよいということになりますね。
コードで書くと、if(0xDB40 == c) で判定し、もう一文字分ポインタ(2バイト)を進めればOKでしょうか。


■ IVS異体字セレクタとサロゲートペアに対応した文字数カウント
上記で調べた事を頼りに文字数カウント関数を作成しました。
int test_strlen(wchar_t*wstr){
	int i=0;
	while(*wstr){
		if(0xDB40 == *wstr) {//IVS異体字セレクタを読み飛ばす
			if(*++wstr) wstr++;
		}else{
			if(0xD800<=*wstr && 0xDBFF>=*wstr) {//サロゲートペアは一文字進める
				wstr++;
			}
			if(*wstr){
				wstr++;
				i++;//文字のカウント
			}
		}
	}
	return i;
}


■ Unicodeと今までのマルチバイト文字セット両対応
両対応するために、マクロを使用しており、変数型や関数などもコンパイル時にゴリゴリと書き換えられます。
文字列を扱う部分では必ず用意されたマクロを使用する必要があります。
関数でも変数型でもデータの型でもマクロを使用します。

Unicodeを使うか使わないかは、VisualC++プロジェクトのプロパティ→構成プロパティ→全般→文字セット
→Unicode文字セットを使用する
→マルチバイト文字セットを使用する

により切り替えることができる。

▼ 切り替えの仕組み
WinNT.hを見ると

#ifdef UNICODE
typedef WCHAR TCHAR
#else
typedef char TCHAR
#endif


つまり、UNICODEが定義されている場合には
TCHARはWCHARに置き換えられ、定義されていない場合にはTCHARはcharになる。
ちなみに
typedef wchar_t WCHAR;
WCHAR は wchar_t に変換される。

▼ リテラル文字の変換
開発環境の設定により、マルチバイトプロジェクトなら何もしない、UnicodeプロジェクトだとUnicodeに変換される、_TEXT _T のマクロを使用する必要がある。
#include <stdio.h>
#include <tchar.h>
#include <locale.h>

int main()
{
	TCHAR*str1 = _T("aaaa");
	TCHAR c1 = _T('a');
	TCHAR*str2 = _TEXT("aaaa");
	TCHAR c2 = _TEXT('a');
	return 0;
}

▼ 関数の切り替え
char型の代わりにTCHARを使うと、マクロにより自動的にUnicode(wchar_t)とマルチバイト(char)の変数型が切り替えられる。
そのため、TCHARを使った場合にはTCHAR用の文字列操作関数を利用する必要があります。
詳細はtchar.hを見ると書かれている。
標準関数   対応関数
printf     _tprintf    
sprintf    _stprintf   
scanf      _tscanf     
fgetc      _fgettc     
fgets      _fgetts     
fputc      _fputtc     
fputs      _fputts     
getc       _gettc      
getchar    _gettchar   
gets       _getts      
gets       _getts      
putc       _puttc      
puts       _putts      
strtod     _tcstod     
strtol     _tcstol     
strtoul    _tcstoul    
atof       _tstof      
atol       _tstol      
atoi       _tstoi      
atoi       _ttoi       
atol       _ttol       
strcat     _tcscat     
strcpy     _tcscpy     
strlen     _tcslen     
system     _tsystem    
fopen      _tfopen     

■ まとめ
Unicodeとマルチバイトの両方を対応するには、charを使う部分をTCHARで書いて、文字列や文字を_TEXT("")や_T("")で囲み、文字列操作はTCHAR用の関数を使う。
アプリ作成に必要なAPIも実はマクロであり、プロジェクトのプロパティの設定にしたがって自動的に切り替わるようです。

文字数のカウントはUnicodeとマルチバイト(全角半角)で差異が発生するので注意が必要ということみたいです。
サロゲートペアと異体字セレクタの注意も必要ですね。


▲トップページ > プログラミングの実験