Dlaczego składnia C dla tablic, wskaźników i funkcji została zaprojektowana w ten sposób?


16

Po obejrzeniu (i zadaniu!) Tylu pytań podobnych do

Co int (*f)(int (*a)[5])znaczy w C?

a nawet widząc, że stworzyli program, który ma pomóc ludziom zrozumieć składnię C, nie mogę przestać się zastanawiać:

Dlaczego składnia C została zaprojektowana w ten sposób?

Na przykład, gdybym projektował wskaźniki, tłumaczyłbym „wskaźnik na 10-elementową tablicę wskaźników” na

int*[10]* p;

i nie

int* (*p)[10];

co wydaje mi się, że większość ludzi się zgodzi, jest o wiele mniej proste.

Zastanawiam się więc, dlaczego ta nieintuicyjna składnia? Czy był jakiś konkretny problem, który rozwiązuje składnia (być może dwuznaczność?), Którego nie jestem świadomy?


2
Wiesz, że nie ma prawdziwej odpowiedzi na to i takie pytania. Dobrze? Otrzymasz tylko domysły.
BЈовић

7
@VJo - może istnieć „prawdziwa” (tj. Obiektywna) odpowiedź - autorzy języków i komitety normalizacyjne wyraźnie uzasadniają (lub przynajmniej wyjaśniają) wiele z tych decyzji.
detly

Nie sądzę, aby proponowana składnia była z konieczności bardziej lub mniej „intuicyjna” niż składnia C. C jest tym, czym jest; gdy się tego nauczysz, już nigdy nie będziesz mieć tych pytań. Jeśli się tego nie nauczyłeś ... to może prawdziwy problem.
Caleb

1
@Caleb: Zabawne, że tak łatwo doszedłeś do wniosku, ponieważ nauczyłem się tego i wciąż mam to pytanie ...
user541686,

1
cdeclPolecenie jest bardzo przydatny do dekodowania złożone zgłoszenia C. Istnieje również interfejs sieciowy na cdecl.org .
Keith Thompson,

Odpowiedzi:


16

Rozumiem jego historię, ponieważ opiera się ona na dwóch głównych punktach ...

Po pierwsze, autorzy języków woleli, aby składnia była skoncentrowana na zmiennych niż na typach. Oznacza to, że chcieli, aby programista spojrzał na deklarację i pomyślał: „jeśli napiszę wyrażenie *func(arg), to spowoduje, że int; jeśli napiszę *arg[N], będę mieć liczbę zmiennoprzecinkową” zamiast „ funcmusi ” być wskaźnikiem funkcji biorącej to i zwracanie tego ”.

Wejście C Wikipedia twierdzi, że:

Ideą Ritchiego było deklarowanie identyfikatorów w kontekstach przypominających ich użycie: „deklaracja odzwierciedla użycie”.

... powołując się na p122 K & R2, które, niestety, nie muszę ręcznie szukać rozszerzonej oferty dla ciebie.

Po drugie, naprawdę bardzo trudno jest znaleźć składnię deklaracji, która jest spójna, gdy mamy do czynienia z dowolnymi poziomami pośrednimi. Twój przykład może zadziałać dobrze w wyrażaniu typu, który wymyśliłeś tam od razu, ale czy skaluje się do funkcji przyjmującej wskaźnik do tablicy tych typów i zwracającej jakiś inny ohydny bałagan? (Może tak, ale sprawdziłeś? Czy możesz to udowodnić? ).

Pamiętaj, że część sukcesu C wynika z faktu, że kompilatory zostały napisane dla wielu różnych platform, dlatego lepiej byłoby zignorować pewien stopień czytelności, aby ułatwić kompilatorom pisanie.

Powiedziawszy to, nie jestem ekspertem od gramatyki języka ani pisania kompilatorów. Ale wiem wystarczająco dużo, aby wiedzieć, że jest wiele do zrobienia;)


2
„ułatwianie pisania kompilatorom” ... z wyjątkiem tego, że C jest znane z tego, że jest trudne do analizy (tylko C ++).
Jan Hudec

1
@JHHec - Cóż ... tak. To nie jest wodoszczelne stwierdzenie. Ale podczas gdy C jest niemożliwy do przeanalizowania jako gramatyki bezkontekstowej, gdy jedna osoba wymyśli sposób, aby go przeanalizować, przestaje to być trudnym krokiem. Faktem jest, że był bardzo płodny na początku, ponieważ ludzie byli w stanie z łatwością wymachiwać kompilatorami, więc K&R musiał zachować pewną równowagę. (W niesławnej książce The Rise of „Worse is Better” Richard bierze za pewnik - i lamentuje - fakt, że łatwo jest napisać kompilator C na nową platformę.)
detly

