nullで終了する文字列の根拠は何ですか?


281

私はCとC ++が大好きですが、ヌル終了文字列の選択に頭を悩まさずにはいられません。

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

これらの事柄のいくつかはCよりも最近明らかになったので、Cがそれらを知らなかったのは理にかなっています。しかし、Cが登場する前に、いくつかは明白でした。明らかに優れた長さの接頭辞の代わりにnullで終了する文字列が選択されたのはなぜですか?

編集:一部の人は上記の私の効率の点で事実を尋ねました(そして私がすでに提供したものを好きではありませんでした)ので、それらはいくつかのことに由来します:

  • nullで終了する文字列を使用した連結では、O(n + m)時間の複雑さが必要です。多くの場合、長さの接頭辞にはO(m)のみが必要です。
  • nullで終了する文字列を使用する長さには、O(n)時間の複雑さが必要です。長さの接頭辞はO(1)です。
  • 長さと連結は、最も一般的な文字列操作です。nullで終了する文字列の方が効率的である場合がいくつかありますが、これらはそれほど頻繁には発生しません。

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

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

上記のどれも、長さと連結ほど一般的ではありません。

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

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

しかし、これは正しくありません。nullで終了し、長さの接頭辞が付いた文字列と同じ時間です。(nullで終了する文字列は、新しい終了位置にしたい場所にnullを貼り付け、長さのプリフィックスはプレフィックスから減算するだけです。)


110
すべてのC ++プログラマーが独自の文字列ライブラリを作成することは、常に通過儀礼であると常に思っていました。
Juliet

31
今合理的な説明を期待することについてこれは何ですか。次にx86またはDOSの根拠を聞きたいと思いますか?私に関する限り、最悪のテクノロジーが勝利します。毎回。そして最悪の文字列表現。
jalf

4
なぜプレフィックス文字列の長さが優れていると主張するのですか?結局のところ、Cは他の言語と区別されるnullで終了する文字列を使用したため、人気が高まりました。
ダニエルC.ソブラル

44
@ダニエル:Cは、フォンノイマンマシンで実行可能なプログラムのシンプルで効率的で移植性のある表現であり、Unixで使用されていたため、人気を博しました。nullで終了する文字列を使用することを決定したためです。それが良い設計決定だったとしたら、人々はそれをコピーしたでしょうし、そうではなかったでしょう。彼らは確かにC.から、他のほとんどすべてコピーした
ビリーONeal

4
文字列の1つを破棄する場合、連結は長さ接頭辞付きのO(m)のみです。そうでなければ、同じ速度。Cストリングの最も一般的な使用法は(歴史的に)印刷とスキャンでした。これらの両方で、1つのレジスタを節約するため、ヌル終了はより高速です。
ダニエルC.ソブラル

回答:


195

馬の口

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

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


12
別の関連する引用:「...文字列のセマンティクスは、すべての配列を管理するより一般的な規則によって完全に包括され、その結果、言語はより簡単に記述できます...」
AShelly

151

Cは言語の一部として文字列を持ちません。Cの「文字列」は、charへの単なるポインタです。おそらくあなたは間違った質問をしているでしょう。

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

以下の荒れ狂うスコールに照らして:

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


29
char *temp = "foo bar";Cで有効なステートメントです...ちょっと!それは文字列ではないですか?nullで終了していませんか?
Yanick Rochon、2011

56
@Yanick:これは、最後にnullを含むcharの配列を作成するようコンパイラーに指示するための便利な方法です。それは「文字列」ではありません
ロバートSチャッチョ2010

28
@calavera:しかし、単に「この文字列の内容と2バイトの長さの接頭辞を使用してメモリバッファを作成する」という意味もあるかもしれません
Billy ONeal

14
@Billy:「文字列」は実際にはバイトへのポインタと同等の単なるcharへのポインタなので、処理しているバッファが本当に「文字列」であることが意図されていることをどのようにして知ることができますか?これを示すには、char / byte *以外の新しいタイプが必要になります。多分構造体?
Robert S Ciaccio 2010

27
@calaveraは正しいと思います。Cには文字列のデータ型がありません。わかりました、文字列のような文字の配列を考えることができますが、これは常に文字列であることを意味しません(文字列とは、明確な意味を持つ一連の文字を意味します)。バイナリファイルは文字の配列ですが、これらの文字は人間にとって何の意味もありません。
BlackBear

106

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

私が理解しているように、この質問は、「ゼロで終了する文字列の利点は何ですか?」

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

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

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

