C配列が長さを追跡しないのはなぜですか?


77

配列で配列の長さを明示的に格納しない背後にある理由は何Cですか?

私の考えでは、そうするのには圧倒的な理由がありますが、標準(C89)を支持する多くの理由はありません。例えば:

  1. バッファで利用可能な長さがあると、バッファのオーバーランを防ぐことができます。
  2. Javaスタイルarr.lengthは明確であり、プログラマーintが複数の配列を扱う場合にスタックで多くのs を維持する必要がないようにします。
  3. 関数パラメーターはより適切になります。

しかし、おそらく私の考えでは、最もやる気の理由は、通常、長さを維持しないとスペースが節約されないからです。配列のほとんどの使用には動的割り当てが含まれると言って思います。確かに、人々はスタックに割り当てられた配列を使用する場合がありますが、それはたった1つの関数呼び出し*です-スタックは4または8バイト余分に処理できます。

ヒープマネージャーは、動的に割り当てられた配列によって使用される空きブロックサイズを追跡する必要があるため、その情報を使用可能にします(そして、コンパイル時にチェックされる追加のルールを追加します。足で自分を撃つのが好きです)。

私が反対側で考えることができる唯一のことは、長さの追跡がコンパイラをより単純にしたわけではないかもしれないがそれほど単純ではないということです。

*技術的には、自動ストレージを備えた配列を使用して、ある種の再帰関数を作成できます。この(非常に精巧な)場合、長さを格納すると、実際にはより多くのスペースが使用されます。


6
Cが構造体をパラメーターおよび戻り値の型として使用して含まれる場合、「ベクター」(または任意の名前)の構文シュガーを含める必要があると主張できると思います。 。この共通の構造体の言語レベルのサポート(単一の構造体ではなく個別の引数として渡される場合も)は、無数のバグを保存し、標準ライブラリも簡素化します。
ハイド14

3
また、Pascalが私のお気に入りのプログラミング言語ではない理由セクション2.1も洞察力があると感じるかもしれません

34
他のすべての答えにはいくつか興味深い点がありますが、結論としては、アセンブリ言語プログラマがコードを簡単に記述して移植できるように、Cが記述されていると思います。それを念頭に置いて、配列の長さを自動的に配列で保存すると、面倒なことではなく、欠点ではなくなります(他の素敵なキャンディーコーティングの欲求があるように)。これらの機能は最近は素晴らしいように見えますが、当時は、プログラムまたはデータのもう1バイトをシステムに詰め込むのは本当に苦労でした。メモリを無駄に使用すると、Cの採用が大幅に制限されます。
ダンク14

6
あなたの答えの本当の部分は、私が持っている方法ですでに何度も答えられていますが、別のポイントを抽出することができmalloc()ます。それは何度も不思議に思うことです。
glglgl

5
再開の投票。たとえ「K&Rが考えていなかった」としても、どこかに何らかの理由があります。
テラスティン14

回答:


106

配列の長さは静的プロパティであるため、C配列はその長さを追跡します。

int xs[42];  /* a 42-element array */

通常、この長さを照会することはできませんが、とにかく静的であるため必要はありませんXS_LENGTH。長さのマクロを宣言するだけで完了です。

より重要な問題は、C配列が暗黙的にポインターに分解されることです。たとえば、関数に渡される場合です。これはある程度意味があり、いくつかの素晴らしい低レベルのトリックを可能にしますが、配列の長さに関する情報を失います。したがって、Cがこの暗黙的なポインターの劣化を考慮して設計されたのは、より良い質問です。

もう1つの問題は、メモリアドレス自体を除き、ポインタにはストレージが必要ないことです。Cを使用すると、整数をポインターに、ポインターを他のポインターにキャストし、ポインターを配列のように扱うことができます。これを行っている間、Cは配列の長さを作成するほどの狂気ではありませんが、スパイダーマンのモットーを信頼しているようです。


13
間違っていなければ、Cコンパイラは静的配列の長さを追跡していると言っているのではないでしょうか。しかし、これは単にポインタを取得する関数には適していません。
VF1 14

