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

公開:2020年08月04日

はじめまして、新人エンジニアのRです。
入社した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バイト文字 0— —-
マルチバイト文字の
2バイト目以降
10– —-
2バイト文字の
先頭
110- —-
3バイト文字の
先頭
1110 —-
4バイト文字の
先頭
1111 0—
5バイト文字の
先頭
1111 10–
6バイト文字の
先頭
1111 110-
判定 マスク
(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バイトずつ確認
  2. 文字の先頭を見つけたら複数バイトをまとめて1文字分コピー
  3. コピーした1文字を新文字列に追加
  4. 文字列の先頭まで1~3を繰り返す
  5. 新文字列を元文字列にコピー

によって、文字列を反転させます。

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言語でも文字列の複雑な操作ができます。
使いたい関数がマルチバイト文字に対応していないときには、自作に挑戦してみてはいかがでしょうか。

この記事が気に入ったら
いいね ! しよう

Twitter でフォームズのブログを
フォームズ編集部
オフィスで働く方、ホームページを運営されている皆様へ
仕事の効率化、ビジネススキル、ITノウハウなど役立つ情報をお届けします。