Nawiasem mówiąc, cieszę się, że mogę to poprawić - nie wiem aż tak dużo o analizie składni i gramatyce. Opieram się bardziej na wnioskach z faktów historycznych.
detly

12

Wiele osobliwości języka C można wytłumaczyć sposobem działania komputerów podczas jego projektowania. Ilość pamięci była bardzo ograniczona, dlatego bardzo ważne było zminimalizowanie rozmiaru samych plików kodu źródłowego . Praktyka programowania w latach 70. i 80. polegała na upewnieniu się, że kod źródłowy zawiera jak najmniej znaków, a najlepiej, że nie zawiera nadmiernych komentarzy do kodu źródłowego.

Jest to dziś absurdalne, z praktycznie nieograniczoną przestrzenią dyskową na dyskach twardych. Ale jest to część powodu, dla którego C ma tak dziwną składnię w ogóle.


W odniesieniu do wskaźników tablicowych twoim drugim przykładem powinno być int (*p)[10];(tak, składnia jest bardzo myląca). Być może przeczytałbym to jako „wskaźnik na tablicę dziesięciu” ... co ma sens. Gdyby nie nawias, kompilator interpretowałby go jako tablicę dziesięciu wskaźników, co nadałoby deklaracji zupełnie inne znaczenie.

Ponieważ zarówno wskaźniki tablicowe, jak i funkcyjne mają dość niejasną składnię w C, rozsądnym rozwiązaniem jest wpisanie dziwnego charakteru. Być może tak:

Niejasny przykład:

int func (int (*arr_ptr)[10])
{
  return 0;
}

int main()
{
  int array[10];
  int (*arr_ptr)[10]  = &array;
  int (*func_ptr)(int(*)[10]) = &func;

  func_ptr(arr_ptr);
}

Niejasny, równoważny przykład:

typedef int array_t[10];
typedef int (*funcptr_t)(array_t*);


int func (array_t* arr_ptr)
{
  return 0;
}

int main()
{
  int        array[10];
  array_t*   arr_ptr  = &array; /* non-obscure array pointer */
  funcptr_t  func_ptr = &func;  /* non-obscure function pointer */

  func_ptr(arr_ptr);
}

Sprawy mogą stać się jeszcze bardziej niejasne, jeśli mamy do czynienia z tablicami wskaźników funkcji. Lub najbardziej niejasny ze wszystkich: funkcje zwracające wskaźniki funkcji (mało użyteczne). Jeśli nie używasz typedefs do takich rzeczy, szybko oszalejesz.


Ach, wreszcie rozsądna odpowiedź. :-) Jestem ciekawy, w jaki sposób konkretna składnia zmniejszyłaby rozmiar kodu źródłowego, ale w każdym razie jest to wiarygodny pomysł i ma sens. Dzięki. +1
541686,

Powiedziałbym, że mniej chodziło o rozmiar kodu źródłowego, a więcej o pisaniu kompilatora, ale zdecydowanie +1 za „typdef od dziwności”. Moje zdrowie psychiczne poprawiło się dramatycznie w dniu, w którym zdałem sobie sprawę, że mogę to zrobić.
detly

2
[Potrzebne cytowanie] na temat rozmiaru kodu źródłowego. Nigdy nie słyszałem o takim ograniczeniu (choć może to coś „wszyscy wiedzą”).
Sean McMillan

1
Dobrze kodowałem programy w latach 70. w COBOL, Assembler, CORAL i PL / 1 na zestawach IBM, DEC i XEROX i nigdy NIE spotkałem ograniczenia rozmiaru kodu źródłowego. Ograniczenia rozmiaru tablicy, rozmiaru pliku wykonywalnego, rozmiaru nazwy programu - ale nigdy rozmiaru kodu źródłowego.
James Anderson,

1
@Sean McMillan: Nie sądzę, aby rozmiar kodu źródłowego był ograniczeniem (weź pod uwagę, że w tym czasie języki mówiące jak Pascal były dość popularne). I nawet gdyby tak było, myślę, że bardzo łatwo byłoby wstępnie przeanalizować kod źródłowy i zastąpić długie słowa kluczowe krótkimi kodami jednobajtowymi (jak np. Niektóre podstawowe interpretatory). Uważam więc, że argument „C jest zwięzły, ponieważ został wymyślony w okresie, gdy dostępna była mniejsza ilość pamięci”, jest nieco słaby.
Giorgio

7

To całkiem proste: int *poznacza, że *pjest to int; int a[5]oznacza, że a[i]jest int.

int (*f)(int (*a)[5])

Oznacza, że *fjest to funkcja, *ajest tablicą pięciu liczb całkowitych, podobnie fjak funkcja pobierająca wskaźnik do tablicy pięciu liczb całkowitych i zwracająca liczbę całkowitą. Jednak w C nie jest przydatne przekazywanie wskaźnika do tablicy.