25
@ VF1はい。しかし、重要なことは、配列とポインターがCで異なることです。コンパイラー拡張機能を使用していないと仮定すると、通常、配列自体を関数に渡すことはできませんが、ポインターを渡して、配列のようにポインターにインデックスを付けることができます。ポインタには長さが付加されていないことを効果的に訴えています。配列を関数の引数として渡すことができないこと、または配列が暗黙的にポインターに低下することを不平を言うべきです。
アモン

37
「通常、この長さを照会することはできません」-実際にできます、それはsizeof演算子です-intの長さが4バイトであると仮定すると、sizeof(xs)は168を返します。42を取得するには、次のようにします
。sizeof

15
@tcrosleyそれだけ配列の宣言の範囲内で動作します-それからはsizeof(XS)は...あなたを与えるかを見る別の関数へのparamとしてXSを渡してみてください
グウィン・エヴァンス

26
再び@GwynEvans:ポインターは配列ではありません。したがって、「配列をパラメーターとして別の関数に渡す」場合、配列ではなくポインターを渡します。主張してsizeof(xs)いるxsCの設計は、配列は、それらの範囲を離れることはできませんので、配列が別のスコープで異なるものになるだろうですが、露骨に偽です。sizeof(xs)where xsが配列であるsizeof(xs)場所xsがポインタである場所と異なる場合、リンゴとオレンジを比較しているため、それは驚くことではありません。
アモン14

38

これの多くは、当時利用可能なコンピュータに関係していました。コンパイルされたプログラムは限られたリソースのコンピューターで実行する必要があるだけでなく、おそらくもっと重要なことに、コンパイラー自体がこれらのマシンで実行する必要がありました。トンプソンがCを開発したとき、彼は8kのRAMを搭載したPDP-7を使用していました。実際のマシンコードに直接の類似性がなかった複雑な言語機能は、言語に含まれていませんでした。

C履歴を注意深く読むと、上記の内容をより深く理解できますが、マシンの制限によるものではありません。

さらに、言語(C)は、重要な概念、たとえば、実行時に長さが変化するベクトルなど、いくつかの基本的な規則と規則を使用して、重要な概念を記述するかなりの力を示します。... Cのアプローチを、ほぼ同時期の2つの言語、Algol 68とPascal [Jensen 74]のアプローチと比較するのは興味深いことです。Algol 68の配列は固定境界を持っているか、「柔軟」です。言語定義とコンパイラの両方で、柔軟な配列に対応するためにかなりのメカニズムが必要です(すべてのコンパイラが完全に実装するわけではありません)。配列と文字列、そしてこれは閉じ込めを証明しました[Kernighan 81]。

C配列は本質的に強力です。それらに境界を追加すると、プログラマーが境界を使用できる対象が制限されます。このような制限はプログラマーにとっては便利ですが、必然的に制限もあります。


4
これは、元の質問をかなり釘付けにします。それと、Cがオペレーティングシステムを書くのに魅力的なものにする一環として、プログラマーが何をしているかをチェックすることになると、Cが意図的に「軽いタッチ」に保たれたという事実。
ClickRick 14

5
すばらしいリンク、区切り文字を使用するために文字列の長さの格納を明示的に変更しましたto avoid the limitation on the length of a string caused by holding the count in an 8- or 9-bit slot, and partly because maintaining the count seemed, in our experience, less convenient than using a terminator-そのために非常に多く:
Voo 14

5
終端されていない配列は、Cのベアメタルアプローチにも適合します。K&R Cブックは、言語チュートリアル、リファレンス、および標準呼び出しのリストを含む300ページ未満です。私のO'Reilly Regexの本は、K&R Cのほぼ2倍の長さです。
Michael Shopsin 14

22

Cが作成された当時に戻って、文字列ごとに 4バイトの余分なスペースがあったとしても、それはどんなに短くても無駄です!

別の問題があります-Cはオブジェクト指向ではないため、すべての文字列に長さプレフィックスを付ける場合、aではなくコンパイラ組み込み型として定義する必要がありますchar*。特殊なタイプの場合、文字列を定数文字列と比較することはできません。つまり、次のようになります。

String x = "hello";
if (strcmp(x, "hello") == 0) 
  exit;

