C言語に矢印(->)演算子が存在するのはなぜですか?質問する

C言語に矢印(->)演算子が存在するのはなぜですか?質問する

ドット ( .) 演算子は構造体のメンバーにアクセスするために使用され、C の矢印演算子 ( ->) は、問題のポインタによって参照される構造体のメンバーにアクセスするために使用されます。

ポインタ自体には、ドット演算子でアクセスできるメンバーはありません (実際には仮想メモリ内の場所を表す数値にすぎないため、メンバーはありません)。したがって、ドット演算子がポインタで使用される場合にポインタを自動的に逆参照するように定義すれば、あいまいさは発生しません (この情報は、私の知る限り、コンパイル時にコンパイラに認識されます)。

では、なぜ言語作成者は、この一見不必要な演算子を追加して、物事をさらに複雑にすることにしたのでしょうか? 大きな設計上の決定とは何でしょうか?

ベストアンサー1

あなたの質問を 2 つの質問として解釈します。1) なぜ->存在するのか、2) なぜ.自動的にポインターを逆参照しないのか。両方の質問に対する答えは歴史的なルーツを持っています。

なぜ->存在するのでしょうか?

C言語の最初のバージョンの一つ(ここではCRMと呼ぶ)では、C リファレンスマニュアル1975年5月に6th Edition Unixに付属していた「」では、演算子は非常に排他的な意味を持ち、組み合わせと->は同義ではありませんでした。*.

CRMで記述されたC言語は、多くの点で現代のCとは大きく異なっていました。CRMでは、構造体メンバーはバイトオフセットのグローバル概念を実装しており、これは型の制限なしに任意のアドレス値に追加できます。つまり、すべての構造体メンバーの名前は独立したグローバルな意味を持ちます(したがって、一意である必要があります)。たとえば、次のように宣言できます。

struct S {
  int a;
  int b;
};

そしてnameはaオフセット0を表し、nameはbオフセット2を表します(int型のサイズが2でパディングがないと仮定)。言語では、翻訳単位内のすべての構造体のすべてのメンバーが一意の名前を持つか、同じオフセット値を表す必要があります。たとえば、同じ翻訳単位で、さらに次のように宣言できます。

struct X {
  int a;
  int x;
};

名前はa一貫してオフセット0を表すので、それで問題ありません。しかし、この追加の宣言は

struct Y {
  int b;
  int a;
};

aオフセット 2 およびbオフセット 0 として「再定義」しようとしたため、正式には無効になります。

ここで->演算子が登場します。各構造体メンバー名には独自の自己完結的なグローバルな意味があるため、言語は次のような式をサポートしていました。

int i = 5;
i->b = 42;  /* Write 42 into `int` at address 7 */
100->a = 0; /* Write 0 into `int` at address 100 */

最初の代入は、コンパイラによって「アドレス を取得し、それに5オフセットを追加し、結果のアドレスの値に代入する」と解釈されました。つまり、上記はアドレス の値に代入します。 のこの使用法では、左側の式の型は考慮されないことに注意してください。左側は、右辺値の数値アドレス (ポインターまたは整数) として解釈されました。242int42int7->

このようなトリックは、組み合わせでは不可能でし*.

(*i).b = 42;

*iはすでに無効な式であるため、*演算子は とは別であるため、そのオペランドに対してより厳格な型要件を課します。この制限を回避する機能を提供するために、CRM は左側のオペランドの型に依存しない演算子.を導入しました。->

Keithがコメントで指摘したように、->*+の.組み合わせのこの違いは、CRMが7.1.8で「要件の緩和」と呼んでいるものです。ポインタ型であるという要件の緩和を除けばE1、式はE1−>MOS正確に次の式と同等です。(*E1).MOS

その後、K&R C では、CRM で最初に記述された多くの機能が大幅に改良されました。「構造体メンバーをグローバル オフセット識別子として使用する」というアイデアは完全に削除されました。また、演算子の機能は、および組み合わせ->の機能と完全に同一になりました。*.

.なぜポインタを自動的に逆参照できないのですか?

もう一度言いますが、CRM バージョンの言語では、演算子の左オペランドはlvalue.である必要がありました。これがそのオペランドに課せられた唯一の要件でした (そして、それが上で説明したように と異なる点です)。CRM ではの左オペランドが struct 型である必要はなかったことに注意してください。必要なのは lvalue、つまり任意のlvalue であるだけでした。つまり、CRM バージョンの C では次のようなコードを記述できます。->.

struct S { int a, b; };
struct T { float x, y, z; };

struct T c;
c.b = 55;

この場合、 typeに というフィールドがなくても、コンパイラはと呼ばれる連続メモリ ブロックのバイト オフセット 2 に位置する値55に書き込みます。コンパイラは の実際の型についてはまったく考慮しません。コンパイラが考慮するのは、 がlvalue、つまり何らかの書き込み可能なメモリ ブロックであるということだけです。intcstruct Tbcc

さて、これをやると

S *s;
...
s.b = 42;

コードは有効であると見なされ (sも左辺値であるため)、コンパイラは単にポインタs自体のバイトオフセット 2 にデータを書き込もうとします。言うまでもなく、このようなことは簡単にメモリ オーバーランを引き起こす可能性がありますが、言語はそのような問題を考慮に入れていません。

つまり、そのバージョンの言語では、.ポインター型の演算子のオーバーロードに関する提案されたアイデアは機能しません。演算子は、.ポインター (lvalue ポインターまたは任意の lvalue) で使用される場合、すでに非常に特殊な意味を持っています。間違いなく、非常に奇妙な機能です。しかし、当時は存在していました。

もちろん、この奇妙な機能は、.C の改訂版である K&R C でポインターのオーバーロード演算子 (提案どおり) を導入することに対する強力な反対理由にはなりません。しかし、それは実行されていません。おそらく、当時は CRM バージョンの C で書かれたレガシー コードがいくつかあり、それをサポートする必要があったのでしょう。

(1975年のCリファレンスマニュアルのURLは不安定かもしれません。微妙な違いがあるかもしれない別のコピーは、ここ

おすすめ記事