3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【C言語】日本語文字列の字数カウント・文字列切り出し(マルチバイト文字)

Last updated at Posted at 2020-02-02

はじめに

C言語で日本語文字列を扱う場合は、例えば、ワイド文字のライブラリを使用する方法があります(参考:C言語用語集 - ワイド文字 )。

しかし、ワイド文字は、1文字あたりのバイト数を固定して処理することが前提であるため、可変長の文字コード(UTF-8、SHIFT_JIS、EUC-JPなどのマルチバイト文字)を扱うには、別の方策が必要となります。
(以上、yumetodoさんのご指摘を受け一部修正をさせていただきました。)

ここでは、マルチバイト文字における「日本語文字数のカウント」及び「日本語文字列の切り出し」について、サンプルコードとともにまとめておきます。

なお、日本語文字数のカウント(UTF-8、SHIFT_JISのみ)については、1年前に自分のHPにも書いています。
今回は、当時の記述の補正もしつつ、各文字コードについて一覧的に記述をしています。

使用しているコンパイラはgccで、macで実行確認をしました。

1. 日本語文字列の文字数カウント

まずは、単純な文字数のカウントです。

1-1. UTF-8における文字数カウントのサンプルコード

u8len.c
#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における文字数カウントのサンプルコード

sjlen.c
#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における文字数カウントのサンプルコード

euclen.c
#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における文字数切り出しのサンプルコード

u8slice.c
#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における文字数切り出しのサンプルコード

sjslice.c
#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における文字数切り出しのサンプルコード

eucslice.c
#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]というように、大きめのメモリを確保して関数に渡していました。
このメモリを動的に確保する場合のサンプルコードを、以下、参考までに貼っておきます。

u8slice2.c
#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に書いた内容を読み返すと、理解の誤りや至らない記述が多々あることに気付きます。
ということは、今回書いた内容にも、必ずや誤りがあるのだろうと思われます。
お気付きのことなどあれば、ご教示いただけると幸いです。

3
3
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?