web-dev-qa-db-ja.com

nullで終了する文字列の理由は何ですか?

CとC++が大好きなのと同じように、ヌルで終わる文字列の選択に頭を悩ませるしかありません。

  • Cの前に存在する長さの接頭辞(つまりパスカル)文字列
  • 長さの接頭辞付き文字列は、一定時間の長さの検索を許可することにより、いくつかのアルゴリズムを高速化します。
  • 文字列の長さがプレフィックスされていると、バッファオーバーランエラーが発生しにくくなります。
  • 32ビットマシンでも、使用可能なメモリのサイズを文字列に許可すると、長さの接頭辞付き文字列は、nullで終わる文字列よりも3バイトだけ広くなります。 16ビットマシンでは、これは1バイトです。 64ビットマシンでは、4GBが適切な文字列の長さの制限ですが、マシンのWordのサイズに拡張したい場合でも、通常64ビットマシンには十分なメモリがあり、余分な7バイトの並べ替えがnull引数になります。元々のC標準はめちゃくちゃに貧弱なマシン(メモリの観点から)向けに書かれていたのは知っていますが、効率性の論拠はここでは売れません。
  • 他のほとんどすべての言語(つまり、Perl、Pascal、Python、Java、C#など)は、プレフィックスの付いた文字列を使用します。これらの言語は、文字列を使用したほうが効率的であるため、通常、文字列操作のベンチマークでCに勝っています。
  • C++はstd::basic_stringテンプレートを使用してこれを少し修正しましたが、nullで終了した文字列を期待するプレーンな文字配列は依然として普及しています。ヒープの割り当てが必要なため、これも不完全です。
  • ヌルで終了する文字列は、文字列内に存在できない文字(つまり、null)を予約する必要がありますが、長さの接頭辞付き文字列には埋め込みnullを含めることができます。

これらのことのいくつかは、Cよりも最近明らかになったので、Cがそれらのことを知らないことは理にかなっています。しかし、Cが登場する前のいくつかはかなり単純でした。明らかに優れた長さのプレフィックスの代わりに、ヌルで終わる文字列が選択されるのはなぜですか?

[〜#〜] edit [〜#〜]:一部の人はfactsを要求したので(そしてそれらは好きではなかった上記の効率性のポイントについては、既に説明しましたが、いくつかの点から生じています。

  • ヌル終端文字列を使用する連結では、O(n + m)時間の複雑さが必要です。多くの場合、長さのプレフィックスにはO(m)のみが必要です。
  • ヌル終端文字列を使用する長さには、O(n)時間の複雑さが必要です。長さの接頭辞はO(1)です。
  • 長さと連結は、最も一般的な文字列操作です。ヌルで終了する文字列の方が効率的な場合がいくつかありますが、これらは非常に少ない頻度で発生します。

以下の回答から、これらはヌル終端文字列がより効率的ないくつかのケースです:

  • 文字列の先頭を切り取り、それを何らかのメソッドに渡す必要がある場合。長さの接頭辞はおそらく整列規則に従う必要があるため、元の文字列を破棄することを許可されている場合でも、長さの接頭辞を使用してこれを実際に一定の時間で行うことはできません。
  • 文字列を文字ごとにループするだけの場合、CPUレジスタを保存できる場合があります。これは、文字列を動的に割り当てていない場合にのみ機能することに注意してください(その後、文字列を解放する必要があるため、保存したCPUレジスタを使用して、mallocや友人から最初に取得したポインタを保持する必要があります)。

上記のどれも長さと連結とほぼ同じくらい一般的ではありません。

以下の回答にはもう1つ断言があります。

  • 文字列の終わりを切り取る必要があります

ただし、これは正しくありません。ヌルで終了し、プレフィックスの長さのある文字列の場合と同じ時間です。 (Nullで終了した文字列は、新しい終端を配置したい場所にNULLを貼り付けるだけで、長さのプレフィックスはプレフィックスから単に減算します。)

269
Billy ONeal

馬の口 から

BCPL、B、またはCのいずれも、言語の文字データを強力にサポートしていません。それぞれ、文字列を整数のベクトルのように扱い、いくつかの規則で一般的な規則を補足します。 BCPLとBの両方で、文字列リテラルは、セルにパックされた文字列の文字で初期化された静的領域のアドレスを示します。 BCPLでは、最初のパックされたバイトには文字列の文字数が含まれます。 Bでは、カウントはなく、文字列は特殊文字で終了します。Bは*e。この変更は、8ビットまたは9ビットのスロットにカウントを保持することによって引き起こされる文字列の長さの制限を部分的に回避するために行われました。

デニス・M・リッチー、C言語の開発

190
Hans Passant

Cには、言語の一部として文字列がありません。 Cの「文字列」は、charへの単なるポインタです。だからあなたは間違った質問をしているのかもしれません。

「文字列型を除外する理由は何ですか」がより関連性があるかもしれません。それに対して、Cはオブジェクト指向言語ではなく、基本的な値型のみを持っていることを指摘します。文字列は、他の型の値を何らかの方法で組み合わせて実装する必要がある、より高いレベルの概念です。 Cは抽象化の下位レベルにあります。

以下の激怒のスコールに照らして:

これは馬鹿げた質問や悪い質問だと言っているのではなく、文字列を表現するCの方法が最良の選択であることを指摘したいだけです。 Cには、文字列をデータ型としてバイト配列と区別するメカニズムがないという事実を考慮すると、質問はより簡潔になります。これは、今日のコンピューターの処理能力とメモリ能力を考慮して、最良の選択ですか?おそらくない。しかし、後知恵は常に20/20であり、すべてが:)

151

質問はLength Prefixed Strings (LPS) vs zero terminated strings (SZ)のように求められますが、ほとんどの場合、接頭辞付きの長さの文字列の利点が明らかになります。それは圧倒的に思えるかもしれませんが、正直に言うと、LPSの欠点とSZの利点も考慮する必要があります。

私が理解しているように、質問は「ゼロ終了文字列の利点は何ですか?」と尋ねる偏った方法として理解されるかもしれません。

ゼロ終端文字列の利点(なるほど):

  • 非常にシンプルで、言語に新しい概念を導入する必要はありません。char配列/ charポインターで可能です。
  • コア言語には、二重引用符の間の文字を一連の文字(実際には一連のバイト)に変換するための最小限の構文糖が含まれています。場合によっては、テキストとまったく関係のないものを初期化するために使用できます。たとえば、xpm画像ファイル形式は、文字列としてエンコードされた画像データを含む有効なCソースです。
  • ちなみに、can文字列リテラルにゼロを入れると、コンパイラはリテラルの最後にもう1つ追加するだけです:"this\0is\0valid\0C"。文字列ですか?または4つの文字列?または大量のバイト...
  • フラットな実装、隠された間接性、隠された整数はありません。
  • 隠されたメモリの割り当ては含まれていません(strdupのような悪名高い非標準関数が割り当てを実行しますが、それはほとんど問題の原因です)。
  • 小規模または大規模なハードウェアに特定の問題はありません(8ビットマイクロコントローラーで32ビットのプレフィックス長を管理する負担、または文字列サイズを256バイト未満に制限する制限を想像してください。
  • 文字列操作の実装は、ほんの一握りの非常に単純なライブラリ関数です
  • 文字列の主な使用に効率的:既知の開始点から順番に読み取られる定数テキスト(主にユーザーへのメッセージ)。
  • 終端のゼロは必須ではありません。バイトのような文字を操作するために必要なすべてのツールが利用可能です。 Cで配列の初期化を実行する場合、NULターミネーターを回避することもできます。適切なサイズを設定するだけです。 char a[3] = "foo";は有効なC(C++ではない)であり、aに最後のゼロを挿入しません。
  • uNIXの観点である「すべてはファイル」と一貫しています。これには、stdin、stdoutなどの固有の長さを持たない「ファイル」が含まれます。オープンな読み取りおよび書き込みプリミティブは非常に低いレベルで実装されることを覚えておく必要があります。ライブラリ呼び出しではなく、システム呼び出しです。また、同じAPIがバイナリファイルまたはテキストファイルに使用されます。ファイル読み取りプリミティブは、バッファアドレスとサイズを取得し、新しいサイズを返します。また、書き込むバッファとして文字列を使用できます。別の種類の文字列表現を使用すると、リテラル文字列を出力用のバッファとして簡単に使用できないか、char*にキャストするときに非常に奇妙な動作をさせる必要があります。つまり、文字列のアドレスを返すのではなく、実際のデータを返すということです。
  • バッファの無駄なコピーなしで、ファイルから読み取られたテキストデータを非常に簡単に操作し、正しい場所にゼロを挿入するだけです(二重引用符で囲まれた文字列は、今日では通常変更不可能なデータに保持されているため、実際のCではそうではありませんセグメント)。
  • サイズに関係なくint値を先頭に追加すると、アライメントの問題が発生します。最初の長さは揃える必要がありますが、文字データに対してこれを行う理由はありません(また、文字列を強制的に揃えると、文字列をバイトの束として扱うときに問題が発生します)。
  • 定数リテラル文字列(sizeof)の長さはコンパイル時に既知です。では、なぜ実際のデータの前にメモリに保存したいのでしょうか?
  • cが(ほぼ)他の皆と同じように、文字列はcharの配列と見なされます。配列の長さはCによって管理されていないため、文字列についても論理的な長さは管理されていません。唯一の驚くべきことは、最後に0項目が追加されることですが、二重引用符で囲まれた文字列を入力するときは、コア言語レベルにすぎません。ユーザーは、長さを渡す文字列操作関数を完全に呼び出すか、代わりにプレーンmemcopyを使用することもできます。 SZは単なる施設です。他のほとんどの言語では、配列の長さが管理されており、文字列でも同じです。
  • 現代ではとにかく1バイトの文字セットでは十分ではなく、多くの場合、文字数がバイト数と大きく異なるエンコードされたUnicode文字列を処理する必要があります。これは、ユーザーがおそらく「単なるサイズ」だけでなく、他の情報も必要とすることを意味します。長さを維持しても、これらの他の有用な情報に関しては何も使用しません(特にそれらを保存するための自然な場所はありません)。

そうは言っても、標準C文字列が実際に非効率的であるというまれなケースで文句を言う必要はありません。ライブラリが利用可能です。その傾向に従えば、標準Cには正規表現サポート関数が含まれていないことを不満に思うはずです...しかし、その目的で使用できるライブラリがあるため、それは実際の問題ではないことを誰もが知っています。したがって、文字列操作の効率が必要な場合、 bstring のようなライブラリを使用しないのはなぜですか?それともC++文字列ですか?

[〜#〜] edit [〜#〜]:最近、 D文字列 を見ました。選択したソリューションがサイズプレフィックスでもゼロ終端でもないことを確認するのは十分に興味深いです。 Cのように、二重引用符で囲まれたリテラル文字列は、不変のchar配列の省略形であり、言語にはそれを意味する文字列キーワードもあります(不変のchar配列)。

ただし、D配列はC配列よりもはるかに豊富です。静的配列の場合、実行時に長さがわかっているため、長さを保存する必要はありません。コンパイラはコンパイル時にそれを持っています。動的配列の場合、長さは利用可能ですが、Dのドキュメントには、どこに保存されるかは記載されていません。私たちが知っているすべてのことについて、コンパイラーは、それをあるレジスターに保持するか、文字データから遠く離れて格納されたある変数に保持するかを選択できます。

通常のchar配列または非リテラル文字列には最終的なゼロがないため、プログラマがDからC関数を呼び出したい場合、プログラマーはそれ自体を配置する必要があります。ただし、リテラル文字列の特定のケースでは、Dコンパイラはまだ各文字列の終わり(C文字列に簡単にキャストしてC関数を簡単に呼び出すことができるように?)が、このゼロは文字列の一部ではありません(Dは文字列サイズでカウントしません)。

私を失望させた唯一のことは、文字列がutf-8であることですが、マルチバイト文字を使用している場合でも、長さは明らかにバイト数を返します(少なくとも私のコンパイラgdcでは本当です)。それがコンパイラのバグなのか、それが目的なのかははっきりしていません。 (OK、私はおそらく何が起こったのかを知っているでしょう。Dコンパイラにutf-8を使用すると言うには、最初に愚かなバイトオーダーマークを付ける必要があります。特にUTF- 8 ASCII互換)。

101
kriss

歴史的な理由があり、 これはウィキペディアの

C(およびその派生言語)が開発された時点では、メモリは非常に限られていたため、1バイトのオーバーヘッドを使用して文字列の長さを格納するのが魅力的でした。当時唯一の一般的な代替手段は、通常「パスカル文字列」と呼ばれていましたが(BASICの初期バージョンでも使用されていました)、先頭バイトを使用して文字列の長さを格納していました。これにより、文字列にNULを含めることができ、長さの検索に必要なメモリアクセスは1つだけです(O(1)(一定)時間)。しかし、1バイトは長さを255に制限します。この長さの制限はCストリングの問題よりもはるかに制限的であったため、一般にCストリングが勝ちました。

60
khachik

Calaveraright ですが、人々が彼の主張を理解していないように見えるため、コード例をいくつか示します。

最初に、Cが何であるかを考えてみましょう。すべてのコードが機械語にかなり直接翻訳されている単純な言語です。すべての型はレジスターとスタックに収まり、オペレーティングシステムまたは大きなランタイムライブラリを実行する必要はありません。これらのwriteこの日には競合他社が存在しないことを考えれば、非常に適したタスクです)。

Cがstringintなどのchar型を持っている場合、それはレジスタまたはスタックに収まらない型であり、メモリの割り当てが必要になります(すべてのサポートインフラストラクチャを使用して)任意の方法で処理されます。これらはすべて、Cの基本的な教義に反します。

したがって、Cの文字列は次のとおりです。

char s*;

そのため、長さの接頭辞が付いていると仮定しましょう。 2つの文字列を連結するコードを書きましょう。

char* concat(char* s1, char* s2)
{
    /* What? What is the type of the length of the string? */
    int l1 = *(int*) s1;
    /* How much? How much must I skip? */
    char *s1s = s1 + sizeof(int);
    int l2 = *(int*) s2;
    char *s2s = s2 + sizeof(int);
    int l3 = l1 + l2;
    char *s3 = (char*) malloc(l3 + sizeof(int));
    char *s3s = s3 + sizeof(int);
    memcpy(s3s, s1s, l1);
    memcpy(s3s + l1, s2s, l2);
    *(int*) s3 = l3;
    return s3;
}

別の代替方法は、構造体を使用して文字列を定義することです。

struct {
  int len; /* cannot be left implementation-defined */
  char* buf;
}

この時点で、すべての文字列操作には2つの割り当てが必要になります。これは、実際には、ライブラリを処理してそれを処理することを意味します。

面白いのは...そのような構造体がCに存在するdo!これらは、ユーザー処理への日々のメッセージの表示には使用されません。

したがって、Calaveraのポイントは次のとおりです。Cには文字列型はありません。それで何かをするためには、ポインタを取り、それを2つの異なる型へのポインタとしてデコードする必要があります。それは文字列のサイズと非常に関連性があり、「実装定義」として残すことはできません。

今、Ccanはとにかくメモリを処理し、ライブラリのmem関数(<string.h>、偶数!)メモリーをポインターとサイズのペアとして処理するために必要なすべてのツールを提供します。 Cのいわゆる "strings"は、たった1つの目的のために作成されました。テキスト端末向けのオペレーティングシステムを作成するコンテキストでメッセージを表示します。そして、そのためには、ヌル終端で十分です。

31

明らかにパフォーマンスと安全性のために、strlenまたはそれに相当するものを繰り返し実行するのではなく、文字列を操作している間、文字列の長さを維持したいでしょう。ただし、文字列の内容の直前に固定位置に長さを格納することは、非常に悪い設計です。 JörgenがSanjitの答えに対するコメントで指摘したように、文字列の末尾を文字列として扱うことはできません。たとえば、path_to_filename または filename_to_extension新しいメモリを割り当てることなく(および障害とエラー処理の可能性を招くことなく)不可能。そしてもちろん、文字列の長さフィールドが何バイトを占めるべきか誰も同意できないという問題があります(長い文字列の処理を妨げる16ビットフィールドまたは24ビットフィールドを使用する多くの悪い「パスカル文字列」言語)。

プログラマが長さを保存するかどうか、場所、方法を選択できるようにするCの設計は、はるかに柔軟で強力です。しかし、もちろんプログラマーは賢くなければなりません。 Cは、クラッシュしたり、停止したり、敵にルートを与えたりするプログラムで愚かさを罰します。

19
R..

遅延、レジスターの質素性、および任意の言語のアセンブリー、特にアセンブリーの1ステップであるCを考慮した移植性(したがって、多くのアセンブリーのレガシー・コードを継承します)。ヌル文字はこれらのASCII日では役に立たないので、おそらく(EOF control char)と同じくらい良い)として同意するでしょう。

擬似コードで見てみましょう

function readString(string) // 1 parameter: 1 register or 1 stact entries
    pointer=addressOf(string) 
    while(string[pointer]!=CONTROL_CHAR) do
        read(string[pointer])
        increment pointer

合計1レジスタ使用

ケース2

 function readString(length,string) // 2 parameters: 2 register used or 2 stack entries
     pointer=addressOf(string) 
     while(length>0) do 
         read(string[pointer])
         increment pointer
         decrement length

使用される合計2つのレジスタ

当時は近視眼的に見えるかもしれませんが、コードとレジスターのfru約を考えてみてください(当時はPREMIUMでしたが、当時はパンチカードを使用していました)。したがって、(プロセッサの速度をkHz単位でカウントできる場合)高速であるため、この「ハック」は非常に優れており、レジスタレスプロセッサに簡単に移植できます。

引数のために、2つの一般的な文字列操作を実装します

stringLength(string)
     pointer=addressOf(string)
     while(string[pointer]!=CONTROL_CHAR) do
         increment pointer
     return pointer-addressOf(string)

複雑さO(n)ここで、ほとんどの場合、パスカル文字列はO(1)です。これは、文字列の長さが文字列構造(また、この操作はより早い段階で実行する必要があることを意味します)。

concatString(string1,string2)
     length1=stringLength(string1)
     length2=stringLength(string2)
     string3=allocate(string1+string2)
     pointer1=addressOf(string1)
     pointer3=addressOf(string3)
     while(string1[pointer1]!=CONTROL_CHAR) do
         string3[pointer3]=string1[pointer1]
         increment pointer3
         increment pointer1
     pointer2=addressOf(string2)
     while(string2[pointer2]!=CONTROL_CHAR) do
         string3[pointer3]=string2[pointer2]
         increment pointer3
         increment pointer1
     return string3

複雑さO(n)で、文字列の長さを追加しても操作の複雑さは変わりませんが、時間は3倍短くなります。

一方、Pascal文字列を使用する場合、レジスタの長さとビットエンディアンを考慮してAPIを再設計する必要があります。Pascal文字列は、長さが1バイト(8ビット)で格納されているため、255文字(0xFF) )、さらに長い文字列(16ビット->何でも)が必要な場合は、コードの1つのレイヤーのアーキテクチャを考慮する必要があります。これは、長い文字列が必要な場合、ほとんどの場合、互換性のない文字列APIを意味します.

