● 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みたいなものか。
前半 D800 〜 DBFF と後半 DC00 〜 DFFF とかややこしい事になるので割愛
0xD800 〜 0xDBFFの範囲(= この数も含む)に値が入っていれば次の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とマルチバイト(全角半角)で差異が発生するので注意が必要ということみたいです。
サロゲートペアと異体字セレクタの注意も必要ですね。
▲トップページ
>
プログラミングの実験