ヌル終端文字列とは
コンピュータプログラミングにおいて、ヌル終端
文字列(null-terminated string)とは、
文字の
配列の終端を、特殊な
文字である
ヌル文字(`\0`)で示す
文字列の表現方法です。これは主に
C言語で使用されるため、C
文字列(C string)とも呼ばれます。また、
ASCIIコードの後にゼロが続くことから、
ASCIIZとも呼ばれることがあります。他にも、ゼロ終端
文字列(zero-terminated string)という呼び方もあります。
ヌル終端
文字列の重要な特徴として、その長さは
文字列の先頭から
ヌル文字を探すことでしか判別できない点が挙げられます。そのため、
文字列の長さを求める計算量は、
文字列の長さに比例します(O(n))。さらに、
ヌル文字そのものを
文字列内に含めることはできません。必ず
文字列の終端に1つだけ存在します。
歴史
ヌル終端
文字列の起源は、
PDP-11の
アセンブリ言語の`.ASCIZ`
ディレクティブや、
PDP-10のMACRO-10の`ASCIZ`
ディレクティブに遡ります。これらの技術は
C言語開発以前から存在していましたが、その後、他の形式の
文字列も広く用いられるようになりました。
C言語開発当時、メモリ資源は非常に限られていました。そのため、
文字列長を格納するためのオーバーヘッドを1バイトに抑えられるヌル終端
文字列は、非常に魅力的でした。当時よく使われていた「
Pascal文字列」では、
文字列長を
配列の先頭に1バイトの数値で格納していました。この方式では、
文字列の最大長は255
文字に制限されますが、
ヌル文字を
文字列に含めることができ、
文字列長を求めるのは1回のメモリアクセスで済む(O(1))というメリットがありました。
しかし、
C言語の開発者であるデニス・リッチーは、既にBCPLで確立されていたヌル終端方式を採用しました。これは、
文字列長の制限を避けるためと、
文字列長を保持するよりも終端
文字で示す方が自身の経験上使いやすかったためです。
C言語の設計は、CPUの命令セットにも影響を与えました。1970年代から1980年代のCPU(例えば、ザイログの
Z80やDECの
VAX)には、
文字列長が先頭に置かれた
文字列を処理する命令が存在しましたが、ヌル終端
文字列の普及により、CPU設計者はヌル終端
文字列を考慮に入れるようになりました。例えば、IBMは1992年にIBM ES/9000 520に「Logical String Assist」命令を追加しました。
FreeBSDの開発者ポール=ヘニング・カンプは、2バイトの
文字列長の利用に対するC
文字列の勝利を、「最も高価な1バイトの間違い」と評しました。
実装
C言語では、ヌル終端
文字列が基本的な
文字列型として実装されています。
標準Cライブラリには、ヌル終端
文字列を扱うための以下の様な関数が豊富に用意されています。
文字列の長さを求める
文字列を別の
文字列にコピーする
文字列を別の
文字列に追加する(結合する)
文字列内で特定の
文字が最初(または最後)に出現する場所を特定する
文字列内で指定された
文字群を含む(または含まない)最初の場所を特定する
文字列内で指定された
文字列が最初に出現する場所を特定する
2つの
文字列を辞書順で比較する
文字列を分割する
数値や
文字列を出力可能な形式に変換する
文字列を数値に変換する
1バイト
文字や
マルチバイト文字の
文字列と
ワイド文字の
文字列を相互に変換する (C95以降)
制限
ヌル終端
文字列は、実装がシンプルである反面、エラーやパフォーマンスの問題を引き起こしやすいという欠点があります。
セキュリティ上のリスク:
文字列を宣言する際に、
ヌル文字のための領域を確保し忘れると、
文字列が最大長に達した場合、
ヌル文字が隣接したメモリ領域に書き込まれる可能性があります。
ヌル文字を格納し忘れることもバグの原因となります。テスト時、以前使用したメモリ領域に
ヌル文字が偶然残っていると、バグが発見されないことがあります。
固定サイズのバッファに
文字列をコピーする際、バッファのサイズを考慮しないと、バッファオーバーランが発生する危険性があります。
文字列とバイナリデータの区別:
[文字]]列にヌル文字]を格納できないため、[[文字列データとバイナリデータを明確に区別し、それぞれ異なる関数で処理する必要があります。
文字列長の計算:
*
文字列長を求める際に、
文字列全体を走査する必要があるため、計算量O(n)となり、効率が悪い場合があります。
ただし、
文字列長の計算は、他のO(n)の処理と組み合わせて使用することで、効率を改善できる場合もあります。`strlcpy`の実装はそのような例です。
文字エンコード
ヌル終端
文字列では、
文字配列内の値が0の要素が終端記号として使われるため、値が0となる
文字を含まないエンコード方式が必要です。1バイト単位でエンコードする場合は、値が0となるバイトを含めることはできません。
ASCIIでは0x00を、
UnicodeではU+0000を
ヌル文字として定義しているため、ヌル終端
文字列に
ヌル文字をそのまま含めることはできません。そのため、
ヌル文字を含まない、あるいは
ヌル文字を別の
文字または
文字シーケンスで代替した、
ASCIIや
Unicodeのサブセットを使用することがあります。
いくつかのシステムでは、
UTF-8の代わりに「修正
UTF-8」(Modified
UTF-8)を使用します。これは、
ヌル文字を2つの0でないバイト(0xC0, 0x80)で表現し、ヌル終端
文字列に格納できるようにしたものです。ただし、これはセキュリティ上のリスクがあるため、標準の
UTF-8の規格外です。修正
UTF-8では、C0 80 NULはセキュリティ確認では
文字列終端とみなされるかもしれませんが、実際の使用時には
文字として扱われる可能性があります。
Javaの
文字列クラス`String`はヌル終端ではなく、長さ情報を別途保持しているため、内部シーケンスに
ヌル文字を直接含めることができます。ただし、エンコードを指定してバイト
配列から
Java文字列を生成する場合や、
Java Native Interfaceで
Java文字列を
C言語の`char`型ヌル終端
文字列に変換する場合など、修正
UTF-8がエンコードとして使用されることがあります。
UTF-16はエンコーディングの単位に2バイト(16
ビット)の整数値を使用し、上位バイト/下位バイトの両方あるいはいずれかの値が0になり得るので、1バイト(8
ビット)単位のヌル終端
文字列に格納することはできません。しかし、いくつかの言語やライブラリでは、2バイト
整数型を要素とする
配列を用いて、16
ビットの
ヌル文字で終端することで
UTF-16のヌル終端
文字列を実装しています。この場合、従来の1バイトの
ヌル文字を想定している
文字列操作関数は使用できず、16
ビットのヌル終端
文字列専用の関数が必要となります。
Microsoft Windowsでは、
ワイド文字が2バイト
文字型として定義され、
ワイド文字の
配列を
UTF-16のヌル終端
文字列として扱います。
発展
C
文字列の処理における誤りを減らすため、様々な対策が講じられてきました。
標準Cライブラリでは、`gets`や`strcpy`のような危険な関数を廃止し、より安全で使いやすい`gets_s`や`strlcpy/strcpy_s`、`strdup`などの関数が追加されました。また、C
文字列にオブジェクト指向のラッパーを追加し、安全な呼び出しのみを許可する方法も試みられています。
現代の実行環境では、メモリ空間が32
ビット以上で、仮想メモリ機構をサポートし、物理メモリも十分に搭載されているため、
文字列長を格納する領域を過剰に節約する必要性は薄れてきました。そのため、多バイトの
文字列長も許容されるようになりました。
文字列長の保持に使用される領域によるメモリオーバーヘッドが懸念されるような、小さな
文字列が多数ある場合でも、
ハッシュテーブルや
コピーオンライト(
参照カウント)を使用することで、より少ないメモリで管理できるようになっています。
C
文字列の後継として、カプセル化された
データ構造内に、
文字列長を格納するための32
ビット以上のサイズのフィールドを持つものが多くなっています。例えば、
C++のSTLの`std::string`、
Qtの`QString`、ATL/MFCの`CString`、
Core Foundationの`CFString`、Foundation Kitの`NSString`などがあります。これらのより複雑な
文字列格納構造を、string(ひも)に対してrope(ロープ)と呼ぶことがあります。
文字列長を保持するフィールドの値と、実際にバッファに格納されているデータの整合性・一貫性を保つため、通常の
文字列は
イミュータブルな
データ型として、読み取り操作のみを許可する設計となっている言語も多くあります。
文字列の連結や部分
文字列の取得をする際は、新しい
文字列のインスタンスを返すようになっており、
文字列のバッファを直接操作する場合は専用のクラスを使用します(
Javaの`StringBuilder`など)。