例:

1つのファイルは、8ビットコンピューターで追加された文字列APIで書き込まれ、32ビットコンピューターで読み取る必要があります。レイジープログラムは、4バイトが文字列の長さであると見なし、その多くのメモリを割り当てますその後、そのバイト数を読み取ろうとします。別の場合は、PPC 32バイト文字列読み取り(リトルエンディアン)をx86(ビッグエンディアン)に読み込みます。もちろん、一方が他方によって書き込まれていることがわからない場合は、問題が発生します。 1バイトの長さ(0x00000001)は16777216(0x0100000)になり、1バイトの文字列を読み取るための16 MBです。もちろん、1つの標準に同意する必要がありますが、16ビットのユニコードでもほとんどエンディアンではありません。

もちろん、Cにも問題がありますが、ここで提起された問題による影響はほとんどありません。

13
dvhh

多くの点で、Cは原始的でした。そして、私はそれを愛していました。

これはアセンブリ言語よりも上位のステップであり、記述と保守がはるかに簡単な言語とほぼ同じパフォーマンスを提供します。

Nullターミネータは単純であり、言語による特別なサポートは必要ありません。

振り返ってみると、それほど便利ではないようです。しかし、私は80年代にアセンブリ言語を使用していましたが、当時は非常に便利でした。ソフトウェアは絶えず進化しており、プラットフォームとツールはますます洗練されていると思います。