その静的文字列を文字列に変換するか、長さの接頭辞を考慮するために異なる文字列関数を使用するには、特別なコンパイラの詳細が必要です。

しかし、最終的には、Pascalとは異なり、長さプレフィックスの方法を選択しなかったと思います。


10
境界チェックにも時間がかかります。今日の用語ではささいなことですが、4バイトを気にかけるときに人々が注意を払ったものです。
スティーブンバーナップ14

18
@StevenBurnap:200 MBの画像のすべてのピクセルを通過する内側のループにいる場合、今日でもそれほど些細なことではありません。一般に、Cを記述している場合は高速に実行する必要があり、forループが既に境界を尊重するように設定されている場合、繰り返しごとに無駄な境界チェックで時間を無駄にしたくありません。
マッテオイタリア14

4
@ VF1「バック・イン・ザ・デイ」2バイト(DEC PDP / 11誰か?)
ClickRick 14

7
「過去に戻る」だけではありません。CがOSカーネル、デバイスドライバー、組み込みリアルタイムソフトウェアなどの「ポータブルアセンブリ言語」としてターゲットとするソフトウェア用。境界チェックで半ダースの命令を浪費することは重要であり、多くの場合、「境界外」である必要があります(別のプログラムストレージにランダムにアクセスできない場合、デバッガーをどのように作成できますか?)。
ジェームス・アンダーソン14

3
これは、BCPLが長さをカウントした引数を持っていることを考えると、実際にはかなり弱い引数です。Pascalと同じように、1ワードに制限されていたため、通常は8または9ビットのみであり、これは少し制限されていました(文字列の一部を共有する可能性も排除しますが、その最適化はおそらくあまりにも高度でしたが)。そして、配列に続く長さを持つ構造体は、本当に特別なコンパイラのサポートを必要としないなどの文字列を宣言し...
VOO

11

Cでは、配列の連続するサブセットも配列であり、そのように操作できます。これは、読み取り操作と書き込み操作の両方に適用されます。サイズが明示的に保存されている場合、このプロパティは保持されません。


6
「デザインが異なる」とは、デザインが異なることに対する理由ではありません。
VF1 14

7
@ VF1:Standard Pascalでプログラミングしたことはありますか?アレイを用いて合理的に柔軟であるCの能力は、アセンブリ上大きな改善(全く安全)とタイプセーフ言語(正確な配列の範囲を含む過剰typesafety)の第一世代であった
MSalters

5
配列をスライスするこの機能は、実際、C89設計の大きな議論です。

古い学校のFortranハッカーは、このプロパティを適切に使用できます(ただし、Fortranの配列にスライスを渡す必要があります)。プログラムやデバッグが複雑で痛みを伴いますが、作業時には高速でエレガントです。
dmckee 14

3
スライスを許可する興味深い設計の選択肢が1つあります。配列と一緒に長さを保存しないでください。配列へのポインターの場合、ポインターとともに長さを保管します。(実際のC配列がある場合、サイズはコンパイル時定数であり、コンパイラーが使用できます。)より多くのスペースを必要としますが、長さを維持しながらスライスできます。&[T]たとえば、Rustはタイプに対してこれを行います。

8

長さでタグ付けされた配列を持つことの最大の問題は、その長さを保存するのに必要なスペースではなく、それをどのように保存するかという問題ではありません長い配列には余分なバイトがありますが、短い配列にも4バイトを使用することは可能です)。より大きな問題は、次のようなコードが与えられることです。

void ClearTwoElements(int *ptr)
{
  ptr[-2] = 0;
  ptr[2] = 0;
}
void blah(void)
{
  static int foo[10] = {1,2,3,4,5,6,7,8,9,10};
  ClearTwoElements(foo+2);
  ClearTwoElements(foo+7);
  ClearTwoElements(foo+1);
  ClearTwoElements(foo+8);
}

コードが最初の呼び出しを受け入れ、ClearTwoElements2番目の呼び出しを拒否できる唯一の方法は、ClearTwoElementsメソッドが、どの場合でも、fooどの部分を認識するかだけでなく、配列の一部への参照を受け取ることを知るのに十分な情報を受け取ることです。これは通常、ポインターパラメーターを渡すコストを2倍にします。さらに、各配列の前にアドレスのポインタが置かれている場合(検証の最も効率的な形式)、最適化されたコードはClearTwoElements次のようになります。

