Dlaczego operator strzałki (->) w C istnieje?

264

Operator dot ( .) służy do uzyskiwania dostępu do elementu struktury, podczas gdy operator strzałki ( ->) w C służy do uzyskiwania dostępu do elementu struktury, do którego odwołuje się dany wskaźnik.

Sam wskaźnik nie ma żadnych elementów, do których można uzyskać dostęp za pomocą operatora kropki (w rzeczywistości jest to tylko liczba opisująca lokalizację w pamięci wirtualnej, więc nie ma żadnych elementów). Tak więc nie byłoby dwuznaczności, gdybyśmy po prostu zdefiniowali operator kropki, aby automatycznie wyłuskać wskaźnik, jeśli jest on używany na wskaźniku (informacja znana kompilatorowi w czasie kompilacji).

Dlaczego więc twórcy języków postanowili skomplikować sprawę, dodając ten pozornie niepotrzebny operator? Jaka jest ważna decyzja dotycząca projektu?

Askaga
źródło
1
Powiązane: stackoverflow.com/questions/221346/… - również możesz zastąpić ->
Krease
16
@Chris To jest o C ++, co oczywiście robi dużą różnicę. Ale ponieważ mówimy o tym, dlaczego C został zaprojektowany w ten sposób, udawajmy, że jesteśmy z powrotem w latach 70. - zanim istniało C ++.
Mysticial
5
Domyślam się, że operator strzałek istnieje, aby wizualnie wyrazić „patrz! Masz tutaj do czynienia ze wskaźnikiem”
Chris,
4
Na pierwszy rzut oka wydaje mi się, że to pytanie jest bardzo dziwne. Nie wszystkie rzeczy są starannie zaprojektowane. Jeśli utrzymasz ten styl przez całe życie, twój świat będzie pełen pytań. Odpowiedź, która uzyskała najwięcej głosów, jest naprawdę pouczająca i jasna. Ale to nie uderza w kluczowy punkt twojego pytania. Podążaj za stylem twojego pytania, mogę zadawać zbyt wiele pytań. Na przykład słowo kluczowe „int” jest skrótem od „integer”; dlaczego słowo kluczowe „double” również nie jest krótsze?
Junwanghe,
1
@jwwanghe To pytanie stanowi poważny problem - dlaczego .operator ma wyższy priorytet niż *operator? Jeśli nie, moglibyśmy mieć * ptr.member i var.member.
milleniumbug

Odpowiedzi:

358

Zinterpretuję twoje pytanie jako dwa pytania: 1) dlaczego w ->ogóle istnieje, i 2) dlaczego .nie wyłacza automatycznie wskaźnika. Odpowiedzi na oba pytania mają historyczne korzenie.

Dlaczego w ->ogóle istnieje?

W jednej z pierwszych wersji języka C (który będę określał jako CRM dla „ C Reference Manual ”, który pojawił się wraz z 6. edycją Unix w maju 1975 r.), Operator ->miał bardzo ekskluzywne znaczenie, nie był synonimem *i .kombinacją

Język C opisany przez CRM pod wieloma względami bardzo różnił się od współczesnego C. W CRM struct członkowie zaimplementowali globalną koncepcję przesunięcia bajtów , którą można dodać do dowolnej wartości adresu bez ograniczeń typu. To znaczy wszystkie nazwiska wszystkich członków struktury miały niezależne znaczenie globalne (i dlatego musiały być unikalne). Na przykład możesz zadeklarować

struct S {
  int a;
  int b;
};

a nazwa aoznaczałaby przesunięcie 0, a nazwa boznaczałaby przesunięcie 2 (przy założeniu, że introzmiar 2 nie ma wypełnienia). Język wymagał, aby wszyscy członkowie wszystkich struktur w jednostce tłumaczenia posiadali unikalne nazwy lub oznaczali tę samą wartość przesunięcia. Np. W tej samej jednostce tłumaczeniowej, którą można dodatkowo zadeklarować

struct X {
  int a;
  int x;
};

i to by było OK, ponieważ nazwa akonsekwentnie oznaczałaby offset 0. Ale ta dodatkowa deklaracja

struct Y {
  int b;
  int a;
};

byłby formalnie nieważny, ponieważ próbował „przedefiniować” ajako przesunięcie 2 i bjako przesunięcie 0.

I tu pojawia się ->operator. Ponieważ nazwa każdego członka struktury ma swoje własne samowystarczalne znaczenie globalne, język obsługiwał takie wyrażenia jak te

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

