作曲・指導・C言語・Linux

金沢音楽制作

金沢音楽制作では、楽曲・楽譜の制作と、作曲や写譜などレッスンを行っています。

扱いが少し面倒なワイド文字

C99から、ワイド文字に対応したwchar_t型と接頭辞Lが追加されました。ワイド文字を扱うには、ワイド文字の入出力と文字列操作に関するwchar.hとロケールを扱うlocale.hの2つが必要です。

ワイド文字は、一文字あたりのバイト数が固定された文字です。日本語等を含む文字列操作の際に利用します。しかし、ロケールの設定が必須だったり、ストリームの指向を意識する必要があるなど、注意点も多くあります。

『Cクイックリファレンス』10頁

マルチバイト文字列とワイド文字列

C言語の文字列は、ASCII文字に日本語といったそれ以外の文字を混ぜると、1バイトの文字と1バイト以上の文字の混合、つまりマルチバイト文字列になります。日本語一文字が何バイトになるかは環境依存です。出力した文字数(バイト数0)を返すprintf()を使って確認してみます。

#include <stdio.h>

int main(void)
{
  int n = printf("こんにちは\n");
  printf("%d byte\n", n);

  return 0;
}
$ ./a.out
こんにちは
16 byte

筆者の環境だと、こんにちはで16バイトが返って来ました。文字数は改行を含めて6文字になるはずですが、全く違う数字です。16バイトの内訳は、日本語一文字がUTF-8で3バイト、改行コードの\nがASCIIで1バイトで、合計16バイトになったのだと思います。Windowsだと、Shift-JISで日本語の各文字が2バイトで、11バイトになるかも知れません。

というわけで、正しく文字数を取得するために、ワイド文字用のwchar_t型を使います。wchar_t型は、16bit幅か32bit幅のUnicodeで実装されていることが多いらしいですが、これも環境依存になります(10頁)。sizeofを使って確認してみましょう。

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

int main(void)
{
  setlocale(LC_ALL, "ja_JP");
  
  wchar_t ws[] = L"こんにちは\n";
  int n = wprintf(L"%ls", ws);

  wprintf(L"count: %ld\n", n);
  wprintf(L"sizeof ws: %ld\n", sizeof ws);
  wprintf(L"sizeof(wchar_t): %ld\n", sizeof(wchar_t));

  return 0;
}
$ ./a.out
こんにちは
count: 6
sizeof ws: 28
sizeof(wchar_t): 4

改行文字\nを含んだ6文字が正しくカウントされ、配列のサイズはヌル文字を含んで28バイトになりました。筆者の環境では、一文字あたり4バイトのUTF-32になったのだと思います。

ワイド文字列の使い所

ワイド文字列は、文字列操作を行いたい時に使います。たとえば、文字数を正しくカウントしたりwhile文で一文字ずつ表示したい場合には、一文字のバイト数が固定されたワイド文字列を利用します。標準出入力するだけならマルチバイト文字列でも問題ないと思います。

次の例は、文字列を逆順にしてかつ文字コードを表示するものです。

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

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

  wchar_t *ws = L"こんにちは 世界";
  wchar_t *wp = ws;

  while (*++ws != L'\0');

  while (--ws >= wp) {
    wprintf(L"%lc: \\x%04lx", *ws, *ws);
    putwc(L'\n', stdout);
  }

  return 0;
}
$ ./a.out
界: \x754c
世: \x4e16
 : \x0020
は: \x306f
ち: \x3061
に: \x306b
ん: \x3093
こ: \x3053

このような処理は、ワイド文字列を使うことで簡単に書くことができます。しかし、いくつかの注意点もあります。たとえば、関数の先頭にあるsetlocale()関数や、改行を追加するのにputc()ではなく、わざわざワイド文字用のputwc()を使っている点です。これらは、次項以降で詳しく見ていきます。

ロケールを設定して出力する

ワイド文字を扱うには、ロケールの設定が必須です。ロケールの設定は、ヘッダファイルlocale.hを読み込みsetlocale()を使います。

setlocale()の第1引数には、文字や通貨、日付といったロケール全体を対象にするLC_ALLを入力します。第2引数には、対象とする言語と地域を入力します。日本語なら"ja_JP"です。""と省略した場合はシステムからロケールを取得します。

ロケールを設定した状態で、ワイド文字を標準出力するwprintf()を使ってみます。第1引数の先頭にもLを付けます。戻り値は、出力した文字数です。

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

int main()
{
  // ロケールをセット
  setlocale(LC_ALL, "ja_JP");

  // L"文字列"と先頭に必ずLを付ける
  int n = wprintf(L"こんにちは 世界\n");

  wprintf(L"count: %ld\n", n);

  return 0;
}
$ ./a.out
こんにちは 世界
count: 8

文字列が正しく表示され、wprintf()の戻り値も正しいことが分かります。

なお、wprintf()の書式指定子は、printf()の書式指定子の前に長さ修飾子のlを付与します。この場合の%ldlong型ではなく、int型になります。つまり、longにしたい場合は、%lldとします。

次のコードはロケールをセットせずにwprintfを使った例です。文字列は表示されず、戻り値もめちゃくちゃになっています。

#include <wchar.h>

int main()
{
  int n = wprintf(L"こんにちは 世界\n");

  // wprintf()の戻り値は出力した文字数
  wprintf(L"count: %ld\n", n);

  return 0;
}
$ ./a.out
count: 4294967295

というわけでロケールを設定は必ず行います。

ワイド文字を操作する

