CX5 SOFTWARE

Visual C++のUNICODE対応

2009.9.2


目次

1. はじめに
1.1. 「こんにちは世界!」と表示できない??
2. UNICODE対応
3. ではTCHARは何?
4. setlocale
5. まとめ

第1章 はじめに

最近、ひさしぶりにVisual C++でコーディングする機会がありました。ここ数年は、開発するものはほぼすべてWebアプリケーション、言語はJavaばかりになり、まれにクライアントアプリを書く機会があってもやはりJava(SwingやEclipse RCP)で開発していました。

Windows上のC++ということで、Visual C++ 2008 Express Editionをダウンロードし、まず、簡単なコンソールアプリケーションを作成してみようとしてみたのですが、UNICODE辺りでちょっとつまずいたので、まとめてみました。

1.1. 「こんにちは世界!」と表示できない??

Visual C++ 2008で、コンソールアプリケーションをプロジェクトウィザードで選択し、ひな型を生成するとメイン関数は以下のようになります。

1
2
3
4
5
6
#include "stdafx.h"
 
int _tmain(int argc, _TCHAR* argv[])
{
    return 0;
}

一般的なCやC++の本に書かれているメイン関数の定義と少し違うことに気付きます。まずは、関数名が「main」ではなく「_tmain」となっています。次に、2番目の引数の型が「char」ではなく「_TCHAR」となっています。

とりあえず、いつものHello Worldを書いてみます。

1
2
3
4
5
6
7
#include "stdafx.h"
 
int _tmain(int argc, _TCHAR* argv[])
{
    printf("Hello World!\n");
    return 0;
}

問題なく「Hello World!」と表示されます。続けて、日本語で表示してみましょう。

1
2
3
4
5
6
7
#include "stdafx.h"
 
int _tmain(int argc, _TCHAR* argv[])
{
    printf("こんにちは世界!\n");
    return 0;
}

こちらはVisual C++ 2005と2008で結果が異なります。Visual C++ 2005では、何も表示されません。Visual C++ 2008では問題なく表示されます。

この後、_tmainと_TCHARのことは忘れて、Win32 APIを呼び出すようなコードを書きはじめると、コンソールに日本語が出力できないことに気付きます。例えば、以下のようにコマンドライン引数を表示するコードを試してみます。

1
2
3
4
5
6
7
8
9
#include "stdafx.h"
 
int _tmain(int argc, _TCHAR* argv[])
{
    if (argc >= 2) {
        printf("%s\n", argv[1]);
    }
    return 0;
}

プログラムの引数に「Hello」と渡しても表示されるのはなぜか「H」だけ、ましてや日本語は完全に文字化けします。どうも、argvのように_TCHARを受け取りprintfで表示しているところで問題が発生しているようです。ですので、_tmainと_TCHARをきちんと調べた方がよさそうです。

第2章 UNICODE対応

_tmainでMSDN Libraryを検索してもあまりたいした情報が得られません。_TCHARで検索すると「Tchar.h における汎用テキストのマッピング」というドキュメントにマッチします。ただ、いきなり細かいことが書いてあり、あまり分かりやすくありません...。

多くの方がご存じだと思いますが、Windowsはだいぶ前(おそらく2000?)から完全にUNICODE化されています。Visual C++もデフォルトとしてUNICODEを用いるようになりました。Visual C++のプロジェクトのプロパティを開くと、以下のようにUNICODEを使用するように設定されていることがわかります。

ややこしいのですが、Windowsの世界ではUNICODEのことをワイド文字(wide character)と呼びます。対して、従来までのシフトJISのことをマルチバイト文字と呼びます。そして、UNICODEをサポートする型や関数は、先頭にwが付いたものとなります。例えば、charはwchar_t、main関数はwmain、printf関数はwprintfになります。ですので、先ほどのソースコードをUNICODE版に書きかえると以下のようになります。

1
2
3
4
5
6
7
8
9
#include "stdafx.h"
 
int wmain(int argc, wchar_t* argv[])
{
    if (argc >= 2) {
        wprintf("%s\n", argv[1]);
    }
    return 0;
}

ただし、これをコンパイルするとwprintfの第一引数のところでconst char*がconst wchar_t*に変換できないとのエラーになります。これは、ダブルクォーテーションで囲った文字列がchar型の文字列定数になるためです。UNICODEの場合wchar_t型の文字列定数を用いなければなりません。それには先頭にLを付ける必要があります。つまり、以下のようになります。

1
2
3
4
5
6
7
8
9
#include "stdafx.h"
 
int wmain(int argc, wchar_t* argv[])
{
    if (argc >= 2) {
        wprintf(L"%s\n", argv[1]);
    }
    return 0;
}

このソースコードをコンパイルし、先ほどと同じようにコマンドライン引数に「Hello」や日本語を指定して実行してみると、今度は正しく表示されます。

第3章 ではTCHARは何?

