はじめに (対象読者・この記事でわかること)

この記事は、C/C++で日本語を含むマルチバイト文字を扱う際に「なぜか文字化けする」「strcmpの結果が期待と違う」といった現象に悩まされている開発者向けです。
setlocaleという1行を追加すれば直るケースもあれば、むしろ新たな不具合を生むケースもあります。本記事ではsetlocaleが実際に何を変更しているか、マルチスレッド環境でどう振る舞うか、日本語Windows/Linuxで何が異なるかを、具体例と共に解説します。読み終えると、ロケールがもたらす「文字コード」「照合順序」「ライブラリ内部の静的バッファ」を正しく制御できるようになります。

前提知識

  • C言語での文字列リテラルとポインタの基礎
  • マルチバイト文字とUTF-8の違いをある程度知っていること
  • スレッドの生成とjoinの基本(マルチスレッドサンプルのため)

setlocaleとは何か、なぜ「魔法の1行」と呼ばれるのか

setlocaleは標準Cライブラリが提供する関数で、プログラム全体で使われる「ロケール(地域依存の設定)」を切り替えます。
日本語環境でよくある指定は以下の3パターンです。

  1. setlocale(LC_ALL, "ja_JP.UTF-8")
    Linux/macOSでUTF-8日本語を有効化。
  2. setlocale(LC_ALL, "Japanese_Japan.932")
    WindowsでShift_JIS(コードページ932)を有効化。
  3. setlocale(LC_CTYPE, "")
    環境変数(LinuxならLANG、Windowsならシステムデフォルト)に従う。

多くの入門書では「これを最初に呼ばないと日本語が文字化けする」とだけ書かれていますが、実際には以下の3つを同時に変更しているため、後述する「照合順序の変化」「マルチスレッドセーフでない内部バッファ」「Windows特有のコードページ問題」が起きます。

  • 文字コード(マルチバイト→ワイド文字変換ルール)
  • 照合順序(strcoll/wcscollで使われる辞書順)
  • フォーマット(金額の桁区切り、日付の順序)

setlocaleの実際の挙動:コードで見る落とし穴

ステップ1:文字コード変換が成功しても「文字化け」が残るケース

以下のコードをLinux(GCC 13)とWindows(MSVC 2022)でコンパイルして実行します。

C
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <locale.h> int main(void) { const char *src = "日本語"; setlocale(LC_CTYPE, ""); /* 環境依存 */ wchar_t wbuf[32]; int len = mbstowcs(wbuf, src, 32); printf("len=%d, 1st wchar=%04X\n", len, (unsigned)wbuf[0]); return 0; }
  • LinuxでLANG=ja_JP.UTF-8のとき:len=3、1st wchar=65E5(「日」)
  • Windowsでchcp 932のとき:len=3、1st wchar=8E9E(「日」のShift_JISをUTF-16に拡張した値)

見かけ上は問題ありませんが、後続のprintfのフォーマットが"%ls"でないとWindowsでは文字化けします。これはWindowsコンソールがUTF-16を期待しているためで、setlocaleだけでは解決しません。

ステップ2:照合順序が変わり、バイナリ一致でなくなる

C
setlocale(LC_COLLATE, "ja_JP.UTF-8"); printf("strcmp(ぁ, あ) = %d\n", strcmp("ぁ", "あ")); /* 負の値 */ printf("strcoll(ぁ, あ) = %d\n", strcoll("ぁ", "あ")); /* 0以上 */

UTF-8のバイト列だと「ぁ」(E3 81 81)は「あ」(E3 81 82)より小さいためstrcmpは負ですが、strcollは仮名の無圏・有圏を無視するため0以上を返します。
ソートアルゴリズムをstrcmpで書いたままsetlocaleを呼ぶと、想定外の順序になることがあります。

ステップ3:マルチスレッドでsetlocaleを呼ぶと競合する

setlocaleはプロセス全体のグローバル状態を変更します。以下のコードを実行すると、スレッドごとに異なるロケールを設定しようとしてデータ競合が起きます。

C
#include <pthread.h> #include <locale.h> #include <stdio.h> void *worker(void *arg) { int id = *(int*)arg; if (id & 1) setlocale(LC_ALL, "C"); else setlocale(LC_ALL, "ja_JP.UTF-8"); return NULL; } int main(void) { pthread_t t[2]; int id[2] = {0, 1}; for (int i = 0; i < 2; ++i) pthread_create(&t[i], NULL, worker, &id[i]); for (int i = 0; i < 2; ++i) pthread_join(t[i], NULL); return 0; }
  • Linux(glibc 2.38)では内部でミューテックスを取っているため遅くなる
  • Windows(MSVC)では公式に「スレッドセーフでない」とされ、クラッシュの可能性あり
    対策としては、setlocaleはmainスレッドで起動時に1回だけ呼ぶか、C11以降のnewlocale/uselocaleを使ってスレッドローカルロケールを利用します。

ハマった点:strftimeの文字コードが環境で変わる

strftime(buf, sizeof(buf), "%a", &tm)は、ロケール依存の短縮曜日名を返します。

  • Linux(ja_JP.UTF-8):「火」→UTF-8のE7 81 AB(3バイト)
  • Windows(Japanese_Japan.932):「火」→Shift_JISの8D(1バイト)

バッファサイズを計算する際に「日本語なら3バイト」と決め打ちすると、Windowsでバッファオーバランします。
回避策は、バッファサイズをstrftimeの戻り値(必要バイト数)に合わせて動的に確保すること、または"%a"の代わりに独自フォーマットを使うことです。

解決策:安全にsetlocaleを使う5つのルール

  1. 呼び出しはプロセス開始直後の1回に留める。
  2. マルチスレッドではuselocaleを使い、グローバル状態を汚さない。
  3. 照合順序が必要な場合のみLC_COLLATEを変更し、それ以外はLC_CTYPEに限定。
  4. Windows/Linuxで文字コードが異なることを忘れず、バッファサイズは動的に確保。
  5. ライブラリがsetlocaleを呼んでいる可能性がある場合は、後で元に戻すlocale_t old = uselocale(new); ... uselocale(old))。

まとめ

本記事では、setlocaleが「文字コード・照合順序・日付フォーマット」を同時に変更すること、マルチスレッドで呼ぶと競合すること、WindowsとLinuxでバイト列が異なることを解説しました。

  • setlocaleはグローバル状態を書き換えるため、呼び出しタイミングを1回に制限する
  • 照合順序が変わるため、strcmp/strcollの使い分けが必須
  • マルチスレッドではuselocaleでスレッドローカルロケールを利用する

この記事を通して、日本語を扱うC/C++アプリケーションで「文字化け」「辞書順の逆転」「クラッシュ」を回避できるようになりました。
次回は、uselocalenewlocaleを使ったスレッドセーフな実装パターンと、WindowsでUTF-8を完全サポートする方法を紹介します。

参考資料

  • The Open Group Base Specifications Issue 7: setlocale
    https://pubs.opengroup.org/onlinepubs/9699919799/functions/setlocale.html
  • Microsoft Learn: setlocale, _wsetlocale
    https://learn.microsoft.com/ja-jp/cpp/c-runtime-library/reference/setlocale-wsetlocale
  • GNU C Library Manual: Locales and Internationalization
    https://www.gnu.org/software/libc/manual/html_node/Locales.html
  • C11標準規格(ISO/IEC 9899:2011)§7.11