6

C の入門書では、ポインターは多かれ少なかれ配列であると主張されることがよくあります。せいぜい、これは大幅な単純化ではないでしょうか。

Cに配列型があり、ポインターとはまったく異なる動作をすることができます。次に例を示します。

#include <stdio.h>

int main(int argc, char *argv[]){
  int a[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
  int *b = a;
  printf("sizeof(a) = %lu\n", sizeof(a));
  printf("sizeof(b) = %lu\n", sizeof(b));
  return 0;
}

出力を与える

sizeof(a) = 40 
sizeof(b) = 8 

または、別の例a = bではコンパイル エラーが発生します (GCC: "配列型の式への代入")。

もちろん、ポインタと配列の間には密接な関係があります。はい、配列変数自体の内容は最初の配列要素のメモリアドレスです。たとえばint a[10] = {777, 1, 2, 3, 4, 5, 6, 7, 8, 9}; printf("a = %ul\n", a);、777 を含むアドレスを出力します。

一方では、配列を構造体に「隠す」と、=演算子を使用するだけで大​​量のデータ (ラッピング構造体を無視した場合は配列) を簡単にコピーできます (それも高速です)。

#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define ARRAY_LENGTH 100000000

typedef struct {int arr[ARRAY_LENGTH];} struct_huge_array;

int main(int argc, char *argv[]){
  struct_huge_array *a = malloc(sizeof(struct_huge_array));
  struct_huge_array *b = malloc(sizeof(struct_huge_array));

  int *x = malloc(sizeof(int)*ARRAY_LENGTH);
  int *y = malloc(sizeof(int)*ARRAY_LENGTH);

  struct timeval start, end, diff;

  gettimeofday(&start, NULL);
  *a = *b;
  gettimeofday(&end, NULL);

  timersub(&end, &start, &diff);
  printf("Copying struct_huge_arrays took %d sec, %d µs\n", diff.tv_sec, diff.tv_usec); 

  gettimeofday(&start, NULL);
  memcpy(x, y, ARRAY_LENGTH*sizeof(int));
  gettimeofday(&end, NULL);

  timersub(&end, &start, &diff);
  printf("memcpy took %d sec, %d µs\n", diff.tv_sec, diff.tv_usec); 

  return 0;
}

出力:

Copying struct_huge_arrays took 0 sec, 345581 µs
memcpy took 0 sec, 345912 µs

ただし、配列自体でこれを行うことはできません。配列x, y(サイズと型が同じ) の場合、式x = yは不正です。

次に、関数は配列を返すことができません。または、配列が引数として使用されている場合、Cはそれらをポインターに折りたたみます。サイズが明示的に指定されているかどうかは気にしないため、次のプログラムは出力を提供しますsizeof(a) = 8

#include <stdio.h>

void f(int p[10]){
  printf("sizeof(a) = %d\n", sizeof(p));
}

int main(int argc, char *argv[]){
  int a[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

  f(a);

  return 0;
}

この配列への嫌悪の背後にある論理はありますか? C に真に堅牢な配列型がないのはなぜですか? あるとしたらどんな悪いことが起こるでしょうか?結局のところ、配列が a に隠されている場合、配列はstructGo Rust などのように動作します。つまり、配列メモリ内のチャンク全体であり、それを渡すと、最初のメモリ アドレスだけでなく、その内容がコピーされます。エレメント。たとえば、次のプログラムの Go のように

package main

import "fmt"

func main() {
    a := [2]int{-777, 777}
    var b [2]int
    b = a
    b[0] = 666

    fmt.Println(a)
    fmt.Println(b)
}

出力を与えます:

[-777 777]
[666 777]
4

3 に答える 3

6

C 言語は、1970 年代初頭にPDP ミニコンピューター上で最初に設計されました。PDP ミニコンピューターは、その 24 kB の巨大なメモリにもかかわらず、部屋の半分を埋めただけだったと報告されています。(MB や GB ではなく、KB です)。

そのメモリにコンパイラを完全に適合させることは、本当の挑戦でした。そのため、C 言語はコンパクトなプログラムを記述できるように設計されており、手動で最適化するためにかなりの数の特殊な演算子 (+=、-、および ?: など) が追加されました。

大きな配列をパラメーターとしてコピーするための機能を追加することは、設計者には思い浮かびませんでした。とにかく役に立たなかったでしょう。

C の前身である B 言語では、配列は個別に割り当てられたストレージへのポインターとして表されていました ( Lars の回答のリンクを参照)。Ritchie は、C でこの余分なポインターを回避したかったため、配列を想定していない場所で使用すると、配列名をポインターに変換できるという考えに至りました。

ストレージ内のポインターの実体化を排除し、代わりに配列名が式で言及されたときにポインターを作成しました。今日の C で生き残っているルールは、配列型の値が式に現れると、配列を構成する最初のオブジェクトへのポインターに変換されるということです。

この発明により、言語のセマンティクスの根本的な変化にもかかわらず、ほとんどの既存の B コードが機能し続けることが可能になりました。

そしてstructs は後になってから言語に追加されました。struct 内で配列をパラメーターとして渡すことができるということは、別のオプションを提供する機能でした。

配列の構文を変更することは、すでに遅すぎました。それはあまりにも多くのプログラムを壊します。すでに何百人ものユーザーがいました...

于 2016-02-24T09:53:32.853 に答える
4

質問のこの部分...

この配列への嫌悪の背後にある論理はありますか? C に真に堅牢な配列型がないのはなぜですか? あるとしたらどんな悪いことが起こるでしょうか?

...実際にはコードの質問ではなく、推測の余地がありますが、短い答えが有益かもしれないと思います.Cが作成されたとき、CはRAMが非常に少なく、CPUが遅いマシンを対象としていました(キロバイトとメガヘルツで測定され、それぞれ)。システム プログラミング言語として Assembler を置き換えることを意図していましたが、他の既存の高水準言語が必要とするオーバーヘッドを導入することはありませんでした。同じ理由で、C は、生成されたプログラムを制御できるため、依然としてマイクロ コントローラーの言語として人気があります。

「堅牢な」配列型を導入すると、コンパイラとランタイムの両方で内部パフォーマンスと複雑さが低下し、すべてのシステムで許容できるわけではありませんでした。同時に、C は、プログラマーが独自の「堅牢な」配列型を作成し、その使用が正当化される状況でのみそれらを使用する機能を提供します。

この文脈でこの記事は興味深いと思いました: Dennis Ritchie: Development of the C Language (1993)

于 2016-02-24T09:57:59.903 に答える