Deklaracje C bardzo rzadko komplikują sprawę.

Możesz także wyjaśnić za pomocą typedefs:

typedef int vec5[5];
int (*f)(vec5 *a);

4
Przepraszam, jeśli to brzmi niegrzecznie (nie mam na myśli, że tak jest), ale myślę, że przegapiłeś cały punkt pytania ...: \
user541686,

2
@ Mehrdad: Nie mogę ci powiedzieć, co było w umyśle Kernighana i Ritchie; Powiedziałem ci logikę składni. Nie wiem o większości ludzi, ale nie sądzę, że twoja sugerowana składnia jest jaśniejsza.
kevin cline

Zgadzam się - nie jest tak trudno zobaczyć tak skomplikowaną deklarację.
Caleb

Konstrukcja C deklaracje poprzedza typedef, const, volatileoraz zdolność do zainicjowania rzeczy wewnątrz deklaracji. Wiele irytujących dwuznaczności składni deklaracji (np. Czy int const *p, *q;powinno się wiązać constz typem, czy z deklaracją) nie mogło powstać w języku pierwotnie zaprojektowanym. Chciałbym, aby język dodał dwukropek między typem a deklaracją, ale zezwolił na jego pominięcie podczas używania wbudowanych typów „słowa zastrzeżonego” bez kwalifikatorów. Znaczenie int: const *p,*q;i int const *: p,*q;byłoby jasne.
supercat,

3

Myślę, że musisz rozważyć * [] jako operatory dołączone do zmiennej. * jest zapisywane przed zmienną, [] po.

Przeczytajmy wyrażenie typu

int* (*p)[10];

Najbardziej wewnętrznym elementem jest zatem p, zmienna

p

oznacza: p jest zmienną.

Przed zmienną występuje *, * operator jest zawsze umieszczany przed wyrażeniem, do którego się odnosi, dlatego

(*p)

oznacza: zmienna p jest wskaźnikiem. Bez () operator [] po prawej stronie miałby wyższy priorytet, tj

**p[]

zostanie sparsowany jako

*(*(p[]))

Następnym krokiem jest []: ponieważ nie ma dalszego (), [] ma zatem wyższy priorytet niż zewnętrzny *

(*p)[]

oznacza: (zmienna p jest wskaźnikiem) do tablicy. Następnie mamy drugi *:

* (*p)[]

oznacza: ((zmienna p jest wskaźnikiem) do tablicy) wskaźników

Wreszcie masz operator int (nazwa typu), który ma najniższy priorytet:

int* (*p)[]

oznacza: (((zmienna p jest wskaźnikiem) do tablicy) wskaźników) na liczbę całkowitą.

Cały system oparty jest więc na wyrażeniach typu z operatorami, a każdy operator ma własne reguły pierwszeństwa. Pozwala to zdefiniować bardzo złożone typy.


0

Nie jest to takie trudne, kiedy zaczynasz myśleć, a C nigdy nie był łatwym językiem. I int*[10]* pnaprawdę nie jest łatwiejsze niż int* (*p)[10] I w jakim typie k byłbyint*[10]* p, k;


2
k byłby nieudanym przeglądem kodu, mogę dowiedzieć się, co zrobi kompilator, być może nawet mi przeszkadza, ale nie mogę ustalić, co zamierzał programista - niepowodzenie ............
mattnz

i dlaczego k miałby nieudany przegląd kodu?
Dainius

1
ponieważ kod jest nieczytelny i niemożliwy do utrzymania. Kod nie jest poprawny do poprawienia, oczywiście poprawny i prawdopodobnie pozostanie poprawny podczas konserwacji. Fakt, że musisz zapytać, jaki będzie typ k, jest znakiem, że kod nie spełnia tych podstawowych wymagań.
mattnz

1
Zasadniczo istnieją 3 (w tym przypadku) deklaracje zmiennych różnych typów w tym samym wierszu, np. Int * p, int i [10] i int k. To niedopuszczalne. Dopuszczalne są wielokrotne deklaracje tego samego typu, pod warunkiem, że zmienne mają jakąś formę relacji, np. Int szerokość, wysokość, głębokość; Pamiętaj, że wiele osób programuje za pomocą int * p, więc co to jest w 'int * p, i;'.
mattnz,

1
@Mattnz próbuje powiedzieć, że możesz być tak sprytny, jak chcesz, ale to wszystko nie ma znaczenia, gdy twoje zamiary nie są oczywiste i / lub twój kod jest źle napisany / nieczytelny. Tego rodzaju rzeczy często powodują zepsuty kod i marnowanie czasu. Plus pointer to inti intnie są nawet tego samego typu, dlatego należy je zadeklarować osobno. Kropka. Posłuchaj mężczyzny. Nie bez powodu ma 18 tys. Przedstawicieli.
Braden Best
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.