Visual C++がUNICODE版のバイナリを生成することはわかりましたが、では、Visual C++のプロジェクトウィザードが生成するソースコードの_tmainと_TCHARは何なのでしょうか。実は、「_T」で始まる型や関数は、コンパイルオプションによりワイド文字列版(UNICODE版)になったり、マルチバイト版(シフトJIS版)になったりするマクロ群です。例えば、_TCHARは、UNICODEを使用するとコンパイルオプションに指定した場合wchar_tになります、そして、マルチバイトを使用すると指定した場合は、charになります。これらの「_T」ではじまるマクロはtchar.hにて定義されています。

では、先ほどのコードをtchar.hで定義されるマクロで書きなおすと以下のようになります。

1
2
3
4
5
6
7
8
9
#include "stdafx.h"
 
int _tmain(int argc, _TCHAR* argv[])
{
    if (argc >= 2) {
        _tprintf(_T("%s\n"), argv[1]);
    }
    return 0;
}

wmainは_tmainに、wprintfは_tprintfに、UNICODE文字定数(L"")は、_T("")に書き換えます。先ほども触れたように、「_T」ではじまる型や関数はすべてマクロです。このため、これらはすべてプリプロセッサにより、wmain、wprintf、L""に変換されます。

ですので、注意しなければならないのは、コンパイルエラーになった場合はwchar_tやwprintfとエラーメッセージに表示されます。

データ型、関数名がtchar.hでどのように定義されているかは、MSDN Libraryの「汎用テキスト マップ」にまとまっています。

第4章 setlocale

これで問題なしと思っていると、次は、ファイルI/Oを行うと正しく文字列が出力されないという問題に直面します。例えば、以下のようなコードです。

1
2
3
4
5
6
7
8
9
#include "stdafx.h"
 
int _tmain(int argc, _TCHAR* argv[])
{
    FILE *fp = _tfopen(_T("a.txt"), _T("w"));
    _ftprintf(fp, _T("こんにちは世界\n"));
    fclose(fp);
    return 0;
}

期待されるのはファイル「a.txt」に「こんにちは世界」という文字列が出力されることですが、試してみると何も出力されていません。きちんとUNICODE版のfprintfを使っていますし、いったい何が問題なのでしょう。

実は、この問題は、先ほど少し触れたVisual C++ 2005でprintfでコンソールに文字列が出力できないという問題と同じ原因です。C++にはsetlocaleという関数で、プログラムのロケール、つまり言語を指定することができます。ただ、ここで問題なのがデフォルトのロケールがなぜか"C"つまりANSIコードになっているのです。Javaであれば、デフォルトエンコーディングは自動的にOSの言語指定から設定されます。例えば、日本語Windowsであれば、シフトJIS(MS932)が選択されます。これが、なぜかVisual C++の場合、デフォルトは"C"になっています。Visual C++ 2005でコンソールに日本語が出力されないのは、このためのようです。はっきりとした情報はありませんが、Visual C++ 2008では、コンソールのみOSの言語指定に従ってロケールが設定され、結果として、正しく日本語が出力されるようです。

では、setlocaleのUNICODE版である_tsetlocaleを使って正しくロケールを指定してみましょう。まずは、OSのデフォルトロケールを指定する方法です。

1
2
3
4
5
6
7
8
9
10
11
#include "stdafx.h"
#include <locale.h>
 
int _tmain(int argc, _TCHAR* argv[])
{
    _tsetlocale(LC_ALL, _T(""));
    FILE *fp = _tfopen(_T("a.txt"), _T("w"));
    _ftprintf(fp, _T("こんにちは世界\n"));
    fclose(fp);
    return 0;
}

setlocale関数を使うためには、ヘッダ「locale.h」をインクルードする必要があります。上記では、そのままソースコードに記述しましたが、stdafx.hに追加する方が毎回コンパイルされることがなくなり、コンパイル速度が速くなります。

setlocale関数の第二引数に「""」を指定すると、OSのデフォルトロケールが指定されます。例えば、日本語に設定されている場合「Japanese_Japan.932」が設定されます。当然ながら、OSの設定が変わると、それに合わせて変更されますので注意が必要です。

もし、常に日本語を選択したいのならば、以下のようにロケールを明記します。

1
2
3
4
5
6
7
8
9
10
11
#include "stdafx.h"
#include <locale.h>
 
int _tmain(int argc, _TCHAR* argv[])
{
    _tsetlocale(LC_ALL, _T("Japanese_Japan.932"));
    FILE *fp = _tfopen(_T("a.txt"), _T("w"));
    _ftprintf(fp, _T("こんにちは世界\n"));
    fclose(fp);
    return 0;
}

Visual C++ 2005においてコンソールに日本語が出力されない問題も、setlocaleを指定すれば解消します。

第5章 まとめ

最後に、注意として、データ型として_TCHARを用いた場合、UNICODEなのかマルチバイトなのかによってメモリ上に確保されるバイト数が異なります。UNICODEの場合は2バイト、マルチバイトの場合は1バイトになります。ですので、「_TCHAR t[10]」と宣言した場合、UNICODEの場合は20バイト、マルチバイトの場合10バイト確保されます。このデータをコピーするメモリをmallocで確保する場合は、単純に「malloc(10)」とできなくなります。代わりに「malloc(10 * sizeof(TCHAR)」とする必要があります。


CX5 SOFTWARE, 2010