Cにおける文字コードの扱い




概要

C言語で文字を扱う際には、 各文字に異なる整数値を割り当てた「文字コード」を用います。 これによって、 実際には整数型の一種である char 型に文字を格納できますし(実際は対応する整数値を格納している)、 整数と同じ比較演算子を用いて文字同士の辞書順比較を行うことができます。

Cでは元々、アルファベット圏の文字を想定しているため、 任意の一文字を char型(1バイト)で表すことにしています。 Cの仕様として文字コードまで決められてはいませんが、 今日では一般的にASCIIが使われます。

しかし、これでは256種類の文字しか区別できず、 日本語のように多数の文字を含む言語が扱えません。 このため、複数バイトで1文字を表す方法が利用されます。 このような「日本語を扱える文字コード」は日常的に多用されていますが、 歴史的な経緯から、JIS, EUC, Shift_JIS, UTF-8など、 いくつかの異なる文字コードが存在しています。 それぞれ基本的なルールおよび個々の文字の割当が異なるため、 実際のデータと異なる文字コードで処理しようとすると、 処理の間違いや「文字化け」といった問題を引き起こします。

最近のC言語の仕様にはこのような「マルチバイト文字」を扱うための wchar_t型が追加されていますが、 内部で使われる文字コードが環境依存であるため、 実用上は不十分です。 さらに最近追加された char16_t などではUTFを扱うことができますが、 現状ではUTF以外の文字コードを用いたテキストデータが大量に存在するため、 やはり不十分なことが多いです。 したがって、 一般のテキストデータを扱うプログラムを作成する際には、 特定の処理系で用意された特定の文字コード用関数を用いるか、 各バイトを直接処理するコードを自作するかになります。 前者の場合はプログラミングが楽ですが、 他のOS・コンパイラではコンパイルできない可能性があります。 後者は環境に依存しないプログラムを作成できますが、 文字コードの仕様を理解する必要があります。

なお、文字コード間の変換を行うツールはフリーウェアなどで多数存在しますので、 「文字コードEUCで処理するプログラムを書いたがデータがShift_JISだった」 といった場合には、 先にデータをツールで変換しておく方法もあります。


Cで日本語文字を扱う場合の問題