9
Jonathan Wood

Cが文字列をPascalの方法で実装したと仮定すると、長さで接頭辞を付けることにより、7文字の文字列は3文字の文字列と同じデータ型ですか?答えが「はい」の場合、前者を後者に割り当てるときにコンパイラはどのようなコードを生成する必要がありますか?文字列は切り捨てられるべきですか、それとも自動的にサイズ変更されますか?サイズを変更する場合、スレッドセーフにするために、その操作をロックで保護する必要がありますか? Cアプローチ側は、好むと好まざるとにかかわらず、これらのすべての問題を回避しました。

8
Cristian

どういうわけか私は質問を理解し、Cの長さ接頭辞付き文字列のコンパイラサポートがないことを意味します。次の例は、少なくとも、次のような構成で、文字列の長さがコンパイル時にカウントされる独自のC文字列ライブラリを開始できることを示しています:

#define PREFIX_STR(s) ((prefix_str_t){ sizeof(s)-1, (s) })

typedef struct { int n; char * p; } prefix_str_t;

int main() {
    prefix_str_t string1, string2;

    string1 = PREFIX_STR("Hello!");
    string2 = PREFIX_STR("Allows \0 chars (even if printf directly doesn't)");

    printf("%d %s\n", string1.n, string1.p); /* prints: "6 Hello!" */
    printf("%d %s\n", string2.n, string2.p); /* prints: "48 Allows " */

    return 0;
}

