ヌル終端文字列

ヌル終端文字列とは



コンピュータプログラミングにおいて、ヌル終端文字列(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をヌル文字として定義しているため、ヌル終端文字列にヌル文字をそのまま含めることはできません。そのため、ヌル文字を含まない、あるいはヌル文字を別の文字または文字シーケンスで代替した、ASCIIUnicodeのサブセットを使用することがあります。

いくつかのシステムでは、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`など)。

もう一度検索

【記事の利用について】

タイトルと記事文章は、記事のあるページにリンクを張っていただければ、無料で利用できます。
※画像は、利用できませんのでご注意ください。

【リンクついて】

リンクフリーです。