Cでプログラムを書いていて、日本語を使いたくなることはよくあります。 このとき、 どの程度のことをしたいかにより、 どこまで難しいことが必要か異なってきます。

  1. 日本語でコメントを書きたい
    コンパイラはコメント内を無視するので、 通常、コンパイルと実行で問題は発生しません。 Unix, Windows間でソースファイルを移動した場合など、 ツールで文字コード変換をしなければ エディタ上では文字化けする可能性が高いですが、 一般にコンパイル・実行は可能です。
  2. メッセージなどを日本語で出力したい
    ダブルクォーテーション内に日本語文字を含んでいても、 「ヌル文字('\0')で終了するバイト列」としては扱えます。 したがって、そのまま printf 関数で出力する程度なら問題ありません。 たとえば、 printf("日本語\n"); といったコードなら大丈夫です。
    ただし、ソースファイルの文字コードが、 使用するコンパイラの想定するものになっていないと、 コンパイル時にエラーになることがあります。 Unix上ならEUCかUTF-8、 Windows上ならShift_JISかUTF-8になるよう、 エディタ内で文字コードを設定してください。 また、Shift_JISでは文字コードを正しく設定しても、 正しく処理されないことがあります。 これは、一部の日本語文字(「表」など)で2バイト目が バックスラッシュの文字コードと一致しており、 コンパイラが誤認識するためです。
    また、コンパイラが通っても、実行時に文字化けすることがあります。 これはxtermなどのコンソールが想定している文字コードが異なる場合に発生します。 コンソールに合わせてソースの文字コードを変更するか、 コンソール側のメニューで使う文字コードを変更してください。
    UTF-8の場合は、文字化けではありませんが printf で桁数を揃えた結果が崩れることがあります。 たとえば、
    printf("%10s", str);
    などとしたとき、EUCやShift_JISではstrに日本語文字が入っていても 英数字10文字分の幅でレイアウトされますが、 UTF-8では幅が狂ってきます。 これは、 printf で文字列の出力幅を指定したとき、マルチバイト文字を想定せず 単純に指定数値分のバイトを出力しているためです。 ターミナルなどで使う等幅フォントでは、 漢字などの日本語文字は英数字2文字分の幅で表示するようになっており、 EUCやShift_JISでは日本語文字が2バイトなのでちょうど計算が合います。 しかし、UTF-8では日本語文字が3バイトなので、計算にずれが生じます。
  3. 日本語の文字列を変数に格納したい
    char型変数の配列が使えますが、 一文字毎に複数バイトが必要なので、配列のサイズ指定時に注意が必要です。
    たとえば、 "hello world" を格納するにはアルファベット・空白で11文字あるので、 ヌル文字の分も含めて配列サイズ12で格納できます。 しかし、 "こんにちは、世界" なら8文字ですが、各文字毎に複数バイト必要です。 必要なサイズは、EUCやShift_JISなら2x8+1=17、 UTF-8では3x8+1=25になります。 漢字やかなと英数字が混在しているような場合は、 それぞれ区別して数える必要があります。
    ただし、プログラム中で元の文字列が与えられている場合は strlen 関数を使うことで、 日本語文字・英数字を問わず、 その文字列のバイト数を得ることができます。
  4. 日本語の文字列を加工したい
    丸ごとコピーする程度なら strcpy が使えますが、 一文字単位で切り出す、 特定の文字を置き換える、 といった処理を行うなら、 その文字コードに対応した関数を用いるか、 自分で処理を書く必要があります。 授業で説明したような 「char 型ポインタ p をインクリメントしながら一文字ずつ処理する」方法では、 マルチバイト文字の場合は 「ある文字コードの前半を指している」 / 「ある文字コードの後半を指している」 といった状況を作ってしまいます。 また、 *p では1バイト分しか参照できません。 さらに、マルチバイト対応の文字コードの多くはASCIIコードと互換性を持っており、 「英数字は1バイトで表し、ASCIIコードと同じ値を割り当てる」 「日本語文字などは2バイト以上で表す」 という方法をとっています。 このため、英数字部分を従来と同様に処理できる一方、 一般の文字列は1文字1バイトの部分と2バイト以上の部分が混在する形になります。 したがって、「先頭から i 番目の文字」 を見つけたい場合、単純に i を2倍する方法では駄目で、 先頭から順に、 シングルバイト/マルチバイトを区別しながら数えていく必要があります。

ツールを使った文字コード変換

Cのソース中に含まれる日本語文字や、 事前に用意しておくデータファイルについては、 エディタやコード変換プログラムを使うことで、 目的に適した文字コードに変換しておけます。

Unix環境では、標準で nkf コマンドが用意されており、 任意の文字コード間の変換ができます。

Windows上では、OSインストール時に用意されているものはありませんが、 コマンドライン型、 ドラッグドロップ型など、 様々なインタフェースのコード変換プログラムがフリーウェアとして公開されています。 「漢字コード変換 フリーウェア」などとして検索すると、多数見つかりますので、 自分の好みにあったものをインストールしてください。

Emacsでは、開いているファイルの文字コードを変更してから保存することができます。 MeadowなどWindows上にEmacsやその類似品をインストールしている場合も、 同様にしてコード変換をすることができます。 M-x(通常はAltキーを押しながらxキー) を押すとエディタ下部にカーソルが表示されるので、 set-buffer-file-coding-system と入力してリターンキーを押してください。 次に目的の文字コードを入力します(例: euc-japan-unix)。 コード名がわからないときは、 TABキーを押すと使えるコード名の一覧が表示されます。 なお、現在開いているファイルの文字コードは、 画面左下の「J」「E」「S」「U」などの表示によって確認できます。


各文字コードの概略とCでの処理コード例

主要な文字コードについて概略を説明し、 Cで文字単位に切り分けるコード例を示します。

以下の符号化方式の説明はかなり大雑把なものであり、 「英数字と一般的な日本語文字が混在するテキストを処理する」 のに最小限必要なレベルのものです。 半角カナや補助漢字など、やや特殊な文字については省略しています。 自分で使うツールの作成や、 事前に用意したデータファイルしか読み込まないゲームプログラムなどでは十分かと思いますが、 任意のテキストを扱うフリーウェアを作成・公開する、 といった場合は、もっと厳密な文字コードの資料を読んでください。


ASCII

1文字に1バイト(8ビット)を使用し、 英数字・記号などを表すことができます。 Cプログラミングでは char 型で簡単に扱うことができます。 また、 EUCやUTF-8など多くの文字コードはASCIIを含んでおり、 ASCIIに含まれる文字はそのまま同じ1バイトの値で表すことができます。