ただし、文字列ポインタを具体的に解放するときと、静的に割り当てられたとき(リテラルchar配列)に注意する必要があるため、これには問題はありません。

編集:質問に対するより直接的な答えとして、私の意見では、これはCが文字列の長さを(コンパイル時定数として)利用できる両方をサポートする方法であり、必要であれば、ポインタとゼロ終端のみを使用する場合のメモリオーバーヘッド。

もちろん、標準ライブラリは一般に文字列の長さを引数として受け取らないため、長さの抽出はchar * s = "abc"のような単純なコードではないため、ゼロで終了する文字列を使用することをお勧めします。私の例が示しています。

7
Pyry Jahkola

「32ビットマシンでも、文字列を使用可能なメモリのサイズにすることができる場合、長さの接頭辞付き文字列は、nullで終わる文字列よりも3バイトだけ広くなります。」

まず、短い文字列の場合、余分な3バイトがかなりのオーバーヘッドになる可能性があります。特に、長さゼロの文字列は4倍のメモリを使用するようになりました。一部のユーザーは64ビットマシンを使用しているため、長さゼロの文字列を格納するために8バイトが必要か、プラットフォームがサポートする最長の文字列に文字列形式が対応できません。

対処するアライメントの問題もあるかもしれません。 「solo\0second\0\0four\0five\0\0seventh」のような7つの文字列を含むメモリブロックがあるとします。 2番目の文字列はオフセット5から始まります。ハードウェアは4の倍数のアドレスに32ビット整数を揃える必要があるため、パディングを追加する必要があり、オーバーヘッドがさらに増加し​​ます。 C表現は、比較するとメモリ効率が非常に高くなります。 (メモリ効率は優れています。たとえば、キャッシュのパフォーマンスに役立ちます。)