編集:私は最近D文字列を調べました。選択したソリューションがサイズプレフィックスでもゼロターミネーションでもないことを確認するのに十分興味深いです。Cの場合と同様に、二重引用符で囲まれたリテラル文字列は、不変のchar配列の単なる省略形であり、言語には、(不変のchar配列)を意味する文字列キーワードもあります。

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

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

少しがっかりしたのは、文字列がutf-8であるはずですが、マルチバイト文字を使用している場合でも、長さは明らかにバイト数を返します(少なくともコンパイラのgdcではtrueです)。それがコンパイラのバグなのか、それとも目的によるものなのか、私にはわかりません。(わかりました。おそらく何が起こったのかはわかったでしょう。Dコンパイラにソースにutf-8を使用するように指示するには、最初に愚かなバイトオーダーマークを付ける必要があります。特にUTF-の場合、エディタがそうしないことを知っているので、私は愚かです。 8はASCII互換であると想定されています)。


7
...続き...私が思うあなたのポイントのいくつかは、単なる間違いです。つまり、「すべてがファイルです」という議論です。ファイルは順次アクセスですが、C文字列はそうではありません。長さのプレフィックスは、最小限の構文糖で行うこともできます。ここでの唯一の合理的な議論は、小さな(つまり8ビット)ハードウェアで32ビットのプレフィックスを管理しようとすることです。長さの大きさは実装で決まると言えば簡単に解決できると思います。結局、それが何をするかstd::basic_stringです。
Billy ONeal

3
@Billy ONeal:私の答えには、実際には2つの異なる部分があります。1つは「コアC言語」の一部であり、もう1つは標準ライブラリが提供するものです。文字列のサポートに関しては、コア言語の1つのアイテム、つまり二重引用符で囲まれた一連のバイトの意味のみです。私はCの振る舞いであなたより幸せではありません。私は魔法のように、すべてのdoubleの終わりにゼロが含まれているため、囲まれたバイトの束が十分に悪いと感じています。\0プログラマが暗黙的なものの代わりにそれを望んでいるとき、私は最後に明示的を好みます。長さを追加するとずっと悪いです。
kriss

2
@Billy ONeal:それは真実ではありません、使用はコアとライブラリとは何かを気にします。最大のポイントは、Cを使用してOSを実装する場合です。そのレベルでは、ライブラリは利用できません。Cは、多くの場合、同じような制限がある組み込みコンテキストやプログラミングデバイスでも使用されます。多くの場合、ジョーズはおそらく今日ではCをまったく使用すべきではありません。「OK、それをコンソールで使用しますか?コンソールはありますか?いいえ?残念です...」
kriss

5
@Billy "まあ、オペレーティングシステムを実装しているCプログラマの.01%にとっては問題ありません。" 他のプログラマーはハイキングをすることができます。Cは、オペレーティングシステムを作成するために作成されました。
ダニエルC.ソブラル

5
どうして?それはそれが汎用言語であると言うので?それを書いた人々がそれを作成したときに何をしていたのかを示していますか?人生の最初の数年間は何が使われましたか?それで、それが私に同意しないと言っているのは何ですか?これは、オペレーティングシステムを記述するために作成された汎用言語です。それはそれを否定しますか?
ダニエルC.ソブラル

61

私は、それには歴史的な理由があり、ウィキペディアでこれを見つけたと思います:

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


2
@muntooうーん...互換性?
khachik

19
@muntoo:これは、既存のCおよびC ++コードの膨大な量を破壊するためです。
Billy ONeal

10
@muntoo:パラダイムは行き来しますが、レガシーコードは永遠です。今後のバージョンのCでは、0で終了する文字列を引き続きサポートする必要があります。サポートしない場合、30年以上のレガシーコードを書き直す必要があります(これは起こりません)。そして、古い方法が利用可能である限り、それは人々が使い慣れているものであるため、人々が使い続けるものです。
John Bode

8
@muntoo:私を信じて。しかし、私はPascal文字列よりも0で終了する文字列を優先します。
ジョンボード

2
レガシーについて話す... C ++文字列は、NULで終了するように義務付けられました。
ジムバルター2015年

32

Calavera正しいですが、人々は彼のポイントを理解していないようですので、いくつかのコード例を提供します。

