【C言語】マルチバイト文字対応strrev関数の作成

はじめまして、新人エンジニアのスズキです。
入社した4月から、C言語を用いた学習を行っています。
今回はC言語における文字列の注意点とマルチバイト文字対応のstrrev関数について解説します。
よろしくお願いします。
この記事の目次
c言語における文字列の注意点
C言語の標準ライブラリの文字列操作系関数の多くは、マルチバイト文字に対応していません。
そのため、日本語の文字列を扱うためには工夫が必要です。
strrev関数
strrev関数とは、文字列の前後を反転させる関数です。
しかし、バイト単位での反転のため、マルチバイト文字が崩れます。
// strrev関数例-コード #include <stdio.h>; void main() { char str[] = "strrev関数"; printf("%s\n", str); strrev(str); printf("%s\n", str); strrev(str); printf("%s\n", str); return; }
// strrev関数例-結果 strrev関数 ��梖�verrts strrev関数
そこで今回は、windows環境以外でも使用でき、かつマルチバイト文字に対応するstrrev関数を自作します。
関数の解説の前に、まずはマルチバイト文字についてまとめます。
マルチバイト文字とは?
マルチバイト文字は、その名の通り複数バイトで構成されています。
1バイト(8ビット)で表せるパターンは256通りしかなく、世界中の言語を表現するには不十分です。
そこで、複数バイトで1文字を表現することで、対応する文字を増やしたものがマルチバイト文字です。
今回はその中でも最も一般的なUnicodeを紹介します。
Unicode
Unicodeとは、文字コードの国際的な業界標準です。
2020年3月リリースのUnicode13.0.0には、世界中の143,859字を収録しています。
Unicodeでは1文字を、4バイト固定のコードポイントで表現します。
UTF-8
UTF-8(UnicodeTransformationFormat-8)とは、Unicodeの符号化方式のひとつです。
UnicodeのコードポイントをUTF-8で符号化して使用します。
UTF-8では1文字を、1~6バイトの16進数で表現します。
HTML5は、UTF-8を用いた符号化を推奨しています。
例)Unicode『あ』
U+00003042(4バイト)
↓UTF-8で符号化
0xE38182(3バイト)
※U+はUnicode(16進数)
※0xは16進数の目印
C言語では、マルチバイト文字を扱う際もバイト単位での操作しか行うことができません。
そのため、マルチバイト文字の操作を行うためには、そのバイトがどんな要素なのか調べる必要があります。
UTF-8におけるマルチバイト文字の判別方法についてまとめます。
UTF-8のマルチバイト文字判別
C言語におけるマルチバイト文字の判別方法は1つではありません。
今回は、標準ライブラリの関数を使う方法とUTF-8の特徴を利用する方法の2つを紹介します。
stdlib.hのmblen関数を使う
おそらく最もオーソドックスな方法です。
mblen関数は文字列の先頭バイトが何バイト文字なのか出力します。
// mblen関数例-コード #include <locale.h>; #include <stdio.h>; #include <stdlib.h>; void main() { char str[] = "mblen関数"; setlocale(LC_CTYPE, ""); for (int i = 0; str[i] != '\0'; i++) { int n = mblen(str + i, MB_CUR_MAX); printf("%d, ", n); } printf("\n"); return; }
// mblen関数例-結果 1, 1, 1, 1, 1, 3, -1, -1, 3, -1, -1,
上位ビットの状態を調べる
文字コードの仕組みを利用する方法です。
UTF-8では、その文字が何バイト文字なのかによって、上位ビットの値が異なります。
つまり、表のようにマスクすることで、何バイト文字なのか判別できます。
判定 | 上位ビットの状態 |
---|---|
1バイト文字 | 0xxx xxxx |
マルチバイト文字の 2バイト目以降 |
10xx xxxx |
2バイト文字の先頭 | 110x xxxx |
3バイト文字の先頭 | 1110 xxxx |
4バイト文字の先頭 | 1111 0xxx |
5バイト文字の先頭 | 1111 10xx |
6バイト文字の先頭 | 1111 110x |
判定 | マスク (2進数) |
判定条件 (2進数) |
---|---|---|
1バイト文字 | 1000 0000 | 0000 0000 |
マルチバイト文字の 2バイト目以降 |
1100 0000 | 1000 0000 |
2バイト文字の先頭 | 1110 0000 | 1100 0000 |
3バイト文字の先頭 | 1111 0000 | 1110 0000 |
4バイト文字の先頭 | 1111 1000 | 1111 0000 |
5バイト文字の先頭 | 1111 1100 | 1111 1000 |
6バイト文字の先頭 | 1111 1110 | 1111 1100 |
判定 | マスク (16進数) |
判定条件 (16進数) |
---|---|---|
1バイト文字 | 80 | 00 |
マルチバイト文字の 2バイト目以降 |
C0 | 80 |
2バイト文字の先頭 | E0 | C0 |
3バイト文字の先頭 | F0 | E0 |
4バイト文字の先頭 | F8 | F0 |
5バイト文字の先頭 | FC | F8 |
6バイト文字の先頭 | FE | FC |
今回のstrrev関数の作成では、文字コードの学習もかねて、上位ビットの状態を調べる方法を使用しました。
それでは、実際のソースコードを見てみましょう。
マルチバイト文字対応my_strrev解説
ヘッダーファイルmy_strrev.hにまとめています。
// my_strrev.h #ifndef SUB_H #define SUB_H #include <stdio.h>; #include <stdlib.h>; #include <string.h>; void my_strrev(char *str); int my_mblen(const char *str); #endif void my_strrev(char *str) { char *new_text; new_text = (char *)calloc(strlen(str) + 1, sizeof(char)); // 文末から文頭に向けて1バイトずつ確認 for (int i = (strlen(str) - 1); i >;= 0; i--) { int count = my_mblen(str + i); // 1バイト以上の文字の先頭 if (count >; 0) { char *chr; chr = (char *)calloc(count + 1, sizeof(char)); // 複数バイトをまとめて1文字文コピー for (int j = 0; j < count; j++) { chr[j + 0] = str[i + j]; chr[j + 1] = '\0'; } // コピーした1文字を新文字列に追加 strcat(new_text, chr); free(chr); } // マルチバイト文字の2バイト目以降 else if (count == -1) { ; } // エラー else { free(new_text); exit(1); } } // 新文字列を元文字列にコピー strcpy(str, new_text); free(new_text); return; } int my_mblen(const char *str) { int value; if ((*str & 0xFE) == 0xFC) { value = 6; // fprintf(stdout, "%dバイト文字の先頭\n", value); } else if ((*str & 0xFC) == 0xF8) { value = 5; // fprintf(stdout, "%dバイト文字の先頭\n", value); } else if ((*str & 0xF8) == 0xF0) { value = 4; // fprintf(stdout, "%dバイト文字の先頭\n", value); } else if ((*str & 0xF0) == 0xE0) { value = 3; // fprintf(stdout, "%dバイト文字の先頭\n", value); } else if ((*str & 0xE0) == 0xC0) { value = 2; // fprintf(stdout, "%dバイト文字の先頭\n", value); } else if ((*str & 0xC0) == 0x80) { value = -1; // fprintf(stdout, "マルチバイト文字の2バイト目以降\n"); } else if ((*str & 0x80) == 0x00) { value = 1; // fprintf(stdout, "%dバイト文字\n", value); } else { value = 0; fprintf(stderr, "エラー:非対応文字\n"); } return value; }
my_strrev関数は、
- 文字列の最後尾から1バイトずつ確認
- 文字の先頭を見つけたら複数バイトをまとめて1文字分コピー
- コピーした1文字を新文字列に追加
- 文字列の先頭まで1~3を繰り返す
- 新文字列を元文字列にコピー
によって、文字列を反転させます。
my_mblen関数は、そのバイトがマルチバイト文字のどこに相当するかを返します。
マスクと判定条件は、先述のものを使用しています。
分類 | 返り値 |
---|---|
6バイト文字の先頭バイト | 6 |
5バイト文字の先頭バイト | 5 |
4バイト文字の先頭バイト | 4 |
3バイト文字の先頭バイト | 3 |
2バイト文字の先頭バイト | 2 |
マルチバイト文字の 2バイト目以降 |
-1 |
1バイト文字 | 1 |
エラー | 0 |
以下のようにインクルードすることで、my_strrev関数とmy_mblen関数が使用可能になります。
// my_strrev.h使用例 #include <stdio.h>; #include "my_strrev.h" void main() { char str[] = "strrev関数"; // my_mblen関数 for (int i = 0; str[i] != '\0'; i++) { int n = my_mblen(str + i); printf("[%d]:%d\n", i, n); } // my_strrev関数 printf("%s\n", str); my_strrev(str); printf("%s\n", str); return; }
まとめ
今回はC言語における文字列の注意点について解説しました。
マルチバイト文字の仕組みを知ることで、C言語でも文字列の複雑な操作ができます。
使いたい関数がマルチバイト文字に対応していないときには、自作に挑戦してみてはいかがでしょうか。