void ClearTwoElements(int *ptr)
{
  int* array_end = ARRAY_END(ptr);
  if ((array_end - ARRAY_BASE(ptr)) < 10 ||
      (ARRAY_BASE(ptr)+4) <= ADDRESS(ptr) ||          
      (array_end - 4) < ADDRESS(ptr)))
    trap();
  *(ADDRESS(ptr) - 4) = 0;
  *(ADDRESS(ptr) + 4) = 0;
}

メソッド呼び出し元は、一般に、配列の先頭へのポインタまたはメソッドへの最後の要素を完全に合法的に渡すことができることに注意してください。メソッドが、渡された配列の外側にある要素にアクセスしようとした場合にのみ、そのようなポインターは問題を引き起こします。したがって、呼び出されたメソッドは、まず、引数を検証するためのポインター演算が範囲外にならないように配列が十分に大きいことを確認してから、引数を検証するためのポインター計算を行う必要があります。そのような検証に費やされる時間は、実際の作業に費やされるコストを超える可能性があります。さらに、メソッドが記述されて呼び出された場合、メソッドはより効率的である可能性があります。

void ClearTwoElements(int arr[], int index)
{
  arr[index-2] = 0;
  arr[index+2] = 0;
}
void blah(void)
{
  static int foo[10] = {1,2,3,4,5,6,7,8,9,10};
  ClearTwoElements(foo,2);
  ClearTwoElements(foo,7);
  ClearTwoElements(foo,1);
  ClearTwoElements(foo,8);
}

オブジェクトを識別するための何かと、その一部を識別するための何かを組み合わせるタイプの概念は、良いものです。ただし、検証を実行する必要がない場合は、Cスタイルのポインターの方が高速です。


配列に実行時サイズがある場合、配列へのポインターは、配列の要素へのポインターと根本的に異なります。後者は(新しい配列を作成せずに)直接元に変換できない場合があります。[]ポインタの構文はまだ存在する可能性がありますが、これらの仮想の「実際の」配列の構文とは異なり、説明する問題はおそらく存在しないでしょう。
ハイド14

@hyde:問題は、オブジェクトのベースアドレスが不明なポインターで算術を許可するかどうかです。また、別の困難を忘れました:構造内の配列。それについて考えると、各ポインターがポインター自体のアドレスだけでなく、上下の法的も含むことを必要とせずに、構造内に格納された配列を指すことができるポインター型があるかどうかはわかりませんアクセスできる範囲。
supercat

交差点。しかし、これはまだamonの答えに還元されると思います。
VF1 14

質問は配列について尋ねます。ポインタはメモリアドレスであり、意図を理解する限り、質問の前提によって変わることはありません。配列は長さを取得し、ポインターは変更されません(ただし、配列へのポインターは、構造体へのポインターによく似た、新しく、固有のタイプである必要があります)。
ハイド14

@hyde:言語のセマンティクスを十分に変更すると、配列に関連する長さを含めることが可能になる場合がありますが、構造内に格納された配列にはいくつかの困難が伴います。セマンティクスをそのまま使用すると、配列の境界チェックは、配列要素へのポインターに同じチェックが適用される場合にのみ有用です。
supercat

7

Cと他のほとんどの第3世代言語、および私が知っているすべての最近の言語との根本的な違いの1つは、Cがプログラマーの生活をより簡単または安全にするように設計されていないことです。プログラマーが自分が何をしているかを知っていて、それだけを正確にやりたいという期待を持って設計されました。「舞台裏」では何もしませんので、驚きはありません。コンパイラレベルの最適化もオプションです(Microsoftコンパイラを使用する場合を除く)。

プログラマーがコードに境界チェックを記述したい場合、Cはそれを行うのに十分簡単に​​しますが、プログラマーはスペース、複雑さ、およびパフォーマンスの観点から対応する価格を支払うことを選択する必要があります。私は長年怒りで使っていませんでしたが、制約に基づいた意思決定の概念を理解するためにプログラミングを教えるときにそれを使っています。基本的に、それはあなたがやりたいことを何でも選択できることを意味しますが、あなたが下すすべての決定には、あなたが知っておく必要がある価格があります。これは、他の人に自分のプログラムに何をしてほしいかを伝え始めるときにさらに重要になります。