まず、Cとは何かを考えてみましょう。すべてのコードが機械語にかなり直接変換されている単純な言語です。それが意味していたことから、すべてのタイプのレジスタにスタックに収まる、それが実行するために、オペレーティング・システムまたは大きなランタイム・ライブラリーを必要としない書き込みが考える、(見事に適していたタスクをこれらの事をは、この日の競争相手になる可能性すらありません)。

Cにorのstringようなタイプがあるint場合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;
}

もう1つの方法は、構造体を使用して文字列を定義することです。

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

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

おもしろいのは、そのような構造体がCに存在することです。これらは、ユーザーハンドリングへの日常のメッセージ表示には使用されません。

したがって、ここでCalaveraのポイントは、Cには文字列型がないことです。。それを使って何かを行うには、ポインターを受け取り、それを2つの異なる型へのポインターとしてデコードする必要があります。これにより、文字列のサイズが非常に適切になり、「実装定義」のままにしておくことはできません。

これで、C とにかくメモリを処理できるmemようになり、ライブラリ内の関数(で<string.h>さえ、!)は、ポインタとサイズのペアとしてメモリを処理するために必要なすべてのツールを提供します。 Cのいわゆる「文字列」は、テキスト端末用のオペレーティングシステムを作成するコンテキストでメッセージを表示するという1つの目的でのみ作成されました。そして、そのためには、ヌル終端で十分です。


2
1. +1。2.言語のデフォルトの動作が長さの接頭辞を使用して作成されていた場合、それを容易にする他の方法があったことは明らかです。たとえば、そこにあるすべてのキャストは、strlen代わりにや友人への通話によって隠されていました。「実装に任せる」という問題に関しては、接頭辞はshortターゲットボックス上のa と同じであると言えます。その後、すべてのキャストは引き続き機能します。3. 1つまたは他のシステムの見た目が悪くなる、1日中人為的なシナリオを思いつくことがあります。
Billy ONeal

5
@ビリーライブラリのことは十分に真実ですが、Cはライブラリの使用を最小限に抑えるか、まったく使用しないように設計されています。たとえば、プロトタイプの使用は、初期には一般的ではありませんでした。接頭辞を言うことshortは、文字列のサイズを効果的に制限します。これは、彼らが熱望していなかったもののようです。私自身は、8ビットのBASICおよびPascal文字列、固定サイズのCOBOL文字列などを扱っていて、無制限のサイズのC文字列の大ファンになりました。現在、32ビットのサイズで実用的な文字列を処理できますが、これらのバイトを早い段階で追加することは問題がありました。
ダニエルC.ソブラル

1
@ビリー:まず、ダニエル、ありがとう...あなたは私が何をしているのか理解しているようです。第二に、ビリー、私はあなたがまだここで行われているポイントを逃していると思います。長さを文字列データ型の前に付けることの長所と短所については、私は論じていません。私が言っていること、およびダニエルが非常に明確に強調したことは、Cの実装でその議論をまったく処理しないという決定があったということです。基本言語に関する限り、文字列は存在しません。文字列の処理方法の決定はプログラマーに委ねられており、ヌル終了が一般的になりました。
Robert S Ciaccio 2010

1
私による+1。もう1つ追加したいことがあります。あなたが提案する構造体は、実際のstring型への重要な一歩を見逃しています。文字を認識していません。これは「char」の配列です(機械語の「char」は、「単語」が人間が文の単語と呼ぶものと同じくらいの文字です)。文字列は、エンコーディングの概念を導入した場合の配列の上に実装できるより上位のchar概念です。
Frerich Raabe、2010

2
@ DanielC.Sobral:また、あなたが言及する構造体は2つの割り当てを必要としません。スタック上にあるように使用するか(buf割り当てのみが必要です)、struct string {int len; char buf[]};1つを割り当てて全体をフレキシブル配列メンバーとして使用して割り当て、それをとして渡しstring*ます。(または、おそらく、struct string {int capacity; int len; char buf[]};明らかなパフォーマンス上の理由から)
Mooing Duck

20

