はじめに
C言語で日本語文字列を扱う場合は、例えば、ワイド文字のライブラリを使用する方法があります(参考:C言語用語集 - ワイド文字 )。
しかし、ワイド文字は、1文字あたりのバイト数を固定して処理することが前提であるため、可変長の文字コード(UTF-8、SHIFT_JIS、EUC-JPなどのマルチバイト文字)を扱うには、別の方策が必要となります。
(以上、yumetodoさんのご指摘を受け一部修正をさせていただきました。)
ここでは、マルチバイト文字における「日本語文字数のカウント」及び「日本語文字列の切り出し」について、サンプルコードとともにまとめておきます。
なお、日本語文字数のカウント(UTF-8、SHIFT_JISのみ)については、1年前に自分のHPにも書いています。
今回は、当時の記述の補正もしつつ、各文字コードについて一覧的に記述をしています。
使用しているコンパイラはgccで、macで実行確認をしました。
1. 日本語文字列の文字数カウント
まずは、単純な文字数のカウントです。
1-1. UTF-8における文字数カウントのサンプルコード
#include <stdio.h>
//u8len関数(UTF8の文字数をカウントする関数)
int u8len(const char *str)
{
int count = 0; // 文字数のカウント用
while (*str != '\0') {
if ((*str & 0xC0) != 0x80) { count++; } //
str++;
}
return count;
}
int main(void)
{
const char *str = "あいうえおABC文字列"; // 対象文字列
printf("文字数(UTF8) = %d\n", u8len(str));
// 出力結果: 文字数(UTF8) = 11
return 0;
}
<実行結果>
$ gcc u8len.c
$ ./a.out
文字数(UTF8) = 11
1バイトずつループさせて、文字数をカウントするか否かをビット演算(*str & 0xC0) != 0x80
で判定しています。
このビット演算の意味については、こちらを参照してください。
1-2. SHIFT_JISにおける文字数カウントのサンプルコード
#include <stdio.h>
//sjlen関数(SHIFT_JISの文字数をカウントする関数)
int sjlen(const char *str)
{
int count = 0; // 文字数のカウント用
int skip = 0; // skip=1の場合は文字カウントをスキップする
while (*str != '\0') {
if (skip) { // 2バイト文字の2バイト目の場合はカウントしない
skip = 0;
} else {
if ((*str & 0xE0) == 0x80 || (*str & 0xE0) == 0xE0) { skip = 1; } //2バイト文字に該当する場合
count++;
}
str++;
}
return count;
}
int main(void)
{
const char *str = "あいうえおABC文字列"; // 対象文字列
printf("文字数(SJIS) = %d\n", sjlen(str));
// 出力結果: 文字数(SJIS) = 11
return 0;
}
<実行結果>
$ gcc sjlen.c
$ ./a.out
文字数(SJIS) = 11
こちらも、1バイトずつループさせて文字数をカウントしています(ビット演算の意味については、こちらを参照)。
次のように、短く書くことも可能ですが、SHIFT_JIS以外の文字列に適用すると、末端NULL文字を飛び越えてループが繰り返される場合があるため、やめた方が良いでしょう(過去の自分のHP記事では、このように書いてました)。
int sjlen(const char *str)
{
int count = 0;
while (*str != '\0'){
count++;
if ((*str & 0xE0) == 0x80 || (*str & 0xE0) == 0xE0) { str++; } //2バイト文字に該当する場合
str++;
}
return count;
}
<参考:macでSHIFT_JISを扱う方法>
macにおいて「SHIFT_JISで記述されたファイル」を扱う場合は、ターミナルの[環境設定]→[詳細]→[テキストエンコーディング]のところで、「日本語 (Shift JIS)」を選択する必要があります。
また、ファイル自体を、SHIFT_JISにエンコードしたい場合は、「macでファイルの文字コードを変換する『nkfコマンド』の使い方とオプション一覧」という記事を参考にしてください。
1-3. EUC-JPにおける文字数カウントのサンプルコード
#include <stdio.h>
// euclen関数(EUC-JPの文字数をカウントする関数)
int euclen(const char *str)
{
int count = 0; // 文字数のカウント用
int skip = 0; // skip=1の場合は文字カウントをスキップする
while (*str != '\0'){
if (skip) { // 2バイト文字の2バイト目の場合はカウントしない
skip--;
} else {
if (*str & 0x80) { skip++; } //2バイト文字に該当する場合
count++;
}
str++;
}
return count;
}
int main(void)
{
const char *str = "あいうえおABC文字列"; // 対象文字列
printf("文字数(EUC) = %d\n", euclen(str));
// 出力結果: 文字数(EUC) = 11
return 0;
}
<実行結果>
$ gcc euclen.c
$ ./a.out
文字数(EUC) = 11
これまでと同様に、1バイトずつループさせて文字数をカウントしています(ビット演算の意味については、こちらを参照)。
EUC-JPで記述されたファイルを使用する場合は、ターミナルの[環境設定]→[詳細]→[テキストエンコーディング]のところで、「日本語 (EUC)」を選択してください(SHIFT_JISの場合と同様です)。
2. 日本語文字列の切り出し
次に、日本語文字列の切り出しです。
RubyやJavaScriptでは、切り出しの開始位置と終了位置を指定すれば、簡単に部分文字列の抽出ができるメソッドがあるので便利です。
以下、そのような関数をC言語で記述する場合のサンプルコードです。
なお、C言語では、文字列の値
自体を戻り値とすることができませんので、ポインタを返すようにしています。
また、実際に実装をする場合は、用意したメモリ以上のデータが入らないようにするなど、セキュリティー上の考慮
が必要ですので、その点は、留意していただければと思います。
2-1. UTF-8における文字数切り出しのサンプルコード
#include <stdio.h>
// u8slice関数(UTF8の文字列を切り出しする関数)
char *u8slice(char *buf, const char *str, int begin, int end)
{
int count = 0;
char *p = buf; // returnするポインタを格納
while (*str != '\0') {
if ((*str & 0xC0) != 0x80) { count++; }
if (count > begin && count <= end) { *buf = *str; buf++; } // 指定範囲の文字を格納
str++;
}
*buf = '\0'; // 末尾にNULL文字を格納
return p;
}
int main(void)
{
const char *str = "あいうえおABC文字列"; // 対象文字列
char buf[128];
char *ans = u8slice(buf, str, 3, 7);
printf("切り出し文字列:%s\n", buf); // bufに格納された文字列の確認
printf("戻り値:%s\n", ans); // 戻り値の確認
return 0;
}
<実行結果>
$ gcc u8slice.c
$ ./a.out
切り出し文字列:えおAB
戻り値:えおAB
文字列をカウントするu8len関数をベースに作成しているので、構造はほぼ同じです。
指定範囲の文字のみを配列buf
に格納するようにしています。
<構文>
char *u8slice(char *buf, const char *str, int begin, int end)
第1引数buf
は、抽出文字列を格納するための変数です。
第2引数str
は、対象となる元の文字列です。
第3引数bigin
は、抽出の開始位置です。
第4引数end
は、抽出の終了位置です。
2-2. SHIFT_JISにおける文字数切り出しのサンプルコード
#include <stdio.h>
// sjslice関数(SHIFT_JISの文字列を切り出しする関数)
char *sjslice(char *buf, const char *str, int begin, int end)
{
int count = 0;
int skip = 0;
char *p = buf; // returnするポインタを格納
while (*str != '\0') {
if (skip) {
skip = 0;
} else {
if ((*str & 0xE0) == 0x80 || (*str & 0xE0) == 0xE0) { skip = 1; } //2バイト文字に該当する場合
count++;
}
if (count > begin && count <= end) { *buf = *str; buf++; } // 指定範囲の文字を格納
str++;
}
*buf = '\0'; // 末尾にNULL文字を格納
return p;
}
int main(void)
{
const char *str = "あいうえおABC文字列"; // 対象文字列
char buf[128];
char *ans = sjslice(buf, str, 3, 7);
printf("切り出し文字列:%s\n", buf); // bufに格納された文字列の確認
printf("戻り値:%s\n", ans); // 戻り値の確認
return 0;
}
<実行結果>
$ gcc sjslice.c
$ ./a.out
切り出し文字列:えおAB
戻り値:えおAB
構文等は、UTF-8の場合と同様です。
2-3. EUC-JPにおける文字数切り出しのサンプルコード
#include <stdio.h>
// eucslice関数(EUC-JPの文字列を切り出しする関数)
char *eucslice(char *buf, const char *str, int begin, int end)
{
int count = 0;
int skip = 0;
char *p = buf; // returnするポインタを格納
while (*str != '\0'){
if (skip) {
skip--;
} else {
if (*str & 0x80) { skip++; } //2バイト文字に該当する場合
count++;
}
if (count > begin && count <= end) { *buf = *str; buf++; } // 指定範囲の文字を格納
str++;
}
*buf = '\0'; // 末尾にNULL文字を格納
return p;
}
int main(void)
{
const char *str = "あいうえおABC文字列"; // 対象文字列
char buf[128];
char *ans = eucslice(buf, str, 3, 7);
printf("切り出し文字列:%s\n", buf); // bufに格納された文字列の確認
printf("戻り値:%s\n", ans); // 戻り値の確認
return 0;
}
<実行結果>
$ gcc eucslice.c
$ ./a.out
切り出し文字列:えおAB
戻り値:えおAB
構文等は、UTF-8の場合と同様です。
2-4. 抽出文字列の格納メモリを動的に確保する場合(参考)
上記の例では、抽出文字列を格納するメモリとして、あらかじめchar buf[128]
というように、大きめのメモリを確保して関数に渡していました。
このメモリを動的に確保する場合のサンプルコードを、以下、参考までに貼っておきます。
#include <stdio.h>
#include <stdlib.h> // malloc, freeで使用
char *u8slice(char *buf, const char *str, int begin, int end);
int u8slicelen(const char *str, int begin, int end);
int main(void)
{
const char *str = "あいうえおABC文字列"; // 対象文字列
int begin = 4; // 開始位置
int end = 9; // 終了位置
int len = u8slicelen(str, begin, end); // 抽出文字列のバイト数を算出
char *buf = (char *)malloc((len + 1) * sizeof(char)); // 格納用メモリを動的に確保
char *ans = u8slice(buf, str, begin, end); // 関数の実行
printf("戻り値:%s\n", ans); // 戻り値の確認
free(buf); // 動的確保したメモリの解放
return 0;
}
// u8slice関数(UTF8の文字列を切り出しする関数)
char *u8slice(char *buf, const char *str, int begin, int end)
{
int count = 0;
char *p = buf; // returnするポインタを格納
while (*str != '\0') {
if ((*str & 0xC0) != 0x80) { count++; }
if (count > begin && count <= end) { *buf = *str; buf++; } // 指定範囲の文字を格納
str++;
}
*buf = '\0'; // 末尾にNULL文字を格納
return p;
}
// u8slicelen関数(UTF8の文字列を切り出しバイトを確認する関数)
int u8slicelen(const char *str, int begin, int end)
{
int count = 0;
int len = 0;
while (*str != '\0') {
if ((*str & 0xC0) != 0x80) { count++; }
if (count > begin && count <= end) { len++; } // 変数lenに指定範囲のバイト数を加算
str++;
}
return len;
}
<実行結果>
$ gcc u8slice2.c
$ ./a.out
戻り値:おABC文
サンプルコードの最後のところに、抽出文字列のバイト数のみを算出するu8slicelen関数
を作成しています。
これで、あらかじめ必要バイト数を算出した上で、malloc関数
(標準ライブラリ)を用いてメモリの動的確保を行っています。
サンプルとして、UTF-8の場合のみ掲載しましたが、他の文字コードでも同様にすればメモリの動的確保が可能です。
最後に
1年前にHPに書いた内容を読み返すと、理解の誤りや至らない記述が多々あることに気付きます。
ということは、今回書いた内容にも、必ずや誤りがあるのだろうと思われます。
お気付きのことなどあれば、ご教示いただけると幸いです。