3
Cは進化したほど「設計された」ものではありませんでした。もともと、次のような宣言は5項目の配列としてint f[5];作成さfれません。代わりに、と同等でしたint CANT_ACCESS_BY_NAME[5]; int *f = CANT_ACCESS_BY_NAME;。前の宣言は、コンパイラーが配列時間を実際に「理解」する必要なく処理できます。単にスペースを割り当てるためにアセンブラディレクティブを出力する必要があり、その後f、配列と関係があることを忘れることがありました。配列型の一貫性のない動作はこれに起因します。
supercat

1
プログラマーは、Cが要求する程度まで何をしているのかを知らないことがわかります。
CodesInChaos

7

簡潔な答え:

Cは低レベルのプログラミング言語であるため、これらの問題を自分で処理することが期待されますが、これにより、Cの実装方法の柔軟性が大幅に向上します。

Cには、長さで初期化される配列のコンパイル時の概念がありますが、実行時には、データの先頭への単一のポインターとして全体が単純に格納されます。配列とともに配列の長さを関数に渡したい場合は、自分で行います。

retval = my_func(my_array, my_array_length);

または、ポインタと長さを持つ構造体、またはその他のソリューションを使用できます。

より高いレベルの言語は、配列型の一部としてこれを行います。Cでは、これを自分で行う責任が与えられますが、その方法を選択する柔軟性も与えられます。 そして、あなたが書いているすべてのコードがすでに配列の長さを知っているなら、変数として長さを渡す必要はまったくありません。

明白な欠点は、ポインターとして渡される配列の固有の境界チェックがないため、危険なコードを作成できることですが、それは低レベル/システム言語の性質とそれらが与えるトレードオフです。


1
+1「そして、あなたが書いているすべてのコードがすでに配列の長さを知っているなら、その長さを変数として渡す必要はまったくありません。」
林果皞

ポインタと長さの構造体のみが言語と標準ライブラリに組み込まれている場合。多くのセキュリティホールを回避できたはずです。
CodesInChaos

その場合、実際にはCではありません。それを行う他の言語があります。Cは低レベルを取得します。
トーマスルーター

Cは低レベルのプログラミング言語として発明され、多くの方言は依然として低レベルのプログラミングをサポートしていますが、多くのコンパイラ作成者は、実際には低レベル言語とは言えない方言を好みます。これらは低レベルの構文を許可し、さらには必要としますが、その動作が構文によって暗示されるセマンティクスと一致しない可能性のある高レベルの構造を推測しようとします。
supercat

5

追加のストレージの問題は問題ですが、私の意見では小さな問題です。結局のところ、ほとんどの場合、とにかく長さを追跡する必要がありますが、amonは静的に追跡できる場合が多いことを指摘しています。

より大きな問題は、長さをどこに保存し、どのくらいの長さにするかです。すべての状況で機能する場所は1つではありません。データの直前にメモリに長さを格納するだけだと言うかもしれません。配列がメモリではなく、UARTバッファのようなものを指している場合はどうなりますか?

長さを残しておくと、プログラマーは適切な状況に合わせて独自の抽象化を作成できます。また、汎用のケースで利用できる既製のライブラリがたくさんあります。本当の問題は、なぜこれらの抽象化がセキュリティに敏感なアプリケーションで使用されないのかということです。


1
You might say just store the length in the memory just before the data. What if the array isn't pointing to memory, but something like a UART buffer?これについてもう少し説明してもらえますか?また、頻繁に発生する可能性のあることや、それはまれなケースですか?
マフディ

私がそれを設計していた場合、として記述された関数の引数T[]は等価T*ではなく、ポインタとサイズのタプルを関数に渡します。固定サイズの配列は、Cのようにポインターに減衰するのではなく、そのような配列スライスに減衰する可能性があります。このアプローチの主な利点は、それ自体が安全であるということではありませんが、それは標準ライブラリを含むすべてのものができる規則ですビルドします。
-CodesInChaos

