C++の文字コードを理解するときは、ひとつの話としてまとめて考えないことが大切です。
実際には、いくつか別の概念が重なっています。
まず、文字そのものに番号を割り当てる仕組みがあります。
次に、その番号をどのようなデータの並びとして保存するかという仕組みがあります。
さらに、C++の型としてどの文字型を使うか、ソースコードの保存形式は何か、入出力先がどの文字コードを期待しているか、といった話も加わります。
この区別が曖昧なままだと、文字化けや文字数の誤認、文字列の切り出し失敗などが起こりやすくなります。
UnicodeとUTF-8は同じではない
文字コードの説明では、UnicodeとUTF-8が同じもののように扱われることがありますが、これは正確ではありません。
Unicodeは、世界中の文字に番号を割り当てる仕組みです。
つまり、文字の一覧表に近いものです。
一方で、UTF-8やUTF-16やUTF-32は、その番号を実際のデータとして表現するための方式です。
この違いはとても重要です。
Unicodeは文字の定義であり、UTF-8などはその保存方法です。
ここを分けて理解しておくと、C++の文字列型や文字リテラルの違いも整理しやすくなります。
ASCIIの位置づけ
ASCIIは英数字や一部の記号を表すための古い文字集合です。
英語圏を中心とした基本的な文字だけを扱う前提で作られているため、日本語のような多くの文字は含まれていません。
C++では歴史的に、char がASCII的な文字を扱う感覚で使われることが多くありました。
ただし現代では、char は単純に1バイトのコード単位を格納する型として扱う意識のほうが安全です。
UTF-8の特徴
UTF-8は、Unicodeの文字を8ビット単位の並びで表現する方式です。
英数字のような基本的な文字は1バイトで表され、日本語のひらがなや多くの漢字は3バイトになることが多く、絵文字などは4バイトになることがあります。
UTF-8が広く使われている理由は、ASCIIとの互換性が高く、Webやテキストファイルや多くの開発環境との相性がよいからです。
現代のC++でも、テキストデータの中心をUTF-8に置く考え方はとても実用的です。
ただし、UTF-8は可変長です。
見た目では1文字でも、内部では複数バイトになっていることがあります。
この点が、日本語処理で特に重要になります。
UTF-16とUTF-32の特徴
UTF-16は16ビット単位でUnicode文字を表す方式です。
多くの文字は1個の16ビット単位で表せますが、一部の文字、特に補助平面の文字は2個の16ビット単位を使って表されます。これがサロゲートペアです。
UTF-32は32ビット単位で表す方式で、1つのコードポイントを1つの32ビット値で扱いやすいという利点があります。
その一方で、メモリ消費は大きくなります。
UTF-16やUTF-32は、文字処理の場面では便利なことがありますが、ファイル保存やネットワーク通信ではUTF-8のほうが扱いやすいことが多くあります。
C++の文字型は文字コードそのものではない
C++には複数の文字型がありますが、これらはそのまま文字コードそのものを意味するわけではありません。
ここは誤解しやすい部分です。
char は基本的な文字型ですが、UTF-8の文脈では1文字を表す型というより、1コード単位を持つ型として考えるほうが安全です。
char8_t はUTF-8のコード単位を表すための型です。
これはC++20で導入されました。
char16_t はUTF-16のコード単位を扱うための型であり、char32_t はUTF-32のコード単位を扱うための型です。
wchar_t はワイド文字型ですが、サイズも意味合いも処理系に依存します。
そのため、移植性の高いUnicode共通型とみなすのは危険です。
char の理解で重要なこと
char はC++の基本的な文字型ですが、現代的な文字コードの理解では「1文字」ではなく「1バイトのコード単位」として扱うほうが実際に合っています。
特にUTF-8では、英数字なら1文字が1バイトでも、日本語や絵文字は複数バイトになることがあります。
そのため、char ひとつを1文字と考えると、多バイト文字を正しく扱えません。
この認識は、文字列の長さを考えるときにも、1文字ずつ処理しようとするときにも非常に大切です。
wchar_t は環境によって意味が変わる
wchar_t は便利そうに見えますが、扱いには注意が必要です。
理由は、サイズや内部表現が実装依存だからです。
実務上は、Windowsでは16ビットで使われることが多く、UTF-16のコード単位に近い扱いになります。
一方で、多くの非Windows環境では32ビットで使われることが多く、UTF-32のコード単位に近い扱いになることがあります。
ただし、これは規格として一律に保証されているわけではありません。
環境によって振る舞いが変わるため、wchar_t を絶対的なUnicode標準型のように考えるのは避けたほうが安全です。
char8_t とUTF-8文字列リテラルの関係
C++20以降では、UTF-8文字列リテラルは char8_t 系の型として扱われます。
これはUTF-8であることを型の上でも明確にできるという意味で、意図が伝わりやすい仕組みです。
ただし、この変更によって、従来の char ベースの文字列とはそのまま接続しにくい場面も出てきます。
つまり、UTF-8を明示したいときには便利である一方で、既存の std::string 中心のコードやライブラリとの境界では注意が必要です。
この点は、C++20以降の文字コードまわりで特に押さえておきたいところです。
std::string はUnicode専用型ではない
std::string は、Unicode文字列専用の型ではありません。
中身は char の並びです。
したがって、UTF-8を格納することはできますが、それは「UTF-8文字列専用の高水準型」というより、「UTF-8のコード単位列を持っている」という理解が正確です。
そのため、std::string の長さは見た目の文字数を返すわけではありません。
返るのは、あくまで char 要素の個数です。
UTF-8の文字列なら、通常はコード単位数になり、実務上はほぼバイト数と考えて差し支えありません。
ここを文字数だと思い込むと、日本語や絵文字を含む文字列で簡単に誤りが起きます。
文字数とバイト数は一致しない
C++で日本語を扱うとき、もっとも重要な注意点のひとつがこれです。
見た目の文字数と、内部のバイト数は一致しません。
UTF-8では、英数字は1バイトで表されることが多い一方、日本語は複数バイトになることが多くあります。
そのため、見た目では短い文字列でも、内部的にはもっと長いデータ列になっています。
この違いを無視すると、文字列の長さ判定、切り出し、途中削除、1文字ずつの処理など、さまざまな操作で不具合が起きます。
コード単位とコードポイントは違う
UTF-8やUTF-16を理解するうえでは、コード単位とコードポイントの違いも大切です。
コードポイントは、Unicodeが割り当てている文字番号です。
一方で、コード単位は、その番号を実際に保存する際の単位です。
UTF-8では8ビット単位、UTF-16では16ビット単位、UTF-32では32ビット単位で保存されます。
つまり、1文字が1コードポイントであっても、保存時には複数のコード単位に分かれることがあります。
C++の char、char8_t、char16_t、char32_t は、このコード単位と強く関係しています。
したがって、文字型を理解するときは、見た目の文字ではなく、コード単位を扱っているという視点が必要です。
コードポイント数と見た目の文字数も一致しない
さらに注意が必要なのは、コードポイント数がそのまま見た目の文字数になるとは限らないことです。
たとえば、濁点の組み合わせ、絵文字の修飾、国旗や家族絵文字のような複合表現では、見た目では1文字に見えても、内部では複数のコードポイントから構成されることがあります。
このため、テキスト処理には少なくとも3段階の数え方が存在します。
バイト数、コードポイント数、見た目の文字数です。これらは一致しない場合があります。
厳密に見た目の文字数を扱うには、書記素クラスタという考え方まで必要になります。
ソースコードの保存形式にも注意が必要
C++では、ソースコードに直接日本語を書くことができますが、それが正しく解釈されるかどうかは、単純に文字列型だけで決まるわけではありません。
ソースファイルが何のエンコーディングで保存されているか、コンパイラがソース文字をどう解釈するか、通常の文字列リテラルをどの文字集合に変換するか、といった要素が関わります。
そのため、文字コードを意識した開発では、ソースファイルをUTF-8で保存し、必要に応じてUTF-8文字列リテラルなどを使って意図を明確にするほうが安全です。
表示先が対応していなければ文字化けする
内部で正しくUTF-8を保持していたとしても、表示先がそのエンコーディングを理解できなければ文字化けします。
これは、文字列の中身が正しいかどうかと、画面に正しく表示されるかどうかは別問題だということです。
コンソール、ログ、エディタ、ファイルビューア、OSの設定などが関係します。
つまり、文字コードの問題は「変数の中身」だけで終わりません。
入力元、保存先、出力先まで含めて一貫して考える必要があります。
Windows環境で混乱しやすい理由
Windowsでは文字コード事情が特に複雑になりやすい傾向があります。
歴史的に複数の文字コードが混在してきたためです。
実務では、ファイルやWebやAPIではUTF-8が使われることが多い一方で、WindowsのネイティブなAPI周辺ではUTF-16系が関わることがよくあります。
また、wchar_t の扱いもWindows固有の事情と結びつきやすくなります。
そのため、内部ではUTF-8を中心にしつつ、Windows APIなど外部との境界で必要に応じてUTF-16へ変換する、という考え方はかなり実用的です。
std::wstring の位置づけ
std::wstring は wchar_t の並びです。
環境によって wchar_t の意味が変わるため、これを移植性の高いUnicode共通表現とみなすのは適切ではありません。
一方で、特定の環境、特にWindowsのAPIとの連携では役立つ場面があります。
したがって、常に避けるべき型というより、用途が限定されやすい型として理解するのが自然です。
移植性を重視するなら慎重に使うべきですが、特定環境向けの実装では十分実用的です。
文字コード変換は標準ライブラリだけでは扱いにくい
C++では文字コード変換が必要になる場面がありますが、標準ライブラリだけで快適に完結するとは言いにくいのが実情です。
かつては <codecvt> 系が使われることがありましたが、これは現在では新規実装で積極的に採用する対象ではありません。
古い仕組みとしての性格が強く、現代のUnicode処理に対して十分とは言いづらい部分があります。
そのため、実務では外部ライブラリやOSのAPIなどを活用して変換処理を行うことが多くなります。
よくある誤解
よくある誤解のひとつは、std::string の長さを文字数だと思ってしまうことです。
UTF-8では、これは通常成り立ちません。
次に多いのは、文字列を途中で切り出すときに、任意の位置で区切っても問題ないと思ってしまうことです。
UTF-8は可変長なので、文字の途中で切ると壊れたデータになります。
さらに、char を1つ取り出せば1文字だと考えてしまう誤解もあります。
UTF-8文字列では、char は多くの場合、1文字ではなく1コード単位です。
こうした誤解はすべて、「見た目の文字」と「内部表現」を同じものとして扱ってしまうことから起こります。
実務で考えやすい基本方針
現代のC++で日本語や多言語テキストを扱うなら、基本方針はかなり整理できます。
まず、テキストデータの中心をUTF-8に置くことです。
ソースコードもUTF-8で保存し、ファイルやWebやAPIとのやり取りもUTF-8を基準にすると、一貫性を保ちやすくなります。
次に、std::string や char8_t 系の型を使う場合でも、それが「見た目の文字列」ではなく「コード単位列」であることを忘れないことです。
そして、文字単位の切り出しや文字数カウントのような処理では、単純なバイト操作で済ませず、文字境界や必要に応じて書記素クラスタまで意識することが重要です。
さらに、OSや外部APIとの境界では、その相手が求めるエンコーディングに明示的に変換する、という考え方が安定します。
理解の要点
C++の文字コードで押さえるべき本質はシンプルです。
Unicodeは文字の番号体系であり、UTF-8やUTF-16やUTF-32はその表現方法です。
C++の文字型は、その表現方法と関係は深いものの、単純に一対一で同じものではありません。
std::string はUnicode専用型ではなく、UTF-8を入れたとしても、それは文字数ベースの構造ではありません。
wchar_t や std::wstring は環境依存が大きく、万能な解決策にはなりません。
そして、見た目の文字、コードポイント、コード単位、バイト数は、それぞれ別物です。
この区別がつくようになると、C++の文字コードに関する混乱はかなり減ります。
まとめ
C++の文字コードを正しく理解するには、文字の意味と、保存形式と、C++の型と、入出力環境を分けて考えることが必要です。
UTF-8を中心に考える方針は現代の実務では非常に有効ですが、UTF-8だから何でも簡単になるわけではありません。
特に日本語や絵文字を含む文字列では、1文字が複数バイトになること、見た目の文字数と内部表現が一致しないことを前提にする必要があります。
この前提を押さえたうえで、内部表現、ソースコード、入出力先のエンコーディングを一貫して設計すると、文字化けや文字列処理の不具合を大きく減らせます。
以上、C++の文字コードについてでした。
最後までお読みいただき、ありがとうございました。
