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


264

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

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

では、言語の作成者が、この一見不要に見える演算子を追加することで、物事をより複雑にすることに決めたのはなぜですか?大きな設計上の決定は何ですか?


1
関連:stackoverflow.com/questions/221346/…- また、上書きできます->
Krease

16
@ChrisこれはC ++に関するもので、もちろん大きな違いがあります。しかし、なぜ Cがこのように設計されたかについて話しているので、C ++が存在する前の1970年代に戻ったとしましょう。
Mysticial

5
私の推測では、矢印演算子は「それを見る!ここでポインタを操作している」という視覚的に表現するために存在しているということです
Chris

4
一見すると、この質問は奇妙だと思います。すべてが慎重に設計されているわけではありません。一生このスタイルを維持すると、あなたの世界は疑問に満ちたものになるでしょう。ほとんどの票を獲得した答えは本当に有益で明確です。しかし、それはあなたの質問の要点には達しません。あなたの質問のスタイルに従ってください、私はあまりにも多くの質問をすることができます。たとえば、キーワード「int」は「integer」の省略形です。キーワード「double」も短くならないのはなぜですか?
junwanghe

1
@junwangheこの質問は実際には有効な懸念を表しています-なぜ.演算子が演算子よりも優先順位が高いのです*か?そうでない場合は、* ptr.memberおよびvar.memberを使用できます。
Milleniumbug

回答:


358

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

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

(私は「のためのCRMとして参照するC言語の非常に最初のバージョンのいずれかでCリファレンス・マニュアル 1975年5月に第6版の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、オフセット追加2それと割り当てるため42int生じたアドレスの値」。つまり、上記はアドレスの値に割り当て42られintます7。このの使用は->、左側の式のタイプを気にしなかったことに注意してください。左側は右辺値の数値アドレス(ポインターまたは整数)として解釈されました。

この種の策略は、*およびの.組み合わせでは不可能でした。できなかった

(*i).b = 42;

以降は、*iすでに無効な式です。*それから分離されているので、オペレータは.、そのオペランドに厳しくタイプの要件を課します。この制限を回避する機能を提供するために、CRMは->演算子を導入しました。これは、左側のオペランドのタイプから独立しています。

キースはコメントで述べたように、この差の->*+ .の組み合わせは、どのようなCRMは7.1.8で、「要件の緩和」と言及されている。その要件の緩和を除きE1、式はポインタ型のものE1−>MOSとまったく同じです(*E1).MOS

その後、K&R Cでは、CRMで最初に説明された多くの機能が大幅に作り直されました。「グローバルオフセット識別子としての構造体メンバー」の概念は完全に削除されました。そして、->オペレーターの機能は、の機能*.組み合わせと完全に同一になりました。

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

この場合も、CRMバージョンの言語では、.演算子の左のオペランドは左辺値である必要がありました。それがそのオペランドに課された唯一の要件でした(そしてそれが、上で->説明したように、それがとの違いをもたらしたものです)。CRM の左のオペランドが構造体型を持つ必要がないことに注意してください.。左辺値、任意の左辺値である必要があります。つまり、CのCRMバージョンでは、次のようなコードを記述できます。

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

struct T c;
c.b = 55;

この場合、型にはという名前のフィールドがなくても、コンパイラーはと呼ばれる連続メモリブロックのバイトオフセット2に位置55するint値に書き込みます。コンパイラは実際のタイプをまったく気にしません。それが気にしたのはそれが左辺値でした:ある種の書き込み可能なメモリブロックです。cstruct Tbcc

これを行った場合、

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

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

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

もちろん、この奇妙な機能は.、C-K&R Cのリワークバージョンにポインタのオーバーロードされた演算子を導入することに対して非常に強い理由ではありません。しかし、それは行われていません。たぶん当時は、CバージョンのCRMで記述されたレガシーコードがいくつかサポートされていたはずです。

(1975 CリファレンスマニュアルのURLは安定していない場合があります。微妙な違いがあると思われる別のコピーがここにあります。)


10
また、引用されたCリファレンスマニュアルのセクション7.1.8には、「E1がポインター型であるという要件の緩和を除いて、式 '' E1-> MOS ''は ''(* E1).MOS 'とまったく同じです。 」
キース・トンプソン、

1
なぜそれが*iアドレス5にあるデフォルトの型(int?)の左辺値を持っていなかったのですか?その場合、(* i).bも同じように機能します。
Random832

5
@Leo:まあ、一部の人々はより高いレベルのアセンブラとしてC言語を空想します。Cの歴史のその時代には、言語は実際にはより高いレベルのアセンブラでした。
AnT

29
ええと。したがって、これは、UNIXの多くの構造(例struct stat:)がフィールド(例:)の前に付ける理由を説明していますst_mode
icktoofay 2013年

5
@ perfectionm1ng:bell-labs.comがAlcatel-Lucentに乗っ取られたようで、元のページはなくなっています。別のサイトへのリンクを更新しましたが、そのサイトがどれくらい長く続くかはわかりません。とにかく、「リッチーcリファレンスマニュアル」を検索すると、通常はドキュメントが見つかります。
AnT 2013年

46

歴史的な(良い報告済みの)理由を超えて、演算子の優先順位には少し問題もあります。ドット演算子はスター演算子よりも優先順位が高いため、構造体へのポインタを含む構造体へのポインタを含む構造体がある場合...これら2つは同等です。

(*(*(*a).b).c).d

a->b->c->d

しかし、2番目は明らかに読みやすくなっています。矢印演算子の優先順位は最も高く(ドットと同じ)、左から右に関連付けられます。これは、構造体へのポインタと構造体へのポインタの両方にドット演算子を使用するよりも明確だと思います。これは、宣言を確認する必要がない式からの型であり、別のファイルにある可能性があるためです。


2
構造体と構造体へのポインタの両方を含むネストされたデータ型を使用すると、各サブメンバーアクセスに適切な演算子を選択することを考える必要があるため、状況がさらに難しくなる可能性があります。あなたはab-> c-> dまたはa-> bc-> dで終わるかもしれません(私はfreetypeライブラリーを使用しているときにこの問題がありました-私は常にソースコードを調べる必要がありました)。また、これは、ポインターを処理するときにコンパイラーにポインターを自動的に逆参照させることができない理由を説明していません。
Askaga

3
あなたが述べている事実は正しいですが、彼らは私の元の質問にまったく答えません。a->と*(a)の同等性について説明します。表記法(すでに他の質問で何度も説明されています)に加えて、言語設計についての漠然とした説明はやや恣意的です。あなたの答えはあまり役に立たなかったので、反対票を投じました。
Askaga

16
@ effeffe、OPは、言語がa.b.c.dとして簡単に解釈され(*(*(*a).b).c).d->オペレーターを役に立たなくする可能性があると述べています。したがって、OPのバージョン(a.b.c.d)は(と比較してa->b->c->d)読み取り可能です。それが、あなたの答えがOPの質問に答えない理由です。
Shahbaz 2013年

4
@Shahbazこれは、Javaプログラマーの場合に当てはまる可能性があり、C / C ++プログラマーは2つの非常に異なることを理解しa.b.c.d、次のa->b->c->dように理解します。 )、2つ目は3つのメモリアクセスで、4つの異なるオブジェクトを経由してポインタを追跡します。これはメモリレイアウトの大きな違いです。Cは、これら2つのケースを非常にはっきりと区別するのに適していると思います。
cmaster-2017年

