文字と文字列と文字コードのお話


プログラミングをする上で文字にかかわることを避けることはほぼ出来ない。
今回は文字に関する概要をまとめていく。

文字と文字列について

文字とは1字の事を指し、文字列とは文字の集合体を指します。
ただし言語や文字コードによっては1字でも文字列として扱わなくてはいけない場合があります。
これは後述で説明します。

文字コードについて

文字コードは文字の表のことを指します。
コンピュータは数値しか扱えないため、「この数値の時はこの文字として扱いなさい」と決めてあげる必要があります。
その時、どの数値がどの文字に対応するかを決めるのが文字コードで、ASCII、Shift-JIS、Unicodeなどの種類があります。

シングルバイト文字とマルチバイト文字について

1byteで表現する文字をシングルバイト文字、複数byteで表現するものマルチバイト文字といいます。
これらは文字コードの種類によって分かれます。
ASCIIはシングルバイト文字、Shift-JISとEUCとUnicodeはマルチバイト文字となります。
マルチバイトは固定長のものと可変長のものがあります。

各文字コードの簡単な説明

ASCII

1byteで英数字と記号を扱う

Shift-JIS

1~2byteの可変長でASCIIに日本語を追加した文字コード。
半角は1byte、全角は2byteで扱う。

Unicode

世界的に使われる文字コードで複数のスキームが存在する。
よく使われるスキームを以下に抜粋する。

UTF-8

1~4byteの可変長文字列で、ASCIIをベースに様々な文字を扱える文字コード。
最近のネットではほとんどUTF-8が使われる。

UTF-16

2byteの固定長文字列で使える幅が割りと広い文字コード。
C#の内部文字コードはUTF-16を採用している。

UTF-32

4byteの固定長文字列でUTF-16より更に幅広い文字を扱える文字コード。

■余談1
UTF-16は正確には固定長ではなく、サロゲートペアを使う可変長である。
しかし、実務レベルにおいて可変長として扱うことはないため
固定長として扱ったほうがプログラムとして都合がよいことが多い。

プログラミング言語における文字の扱いについて

基本知識

C,C++のchar型は1byteでC#のchar型は2byteである。
そのため、C#はUTF-16の全ての文字を扱うことが可能だが
C,C++の場合、マルチバイト文字を扱うことは出来ない。

C#

Char.cs
// 正常に動作する
char c = 'あ';

C++

Char.cpp
// マルチバイト文字であるため、正常に動作しない(1byteに切り詰められる)
char c = 'あ';

■余談2
Cでは、sizeof('a')とsizeof('あ')は、sizeof(int)と同等であるが
C++では、sizeof('a')はsizeof(char)と、sizeof('あ')はsizeof(int)と同等である。
これはC++では型の厳密性が高くなったために変更されたと思われる。

環境というものを知る

環境とは、プログラミングを行う状況のことであり
実務レベルでいうとプログラミング開発ツール(と厳密には追加でCPU)のことである。
環境によって使用する文字コードは変化するため、以下のコードはShift-JIS環境とUTF-8環境ではまるで意味が異なる。

Char.cpp
// Shift-JIS環境では1byteであるため正常に動作するが、UTF-8環境では3byteであるため警告がでる
char c = 'ア';

■余談3
環境というのはchar型のビット数にも影響がある。
C,C++のchar型は1byteと決まっているが、1byteが8bitであるということは決まっていない。
そのため、1byteが16bitの環境も組み込み系で存在するらしい。

ワイド文字の登場

C,C++にはwchar_tというワイド文字型というのが存在する。
ワイド文字は固定長の文字を扱う型で、環境により2byteと4byteの場合がある。
大抵の場合、これらの型にはUTF-16かUTF-32を当てはめて使用する。
なお、C++11には環境に依存しないchar16_t, char32_tという型が存在している。
wchar_t型を扱うコードは以下となる。

wchar_t.cpp
// 文字の頭にLをつける
wchar_t wc = L'あ';

ロケールのお話

wchar_tを正式に使う場合、現在の環境をsetlocalで設定してあげる必要があります。
以下はShift-JISの環境下でロケールをセットしてあげる手順です。

local.cpp
#include <stdio.h>
#include <locale.h>

int main()
{
    // ワイド文字が使用するロケールがわからないため、正常に表示されない
    wprintf(L"%c\n", L'あ');

    // ロケールを設定してやる(Shift-JISであることを教える)
    setlocale(LC_ALL, "Japanese");

    // 正常に表示される
    wprintf(L"%c\n", L'い');
}

文字列の終端を知る方法

プログラミング上で文字列を管理するとき、その終端を知る方法は2つある。
一つは終端文字(NULL文字)を設定すること。
一つは文字列の長さを記録しておき、先頭から長さで終端と判定すること。

Cは終端文字判定、C++は終端文字判定と長さの両方、C#は長さで判定することが出来ます。
各長所と短所について説明します。

■終端文字判定
・長所
どれだけ長い文字列でも終端判定に必要なのは1byteである。

・短所
1byteずつ判定する以外に文字列の終端を検索する方法がないため、文字列が長いと終端判定まで時間がかかる。

■長さ判定
・長所
終端の位置が即座にわかる。

・短所
長さを確保するメモリ領域が必要となる。(大抵の場合4byte)

総合的に判断すると、長さを保持していた場合のほうがメリットが大きいです。
今時、文字列ごとの長さ保持の4byteが負担になることはありませんし、文字列連結などの
処理に負担をかけることのほうがコストが大きいためです。

■余談4
あるメール受信プログラムで、複数のメールの文字列を一つの文字列にまとめて表示しようとしたところ
strcatで連結していたら30分かかっていたが、連結後の終端文字のポインタを返す自作のstrcatを作って
連結したところ2秒で終わったという。
そのくらい、文字列の終端を検索するという行為は遅い。

■余談5
プログラミング言語pascalでは先頭の8bitに文字列の長さを詰めて管理していました。
そのため、pascalでは256文字より大きい文字列は扱えません。