明らかに、パフォーマンスと安全性のために、繰り返し実行するstrlenか同等のものではなく、作業中に文字列の長さを維持することをお勧めします。ただし、文字列の内容の直前の固定位置に長さを格納することは、非常に悪い設計です。JörgenがSanjitの回答のコメントで指摘したように、文字列の末尾を文字列として扱うことができなくなります。これにより、たとえば、新しいメモリを割り当てずに、一般的な操作の多くを実行したりpath_to_filenamefilename_to_extension不可能にしたりします(失敗やエラー処理の可能性を招きます)。 。そしてもちろん、文字列の長さフィールドが占めるバイト数に誰も同意できないという問題があります(多くの不良な「Pascal文字列」

長さを格納するかどうか、どこに、どのように格納するかをプログラマに選択させるCの設計は、はるかに柔軟で強力です。しかしもちろん、プログラマーは賢くなければなりません。Cは、クラッシュしたり、グラインドして停止したり、敵にルートを与えたりするプログラムで愚かさを罰します。


+1。長さを格納するための標準的な場所があればいいのですが、長さの接頭辞のようなものが必要な人は、どこにでも大量の「接着コード」を書く必要がありませんでした。
Billy ONeal

2
文字列データに関連して可能な標準的な場所はありませんが、もちろん、個別のローカル変数(後者が不便で前者が無駄ではない場合に渡すのではなく再計算する)またはポインターを持つ構造を使用できます。文字列に(さらに良いことに、構造体が割り当て目的でポインタを「所有」しているかどうか、またはそれが他の場所で所有されている文字列への参照であるかどうかを示すフラグです。もちろん、柔軟な配列メンバーを構造体に含めて、柔軟に割り当てることができます自分に合った構造のストリング
R .. GitHub STOP HELPING ICE

13

怠惰、レジスターの簡素化、移植性。あらゆる言語のアセンブリガットを考慮します。特にCは、アセンブリの1つ上のステップです(したがって、多くのアセンブリレガシーコードを継承します)。ヌル文字はそれらのASCIIの日には役に立たないので同意します(そしておそらくEOF制御文字と同じくらい良いでしょう)。

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

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レジスタ

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

議論のために、私は2つの一般的な文字列操作を実装します

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

複雑さO(n)。ほとんどの場合、PASCAL文字列は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バイトが文字列の長さであると見なし、大量のメモリを割り当てます。次に、その数のバイトを読み取ろうとします。もう1つのケースは、x86(ビッグエンディアン)へのPPC 32バイト文字列の読み取り(リトルエンディアン)です。もちろん、一方が他方によって書き込まれることがわからない場合は問題があります。1バイトの長さ(0x00000001)は16777216(0x0100000)になり、1バイトの文字列を読み取るには16 MBになります。もちろん、人々は1つの規格に同意するべきだと言うでしょうが、16ビットのユニコードでさえ、ほとんどエンディアンはありませんでした。

もちろん、Cにも問題がありますが、ここで取り上げられた問題の影響はほとんどありません。


2
@deemoowoor:Concat:O(m+n)nullterm文字列を使用して、O(n)他のすべての場所で一般的です。他の場所O(n)ではnullterm文字列を使用した長さO(1)。参加:他の場所O(n^2)ではnullterm文字列を使用しO(n)ます。nullで終了する文字列の方が効率的な場合があります(つまり、1をポインターに追加するだけ)。ただし、連結と長さは、最も一般的な操作です(長さは、少なくともフォーマット、ファイル出力、コンソール表示などに必要です)。 。長さをキャッシュして償却するO(n)場合、長さは文字列と一緒に保存する必要があると私は単に主張しました。
Billy ONeal

1
今日のコードでは、このタイプの文字列は非効率的でエラーが発生しやすいことに同意しますが、たとえば、コンソールの表示では、文字列の長さを実際に表示する必要はなく、ファイル出力で文字列について知る必要はありませんでした。長さ(外出先でのみクラスターを割り当てる)。このときの文字列フォーマットは、ほとんどの場合、固定文字列長で行われました。とにかく、Cでの連結がO(n ^ 2)の複雑さである場合、悪いコードを書く必要があります
。O

1
@dvhh:私はn ^ 2とは言いませんでした-m + nと言いました-それはまだ線形ですが、連結を行うには元の文字列の最後までシークする必要がありますが、長さのプレフィックスではシークしません必要とされている。(これは、線形時間を必要とする長さの実際の別の結果です)
Billy ONeal

1
@Billy ONeal:単なる好奇心から、現在のCプロジェクト(コードの約50000行)で文字列操作関数呼び出しのgrepを行いました。strlen 101、strcpyおよびバリアント(strncpy、strlcpy):85(メッセージ、暗黙のコピーに数百のリテラル文字列も使用)、strcmp:56、strcat:13(およびstrncatを呼び出すための長さ0の文字列への連結) 。プレフィックス付きの長さはstrlenの呼び出しを高速化しますが、strcpyまたはstrcmpの呼び出しを高速化しないことに同意します(strcmp APIが共通のプレフィックスを使用しない場合)。上記のコメントに関して最も興味深いのは、strcatが非常にまれであることです。
KRISS

1
@supercat:実際には、いくつかの実装を見てください。短い文字列は、短いスタックベースのバッファー(ヒープ割り当てなし)を使用しており、大きくなったときにのみヒープを使用します。ただし、アイデアの実際の実装をライブラリとして提供してください。通常、問題は、全体的な設計ではなく、詳細に到達したときにのみ発生します。
クリス

9

多くの点で、Cは原始的でした。そして私はそれが大好きでした。

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

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

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


nullで終了する文字列について、他の何よりもプリミティブなものはもうわかりません。PascalはCよりも古く、長さの接頭辞を使用しています。確かに、文字列あたり256文字に制限されていましたが、16ビットのフィールドを使用するだけでほとんどの場合問題が解決します。
Billy ONeal

文字数が制限されているという事実は、まさにそのようなことをするときに考える必要がある問題のタイプです。はい、長くすることができますが、当時はバイト数が問題でした。そして、16ビットのフィールドはすべての場合に十分な長さになるでしょうか?ちなみに、ヌル終了は概念的にはプリミティブであることを認めなければなりません。
ジョナサンウッド

10
文字列の長さを制限するか、コンテンツ(null文字なし)を制限するか、4〜8バイトカウントの追加のオーバーヘッドを受け入れます。無料の昼食はありません。開始時に、ヌルで終了した文字列は完全に意味がありました。アセンブリでは、文字の先頭ビットを使用して文字列の終わりをマークし、さらに1バイト節約しました。
Mark Ransom

まさに、マーク:無料のランチはありません。それは常に妥協です。最近では、同じ種類の妥協をする必要はありません。しかし、当時、このアプローチは他のアプローチと同じように優れていました。
ジョナサンウッド

8

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


2
エラー。Cのアプローチでは、7文字の長い文字列を3文字の長い文字列に割り当てることはできません。
Billy ONeal

@ビリー・オニール:どうして?この場合、私が理解している限り、すべての文字列は同じデータ型(char *)なので、長さは関係ありません。パスカルとは異なり。しかし、これは長さの接頭辞が付いた文字列の問題ではなく、Pascalの制限でした。
Oliver Mason

4
@ビリー:私はあなたがクリスチャンのポイントを再表現したと思います。Cはこれらの問題にまったく対処しないことで対処します。Cの観点から、実際には文字列の概念が含まれているとまだ考えています。これは単なるポインタなので、好きなように割り当てることができます。
Robert S Ciaccio 2010

2
**マトリックスのようです: "文字列がありません"。
Robert S Ciaccio

1
@calavera:それが何かを証明する方法はわかりません。長さの接頭辞を使用して同じ方法で解決できます...つまり、割り当てをまったく許可しません。
Billy ONeal

8

どういうわけか私は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"、私の例が示すように単純なコードではないため、ゼロで終了する文字列での作業が推奨される方法のようです。


問題は、ライブラリが構造体の存在を認識せず、埋め込まれたnullなどの処理を誤っていることです。また、これは私が尋ねた質問には実際には答えません。
Billy ONeal

1
それは本当だ。したがって、より大きな問題は、プレーンな古いゼロで終了する文字列よりも、文字列パラメータをインターフェイスに提供するための標準的な方法がないことです。私はまだ主張しますが、ポインターと長さのペアでのフィードをサポートするライブラリーがあります(まあ、少なくともそれらでC ++ std :: stringを構成できます)。
Pyry Jahkola

2
長さを格納する場合でも、ヌルが埋め込まれた文字列を許可しないでください。これは基本的な常識です。データにnullが含まれている可能性がある場合は、文字列を期待する関数で使用しないでください。
R .. GitHub ICEのヘルプの停止

1
@supercat:セキュリティの観点から、その冗長性を歓迎します。そうでなければ、無知な(または睡眠不足の)プログラマーは、バイナリデータと文字列を連結し、それらを[null-terminated]文字列を期待するものに渡すことになります...
R .. GitHub STOP HELPING ICE

1
@R ..:nullで終了する文字列を期待するchar*メソッドは通常を期待しますが、null終了を期待しない多くのメソッドもを期待しchar*ます。タイプを分離することのより重要な利点は、Unicodeの動作に関連します。文字列が特定の種類の文字を含むことがわかっているかどうか、または含まれていないことがわかっているかどうかのフラグを維持することは、文字列の実装にとって価値があるかもしれません。基本的な多言語平面を超える文字は桁違いに速くなります...
スーパーキャット2015年

6

「32ビットマシンでも、文字列が使用可能なメモリのサイズになるようにすると、プレフィックス付きの長さの文字列は、ヌルで終了する文字列よりも3バイトだけ広くなります。」

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

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


私はこの問題のすべてに対処したと思います。はい、x64プラットフォームでは、32ビットのプレフィックスはすべての可能な文字列に適合しません。一方、nullで終了する文字列ほど大きな文字列は必要ありません。何をするにも、40億バイトすべてを調べて、実行したいほとんどすべての操作の最後を見つける必要があるためです。また、nullで終了する文字列が常に悪であると言っているわけではありません。これらのブロック構造の1つを構築していて、特定のアプリケーションがそのような構造によって高速化されている場合は、それを試してください。言語のデフォルトの動作がそれを行わないことを望みます。
Billy ONeal

2
私の意見では効率の問題を過小評価していたので、私はあなたの質問のその部分を引用しました。メモリ要件を2倍または4倍にする(それぞれ16ビットおよび32ビット)と、パフォーマンスが大幅に低下する可能性があります。長い文字列は遅くなる可能性がありますが、少なくともサポートされていて機能します。私のもう1つのポイント、アラインメントについては、まったく触れていません。
ブランドン

UCHAR_MAXを超える値は、バイトアクセスとビットシフトを使用してパックおよびアンパックされたように動作するように指定することで、配置を処理できます。適切に設計された文字列タイプは、ゼロで終了する文字列に本質的に匹敵するストレージ効率を提供すると同時に、追加のメモリオーバーヘッドのないバッファーの境界チェックを可能にします(プレフィックスの1ビットを使用して、バッファーが「フル」かどうかを示します。でなく、最後のバイトがゼロ以外の場合、そのバイトは残りのスペースを表します。バッファがいっぱいでなく、最後のバイトがゼロの場合、最後の256バイトは使用されないため、...
supercat

...そのスペース内に未使用のバイトの正確な数を保存でき、追加のメモリコストはゼロです)。接頭辞を使用するコストは、文字列の長さを渡さなくてもfgets()などのメソッドを使用できることによって相殺されます(バッファーはそれらの大きさを知っているため)。
スーパーキャット2015年

4

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


5
え?長さの接頭辞では機能しない「高速ポインタ操作」は何ですか?さらに重要なことに、長さの接頭辞を使用する他の言語は、C wrt文字列操作よりも高速です。
Billy ONeal

12
@billy:長さの接頭辞付きの文字列では、文字列ポインタを取り、それに4を追加するだけでは、長さの接頭辞がないため、有効な文字列であるとは期待できません(とにかく有効ではありません)。
ヨルゲンSigvardsson

3
@j_random_hacker:asciiz文字列(O(n)の代わりにO(m + n))の連結ははるかに悪く、連結はここにリストされている他のどの操作よりもはるかに一般的です。
Billy ONeal 2010

3
nullで終了する文字列を使用すると、コストが高くなる小さな操作が1つありますstrlen。それは少し難点だと思います。
jalf

10
@Billy ONeal:もが正規表現もサポートしています。だから何 ?目的に合ったライブラリを使用してください。Cは最大効率とミニマリズムであり、バッテリーは含まれていません。Cツールでは、構造体を使用して、Length Prefixed文字列を非常に簡単に実装することもできます。また、独自の長さとcharバッファを管理することで文字列操作プログラムを実装することを禁じるものはありません。これは通常、効率を上げてCを使用するときに行うことであり、charバッファーの最後にゼロを期待する少数の関数を呼び出さなくても問題ありません。
kriss

4

まだ言及されていない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」の数はCPUアーキテクチャによって異なるため、「char」が常に8ビットであるとは限らないため、このようなスキームは複雑になります。


プレフィックスは、そのままで、実装定義のサイズにすることができますsizeof(char)
Billy ONeal、2012年

@BillyONeal:sizeof(char)1つです。常に。プレフィックスを実装定義のサイズにすることもできますが、これは扱いにくいでしょう。さらに、「正しい」サイズがどうあるべきかを知る実際の方法はありません。4文字の文字列を多数保持している場合、ゼロパディングは25%のオーバーヘッドを課し、4バイトの長さのプレフィックスは100%のオーバーヘッドを課します。さらに、4バイト長のプレフィックスのパックとアンパックに費やされる時間は、4バイト文字列をスキャンしてゼロバイトを探すコストを超える可能性があります。
スーパーキャット2012年

1
ああ、そうです。あなたが正しい。接頭辞は、char以外でも簡単に使用できます。ターゲットプラットフォームでの調整要件がうまくいくものであれば何でもかまいません。私はそこに行くつもりはありません-私はすでにこれを死ぬまで議論しました。
Billy ONeal、2012年

文字列に長さのプレフィックスが付いていると仮定すると、おそらく最も健全なことはsize_t接頭辞になるでしょう(メモリの浪費は控えめです- それ最も健全です---メモリに収まる可能性のあるあらゆる長さの文字列を許可します)。実際、それ Dが行うことの一種です。配列はstruct { size_t length; T* ptr; }で、文字列はの配列ですimmutable(char)
TimČas、2015

@TimČas:文字列を単語に揃える必要がない限り、多くのプラットフォームで短い文字列を処理するコストは、長さをパックおよびアンパックする要件によって支配されます。私はそれが実用的であるとは本当に思っていません。文字列を内容にとらわれない任意のサイズのバイト配列にしたい場合は、長さを文字データへのポインタとは別にして、リテラル文字列の両方の情報を取得できる言語を使用する方が良いと思います。
スーパーキャット2015

2

Cを取り巻く多くの設計上の決定は、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);
}

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-一部の操作では速度がわずかに低下し、長い文字列ではわずかなオーバーヘッドが発生する代わりに、文字列を処理するメソッドで、文字列へのポインター、境界チェックされた文字列バッファー、または別の文字列の部分文字列を識別するデータ構造。"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によって返される文字列の寿命はのそれらによって制限されることに注意してくださいssrc方法が必要とする理由は、これまでである(短いありました、inf -それはローカルだった場合、このメソッドが戻ってきたとき、それは死ぬに渡されます)。

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

