1. ホーム
  2. c

[解決済み】C言語の矢印(->)演算子はなぜ存在するのですか?

2022-04-17 03:45:50

質問

ドット( . ) 演算子は構造体のメンバにアクセスするために使用され、矢印演算子 ( -> C言語では、当該ポインタが参照する構造体のメンバにアクセスするために使用されます。

ポインタ自身はドット演算子でアクセスできるようなメンバを持っていません(実際には仮想メモリ上の位置を表す数値に過ぎないので、メンバは持っていません)。つまり、ポインタに対してドット演算子が使われた場合、自動的にポインタの参照が解除されるように定義すれば、曖昧さはなくなります(コンパイラがコンパイル時に知っている情報です)。

では、なぜ言語開発者は、この一見不要な演算子を追加して、物事をより複雑にすることにしたのでしょうか?設計上の大きな判断は何なのだろうか?

どのように解決するのか?

ご質問を2つの質問に分けて解釈します。1) なぜ -> が存在する理由と、2)なぜ . は自動的にポインタの参照を解除しない。この2つの疑問に対する答えは、歴史的なルーツにある。

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

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

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

struct S {
  int a;
  int b;
};

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

struct X {
  int a;
  int x;
};

という名前なので、それでOKでしょう。 a は一貫してオフセット 0 を表します。 しかし、この追加宣言は

struct Y {
  int b;
  int a;
};

は形式的に無効である。なぜなら、これは "redefine" を試みたからである。 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 オフセットを追加 2 を指定し 42int の値を、結果のアドレスで表示します。つまり、上記は 42int アドレスの値 7 . なお、この -> は、左辺の式の型を気にしませんでした。左辺はrvalueの数値アドレス(ポインタであれ整数であれ)として解釈された。

このようなトリックが可能なのは *. の組み合わせになります。を行うことはできませんでした。

(*i).b = 42;

から *i はすでに無効な表現です。そのため * 演算子とは別のものであるため . このため、オペランドにはより厳しい型要求が課せられます。この制限を回避する機能を提供するために,CRMは -> 演算子で、左側のオペランドの型に依存しない。

Keithがコメントで指摘しているように、この ->* + . の組み合わせは、CRMが7.1.8で言うところの「要求事項の緩和」である。 という要件が緩和された以外は E1 はポインタ型であるため、式 E1−>MOS とは全く同じ意味です。 (*E1).MOS

その後、K&R Cでは、CRMで説明した機能の多くが大幅に作り直された。構造体メンバをグローバルオフセット識別子とする考え方は、完全に削除されました。また -> 演算子の機能と完全に同一になりました。 *. の組み合わせになります。

なぜ . は自動的にポインタを参照するのですか?

繰り返しになりますが、CRMバージョンの言語では、左オペランドが . 演算子は . それが だけ という要件がそのオペランドに課せられています(そして、それが -> ということである。) なお、CRMは ではなく の左オペランドを必要とします。 . は構造体型である必要があります。それは単にlvalueであることを要求しているだけです。 任意の lvalueを使用します。つまり、CRMバージョンのC言語では、次のようなコードを書くことができます。

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

struct T c;
c.b = 55;

この場合、コンパイラは次のように書きます。 55int という連続したメモリブロックのバイトオフセット2に位置する値です。 c であるにもかかわらず、タイプ struct T という名前のフィールドはありませんでした。 b . コンパイラは、実際の型である c を使用します。気になるのは c はlvalueであり、書き込み可能なメモリブロックの一種である。

ここで、もしあなたがこのようなことをした場合に注意してください。

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

の場合、コードは有効であるとみなされます。 s は lvalue でもあるため、コンパイラは単にデータ をポインタ s それ自体 バイトオフセット 2 である。言うまでもなく、このようなことは簡単にメモリオーバーになるのだが、この言語はそのようなことには無頓着であった。

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

もちろん、この奇妙な機能は、オーバーロードされた . 演算子でポインタを指定できるようにすることを提案しました。たぶん当時はCRMバージョンのCで書かれたレガシーコードがあり、それをサポートする必要があったのでしょう。

(1975年版C言語リファレンスマニュアルのURLは安定していない可能性があります。別のコピー(おそらく微妙な違いがある)は、次のとおりです。 こちら .)