1

C言語の開発から:

構造は、マシンのメモリに直感的な方法でマップする必要があるように見えましたが、配列を含む構造では、配列のベースを含むポインタを隠しておく適切な場所も、それを配置する便利な方法もありませんでした初期化されました。たとえば、初期のUnixシステムのディレクトリエントリは、Cでは次のように記述されます。
struct {
    int inumber;
    char    name[14];
};
抽象オブジェクトを特徴付けるだけでなく、ディレクトリから読み取られる可能性のあるビットのコレクションを記述する構造も必要でした。コンパイラnameは、セマンティクスが要求するポインタをどこに隠すことができますか?構造がより抽象的に考えられていて、ポインターのスペースが何らかの形で隠されていたとしても、複雑なオブジェクト、おそらく構造を含む配列を任意の深さで指定したものを割り当てるときに、これらのポインターを適切に初期化する技術的な問題をどのように処理できますか?

このソリューションは、タイプレスBCPLと型付きCの間の進化連鎖における重要なジャンプを構成しました。ストレージ内のポインターの実体化を排除し、代わりに式で配列名が言及されたときにポインターを作成しました。今日のCで生き残っているルールは、配列型の値が式に現れると、配列を構成する最初のオブジェクトへのポインターに変換されるということです。

その一節は、ほとんどの状況で配列式がポインターに崩壊する理由を扱っていますが、配列の長さが配列自体と一緒に保存されない理由にも同じ理由が当てはまります。型定義とメモリ内のその表現との間で1対1のマッピングが必要な場合(リッチーが行ったように)、そのメタデータを保存するのに適した場所はありません。

また、多次元配列についても考えてください。各次元の長さメタデータはどこに保存しますか

T *p = &a[0][0];

for ( size_t i = 0; i < rows; i++ )
  for ( size_t j = 0; j < cols; j++ )
    do_something_with( *p++ );

-2

質問は、Cに配列があると仮定しています。ありません。配列と呼ばれるものは、データとポインター演算の連続シーケンスに対する操作のための単なる構文上の砂糖です。

以下のコードは、実際には文字列であることを知らないintサイズのチャンクで、srcからdstにデータをコピーします。

char src[] = "Hello, world";
char dst[1024];
int *my_array = src; /* What? Compiler warning, but the code is valid. */
int *other_array = dst;
int i;
for (i = 0; i <= sizeof(src)/sizeof(int); i++)
    other_array[i] = my_array[i]; /* Oh well, we've copied some extra bytes */
printf("%s\n", dst);

なぜCは単純化されて適切な配列を持たないのですか?この新しい質問に対する正しい答えがわかりません。しかし、一部の人は、Cが(多少)より読みやすく、移植性の高いアセンブラーであるとよく言います。


2
あなたが質問に答えたとは思わない。
ロバートハーヴェイ14

2
あなたが言ったことは本当ですが、尋ねる人はなぜそうなのかを知りたがっています。

9
Cのニックネームの1つは「ポータブルアセンブリ」であることを忘れないでください。標準の新しいバージョンでは、より高いレベルの概念が追加されていますが、中核には、ほとんどの非自明なマシンに共通する単純な低レベルの構造と命令で構成されています。これにより、言語で行われたほとんどの設計上の決定が行われます。実行時に存在する変数は、整数、浮動小数点数、およびポインターのみです。命令には、算術演算、比較、ジャンプが含まれます。それ以外のほとんどすべては、その上に構築される薄層です。

8
他の構成要素と同じバイナリを実際に生成できないことを考えると、Cに配列がないと言うのは間違っています(少なくとも、配列サイズを決定するために#definesの使用を検討している場合はそうではありません)。Cの配列「連続したデータのシーケンス」であり、それについて甘いものはありません。ポインタを配列のように使用することは、ここでは(明示的なポインタ演算の代わりに)構文上の砂糖であり、配列自体ではありません。
ハイド14

2
はい、次のコードを検討してくださいstruct Foo { int arr[10]; }arrポインタではなく配列です。
スティーブンバーナップ14
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.