もちろん、K&Rはそのようなことを実装していませんが、文字列の処理に多くの労力を費やしたくなかった可能性が高いです。


単一のパラメーターとして渡せるchar* arrフォームstruct { int length; char characters[ANYSIZE_ARRAY] };などの構造を指すのを妨げるものはありません。
Billy ONeal 2015年

@BillyONeal:このアプローチには2つの問題があります。(1)文字列全体を渡すことのみが許可されますが、現在の方法では文字列の末尾を渡すこともできます。(2)小さいストリングで使用すると、かなりのスペースが無駄になります。K&Rが文字列にある程度の時間を費やしたい場合は、物事をより堅牢にすることができたかもしれませんが、新しい言語が10年後に使用されるようになるつもりはなかったと思います。
スーパーキャット2015年

1
呼び出し規約についてのこのビットは、現実とは関係のない、まあまあの話です...それはデザインの考慮事項ではありませんでした。そして、レジスタベースの呼び出し規約はすでに「発明」されていました。また、構造体は最初のクラスではなかったため、2つのポインタなどのアプローチは選択肢になりませんでした。プリミティブのみが割り当て可能または渡し可能でした。構造体のコピーはUNIX V7までは届きませんでした。文字列ポインタをコピーするためだけにmemcpy(これも存在しませんでした)が必要なのは冗談です。言語デザインを装っている場合は、分離された関数だけでなく、完全なプログラムを書いてみてください。
ジムバルター2015年

