私はあなたの質問を2つの質問として解釈します:1)なぜ->
存在するのか、2)なぜ.
ポインターを自動的に逆参照しないのか。両方の質問への回答には歴史的なルーツがあります。
なぜ->
存在するのですか?
C言語の最初のバージョンの1つ(1975年5月に第6版Unixに付属した「Cリファレンスマニュアル」のCRMと呼びます)では、演算子->
は非常に排他的な意味を持ち、同義語*
や.
組み合わせではありませんでした
CRMによって記述されたC言語は、多くの点で現代のCとは非常に異なっていました。CRMの構造体メンバーは、バイトオフセットのグローバルな概念を実装しました。これは、タイプの制限なしに任意のアドレス値に追加できます。つまり、すべての構造体メンバーのすべての名前は、独立したグローバルな意味を持っていました(したがって、一意である必要がありました)。たとえば、宣言することができます
struct S {
int a;
int b;
};
namea
はオフセット0を表し、nameはオフセット2を表します(サイズ2のタイプで、パディングがないとb
仮定)。int
言語では、変換ユニット内のすべての構造体のすべてのメンバーが一意の名前を持っているか、同じオフセット値を表している必要があります。たとえば、同じ翻訳単位で、追加で宣言できます
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
、それにオフセットを追加して、結果のアドレスの値に2
割り当てる」と解釈されました。つまり、上記はアドレスの値に割り当てられます。このの使用は、左側の式のタイプを気にしないことに注意してください。左側は、右辺値の数値アドレス(ポインターまたは整数)として解釈されました。42
int
42
int
7
->
*
この種のトリックは、.
組み合わせでは不可能でした。あなたはできませんでした
(*i).b = 42;
*i
はすでに無効な式なので。*
演算子は、とは別であるため、.
オペランドにさらに厳密な型要件を課します。この制限を回避する機能を提供するために、CRM->
は、左側のオペランドのタイプに依存しない演算子を導入しました。
->
キースがコメントで述べたように、と*
+の組み合わせのこの違い.
は、CRMが7.1.8で「要件の緩和」と呼んでいるものです。ポインタ型の要件の緩和を除いてE1
、式E1−>MOS
は正確に次のようになります。(*E1).MOS
その後、K&R Cでは、CRMで最初に説明された多くの機能が大幅に作り直されました。「グローバルオフセット識別子としての構造体メンバー」の概念は完全に削除されました。そして、オペレーターの機能は、組み合わせ->
の機能と完全に同一になりました。*
.
.
ポインタを自動的に逆参照できないのはなぜですか?
この場合も、言語のCRMバージョンでは、演算子の左側のオペランドは左辺値.
である必要がありました。それがそのオペランドに課せられた唯一の要件でした(そしてそれが、上で説明したように、それをとは異なるものにしました)。CRMでは、の左オペランドに構造体型が必要ないことに注意してください。左辺値、任意の左辺値である必要がありました。これは、CRMバージョンのCでは、次のようなコードを記述できることを意味します。->
.
struct S { int a, b; };
struct T { float x, y, z; };
struct T c;
c.b = 55;
この場合、コンパイラは、タイプに。という名前のフィールドがない場合でも、と呼ばれる連続メモリブロックのバイトオフセット2に配置55
された値に書き込みます。コンパイラは、実際のタイプをまったく気にしません。それが気にしたのはそれが左辺値だったということだけです:ある種の書き込み可能なメモリブロック。int
c
struct T
b
c
c
これを行った場合は注意してください
S *s;
...
s.b = 42;
コードは有効であると見なされ(左辺値でもあるため)、コンパイラはバイトオフセット2でポインタ自体s
にデータを書き込もうとします。言うまでもなく、このようなことは簡単にメモリオーバーランを引き起こす可能性がありますが、言語そのような問題には関心がありませんでした。s
つまり、そのバージョンの言語では、ポインター型の演算子のオーバーロードについて提案されたアイデア.
は機能しません。演算子.
は、ポインター(左辺値ポインターまたは任意の左辺値)で使用すると、すでに非常に特殊な意味を持っていました。それは間違いなく非常に奇妙な機能でした。しかし、それは当時そこにありました。
もちろん、この奇妙な機能は.
、C-K&R Cの作り直されたバージョンで(あなたが提案したように)ポインターのオーバーロードされた演算子を導入することに対するそれほど強力な理由ではありません。しかし、それは行われていません。たぶんその時、サポートされなければならなかったCのCRMバージョンで書かれたいくつかのレガシーコードがありました。
(1975 CリファレンスマニュアルのURLは安定していない可能性があります。微妙な違いがあるかもしれませんが、別のコピーがここにあります。)