2
@Shahbaz私は、Javaプログラマーの侮辱として、完全に暗黙的なポインターを持つ言語に単純に慣れているという意味ではありませんでした。私がJavaプログラマーとして育ったなら、私はおそらく同じように考えるでしょう...とにかく、私は実際に、Cで見られる演算子のオーバーロードは最適ではないと思います。しかし、私たちは皆、ほとんどすべてのことについて演算子を寛大に過負荷にする数学者に甘やかされてきたことを認めます。利用可能なシンボルのセットがかなり限られているので、私は彼らの動機も理解しています。結局のところ、それはあなたがどこで線を引くかだけの問題だと思います...
cmaster-モニカを復活させる

19

Cはまた、曖昧なものを作らないようにするのに優れています。

ドットがオーバーロードして両方を意味する可能性があることは確かですが、矢印は、コンパイラーが2つの互換性のない型を混在させない場合と同様に、プログラマーがポインターを操作していることを確実に認識します。


4
これが簡単で正しい答えです。Cは、ほとんどIMO℃で約最高のものの一つである過負荷を回避しようとします
jforberg

10
Cには、あいまいで曖昧なものがたくさんあります。暗黙的な型変換があり、数学演算子がオーバーロードされています。連鎖インデックスは、多次元配列またはポインターの配列のインデックスを作成するかどうかに応じて、まったく異なる処理を実行します。また、何かが何かを隠すマクロである可能性があります(大文字の命名規則は役立ちますが、Cはそうしません) t)。
PSkocik
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.