1
「これは、文字列の処理に多くの労力を費やしたくなかったためだろう」-ナンセンス。初期のUNIXのアプリケーションドメイン全体が文字列処理でした。それがなかったら、私たちはそれについて聞いたことがないでしょう。
ジムバルター2015年

1
「charバッファが長さを含むintで始まる」ことはこれ以上不思議なことではないと思います- str[n]適切なchar を参照するようにする場合です。これらは、これについて議論している人々が考えいない種類のことです。
ジムバルター2015年

2

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

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

ここで他のすべての回答を確認した後、これが真実であっても、Cがnullで終了する「文字列」を持っている理由の一部に過ぎないと確信します。その投稿は、文字列などの単純なものが実際には非常に難しい場合があることを非常によく示しています。


2
ほら、私はジョエルをたくさんのことで尊敬しています。しかし、これは彼が推測しているものです。ハンスパッサントの答えは、Cの発明者から直接来ています。
Billy ONeal

1
はい。しかし、スポルスキーの言うことが真実であれば、それは彼らが言及していた「便利さ」の一部だったでしょう。そのため、この回答を含めました。
BenK 2016年

AFAIK .ASCIZは、一連のバイトを作成するための単なるアセンブラステートメントで、その後にが続き0ます。これは、ゼロ終端文字列が当時確立された概念であることを意味します。(バイトをコピーする)と(最後にコピーされたバイトがゼロでない場合は分岐する)からなるタイトなループを作成できることを除いて、ゼロで終了する文字列がPDP- *のアーキテクチャに関連しているという意味ではありませMOVBBNE
エイドリアンW

