配列、ポインター、および関数のC構文がこのように設計されたのはなぜですか?


16

次のような多くの質問を見た(そして尋ねた!)後

int (*f)(int (*a)[5])C ではどういう意味ですか?

そして、人々がC構文を理解するのを助けるためのプログラムを作っとさえ見て、私は不思議に思わずにはいられません:

Cの構文がこのように設計されたのはなぜですか?

たとえば、ポインターを設計している場合、「ポインターの10要素配列へのポインター」を次のように変換します。

int*[10]* p;

そしてありません

int* (*p)[10];

ほとんどの人が同意するだろうと私は思うが、それほど簡単ではない。

だから、なぜ、直感的でない構文ですか?構文が解決する特定の問題(おそらく曖昧さ?)がありますか?


2
これに対する本当の答えはありませんし、そのような質問もあります。正しい?あなたが得るものは単なる推測です。
BЈовић

7
@VJo-「本当の」(つまり客観的な)答えがあるかもしれません-言語の作者と標準化委員会は同様にこれらの決定の多くを明示的に正当化(または少なくとも説明)しています。
確実に

提案された構文は、必ずしもC構文よりも「直感的」だとは思いません。Cはそれです。一度学習すれば、これらの質問は二度とありません。あなたがそれを学んでいないなら...まあ、多分それは本当の問題です。
カレブ

1
@Caleb:おかしい、私はそれを学んだし、私はまだ、この質問...持っていたので、あなたは、そう簡単にそれを締結する方法
Mehrdad

1
このcdeclコマンドは、複雑なC宣言のデコードに非常に便利です。cdecl.orgにはWebインターフェースもあります。
キーストンプソン

回答:


16

それの歴史の​​私の理解は、それが2つの主要なポイントに基づいているということです...

まず、言語の作成者は、構文をタイプ中心ではなく変数中心にすることを好みました。つまり、プログラマーが宣言を見て、「式を書くと、*func(arg)結果が得られintます;書く*arg[N]と、フロートがfunc必要になる」ではなく、「これをとる関数へのポインターでなければならない」そして、返すことを」。

ウィキペディアCエントリは、次のことを主張しています。

リッチーのアイデアは、使用法に似たコンテキストで識別子を宣言することでした:「宣言は使用法を反映します」。

... K&R2のp122を引用します。悲しいかな、私はあなたのために拡張見積を見つけるために手渡す必要はありません。

第二に、実際には、任意のレベルの間接参照を扱う場合に一貫性のある宣言の構文を考え出すのは本当に難しいです。あなたの例は、あなたがすぐに考えたタイプを表現するのにうまくいくかもしれませんが、それらのタイプの配列へのポインタを取り、他の恐ろしい混乱を返す関数にスケーリングしますか?(たぶんそうですが、確認しましたか?それを証明できますか?)。

Cの成功の一部は、コンパイラが多くの異なるプラットフォーム用に作成されたという事実によるものであるため、コンパイラを記述しやすくするために、ある程度の読みやすさを無視した方が良いかもしれません。

そうは言っても、私は言語文法やコンパイラー作成の専門家ではありません。しかし、私は知っておくべきことがたくさんあることを知るのに十分知っています;)


2
「コンパイラを記述しやすくする」... Cが解析しにくいことで悪名高い(C ++のみがトップ)。
Jan Hudec

1
@JanHudec-ええと...ええ。それは防水の声明ではありません。しかし、Cは文脈自由文法として解析することは不可能ですが、1人の人がそれを解析する方法を見つけたら、これは難しいステップではなくなります。事実、初期の頃は、コンパイラーを簡単にバングアウトできるため多かったので、K&Rはバランスをとっていたに違いありません。(リチャード・ガブリエルの悪名高いの台頭「さらに悪いことには優れている」 -と嘆い- 、彼は当然のかかる。それが新しいプラットフォーム用のCコンパイラを書くのは簡単だという事実を)
detly