実際には、ASCIIでは7ビットしか使っておらず、 英数字・記号・制御文字などに0x00〜0x7fが割り当てられています。 0x80〜0xffの内容は環境により異なり、 日本ではカナ(いわゆる「半角カナ」)などに使われます。 マルチバイトの文字コードやプログラムの作り方によっては、 0x80〜0xffの部分が正しく扱えない可能性もあり、 環境に依存せず確実に使えるのは0x00〜0x7fの範囲です。

コード例

    char *str = "Hello, world.";
    char buf[256];
    char *sp = str, *bp = buf;
    while (*sp != '\0'){
        printf("%x ", *sp);
        *bp = '|';
        bp++;
        *bp = *sp;
        bp++;
        sp++;
    }
    *bp = '\0';
    printf("\n%s\n", buf);

実行結果

48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 2e 
|H|e|l|l|o|,| |w|o|r|l|d|.

EUC

主にLinuxなどUnix系OSで使われている文字コードです。 最近は、デフォルトがUTF-8になりつつあります。

日本語文字は2バイトで表現され、 1,2バイト目共に0x80〜0xffの範囲にあります。 したがって、半角カナなどを使わないのであれば、 各バイトについて最上位ビットをチェックして、 0なら英数字、1なら日本語文字と判断できます。 ただし、最上位ビットが1であっても、 それが日本語文字の1,2バイト目どちらかはわかりません。 日本語文字が複数並んでいる場合に、 日本語文字間の切れ目で分割したい場合などには、 文字列の先頭から順に見ていく必要があります (適当な位置で分割して出力すると、 ある日本語文字の1バイト目だけ出力してしまう可能性があり、 その場合文字化けを起こします)。

コード例

   unsigned char *str = "〒123-4567 maison○□12号室";
    unsigned char buf[256];
    unsigned char *sp = str, *bp = buf;
    unsigned char c = 0;
    while (*sp != '\0'){
        printf("%x ", *sp);
        if (*sp & 0x80){ // 2バイト文字の場合
            if (c == 0){ // 1バイト目なら区切り文字を入れてこのバイトを保存
                *bp = '|';
                bp++;
                c = *sp;
            }
            else { // 2バイト目なら、保存した1バイト目と合わせてbufに格納
                *bp = c;
                bp++;
                *bp = *sp;
                bp++;
                c = 0;
            }
        }
        else { // 1バイト文字の場合
            *bp = '|';
            bp++;
            *bp = *sp;
            bp++;
        }
        sp++;
    }
    *bp = '\0';
    printf("\n%s\n", buf);

実行結果

a2 a9 31 32 33 2d 34 35 36 37 20 6d 61 69 73 6f 6e a1 fb a2 a2 31 32 b9 e6 bc bc 
|〒|1|2|3|-|4|5|6|7| |m|a|i|s|o|n|○|□|1|2|号|室

Shift_JIS

8ビットパソコン時代からPCで使われ、 MS-DOS/Windowsで使われている文字コードです。 最近のWindowsでは、UTF-8が標準になっています。

EUCと同様に日本語文字は2バイトで表現されていますが、 半角カナと共存させるため、 この2バイトが取り得る値の範囲が複雑になっています。 具体的には、 1バイト目の範囲は0x81〜0x9fと0xe0〜0xfc、 2バイト目の範囲が0x40〜0x7eと0x80〜0xfcです。

2バイト目の値の範囲がASCIIと重なっているため、 任意のバイトを見ただけでは英数字と日本語文字(の2バイト目)の区別が付かない、 Shift_JISを想定していないプログラムでは一部の日本語文字がバックスラッシュと誤認識される、 といった欠点があります。 文字列の先頭から1バイトずつ順に見ていくのであれば、 EUCと同様に処理できます。