Cが古く、たるんだ、老朽化し​​た言語であることを示すことを前提としています。
purec '30

2

ない理由は必ずしもなく、長さがエンコードに対位法

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

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

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

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

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

  6. 連結:「O(n + m)対O(m)」連結後の文字列の合計長としてmを参照していると仮定します。これは、どちらも最小の操作数でなければならないためです(タックすることはできません) -文字列1に対して、再割り当てが必要な場合はどうなりますか?)。そして、nは、事前計算のためにもう実行する必要のない神秘的な操作量であると想定しています。もしそうなら、答えは簡単です:事前計算。もしは、常に再割り当てする必要のない十分なメモリがあることを主張しており、それがBig-O表記の基礎であり、答えはさらに簡単です:文字列1の終わりに割り当てられたメモリのバイナリ検索。明らかに、文字列1の後に無限のゼロの大きな見本があり、reallocを心配する必要がありません。そこで、簡単にnをlog(n)に取得し、私はかろうじて試しました。log(n)をリコールすると、実際のコンピューターでは本質的に64までの大きさになります。これは、本質的にO(m)であるO(64 + m)と本質的に同じです。(そして、はい、今日使用されている実際のデータ構造のランタイム分析でロジックが使用されています。それは私の頭の上でのでたらめではありません。)

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

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

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

  10. あなたはごまかすことができます。実際には単なるポインタです。あなたはそれを文字列として読まなければならないと誰が言ったのですか?それを単一の文字または浮動小数点数として読みたい場合はどうでしょうか?反対のことを行い、floatを文字列として読み取りたい場合はどうなりますか?注意するなら、NULターミネーションでこれを行うことができます。length-prefixを使用してこれを行うことはできません。これは、通常、ポインターとは明らかに異なるデータ型です。ほとんどの場合、文字列をバイト単位で作成して長さを取得する必要があります。もちろん、フロート全体(おそらく内部にNULがある)のようなものが必要な場合は、とにかくバイト単位で読み取る必要がありますが、詳細は決定する必要があります。

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