5
Brangdon

まだ言及されていない点が1つあります。Cが設計されたとき、「char」が8ビットではないマシンがたくさんありました(今日でもそうでないDSPプラットフォームがあります)。文字列に長さ接頭辞を付けると決定した場合、「char」の長さの接頭辞をいくつ使用する必要がありますか? 2を使用すると、8ビット文字と32ビットのアドレス空間を持つマシンの文字列長に人為的な制限が課され、16ビット文字と16ビットのアドレス空間を持つマシンのスペースが無駄になります。

任意の長さの文字列を効率的に保存したい場合、および 'char'が常に8ビットの場合、速度とコードサイズを多少犠牲にして、偶数のプレフィックスが付いた文字列をスキームとして定義できますNはN/2バイト長で、奇数値Nと偶数値M(逆読み)が前に付いた文字列は((N-1)+ M * char_max)/ 2などになります。文字列を保持するために一定量のスペースを提供すると主張する場合、そのスペースに先行して十分なバイト数が最大長を処理できるようにする必要があります。ただし、「char」が常に8ビットであるとは限らないため、文字列の長さを保持するのに必要な「char」の数はCPUアーキテクチャによって異なるため、このようなスキームは複雑になります。

4
supercat

Null終了により、高速なポインターベースの操作が可能になります。

