VC++での日本語文字の扱い

デフォルトではシフトJIS

Visual StudioでVisual C++の空のプロジェクトを作成し、下記のようなソースを書いて実行してみます。

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

int main(void)
{
    char s[] = "あいうえお";
    int len = strlen(s);

    printf("%s\n", s);
    printf("length = %d\n", len);
    for (int i = 0; i < len; i++) {
        unsigned char b = (unsigned char)s[i];
        printf("%02X ", b);
    }
    printf("\n");

    return 0;
}

【実行結果】

あいうえお
length = 10
82 A0 82 A2 82 A4 82 A6 82 A8

日本語文字が1文字2バイトで扱われており、'あ' に対応するコードが 82A0 であることから、シフトJISであることがわかります。また、ソースファイルを秀丸VSCodeのような文字コードを確認できるテキストエディタで開いてみると、シフトJISであることがわかります。そして、コンソール上に正しく ”あいうえお” と表示できていることからコンソール出力もシフトJISであることがわかります。

つまり、デフォルトではソースファイル、実行バイナリ、コンソール出力ともシフトJISになっています。

ソースファイルをUTF-8で保存

秀丸などのテキストエディタでソースファイルをUTF-8で保存しなおします。するとビルドエラーが発生します。どうも Visual Studio はデフォルトではBOM無しのUTF-8を自動認識できないようです。BOM付きUTF-8で保存してやると、ビルドが成功します。ところが実行結果を見ると先ほどと変化がなく、あいかわらずシフトJISになっています。つまり、ソースファイルをUTF-8にしてもビルド時にはシフトJISに変換されるようです。

実行バイナリをUTF-8でビルド

Visual Studioでプロジェクトの「プロパティ」→「構成プロパティ」→「C/C++」→「コマンドライン」で「追加のオプション」に下記を設定すると、ソースファイルも実行バイナリもUTF-8で扱われます。こうすればソースファイルはBOM無しのUTF-8でも大丈夫です。

/utf-8

【実行結果】

縺ゅ>縺・∴縺・
length = 15
E3 81 82 E3 81 84 E3 81 86 E3 81 88 E3 81 8A

日本語文字が1文字3バイトで扱われており、'あ' に対応するコードが E38182 であることから、UTF-8であることがわかります。いっぽうで、コンソール出力は文字化けしており、特徴的な '縺' (シフトJISで E381) が出現しています。このことからコンソール出力は依然としてシフトJISであることがわかります。

コンソールをUTF-8に設定

コンソール出力をUTF-8に設定するには、下記のように SetConsoleOutputCP関数を使います。

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

int main(void)
{
    SetConsoleOutputCP(CP_UTF8);

    char s[] = "あいうえお";
    (以下同文)

【実行結果】

あいうえお
length = 15
E3 81 82 E3 81 84 E3 81 86 E3 81 88 E3 81 8A

ワイド文字 (UTF-16) で扱う

ここまでは文字をchar型で扱ってきました。char型では日本語文字はマルチバイト文字として扱われ、シフトJISでは1文字2バイト、UTF-8では1文字3バイトになります(※)。strlen関数は単純に終端文字までのバイト数を数えるので、日本語の文字数を正しく数えることはできません。

これに対し、wchar_t 型は1文字2バイトのワイド文字を扱います。wchar_t 型では英数字も日本語文字も等しく1文字2バイトになります(※)

※ 絵文字や特殊な漢字など、ごく一部の文字は1文字4バイトになりますが、この記事では扱いません。

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

int main()
{
    setlocale(LC_ALL, "");

    wchar_t ws[] = L"あいうえお";
    int len = wcslen(ws);

    wprintf(L"%ls\n", ws);
    printf("length = %d\n", len);
    for (int i = 0; i < len; i++) {
        unsigned short w = (unsigned short)ws[i];
        printf("%04X ", w);
    }
    printf("\n");

    return 0;
}

【実行結果】

あいうえお
length = 5
3042 3044 3046 3048 304A

1文字2バイトで扱われており、'あ' に対応するコードが 3042 であることから、UTF-16であることがわかります。また、wcslen関数は文字数を正しく数えています。L"あいうえお" のようにワイド文字列リテラルには L を付けることに注意してください。ソースファイルがシフトJISであるかUTF-8であるかによらず、ワイド文字リテラルはビルド時にUTF-16に変換されます。

TCHAR型とは?

TCHAR型はUnicode環境(ワイド文字対応環境)とUnicode非対応環境とのソース互換のためにある型で、ビルド時にwchar_t型かchar型かのいずれかになります。_tcsnlen関数などの_tcs系関数は、ビルド時にwcs系関数かstr系関数かになります。Unicode環境で下記のソースをビルドすると、実行結果は前述の wchar_t を使った場合と同じになります。

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

int main()
{
    setlocale(LC_ALL, "");

    TCHAR ts[] = _T("あいうえお");
    int len = _tcslen(ts);

    _tprintf(_T("%s\n"), ts);
    printf("length = %d\n", len);
    for (int i = 0; i < len; i++) {
        unsigned short w = (unsigned short)ts[i];
        printf("%04X ", w);
    }
    printf("\n");

    return 0;
}

歴史的経緯

昔のコンピュータは英数字しか使えず、1バイトのASCIIコードで表していました。そのため C言語では文字を扱うchar型は1バイトでした。やがて日本語文字はシフトJISコードなどを用いてマルチバイト文字として扱うようになりましたが、1文字のバイト数が可変のため不便がありました。その後、世界共通の文字コードとしてUnicodeが生まれました。Unicodeのワイド文字(UTF-16)では世界中のほとんどの文字を2バイトで表します。しかし今さらC言語のchar型を2バイトに変更するわけにはいかず、ワイド文字を扱う型としてwchar_t型が追加されました。