コード例

    unsigned char *str = "〒123-4567 maison○□12号室";
    unsigned char buf[256];
    unsigned char *sp = str, *bp = buf;
    unsigned char c = 0;
    while (*sp != '\0'){
        printf("%x ", *sp);
        // 2バイト文字の1バイト目なら区切り文字を入れてこのバイトを保存
        if ((c == 0) && (0x81 <= *sp && *sp <= 0x9f) || (0xe0 <= *sp && *sp <= 0xfc)){
            *bp = '|';
            bp++;
            c = *sp;
        }
        else if (c != 0){  // 2バイト文字の1バイト目なら保存した1バイト目と合わせてbufに格納
            *bp = c;
            bp++;
            *bp = *sp;
            bp++;
            c = 0;
            }
        else { // 1バイト文字の場合
            *bp = '|';
            bp++;
            *bp = *sp;
            bp++;
        }
        sp++;
    }
    *bp = '\0';
    printf("\n%s\n", buf);

実行結果

81 a7 31 32 33 2d 34 35 36 37 20 6d 61 69 73 6f 6e 81 9b 81 a0 31 32 8d 86 8e ba 
|〒|1|2|3|-|4|5|6|7| |m|a|i|s|o|n|○|□|1|2|号|室

UTF-8

Unicodeを符号化したもので、英字・日本語文字だけでなく、 中国語文字など多言語の文字を混在させられます。 最近ではUnix, Windowsともに、これを標準にすることが多くなっています。

ASCIIに含まれる文字については、そのまま1バイトで表します。 EUC, Shift_JISと異なるのは、その他の文字のバイト数が一定ではなく、 2〜6バイトになります。 ただし、一般の日本語文字に限定すれば、 1文字あたり3バイトです。

各バイトについて、上位ビットが以下のようになっています。

したがって、任意のバイトを見て、上記のいずれであるかの判定ができます。 また、1バイトの英数字しか想定していないプログラムでも、 多バイト文字の一部を1バイト文字と誤認識することがありません。 その代わり、日本語文字を3バイトで表すため、 EUC/Shift_JISと比べてデータ量が1.5倍近くになってしまう、 前記のように等幅フォントでバイト数と表示幅が一致しない、 といった欠点があります。

また、UTF-8で書かれた任意のテキストに対応するには、 2バイト文字や4バイト文字にも対応する必要があります。 以下の例は、 1バイトの英数字と3バイトの日本語文字しか含まれていないことを前提とする、 手抜きコードです。

コード例

    unsigned char *str = "〒123-4567 maison○□12号室";
    unsigned char buf[256];
    unsigned char *sp = str, *bp = buf;
    unsigned char c1 = 0, c2 = 0;
    while (*sp != '\0'){
        printf("%x ", *sp);
        if ((*sp & 0xf0) == 0xe0){ // 上位4ビットが1110なら、3バイト文字の1バイト目
            *bp = '|';
            bp++;
            c1 = *sp;
        }
        else if ((*sp & 0xc0) == 0x80){ // 上位2ビットが10なら、他バイト文字の2バイト目以降
            if (c2 == 0){
                c2 = *sp;
            }
            else {
                *bp = c1;
                bp++;
                *bp = c2;
                bp++;
                *bp = *sp;
                bp++;
                c1 = c2 = 0;
            }
        }
        else if ((*sp & 0x80) == 0){ // 1バイト文字の場合
            *bp = '|';
            bp++;
            *bp = *sp;
            bp++;
        }
        sp++;
    }
    *bp = '\0';
    printf("\n%s\n", buf);
    return 0;

実行結果

e3 80 92 31 32 33 2d 34 35 36 37 20 6d 61 69 73 6f 6e e2 97 8b e2 96 a1 31 32 e5 8f b7 e5 ae a4 
|〒|1|2|3|-|4|5|6|7| |m|a|i|s|o|n|○|□|1|2|号|室

UTF-8対応の関数ライブラリ

内容

EUC, Shift_JIS, UTF-8のいずれの環境でも動作する、 文字列操作・出力関数ライブラリです。 現バージョンでは、以下の関数が使用できます。

基本的な使い方

ソースファイルex.c中で上記の関数を使う場合を例に説明します。

  1. 以下のファイルをダウンロードし、 ex.c と同じディレクトリにコピーします。
  2. ex.c の中で、以下のようにしてヘッダファイルを取り込みます。
          #include"jstr.h"
          
    これで、 ex.c の中で上記の関数を呼び出せるようになります。
  3. 以下のようにして、 jstr.c を合わせてコンパイルします。
          gcc -Wall ex.c jstr.c -o ex
          

注意事項

履歴



最終更新: 2023年 4月 13日 木曜日 18:00:46 JST

御意見、御感想は ohno@arch.info.mie-u.ac.jp まで