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

# はじめに
C言語で日本語文字列を扱う場合は、例えば、ワイド文字のライブラリを使用する方法があります（参考：[C言語用語集 - ワイド文字 ](http://www.c-tipsref.com/reference/wchar.html)）。

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

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

なお、日本語文字数のカウント（UTF-8、SHIFT_JISのみ）については、１年前に自分の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;
}
```

**＜実行結果＞**

```terminal:ターミナル
$ gcc u8len.c
$ ./a.out
文字数(UTF8) = 11
```

１バイトずつループさせて、文字数をカウントするか否かをビット演算`(*str & 0xC0) != 0x80`で判定しています。
このビット演算の意味については、[こちら](https://qiita.com/t-yama-3/items/07e9fbba7db73eca8c6f#utf-8%E3%81%AE%E5%89%B2%E5%BD%93%E3%81%A6%E3%82%A8%E3%83%AA%E3%82%A2)を参照してください。

### 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;
}
```

**＜実行結果＞**

```terminal:ターミナル
$ gcc sjlen.c
$ ./a.out
文字数(SJIS) = 11
```

こちらも、１バイトずつループさせて文字数をカウントしています（ビット演算の意味については、[こちら](https://qiita.com/t-yama-3/items/07e9fbba7db73eca8c6f#shift-jis%E3%81%AE%E5%89%B2%E5%BD%93%E3%81%A6%E3%82%A8%E3%83%AA%E3%82%A2)を参照）。

次のように、短く書くことも可能ですが、SHIFT_JIS以外の文字列に適用すると、末端NULL文字を飛び越えてループが繰り返される場合があるため、やめた方が良いでしょう（過去の自分のHP記事では、このように書いてました）。

```c:あまり良くない例
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コマンド』の使い方とオプション一覧](http://kawatama.net/others/mac/1754)」という記事を参考にしてください。

### 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;
}
```

**＜実行結果＞**

```terminal:ターミナル
$ gcc euclen.c
$ ./a.out
文字数(EUC) = 11
```

これまでと同様に、１バイトずつループさせて文字数をカウントしています（ビット演算の意味については、[こちら](https://qiita.com/t-yama-3/items/07e9fbba7db73eca8c6f#euc-jp%E3%81%AE%E5%89%B2%E5%BD%93%E3%81%A6%E3%82%A8%E3%83%AA%E3%82%A2)を参照）。


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;
}
```

**＜実行結果＞**

```terminal:ターミナル
$ gcc u8slice.c 
$ ./a.out 
切り出し文字列：えおAB
戻り値：えおAB
```

文字列をカウントするu8len関数をベースに作成しているので、構造はほぼ同じです。
指定範囲の文字のみを配列`buf`に格納するようにしています。

**＜構文＞**
`char *u8slice(char *buf, const char *str, int begin, int end)`
第１引数`buf`は、抽出文字列を格納するための変数です。
第２引数`str`は、対象となる元の文字列です。
第３引数`bigin`は、抽出の開始位置です。
第４引数`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;
}
```

**＜実行結果＞**

```terminal:ターミナル
$ 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;
}
```

**＜実行結果＞**

```terminal:ターミナル
$ 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;
}
```

**＜実行結果＞**

```terminal:ターミナル
$ gcc u8slice2.c 
$ ./a.out 
戻り値：おABC文
```

サンプルコードの最後のところに、抽出文字列のバイト数のみを算出する`u8slicelen関数`を作成しています。
これで、あらかじめ必要バイト数を算出した上で、`malloc関数`（標準ライブラリ）を用いてメモリの動的確保を行っています。

サンプルとして、UTF-8の場合のみ掲載しましたが、他の文字コードでも同様にすればメモリの動的確保が可能です。

# 最後に
1年前にHPに書いた内容を読み返すと、理解の誤りや至らない記述が多々あることに気付きます。
ということは、今回書いた内容にも、必ずや誤りがあるのだろうと思われます。
お気付きのことなどあれば、ご教示いただけると幸いです。