4
Sanjit Saluja

このブログ投稿 のJoel Spolskyによると、

それは、UNIXとCプログラミング言語が発明されたPDP-7マイクロプロセッサがASCIZ文字列型を持っているためです。 ASCIZは、「最後にZ(ゼロ)が付いたASCII」を意味しました。

ここで他のすべての答えを見た後、これが本当であっても、Cがヌルで終わる「文字列」を持つ理由の一部にすぎないと確信しています。この投稿は、文字列のような単純なものが実際に非常に難しいことがあるということを非常に明らかにしています。

2
BenK

理論的根拠ではなくであるが、長さエンコードのカウンターポイント

  1. メモリに関する限り、特定の形式の動的な長さのエンコーディングは静的な長さのエンコーディングよりも優れており、すべて使用法に依存します。証拠としてUTF-8を見てください。基本的に、単一の文字をエンコードするための拡張可能な文字配列です。これは、拡張バイトごとに1ビットを使用します。 NUL終端は8ビットを使用します。長さプレフィックス64ビットを使用することで、合理的に無限長と呼ぶこともできます。余分なビットのケースをヒットする頻度が決定要因です。極端に大きな文字列は1つだけですか? 8ビットまたは64ビットを使用している場合、誰が気にしますか?多くの小さな文字列(つまり英語の単語の文字列)?その場合、プレフィックスコストは大きな割合になります。

  2. 時間を節約できる長さの接頭辞付き文字列は本物ではないです。提供されたデータに長さを提供する必要があるかどうか、コンパイル時にカウントするか、文字列としてエンコードする必要がある動的データが本当に提供されるかどうか。これらのサイズは、アルゴリズムのある時点で計算されます。 nullで終了する文字列のサイズを格納する別の変数を提供できます。これにより、時間の節約が無駄になります。最後に余分なNULがあります...しかし、長さエンコードにそのNULが含まれていない場合、文字通り2つの間に違いはありません。アルゴリズムの変更はまったく必要ありません。コンパイラ/ランタイムに実行させる代わりに、手動で自分で設計する必要があるだけです。 Cは主に手動で物事を行うことについてです。

  3. オプションの長さプレフィックスはセールスポイントです。アルゴリズムの追加情報は必ずしも必要ではないので、すべての文字列に対してそれを行う必要があるため、事前計算+計算時間がO(n)を下回ることはありません。 (つまり、ハードウェア乱数ジェネレータ1-128。「無限の文字列」から引き出すことができます。文字列を非常に高速に生成するとします。したがって、文字列の長さは常に変化します。多くのランダムなバイトがあります。リクエスト後に取得できる次の未使用バイトが必要です。デバイスで待機することもできますが、文字のバッファを先読みすることもできます。無駄な計算の無駄。nullチェックの方が効率的です。)

  4. 長さプレフィックスは、バッファオーバーフローに対する適切な保護手段ですか?ライブラリ関数の適切な使用と実装も同様です。不正なデータを渡すとどうなりますか?私のバッファの長さは2バイトですが、関数に7であることを伝えます! 例:gets()が既知のデータで使用されることを意図していた場合、テストした内部バッファチェックがあった可能性がありますコンパイルされたバッファとmalloc()呼び出しで、まだ仕様に従います。未知のSTDINが未知のバッファに到達するためのパイプとして使用することを意図していた場合、バッファサイズがわからないことは明らかです。これは、長さargが無意味であることを意味します。さらに言えば、一部のストリームと入力に長さプレフィックスを付けることはできません。できません。つまり、長さのチェックはアルゴリズムに組み込まれる必要があり、タイピングシステムの魔法の部分ではありません。 TL; DR NUL終了は決して安全である必要はありませんでした。

  5. counter-counter point: NULターミネーションはバイナリで迷惑です。ここでlength-prefixを実行するか、何らかの方法でNULバイトを変換する必要があります:エスケープコード、範囲の再マッピングなど。これはもちろん、より多くのメモリ使用量/削減された情報/より多くの操作/バイトを意味します。ここでは、長さプレフィックスが主に戦争に勝ちます。変換の唯一の利点は、長さプレフィックス文字列をカバーするために追加の関数を記述する必要がないことです。つまり、より最適化されたsub-O(n)ルーチンでは、コードを追加することなく、それらをO(n)同等のものとして自動的に動作させることができます。欠点は、もちろん時間/メモリ/ NULの重い文字列で使用した場合の圧縮の無駄バイナリデータを操作するために複製するライブラリの量に応じて、長さプレフィックス文字列だけで作業することは理にかなっているかもしれません。長さプレフィックス文字列でも同じことができます... -1長さはNUL終了を意味し、NUL終了文字列をlength-terminated内で使用できます。

  6. 連結: "O(n + m)vs O(m)"連結後の文字列の全長としてmを参照していると仮定しています両方ともその数の操作を最小限に抑える必要があるためです(文字列1にタックオンすることはできません。再割り当てが必要な場合はどうなりますか?)。また、nは、事前計算のために必要のない神話的な量の操作であると想定しています。その場合、答えは簡単です:事前計算。 Ifあなたは常に再割り当てする必要のない十分なメモリがあると主張しており、それがbig-O表記の基礎ですより簡単:文字列1の終わりに割り当てられたメモリでバイナリ検索を実行します。明らかに、文字列1の後に無限のゼロの大きな見本があり、reallocを心配しません。そこで、log(n)に簡単にnを追加しましたが、やっと試してみました。 log(n)を思い出すと、実際のコンピューターでは本質的に64に過ぎません。これは、本質的にO(m)であるO(64 + m)と言っているようなものです。 (そして、はい、そのロジックは、今日使用されているrealデータ構造の実行時分析で使用されています。頭のてっぺんではありません。 )

  7. Concat()/ Len() 再び:結果をメモします。簡単です。可能/必要に応じて、すべての計算を事前計算に変換します。これはアルゴリズムの決定です。言語の強制的な制約ではありません。

  8. NUL終端を使用すると、文字列の接尾辞の受け渡しが簡単/可能になります。 length-prefixの実装方法によっては、元の文字列を破壊する可能性があり、不可能な場合もあります。コピーを要求し、O(1)の代わりにO(n)を渡す。

  9. 引数の受け渡し/逆参照は、NUL終了の場合と長さの接頭辞の場合の方が少ない明らかに、渡す情報が少ないためです。長さが必要ない場合は、これにより多くのフットプリントが節約され、最適化が可能になります。

  10. チートできます。それは本当に単なるポインタです。あなたはそれを文字列として読む必要があると誰が言いますか?単一の文字またはフロートとして読みたい場合はどうしますか?逆を行い、フロートを文字列として読み取りたい場合はどうなりますか?注意が必要な場合は、NUL終了でこれを行うことができます。 length-prefixを使用してこれを行うことはできません。通常はポインターとは明らかに異なるデータ型です。ほとんどの場合、文字列をバイト単位で作成し、長さを取得する必要があります。もちろん、entirefloat(おそらく内部にNULがある)のようなものが必要な場合は、とにかくバイト単位で読み取る必要がありますが、詳細は決定するためにあなたに任されています。

