UTF-8 解答ページ | Programming Place Plus 新C++編

トップページ新C++編UTF-8

このページの概要

このページは、練習問題の解答例や解説のページです。



解答・解説

問題1 (確認★)

ASCII で正常に読めるように作られたテキストファイルを、UTF-8 (BOM付き) で保存しなおしたら、ファイルの大きさは増えますか?


UTF-8 では、ASCII で表現できる文字は、ASCII とまったく同じ表現になります(本編解説)。したがって、ASCII で表現できていたのなら、UTF-8 にしても変化しないので、大きさは変わりません。

しかし、BOM付きにする場合は、BOM の分だけ(3バイト)大きさが増えます(本編解説)。

テキストエディタで適当なファイルを作って、まず ASCII で保存したときのサイズを調べ、UTF-8 (BOM付き) で保存しなおしてサイズを調べてみるといいでしょう。Windows なら「メモ帳」で試すとやりやすいです。【ファイル】>【名前を付けて保存】を選び、【文字コード】を切り替えて保存します。「ANSI」が ASCII を意味しています。「UTF-8 (BOM付き)」で保存しなおすと、3バイト増えます。

問題2 (応用★★)

UTF-8 (BOM付き) のテキストファイルを用意して、そのファイルの内容を読み取ると、どうなるか確認してみてください。また、BOM を取り除いた部分だけを読み取るにはどうすれば良いでしょうか?


UTF-8 (BOM付き) のテキストファイルは、適当なテキストエディタで作成します。Windows の「メモ帳」でも、【ファイル】>【名前を付けて保存】を選び、【文字コード】を「UTF-8 (BOM付き)」で保存すれば作成できます。

ここでは、「あいうえお」とだけ書き込んだファイルにしたとします。この場合、「あいうえお」の手前に BOM の 3バイトが付加された状態になります。そのため、ファイル全体を普通に読み込んで出力させると、「あいうえお」の手前に余計なゴミのようなものが出力されます。

#include <fstream>
#include <iostream>
#include <string>
#ifdef _WIN32
#define NOMINMAX
#include <Windows.h>
#endif

int main()
{
#ifdef _WIN32
    SetConsoleOutputCP(CP_UTF8);  // コマンドプロンプトでの表示に UTF-8 を使う
#endif

    std::ifstream ifs("test.txt");
    std::string s {};
    ifs >> s;
    std::cout << s << "\n";
}

実行結果 (UTF-8 で表示):

?あいうえお

BOM を取り除くためには、先頭の 3バイトを捨てればいいです。これまでのページの知識でできるのは、1度普通に読み込んだあと、std::string の substr関数(「マルチバイト文字」のページを参照)を使って、先頭の 3文字を捨てることです。

#include <fstream>
#include <iostream>
#include <string>
#ifdef _WIN32
#define NOMINMAX
#include <Windows.h>
#endif

int main()
{
#ifdef _WIN32
    SetConsoleOutputCP(CP_UTF8);  // コマンドプロンプトでの表示に UTF-8 を使う
#endif

    std::ifstream ifs("test.txt");
    std::string s {};
    ifs >> s;
    s = s.substr(3); // BOM を捨てる
    std::cout << s << "\n";
}

実行結果 (UTF-8 で表示):

あいうえお

(補足)BOM を確認のうえで捨てるには

UTF-8 の BOM は 16進数表記で 0xEF 0xBB 0xBF です。そこで、本当に先頭の 3バイトが BOM であることを確認したうえで取り除くほうが確実といえます。16進数表記の整数を扱う方法はまだ説明していませんが、次のようにして実現できます。

#include <fstream>
#include <iostream>
#include <string>
#ifdef _WIN32
#define NOMINMAX
#include <Windows.h>
#endif

int main()
{
#ifdef _WIN32
    SetConsoleOutputCP(CP_UTF8);
#endif

    std::ifstream ifs("test.txt");
    std::string s {};
    ifs >> s;

    // BOM があれば捨てる
    if (s.length() >= 3) {
        if (static_cast<unsigned char>(s[0]) == 0xEF
         && static_cast<unsigned char>(s[1]) == 0xBB
         && static_cast<unsigned char>(s[2]) == 0xBF
        ) {
            s = s.substr(3);
        }
    }

    std::cout << s << "\n";
}

実行結果 (UTF-8 で表示):

あいうえお

BOM をチェックするにあたって、まず 3バイト以上読み込めていることを確認します。BOM が付いていない場合、ファイルの大きさが 3バイトに満たない可能性があるため、範囲外アクセスを起こしかねないからです。

std::string の要素は char型ですが、これは符号の有無が処理系定義です(「整数型」のページを参照)。BOM を構成する各バイト (0xEF 0xBB 0xBF) はいずれも、10進数でいえば +128以上の数なので(それぞれ、239、187、191)、もし符号付き整数型 (signed char に相当) する型であった場合、表現の上限値(+127)を超えてしまうため、負数であるかのように扱われることになります。そのため、そのまま s[0] == 0xEF のような比較を行ってしまうと正しい結果になりません。そこで、unsigned char型にキャストして、確実に 0xEF 0xBB 0xBF を表現できるようにしたうえで比較させています。

問題3 (調査★★)

UTF-8 で、トランプのマークを1つ表現するには、何バイト必要でしょうか?


インターネットで UTF-8 の文字の一覧を探してみるのもいいですし、プログラムで試してみるのもいいでしょう。

「♠」を調べてみると、UTF-8 では 16進数で e299a0 であることが分かります。Windows の「電卓」で、16進数 e299a0 が、2進数で何桁あるのか調べてみると、24ビットであることが分かるので、これは 3バイトのようです(「電卓」の使い方は、Windows編>「電卓で基数変換する」を参照してください)。

2660 を見つけたかもしれませんが、それは Unicode のコードポイントです。UTF-8 での表現とは別のものです(本編解説)。

std::string の変数に入れて、length関数を使っていくつが返ってくるか見るのも一つの手です。

#include <iostream>
#include <string>

int main()
{
    std::string s {u8"♠"};
    std::cout << s.length() << "\n";
}

実行結果:

3

std::string の length関数が返す値は、「見た目どおりの文字数」ではなく、「char型で何個分の文字があるか」です(「マルチバイト文字」のページを参照)。3 が返されてきたということは、char型で 3個分、つまり 3バイトということになります。


参考リンク



更新履歴




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る