ちなみに、これについては修正できてうれしいです。構文解析と文法についてはあまり知りません。私は、歴史的事実からさらに推測します。
確実に

12

C言語の奇妙な点の多くは、設計時のコンピューターの動作によって説明できます。ストレージメモリの量は非常に限られていたため、ソースコードファイル自体のサイズを最小限に抑えることが非常に重要でした。70年代および80年代のプログラミングは、ソースコードに含まれる文字をできるだけ少なくし、できればソースコードのコメントが過剰に含まれないようにすることでした。

もちろん、これは今日ではとんでもないことであり、ハードドライブにほぼ無制限のストレージスペースがあります。しかし、それはCが一般にそのような奇妙な構文を持っている理由の一部です。


特に配列ポインターに関しては、2番目の例がありますint (*p)[10];(そう、構文は非常に紛らわしいです)。多分それを「10の配列への整数ポインタ」として読むでしょう...これはいくらか理にかなっています。括弧がない場合、コンパイラーは代わりに10個のポインターの配列として解釈し、宣言にまったく異なる意味を与えます。

配列ポインターと関数ポインターはどちらもCで非常に曖昧な構文を持っているため、行うべき賢明なことは、奇妙さをtypedefすることです。おそらくこのように:

わかりにくい例:

int func (int (*arr_ptr)[10])
{
  return 0;
}

int main()
{
  int array[10];
  int (*arr_ptr)[10]  = &array;
  int (*func_ptr)(int(*)[10]) = &func;

  func_ptr(arr_ptr);
}

あいまいでない、同等の例:

typedef int array_t[10];
typedef int (*funcptr_t)(array_t*);


int func (array_t* arr_ptr)
{
  return 0;
}

int main()
{
  int        array[10];
  array_t*   arr_ptr  = &array; /* non-obscure array pointer */
  funcptr_t  func_ptr = &func;  /* non-obscure function pointer */

  func_ptr(arr_ptr);
}

関数ポインタの配列を扱う場合、事態はさらに曖昧になる可能性があります。またはそれらのすべての最もあいまいな:関数ポインタを返す関数(やや便利)。このようなことにtypedefを使用しないと、すぐに狂気に陥ります。


ああ、ついに合理的な答え。:-)特定の構文が実際にソースコードサイズを縮小する方法については興味がありますが、とにかくそれはもっともらしいアイデアであり、理にかなっています。ありがとう。+1
Mehrdad

ソースコードのサイズについては少なく、コンパイラの記述についてはもっと重要だったと思いますが、「typdef away the weirdness」は間違いなく+1です。これを実現できると気づいた日、私の精神的健康は劇的に改善しました。
確実に

2
[引用が必要]ソースコードのサイズについて。私はそのような制限について聞いたことがありません(多分それは「誰もが知っている」何かかもしれません)。
ショーンマクミラン

1
さて、IBM、DEC、XEROXキットのCOBOL、Assembler、CORAL、PL / 1で70年代にプログラムをコーディングしましたが、ソースコードサイズの制限に遭遇したことはありません。配列サイズ、実行可能サイズ、プログラム名サイズの制限-ソースコードサイズは制限されません。
ジェームズアンダーソン

1
@Sean McMillan:ソースコードのサイズが制限だとは思いません(当時はPascalのような冗長な言語がかなり普及していたと考えてください)。そして、たとえそうだったとしても、ソースコードを事前に解析し、長いキーワードを短い1バイトコードに置き換えるのは非常に簡単だったと思います(たとえば、ある種のBasicインタープリター)。だから、「Cは簡潔で、利用可能なメモリが少ない時代に発明されたからです」という議論は少し弱いと思います。
ジョルジオ

7

それは非常に簡単です。int *pつまり*p、intであることを意味します。int a[5]それa[i]はintであることを意味します。

int (*f)(int (*a)[5])

これ*fは関数で*aあり、5つの整数の配列であるためf、5つの整数の配列へのポインターを取り、intを返す関数です。ただし、Cでは、配列にポインターを渡すことは役に立ちません。