TL; DRバイナリデータを使用していますか?いいえの場合、NUL終端によりアルゴリズムの自由度が高まります。はいの場合、コード量対速度/メモリ/圧縮が主な関心事です。 2つのアプローチまたはメモ化のブレンドが最適かもしれません。

2
Black

Cを取り巻く多くの設計上の決定は、最初に実装されたとき、パラメータの受け渡しがいくぶん高価だったという事実に起因しています。たとえば、.

void add_element_to_next(arr, offset)
  char[] arr;
  int offset;
{
  arr[offset] += arr[offset+1];
}

char array[40];

void test()
{
  for (i=0; i<39; i++)
    add_element_to_next(array, i);
}

versus

void add_element_to_next(ptr)
  char *p;
{
  p[0]+=p[1];
}

char array[40];

void test()
{
  int i;
  for (i=0; i<39; i++)
    add_element_to_next(arr+i);
}

後者は、2つではなく1つのパラメーターを渡すだけでよいため、わずかに安くなりました(したがって、推奨されました)。呼び出されるメソッドが配列のベースアドレスやその中のインデックスを知る必要がない場合、2つを組み合わせた単一のポインタを渡す方が値を個別に渡すよりも安価です。

Cが文字列の長さをエンコードできる多くの合理的な方法がありますが、それまでに発明されたアプローチには、文字列のベースアドレスを受け入れるために文字列の一部を操作できる必要なすべての機能があります。 2つの別個のパラメーターとしての目的のインデックス。ゼロバイトの終端を使用することで、その要件を回避できました。他のアプローチは今日のマシンでより良いでしょう(現代のコンパイラはしばしばレジスタにパラメーターを渡し、memcpyはstrcpy()と同等の方法では最適化できません)十分な量産コードはゼロに終端された文字列を使用します。

