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でプログラムを書いていて、日本語を使いたくなることはよくあります。 このとき、 どの程度のことをしたいかにより、 どこまで難しいことが必要か異なってきます。
printf
関数で出力する程度なら問題ありません。
たとえば、
printf("日本語\n");
といったコードなら大丈夫です。printf
で桁数を揃えた結果が崩れることがあります。
たとえば、printf("%10s", str);
printf
で文字列の出力幅を指定したとき、マルチバイト文字を想定せず
単純に指定数値分のバイトを出力しているためです。
ターミナルなどで使う等幅フォントでは、
漢字などの日本語文字は英数字2文字分の幅で表示するようになっており、
EUCやShift_JISでは日本語文字が2バイトなのでちょうど計算が合います。
しかし、UTF-8では日本語文字が3バイトなので、計算にずれが生じます。
char
型変数の配列が使えますが、
一文字毎に複数バイトが必要なので、配列のサイズ指定時に注意が必要です。strlen
関数を使うことで、
日本語文字・英数字を問わず、
その文字列のバイト数を得ることができます。
strcpy
が使えますが、
一文字単位で切り出す、
特定の文字を置き換える、
といった処理を行うなら、
その文字コードに対応した関数を用いるか、
自分で処理を書く必要があります。
授業で説明したような
「char
型ポインタ
p
をインクリメントしながら一文字ずつ処理する」方法では、
マルチバイト文字の場合は
「ある文字コードの前半を指している」
/
「ある文字コードの後半を指している」
といった状況を作ってしまいます。
また、
*p
では1バイト分しか参照できません。
さらに、マルチバイト対応の文字コードの多くはASCIIコードと互換性を持っており、
「英数字は1バイトで表し、ASCIIコードと同じ値を割り当てる」
「日本語文字などは2バイト以上で表す」
という方法をとっています。
このため、英数字部分を従来と同様に処理できる一方、
一般の文字列は1文字1バイトの部分と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で文字単位に切り分けるコード例を示します。
以下の符号化方式の説明はかなり大雑把なものであり、 「英数字と一般的な日本語文字が混在するテキストを処理する」 のに最小限必要なレベルのものです。 半角カナや補助漢字など、やや特殊な文字については省略しています。 自分で使うツールの作成や、 事前に用意したデータファイルしか読み込まないゲームプログラムなどでは十分かと思いますが、 任意のテキストを扱うフリーウェアを作成・公開する、 といった場合は、もっと厳密な文字コードの資料を読んでください。
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|.
主に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|号|室
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|号|室
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|号|室
EUC, Shift_JIS, UTF-8のいずれの環境でも動作する、 文字列操作・出力関数ライブラリです。 現バージョンでは、以下の関数が使用できます。
int jstrwidth(char *str);
str
の表示上の幅を返します。
表示上の幅は、
1バイト文字(半角文字)、
多バイト文字(全角文字)をそれぞれ1,2とし、
文字列内に含まれる文字の幅の合計を計算します。
int jstrsub(char *buf, char *str, int widthmax);
str
を先頭から表示上の幅が
width
になる長さで切り出し、
buf
に格納します。
切り出し位置が多バイト文字の途中になる場合は、
その手前までを切り出します。
実際に切り出した表示上の幅を返します。
int jstrslice(char *buf, char **str, int widthmax);
unsigned int jstrcode(char *str, int *byte);
str
の先頭文字のコードを返します。
また、先頭文字が何バイト文字であるかをbyte
に格納します。str
に
byte
の値を加算しながら
この関数を連続して呼ぶことで、
長い文字列から1文字ずつ文字コードを切り出していくことができます。
void jstrappend(char *str, unsigned int code, int byte);
code
で表される1文字を
ポインタ
str
の位置に格納します。
文字コードと共に、
その文字コードの実際のバイト数(文字列として格納する際に必要なバイト数)を、
byte
)で指定する必要があります。str
に
byte
の値を加算しながら
この関数を連続して呼ぶことで、
文字コードから文字列を組み立てることができます。
jstrcode
と組み合わせることで、
文字列から一文字ずつ切り出して(必要なら加工してから)
別の領域に文字列として格納し直すことができます。
'\0'
を格納してください。
int jprintf(char *fmt, ...);
int jfprintf(FILE *fp, char *fmt, ...);
int jsprintf(char *str, char *fmt, ...);
printf
, fprintf
sprintf
の互換関数です。
"%10s"
などと文字列の出力幅を指定したとき、UTF-8でも正しい幅で出力されます。
FILE *fopen_bomcut(const char *path, const char *mode);
fopen()
の互換関数です。JCODING_UTF8
が定義されており、
モードが読み出しの時
(引数mode
が"r"
のとき)に、
ファイルを開いた後で先頭3バイトをチェックし、BOMならば読み飛ばします。
それ以外の時は、
通常のfopen()
と同じ動作をします。fopen()
を使ってください。
ソースファイルex.c
中で上記の関数を使う場合を例に説明します。
ex.c
と同じディレクトリにコピーします。
ex.c
の中で、以下のようにしてヘッダファイルを取り込みます。
#include"jstr.h"これで、
ex.c
の中で上記の関数を呼び出せるようになります。
jstr.c
を合わせてコンパイルします。
gcc -Wall ex.c jstr.c -o ex
JCODING_EUC
: EUC
JCODING_SJIS
: Shift_JIS
JCODING_UTF8
: UTF-8
#define JCODING_UTF8 #include"jstr.h"のように、ヘッダファイルの取り込み前にマクロ定義を記述するか、
gcc -Wall ex.c jstr.c -o ex -DJCODING_UTF8のようにコンパイラのオプションで定義してください。
FORCE_JPRINTF
を定義すると、
printf
,
fprintf
,
sprintf
が、すべて本ライブラリの互換関数に強制的に置き換えられます。
つまり、本ライブラリを想定せずに書いたソースコードでも、
修正なしに
jprintf
などが使えるようになります。#define FORCE_JPRINTF #include"jstr.h"のようにソースファイル中に書くか、
gcc -Wall ex.c jstr.c -o ex -DFORCE_JPRINTFのようにコンパイルしてください。 なお、置き換えたい個々のソースファイル中で、
jstr.h
を取り込む必要があります。
最終更新: 2023年 4月 13日 木曜日 18:00:46 JST
御意見、御感想は ohno@arch.info.mie-u.ac.jp まで