C言語とは直接関係ありませんが,文字列に関する基礎知識です.
コンピュータが扱えるのは0と1のビット列のみです. このビット列をどのように解釈するか決めたものが基本データ型になりますが, 基本データ型の中に,文字という概念はあっても,文字列という概念はありません. そのため,この文字という概念を並べることで文字列として扱うことになります.
さて,データ型の中に文字という概念はありますが,これは,1文字1バイトのASCIIコードを扱えるデータ型です. そのため,マルチバイト文字(2バイト以上の文字)である日本語は,この基本型である文字では扱うことができません. では,どのようなビット列のときに文字になるのでしょうか.
「abcあいう123」
ここに半角文字のアルファベット3文字,全角文字のひらがな3文字,半角文字の数字3文字があります.
これはいったい何バイトでしょうか?
多くの人は,全角文字のひらがなは2バイト文字だから3+2*3+3で計12バイト必要だ,と判断すると思いますが,
それはある意味正しくて,ある意味間違えています.
日本語を表すためのビット列との対応関係(コード)により,必要なバイト数は異なります.
そのため,文字列に必要なバイト数を知るためには,まず,文字を表す文字コードを決める必要があります.
文字を表現するためには,ビット列と文字の対応付けをする必要があります. その対応関係が文字コードになります. 文字コードの詳しい説明は,Wikipediaの文字コードに任せます.
日本語の場合,WindowsやMacintoshではShift_JISコード,Linux(Unix系)ではEUCコード,メールの送受信ではJISコード, JavaではUnicodeを使うというように,簡単にあげただけでも4種類は存在します. これらの文字コードによっては,先ほどの「abcあいう123」は12バイトどころではなく,それ以上のメモリが必要になります.
日本語文字(2バイト文字)とそれ以外の文字(1バイト文字)の間にコードを切り替えるエスケープシーケンスを入れ,コード体系を切り替える方式です.
すべてのバイトを0x00-0x7Fの7ビットで表現できる特徴があります.
先ほどの文字列は「61 62 63 1B 24 42 24 22 24 24 24 26 1B 28 42 31 32 33」の18バイトになります.
ここで,4バイト目からの"1B 24 42"が2バイト文字コードに切り替えるシフトイン(SI),
13バイト目からの"1B 28 42"が1バイト文字コードに切り替えるシフトアウト(SO)になります.
これらのSI/SOが間に入るため,Shift_JISコードと比較して必要なメモリは増えてしまいます.
エスケープコード(1B)で始まる一連のコードを指しますが, JISコードでは,このエスケープシーケンスを利用してコードを切り替えています.
対象のコード | エスケープシーケンス | 続く文字 |
ASCII | 1B 28 42 | 1バイト |
JIS X 0201-Roman | 1B 28 4A | 1バイト |
JIS X 0208-1978 | 1B 24 40 | 2バイト |
JIS X 0208-1983 | 1B 24 42 | 2バイト |
JIS X 0212-1990 | 1B 24 28 44 | 2バイト |
すべての文字は7ビットであるため,8ビット目を利用した文字判断は出来ません. 次に,文字数カウントのソースを載せておきます. 適当に書いただけなのでどこまで正確か分かりません.ただ,流れは分かると思います.
int count_JIS(const unsigned char *string) { int len = 0; int byte = 1; while(*string){ if(*string == 0x1b){ ++string; if(*string == 0x28){ // 1バイト文字系 ++string; if(*string != 0x42 && *string != 0x4A) break; // 不明なエスケープシーケンス ++string; byte = 1; }else if(*string == 0x24){ // 2バイト文字系 ++string; if(*string != 0x40 && *string != 0x42){ if(*string != 0x28 || *++string != 0x44) break; // 不明なエスケープシーケンス } ++string; byte = 2; }else break; // 不明なエスケープシーケンス }else{ if(*string < 0x1f || *string == 0x7f) ;// 制御文字 else{ if(0x80 < *string) ;// 半角カナ else ++len; } string += byte; } } return len; }
エスケープシーケンス"1B 28 4A"でJIS X 0201に切り替え,半角カナをサポートする亜種(CP50221)もありますが, 本来このシーケンスで指定できるのはJIS X 0201中のラテン文字用図形文字集合だけです. 原則として片仮名用図形文字集合(半角カナ)は8ビット目を必要とするため,7ビットコードであるJISでは使えません.
改行コードは制御コード(1バイト文字)であるため,改行の前には"1B 28 42"が必要です. つまり,JISコード中で改行を行うたびに6バイト分のエスケープシーケンスが紛れ込みます.
Shift_JISの場合,全角文字は2バイトで表されることになっています.
JISで使われていなかったビット列の部分に文字をシフトしてきたコードです.
JISコードのように,エスケープシーケンスは必要ありませんが,
1バイト目が0x81-0x9F,0xE0-0xFC,2バイト目が0x40-0x7E,0x80-0xFCの範囲しか使えないため,
表現できる文字は,11000文字程度になります.
先ほどの文字列をShift_JISで表現すると「61 62 63 82 A0 82 A2 82 A4 31 32 33」の12バイトになります.
1バイトずつ読み込み,0x81-0x9F,0xE0-0xFCの範囲の文字があるかチェックする必要があります. 次に,文字数カウントのソースを載せておきます. 適当に書いただけなのでどこまで正確か分かりません.ただ,流れは分かると思います.
int count_Shift_JIS(const unsigned char *string) { int len = 0; while(*string){ if(*string < 0x1f || *string == 0x7f){ ;// 制御文字 }else if((0x81 <= *string && *string <= 0x9F) || (0xE0 <= *string && *string <= 0xFC)){ // 2バイト文字 ++string; if((0x40 <= *string && *string <= 0x7E) || (0x80 <= *string && *string <= 0xFC)) ++len; else break; // 不明な文字 }else{ // 1バイト文字 if(0x80 < *string) ;// 半角カナ ++len; } ++string; } return len; }
Shift_JISの場合,一部のプログラム(perlなど)でダメ文字問題が発生することがあります.
これは,2バイト文字の2バイト目の部分に0x5Cが使われていることに原因があります.
Shift_JISでは,2バイト目に0x40-0x7Eと0x80-0xFCが許可されています.
しかし,この中の0x5Cはいわゆるエスケープ文字(\)を表しています.
このエスケープ文字は次のバイトと組み合わせて解釈される文字です(2バイトを1バイトとして解釈するわけです).
そのため,本来文字として正しいはずの1バイト目と2バイト目の組み合わせがずれてしまい,
1バイト目と次の文字の1バイト目(本来の2バイト目0x5Cが消えてしまう),
次の文字の2バイト目とさらに次の1バイト目と順にずれてしまいます.
(本来,プログラム側が文字コードを認識して動作すべきだとは思いますが,)
このような文字が来る場合,0x5C 0x5Cの組み合わせは0x5Cとして解釈されることを利用し
あらかじめ0x5Cを追加しておくことが必要になります.
例えば,"予定表"の場合,1文字目と3文字目はダメ文字です.
そのため,"予\定表\"のようにダメ文字の後に問題のエスケープ文字(0x5C)を追加することで正しく表示することができます.
ダメ文字リスト:―ソЫ\噂浬欺圭構蚕十申曾箪貼能表暴予禄兔喀媾彌拿杤歃濬畚秉綵臀藹觸軆鐔饅鷭xx
良く使われてしまうダメ文字は"ソ,十,能,表,予"あたりでしょうか.
原則として日本語を2バイトとして表現した方法ですが,亜種として一部の拡張文字を3バイトで表現するコードもあります.
日本語文字は,1バイト目,2バイト目共に0x80-0xFFの範囲にあることが特徴です.
3バイト必要とするのは,第4水準文字であり,通常使われることはないと思います.
先ほどの文字列をEUCで表現すると「61 62 63 A4 A2 A4 A4 A4 A6 31 32 33」の12バイトになります.
1バイトずつ読み込み,0x80-0xFFの範囲の文字があるかチェックする必要があります. 次に,文字数カウントのソースを載せておきます. 適当に書いただけなのでどこまで正確か分かりません.ただ,流れは分かると思います.
int count_EUC(const unsigned char *string) { int len = 0; while(*string){ if(*string < 0x1f || *string == 0x7f){ ;// 制御コード }else if(*string == 0x8E){ if(0xA1 <= *++string && *string <= 0xFE) // 半角カナ ++len; else break; // 不明な文字 }else if(*string == 0x8F){ // 3バイト文字 if((0xA1 <= *++string && *string <= 0xFE) && (0xA1 <= *++string && *string <= 0xFE)) ++len; else break; // 不明な文字 }else if(0xA1 <= *string && *string <= 0xFE){ // 2バイト文字 if(0xA1 <= *++string && *string <= 0xFE) ++len; else break; // 不明な文字 }else{ // 1バイト文字 ++len; } ++string; } return len; }
ASCII文字は1バイト,それ以外の文字を2-6バイトで表現した方法です.
日本語文字は大半が3バイトで表現されます.
先ほどの文字列をUTF-8で表現すると「61 62 63 E3 81 82 E3 81 84 E3 81 86 31 32 33」の15バイトになります.
1バイト目を読めば何バイトの文字か分かるため非常にカウントが楽です.
1バイト目が0xxxxxxxの場合,1バイト文字でASCIIコードと同じです.
10xxxxxxの場合,他のマルチバイト文字の続き文字になります.
110xxxxxの場合,2バイト文字の先頭になります.
以下同様に,1110xxxxの場合は3バイト文字,11110xxxの場合は4バイト文字,
111110xxの場合は5バイト文字1111110xの場合は6バイト文字の先頭になりますが,
実際には4バイト文字までしか定義されていないため,0xf8以上の文字が出た場合,エラーにしてしまっても構わないかもしれません.
一応定義上は存在するようです.
int count_UTF8(const unsigned char *string) { int len = 0; while(*string){ if(*string < 0x1f || *string == 0x7f){ // 制御コード }else if(*string <= 0x7f){ ++len; // 1バイト文字 }else if(*string <= 0xbf){ ; // 文字の続き }else if(*string <= 0xdf){ ++len; // 2バイト文字 }else if(*string <= 0xef){ ++len; // 3バイト文字 }else if(*string <= 0xf7){ ++len; // 4バイト文字 }else if(*string <= 0xfb){ ++len; // 5バイト文字 }else if(*string <= 0xfd){ ++len; // 6バイト文字 }else{ ; // 使われていない範囲 } ++string; } return len; }
世界中すべての文字を16ビットで表そうとした方法です. ただ,実際には16ビットで収まらず,一部の文字は32ビットで表すことになっています. 文字を表現するためのバイト数は増えてしまうものの, このコード体系はすべての文字を一つのコードで表すことができることからWindowsXPの内部表現などで使われています.
16ビットで表す場合,8ビットの場合と違いビッグエンディアンとリトルエンディアンの問題が発生します.
通常Unicodeと言った場合,リトルエンディアンとするようです.
先ほどの文字列をUTF-16LEで表現すると「61 00 62 00 63 00 42 30 44 30 46 30 31 00 32 00 33 00」の18バイトになりますし,
UTF-16BEで表現すると「00 61 00 62 00 63 30 42 30 44 30 46 00 31 00 32 00 33」の18バイトになります.
Unicodeの場合,テキストの先頭にエンディアンを区別するためのBOMを付けることがあります. このBOMは0xFEFFであり,リトルエンディアンの場合は「FF FE」,ビッグエンディアンの場合は「FE FF」となります. これにより,テキストのエンディアンを正しく区別することができます.
BOMが付いていればそれに従い,付いていなければLittle Endianとして扱うようにしてあります.
int chechEndian() { unsigned short x = 0x0001; return 0x01 == *(char *)&x; } void Swap(unsigned char *x) { unsigned char t = *x; *x = *(x+1); *(x+1) = t; } int count_UTF16(const unsigned char *str) { int len = 0; const unsigned short *string = (unsigned short *)str; int bSwap = 0; int bOrder = 1; // Little Endian if(chechEndian() == 0) bOrder = 0; // Big Endian if(*str == 0xFE && *(str+1) == 0xFF){ if(bOrder == 1) bSwap = 1; ++string; }else if(*str == 0xFF && *(str+1) == 0xFE){ if(bOrder == 0) bSwap = 1; ++string; } while(*string){ if(bSwap) Swap((unsigned char *)string); if( *string <= 0x001f || *string == 0x007f){ ; // 制御コード }else if( (0xD800 <= *string && *string <= 0xDBFF) || (0xDC00 <= *string && *string <= 0xDFFF)){ if( (0xD800 <= *++string && *string <= 0xDBFF) || (0xDC00 <= *string && *string <= 0xDFFF)) ++len; // 32ビット文字 else break; // 不明な文字 }else{ ++len; // 16ビット文字 } ++string; } return len; }
下記に簡単な判別例を挙げましたが,実際にはもっと複雑な判断がされます. 特に,他のコード中には出てこない文字(ビット列)が存在するならば, ほぼ確実にコードを確定することができます.
このように,文字コードが異なると必要なバイト数も異なります.ですが,バイト数は違うものの表現している文字列は同じです. 単純にバイト数から文字列の長さ(文字数)を知ることはできないのです. バイト列が存在したとき,その文字数を知るためには, "文字を表現しているコード"を知る必要があり,その体系に従ってバイト単位の解析をする必要があります.
この記事へのコメントはせりかログの文字数のカウントあたりにしてください.