Pierwszy przydział został zinterpretowany przez kompilator jako „adres odbioru 5, dodać przesunięcie 2do niego i przypisać 42do intwartości uzyskanej w adresie”. Czyli powyżej byłoby przypisać 42do intwartości w adresie 7. Zauważ, że to użycie ->nie dbało o rodzaj wyrażenia po lewej stronie. Lewa strona została zinterpretowana jako adres numeryczny wartości (może to być wskaźnik lub liczba całkowita).

Tego rodzaju oszustwo nie było możliwe przy użyciu *i .kombinacji. Nie mogłeś zrobić

(*i).b = 42;

ponieważ *ijest już niepoprawnym wyrażeniem. *Operatora, ponieważ jest oddzielone od .nakłada bardziej rygorystyczne wymogi pisania na jej argumentu. Aby zapewnić możliwość obejścia tego ograniczenia, CRM wprowadził ->operatora, który jest niezależny od rodzaju operandu po lewej stronie.

Jak zauważył Keith w komentarzach, ta różnica między kombinacją ->a *+ .jest tym, co CRM nazywa „rozluźnieniem wymogu” w 7.1.8: Z wyjątkiem rozluźnienia wymogu E1typu wskaźnikowego, wyrażenie E1−>MOSjest dokładnie równoważne z(*E1).MOS

Później w K&R C wiele funkcji pierwotnie opisanych w CRM zostało znacznie przerobionych. Idea „struktury członka jako globalnego identyfikatora przesunięcia” została całkowicie usunięta. A funkcjonalność ->operatora stała się w pełni identyczna z funkcjonalnością *i .kombinacją.

Dlaczego nie można .wyrejestrować wskaźnika automatycznie?

Ponownie, w wersji CRM języka lewy argument z .operatorem musiała być lwartość . To był jedyny wymóg nałożony na ten operand (i to go odróżniało ->, jak wyjaśniono powyżej). Zauważ, że CRM nie wymagał, aby lewy operand .miał typ struktury. Wymagało tylko, aby była to wartość, każda wartość. Oznacza to, że w CRM w wersji C można napisać taki kod

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

struct T c;
c.b = 55;

W tym przypadku kompilator będzie pisać 55do o intwartości umieszczonej w bajcie offsetu 2 w ciągłym bloku pamięci znanego jako c, choć typ struct Tnie miał pole o nazwie b. Kompilator nie przejmuje się cw ogóle faktycznym typem . Chodziło tylko o to, żeby cbyła to wartość: jakiś zapisywalny blok pamięci.

Teraz zauważ, że jeśli to zrobiłeś

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

kod będzie uznane za ważne (ponieważ sjest również lwartością) oraz kompilator po prostu próba zapisu danych do wskaźnika ssamego , co bajt przesunięcie 2. Oczywiście, takie rzeczy mogą łatwo spowodować przekroczenie pamięci, ale język nie zajmował się takimi sprawami.

Tj. W tej wersji języka twój pomysł na przeciążanie operatora .dla typów wskaźników nie zadziałałby: operator .miał już bardzo konkretne znaczenie, gdy był używany ze wskaźnikami (ze wskaźnikami wartości lub w ogóle wartościami). To była bardzo dziwna funkcjonalność, bez wątpienia. Ale to było wtedy.

Oczywiście, ta dziwna funkcjonalność nie jest bardzo silnym powodem, aby nie wprowadzać przeciążonego .operatora wskaźników (jak sugerowałeś) w przerobionej wersji C - K&R C. Ale tego nie zrobiono. Być może w tym czasie był napisany jakiś starszy kod w CRM w wersji C, który musiał być obsługiwany.

(Adres do 1975 C Instrukcja referencyjnych nie może być stabilna. Innym kopii, ewentualnie z pewnymi subtelnych różnic jest tutaj ).

ANT
źródło
10
A sekcja 7.1.8 cytowanej instrukcji obsługi C mówi „Z wyjątkiem złagodzenia wymogu, aby E1 był typu wskaźnikowego, wyrażenie„ E1−> MOS ”jest dokładnie równoważne z„ ”(* E1) .MOS” „.”
Keith Thompson
1
Dlaczego nie *ibyła to wartość domyślnego typu (int?) Pod adresem 5? Wtedy (* i) .b działałoby w ten sam sposób.
Random832,
5
@Leo: Cóż, niektórzy ludzie lubią język C jako asembler wyższego poziomu. W tym okresie historii C język rzeczywiście był asemblerem wyższego poziomu.
AnT
29
Huh To wyjaśnia, dlaczego wiele struktur w systemie UNIX (np. struct stat) Poprzedza swoje pola (np st_mode.).
icktoofay
5
@ perfectionm1ng: Wygląda na to, że Alcatel-Lucent przejął bell-labs.com, a oryginalnych stron nie ma. Zaktualizowałem link do innej witryny, chociaż nie mogę powiedzieć, jak długo ta strona pozostanie aktywna. W każdym razie, przeglądanie „podręcznika referencyjnego ritchie c” zwykle znajduje dokument.
AnT
46