Cの宣言がこれを複雑にすることはほとんどありません。

また、typedefを使用して明確にすることができます。

typedef int vec5[5];
int (*f)(vec5 *a);

4
これは失礼に聞こえますが(私はそうではありません)、謝罪しますが、質問のすべてのポイントを見逃したと思います...:\
Mehrdad

2
@Mehrdad:KernighanとRitchieの頭の中に何があったのか、私には言えません。構文の背後にあるロジックを説明しました。私はほとんどの人については知りませんが、あなたの提案された構文がより明確だとは思いません。
ケビンクライン

私は同意します-そのような複雑な宣言を見ることは珍しいです。
カレブ

Cの宣言以前からの設計typedefconstvolatile、および宣言の中のものを初期化する機能。宣言構文の迷惑なあいまいさの多く(たとえば、型または宣言子にint const *p, *q;バインドすべきかどうかconst)は、元々設計された言語では発生しませんでした。言語が型と宣言子の間にコロンを追加したかったが、修飾子なしで組み込みの「予約語」型を使用する場合は省略できました。意味int: const *p,*q;int const *: p,*q;は明らかだったでしょう。
-supercat

3

* []は、変数に付加される演算子として考慮する必要があると思います。*は変数の前に、[]は変数の後に書き込まれます。

型式を読みましょう

int* (*p)[10];

最も内側の要素は変数pです。したがって、

p

意味:pは変数です。

変数の前に*がある場合、*演算子は常にそれが参照する式の前に置かれます。したがって、

(*p)

意味:変数pはポインターです。()がなければ、右側の[]演算子の優先順位が高くなります。つまり、

**p[]

として解析されます

*(*(p[]))

次のステップは[]:()がないため、[]は外側の*よりも優先順位が高いため、

(*p)[]

意味:(変数pはポインター)配列へ。次に、2番目の*があります。

* (*p)[]

意味:((変数pはポインターです)配列への)ポインターの

最後に、最も低い優先度を持つint演算子(型名)があります。

int* (*p)[]

意味:(((変数pは配列へのポインター)ポインターの))整数へ。

したがって、システム全体は演算子を使用した型式に基づいており、各演算子には独自の優先ルールがあります。これにより、非常に複雑なタイプを定義できます。


0

考え始めるときはそれほど難しくなく、Cは決して簡単な言語ではありませんでした。そして、int*[10]* p実際にはないよりも簡単でint* (*p)[10] あり、Kのタイプはでどうなりますかint*[10]* p, k;


2
K私はコンパイラがどうなるかを考え出すことができ、失敗したコードレビューになり、私も気にすることができるが、私はプログラマが意図したものを動作することはできません-失敗............
mattnz

そして、なぜkはコードレビューに失敗しましたか?
ダイニウス

1
コードが判読不能で維持できないためです。コードは修正するために正しくなく、明らかに正しいものであり、メンテナンスを行っても正しいままである可​​能性があります。タイプkが何であるかを尋ねなければならないという事実は、コードがこれらの基本的な要件を満たしていない兆候です。
mattnz

1
基本的に、同じ行に異なるタイプの3つの(この場合)変数宣言があります(例:int * p、int i [10]、int k)。それは受け入れられません。変数が何らかの形式の関係、たとえばintの幅、高さ、深さを持っている場合、同じ型の複数の宣言は受け入れられます。多くの人がint * pを使用してプログラムするので、「int * p、i;」のwhats iを覚えておいてください。
mattnz

1
@mattnzが言おうとしているのは、あなたが望むように賢くなれるということですが、意図が明らかでない場合やコードが不十分に書かれていたり読めなかったりする場合、それはすべて無意味です。この種のものは、しばしばコードの破損と時間の浪費につながります。プラス、pointer to intint彼らは個別に宣言する必要がありますので、しても同じタイプではありません。限目。男の話を聞いてください。彼には理由があり、18kの担当者がいます。
ブレーデンベスト
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.