<wchar.h>に、ワイド文字で扱う型や入出力・文字列操作の関数が用意されています。また、ワイド文字定数あるいはワイド文字リテラルの前には、接頭辞のLを必ず付けます。LはLargeの頭文字だと思います(Printz『C Poker Reference』6頁)。Lを付けないとコンパイルエラーになります。

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

int main()
{
  // ロケールのセットは必須
  setlocale(LC_ALL, "ja_JP");

  // ワイド文字の前にLを付ける
  wchar_t *wc = L"こんにちは 世界";
  wchar_t kuten = L'。';

  const int size = 256;
  wchar_t buf[size];

  // ワイド文字用のsprintf()
  swprintf(buf, size, L"%ls%lc", wc, kuten);

  // ワイド文字用のstrlen()
  int len = wcslen(buf);

  // ワイド文字用のprintf()
  wprintf(L"%ls: %ld字\n", buf, len);

  return 0;
}
$ ./a.out
こんにちは 世界。: 9字

他にも、char型からwchar_t型に変換するmbrtowc()や標準入力するfgetws()などがあります。IBMの「wchar.h」を見ると、見慣れたような関数が用意されているのが分かると思います。

コマンドライン入力をワイド文字に

コマンドの引数として受け取った文字列(argv[])をワイド文字として扱いたい場合は、マルチバイト文字からワイド文字に変換します。変換は、stdlib.hmbstowcs()を使います。

『Cクイックリファレンス』499頁

mbstowcs()の第1引数には、事前に宣言しておいた、変換後の文字列を格納するアドレスを指定します。第2引数には、マルチバイト文字列のアドレス、今回はargv[]を指定します。第3引数には、サイズを指定します。

変換後の文字列を格納する領域は、必要な文字数にsizeof(wchar_t)を掛ければよいと思います。

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

int main(int argc, char *argv[])
{
  setlocale(LC_ALL, "ja_JP");

  for (int i = 1; i < argc; i++) {
    // ワイド文字を格納する配列のサイズ
    const int Size = sizeof(wchar_t) * 100;
    wchar_t ws[Size];

    // マルチバイト文字からワイド文字に変換
    mbstowcs(ws, argv[i], Size);

    int n = wprintf(L"%ls",ws);

    wprintf(L": %ld字\n", n);
  }

  return 0;
}
$ ./a.out hello 世界 'wide文字 面倒'
hello: 5字
世界: 2字
wide文字 面倒: 9字

ストリームの指向は最初に決まる

C言語のストリームには、「バイト指向ストリーム」と「ワイド指向ストリーム」があります。ファイルを開いた直後のストリームは指向が未定ですが、最初に呼ばれた出入力関数に合わせてストリームが設定されます。そして、設定されたストリームに合わない入出力関数は未定義動作になります。

『Cクイックリファレンス』208頁

どういうことかと言うと、最初に呼ばれた出入力関数がprintf()だった場合、以降のストリームは「バイト指向ストリーム」に設定され、以降に「ワイド指向ストリーム」のwfprintf()を使うと未定義動作になります。逆も同じです。次のコードは筆者の環境では動作しましたが、偶然です。

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

int main()
{
  // バイト指向ストリームが設定される
  printf("hello, world\n"); 

  setlocale(LC_ALL, "ja_JP");

  // ワイド指向ストリームは未定義動作となる
  wprintf(L"こんにちは 世界\n");

  return 0;
}

fwide()でストリームの指向を確認してみます。fwide()は、ストリームをバイト指向かワイド指向かをファイルの最初に「一度だけ」決定する関数です。戻り値は、現在のストリームの指向を返します。

『Cクイックリファレンス』435頁

第2引数の値と戻り値
引数の値 動作 戻り値の意味
mode > 0 ワイド指向に変更する ファイルはワイド指向
mode < 0 バイト指向に変更する ファイルはバイト指向
mode = 0 ストリームの指向を変更しない ファイルは指向がない
#include <stdio.h>
#include <locale.h>

int main()
{
  int mode;

  // まだ指向はない
  mode = fwide(stdout, 0);
  printf("mode: %d\n", mode);

  // printf()を呼び出してバイト指向に
  mode = fwide(stdout, 0);
  printf("mode: %d\n", mode);

  // fwide()で指向の変更はできない
  mode = fwide(stdout, 1);
  printf("mode: %d\n", mode);

  return 0;
}
$ ./a.out
mode: 0
mode: -1
mode: -1

最初の戻り値が0なので指向がありませんが、printfが実行されたことでストリームがバイト指向に決定され、戻り値が-1になりました。その後、fwide()でストリームをワイド指向に変更しようとしましたが、-1が返ってきたので、バイト指向のままだということが分かります。

ストリームの指向を切り替える

freopen()を使って、指向を未定に戻すことができます。第1引数はファイル名ですが、NULLにすることで元ファイルに結びつきます。第2引数は、モードで今回は"w"です。第3引数は、stdinstdoutstderrにリダイレクトします。

『Cクイックリファレンス』427頁

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

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

  int mode;

  // バイト指向に設定
  mode = fwide(stdout, -1);
  printf("mode: %d\n", mode);

  // freopenで指向を未定に戻す
  freopen(NULL, "w", stdout);

  // 現在の指向を確認
  mode = fwide(stdout, 0);
  wprintf(L"mode: %ld\n", mode);

  // 前にwprintf()を呼び出したのでワイド指向に
  mode = fwide(stdout, 0);
  wprintf(L"mode: %ld\n", mode);

  return 0;
}
$ ./a.out
mode: -1
mode: 0
mode: 1

バイト指向ストリームが、freopen()後に、未指定に戻り、その後wprintf()が呼ばれたことで、ワイド指向ストリームになっていることが分かります。

更新情報