9は少々ベースから外れていたり、誤って表示されていました。長さのプリフィックスにはこの問題はありません。Lenth は別の変数として渡します。私たちはプリフィクスについて話していましたが、私は夢中になりました。まだ考えるのは良いので、そのままにしておきます。:d
ブラック

1

「Cには文字列がありません」という回答は購入しません。確かに、Cは組み込みの上位レベルの型をサポートしていませんが、Cでデータ構造を表すことができ、それが文字列です。文字列がCの単なるポインタであるという事実は、最初のNバイトが長さとして特別な意味を持つことができないことを意味しません。

Windows / COMの開発者BSTRは、まさにこのようなタイプ、つまり実際の文字データがバイト0から始まっていない、長さの接頭辞が付いたC文字列に非常に精通しています。

したがって、ヌル終了を使用するという決定は、人々が好んだものであり、言語の必要性ではないようです。


-3

gccは以下のコードを受け入れます。

char s [4] = "abcd";

そして、文字列ではなく文字の配列として扱っても問題ありません。つまり、s [0]、s [1]、s [2]、およびs [3]を使用して、またはmemcpy(dest、s、4)を使用してもアクセスできます。ただし、puts(s)を使用すると乱雑な文字が表示され、strcpy(dest、s)を使用するとさらに悪化します。


@Adrian W.これは有効なCです。正確な長さの文字列は特殊なケースであり、NULは省略されています。これは一般的に賢明ではありませんが、FourCCの「文字列」を使用するヘッダー構造体にデータを入力する場合などに役立ちます。
Kevin Thibedeau、

あなたが正しいです。これは有効なCであり、コンパイルされ、kkaaiiで説明されているように動作します。反対票(私のものではない...)の理由は、おそらく、この回答がOPの質問にまったく回答しないためです。
エイドリアンW
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.