Oprócz przyczyn historycznych (dobrych i już zgłoszonych) istnieje również mały problem z pierwszeństwem operatorów: operator kropki ma wyższy priorytet niż operator gwiazdy, więc jeśli masz strukturę zawierającą wskaźnik do struktury zawierającą wskaźnik do struktury ... Te dwa są równoważne:

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

a->b->c->d

Ale drugi jest wyraźnie bardziej czytelny. Operator strzałek ma najwyższy priorytet (podobnie jak kropka) i kojarzy od lewej do prawej. Myślę, że jest to bardziej zrozumiałe niż użycie operatora kropki zarówno dla wskaźników do struct i struct, ponieważ znamy typ z wyrażenia bez konieczności patrzenia na deklarację, która może nawet znajdować się w innym pliku.

effeffe
źródło
2
Z zagnieżdżonymi typami danych zawierającymi zarówno struktury, jak i wskaźniki do struktur, może to utrudnić, ponieważ musisz pomyśleć o wyborze odpowiedniego operatora dla każdego dostępu dla członków. Możesz skończyć z ab-> c-> d lub a-> bc-> d (miałem ten problem podczas korzystania z biblioteki freetype - musiałem cały czas sprawdzać kod źródłowy). Nie wyjaśnia to również, dlaczego kompilator nie mógłby automatycznie wyłapywać wskaźnika podczas obsługi wskaźników.
Askaga
3
Chociaż podane przez ciebie fakty są poprawne, w żaden sposób nie odpowiadają na moje pierwotne pytanie. Wyjaśniasz równość a-> i * (a). notacje (co zostało już wyjaśnione wiele razy w innych pytaniach), a także dając niejasne stwierdzenie, że projektowanie języka jest nieco arbitralne. Twoja odpowiedź nie była dla mnie bardzo pomocna, dlatego głosowanie negatywne.
Askaga
16
@effeffe, OP twierdzi, że język można łatwo interpretować a.b.c.djako (*(*(*a).b).c).d, co czyni ->operatora bezużytecznym. Tak więc wersja OP ( a.b.c.d) jest równie czytelna (w porównaniu do a->b->c->d). Dlatego twoja odpowiedź nie odpowiada na pytanie PO.
Shahbaz
4
@Shahbaz Może tak być w przypadku programisty Java, programista C / C ++ zrozumie a.b.c.di a->b->c->djako dwie bardzo różne rzeczy: Pierwsza to dostęp do pojedynczej pamięci do zagnieżdżonego pod-obiektu (w tym przypadku jest tylko jeden obiekt pamięci ), drugi to trzy dostępy do pamięci, ścigające wskaźniki przez cztery prawdopodobne różne obiekty. To ogromna różnica w układzie pamięci i uważam, że C ma rację, bardzo wyraźnie rozróżniając te dwa przypadki.
cmaster
2
@Shahbaz Nie miałem na myśli, że jako obraza programistów Java, są po prostu przyzwyczajeni do języka z całkowicie niejawnymi wskazówkami. Gdybym został wychowany jako programista Java, prawdopodobnie pomyślałbym w ten sam sposób ... W każdym razie myślę, że przeciążenie operatora, które widzimy w C, jest mniej niż optymalne. Przyznaję jednak, że wszyscy zostaliśmy rozpieszczeni przez matematyków, którzy liberalnie przeciążają swoich operatorów prawie za wszystko. Rozumiem również ich motywację, ponieważ zestaw dostępnych symboli jest raczej ograniczony.
Wydaje
19

C wykonuje również dobrą robotę, nie czyniąc niczego niejednoznacznym.

Oczywiście kropka może być przeciążona, co oznacza obie rzeczy, ale strzałka upewnia programistę, że działa na wskaźniku, tak jak wtedy, gdy kompilator nie pozwala mieszać dwóch niekompatybilnych typów.

mukunda
źródło
4
To jest prosta i poprawna odpowiedź. C głównie stara się uniknąć przeciążenia, które IMO jest jedną z najlepszych rzeczy w C.
jforberg
10
Wiele rzeczy w C jest niejednoznacznych i rozmytych. Istnieją niejawne konwersje typów, operatory matematyczne są przeciążone, indeksowanie łańcuchowe robi coś zupełnie innego w zależności od tego, czy indeksujesz tablicę wielowymiarową, czy tablicę wskaźnika i wszystko może być makro ukrywającym cokolwiek (konwencja nazewnictwa wielkich liter pomaga, ale C nie robi t).
PSkocik