PS--一部の操作でわずかな速度のペナルティと、より長い文字列でのわずかな余分なオーバーヘッドと引き換えに、文字列で動作するメソッドに文字列へのポインタを直接受け入れることができたはずですbounds-checked文字列バッファー、または別の文字列の部分文字列を識別するデータ構造。 「strcat」のような関数は、[現代の構文]のようなものに見えるでしょう。

void strcat(unsigned char *dest, unsigned char *src)
{
  struct STRING_INFO d,s;
  str_size_t copy_length;

  get_string_info(&d, dest);
  get_string_info(&s, src);
  if (d.si_buff_size > d.si_length) // Destination is resizable buffer
  {
    copy_length = d.si_buff_size - d.si_length;
    if (s.src_length < copy_length)
      copy_length = s.src_length;
    memcpy(d.buff + d.si_length, s.buff, copy_length);
    d.si_length += copy_length;
    update_string_length(&d);
  }
}

K&R strcatメソッドより少し大きいですが、境界チェックをサポートしますが、K&Rメソッドはサポートしません。さらに、現在の方法とは異なり、任意の部分文字列を簡単に連結することが可能です。

/* Concatenate 10th through 24th characters from src to dest */

void catpart(unsigned char *dest, unsigned char *src)
{
  struct SUBSTRING_INFO *inf;
  src = temp_substring(&inf, src, 10, 24);
  strcat(dest, src);
}

Temp_substringによって返される文字列のライフタイムは、sおよびsrcのライフタイムによって制限されることに注意してください。これは、メソッドが渡されるのにinfを必要とする理由です。 )。

メモリコストの観点から、最大64バイトの文字列とバッファには1バイトのオーバーヘッドがあります(ゼロで終わる文字列と同じ)。長い文字列は、わずかに多くなります(2バイト間で許容されるオーバーヘッドの量と、必要な最大値が時間/スペースのトレードオフになるかどうか)。長さ/モードバイトの特別な値は、文字列関数にフラグバイト、ポインタ、およびバッファ長(他の文字列に任意にインデックスを付けることができる)を含む構造が与えられたことを示すために使用されます。

もちろん、K&Rはそのようなものを実装しませんでしたが、それはおそらく文字列処理に多くの労力を費やしたくないためです-今日でも多くの言語がかなり貧弱に見える領域です。

2
supercat