Jak zauważa Joel w podcastie Stack Overflow # 34 , w C Programming Language (alias: K & R), wspomniano o tej właściwości tablic w C:a[5] == 5[a]
Joel mówi, że to z powodu arytmetyki wskaźników, ale wciąż nie rozumiem. Dlaczegoa[5] == 5[a]
?
Jak zauważa Joel w podcastie Stack Overflow # 34 , w C Programming Language (alias: K & R), wspomniano o tej właściwości tablic w C:a[5] == 5[a]
Joel mówi, że to z powodu arytmetyki wskaźników, ale wciąż nie rozumiem. Dlaczegoa[5] == 5[a]
?
a[1]
jako ciąg tokenów, a nie ciągów: * ({liczba całkowita} operatora {operator} + {liczba całkowita} 1) jest taka sama jak * ({liczba całkowita} 1 {operator} + {liczba całkowita}) ale to nie to samo co * ({liczba całkowita} operatora {operator} + {operator} +)char bar[]; int foo[];
ifoo[i][bar]
jest używany jako wyrażenie.a[b]
=*(a + b)
dla dowolnegoa
ib
, ale był to swobodny wybór projektantów języków,+
który można zdefiniować jako przemienny dla wszystkich typów. Nic nie mogło zapobiec ich zakazaniei + p
, pozwalając jednocześniep + i
.+
że będzie przemienny, więc może prawdziwym problemem jest wybranie, aby operacje wskaźnika przypominały arytmetykę, zamiast projektowania osobnego operatora przesunięcia.Odpowiedzi:
Standard C definiuje
[]
operatora w następujący sposób:a[b] == *(a + b)
Dlatego
a[5]
oceni, aby:i
5[a]
oceni, aby:a
jest wskaźnikiem do pierwszego elementu tablicy.a[5]
Jest to wartość, która znajduje się 5 elementów dalej oda
, który jest taki sam jak*(a + 5)
iz matematyki elementarnej szkoły wiemy te są równe (dodawanie jest przemienne ).źródło
a[5]
skompiluje się do czegoś podobnegomov eax, [ebx+20]
zamiast[ebx+5]
*(10 + (int *)13) != *((int *)10 + 13)
. Innymi słowy, dzieje się tutaj więcej niż arytmetyka w szkole podstawowej. Komutatywność opiera się krytycznie na tym, że kompilator rozpoznaje, który operand jest wskaźnikiem (i jaki rozmiar obiektu). Innymi słowy(1 apple + 2 oranges) = (2 oranges + 1 apple)
, ale(1 apple + 2 oranges) != (1 orange + 2 apples)
.Ponieważ dostęp do tablicy jest zdefiniowany za pomocą wskaźników.
a[i]
jest zdefiniowany jako oznaczający*(a + i)
przemienny.źródło
*(i + a)
, co można zapisać jakoi[a]
”.*(a + i)
jest przemienny”. Jednak*(a + i) = *(i + a) = i[a]
ponieważ dodawanie jest przemienne.Myślę, że inne odpowiedzi pomijają coś.
Tak,
p[i]
z definicji jest równoważne z tym*(p+i)
, co (ponieważ dodawanie jest przemienne) jest równoważne z*(i+p)
, co (ponownie, z definicji[]
operatora) jest równoważnei[p]
.(A
array[i]
wewnątrz nazwa tablicy jest domyślnie konwertowana na wskaźnik do pierwszego elementu tablicy).Ale zamienność dodawania nie jest w tym przypadku aż tak oczywista.
Gdy oba operandy są tego samego typu, a nawet różnych typów numerycznych, które są promowane do wspólnego typu, przemienność sens:
x + y == y + x
.Ale w tym przypadku mówimy konkretnie o arytmetyce wskaźnika, gdzie jeden operand jest wskaźnikiem, a drugi liczbą całkowitą. (Liczba całkowita + liczba całkowita to inna operacja, a wskaźnik + wskaźnik to nonsens).
Opis
+
operatora w standardzie C ( N1570 6.5.6) mówi:Równie łatwo mógłby powiedzieć:
w takim przypadku oba
i + p
ii[p]
byłyby nielegalne.W języku C ++ mamy naprawdę dwa zestawy przeciążonych
+
operatorów, które można luźno opisać jako:i
z których tylko pierwszy jest naprawdę potrzebny.
Więc dlaczego tak jest?
C ++ odziedziczył tę definicję od C, która otrzymała ją od B (przemienność indeksowania tablic jest wyraźnie wspomniana w Odniesieniu użytkowników do B z 1972 r. ), Która otrzymała ją od BCPL (instrukcja z 1967 r.), Która mogła ją nawet otrzymać wcześniejsze języki (CPL? Algol?).
Pomysł, że indeksowanie tablic jest zdefiniowane w kategoriach dodawania i że dodawanie, nawet wskaźnika i liczby całkowitej, jest przemienne, sięga wielu dziesięcioleci do języków przodków C.
Języki te były znacznie rzadziej pisane niż współczesne C. W szczególności rozróżnianie wskaźników i liczb całkowitych było często ignorowane. (Wcześniejsi programiści C czasami używali wskaźników jako liczb całkowitych bez znaku, zanim
unsigned
słowo kluczowe zostało dodane do języka). Więc pomysł, aby dodanie było nieprzemienne, ponieważ operandy różnych typów prawdopodobnie nie przyszedłby do projektantów tych języków. Jeśli użytkownik chciał dodać dwie „rzeczy”, bez względu na to, czy są to liczby całkowite, wskaźniki lub coś innego, język nie był w stanie temu zapobiec.Z biegiem lat każda zmiana tej reguły złamałaby istniejący kod (chociaż norma ANSI C z 1989 roku mogła być dobrą okazją).
Zmiana C i / lub C ++ w taki sposób, aby wymagała umieszczenia wskaźnika po lewej stronie i liczby całkowitej po prawej stronie, może zepsuć istniejący kod, ale nie nastąpiłaby utrata prawdziwej mocy ekspresyjnej.
Mamy więc teraz
arr[3]
i3[arr]
dokładnie to samo, chociaż ta ostatnia forma nigdy nie powinna pojawić się poza IOCCC .źródło
3[arr]
jest interesującym artefaktem, ale rzadko powinien być używany. Zaakceptowana odpowiedź na to pytanie (< stackoverflow.com/q/1390365/356> ), o którą pytałem jakiś czas temu, zmieniła sposób myślenia o składni. Chociaż technicznie często nie ma właściwego i niewłaściwego sposobu wykonania tych czynności, tego rodzaju funkcje powodują, że myślisz w sposób niezależny od szczegółów implementacji. Korzyści płyną z tego odmiennego sposobu myślenia, który jest częściowo tracony, gdy skupiasz się na szczegółach implementacji.ring16_t
65535 dałobyring16_t
wartość 1, niezależnie od wielkościint
.I oczywiście
Głównym tego powodem było to, że w latach 70., kiedy projektowano C, komputery nie miały dużo pamięci (64 KB było dużo), więc kompilator C nie sprawdzał dużo składni. Dlatego „
X[Y]
” zostało raczej ślepo przetłumaczone na „*(X+Y)
”To wyjaśnia także składnie „
+=
” i „++
”. Wszystko w formie „A = B + C
” miało tę samą skompilowaną formę. Ale jeśli B był tym samym obiektem co A, dostępna była optymalizacja na poziomie zespołu. Ale kompilator nie był wystarczająco jasny, aby go rozpoznać, więc programista musiał (A += C
). Podobnie, jeśliC
tak1
, dostępna była inna optymalizacja na poziomie zespołu, a programista musiał to wyraźnie wyrazić, ponieważ kompilator jej nie rozpoznał. (Ostatnio robią to kompilatory, więc te składnie są obecnie w dużej mierze niepotrzebne)źródło
Jedna rzecz, o której nikt nie wspominał o problemie Diny z
sizeof
:Możesz dodać tylko liczbę całkowitą do wskaźnika, nie możesz dodać dwóch wskaźników razem. W ten sposób, dodając wskaźnik do liczby całkowitej lub liczbę całkowitą do wskaźnika, kompilator zawsze wie, który bit ma rozmiar, który należy wziąć pod uwagę.
źródło
Aby dosłownie odpowiedzieć na pytanie. Nie zawsze tak jest
x == x
odciski
źródło
cout << (a[5] == a[5] ? "true" : "false") << endl;
jestfalse
.x == x
nie zawsze tak jest). Myślę, że taki był jego zamiar. Jest on więc technicznie poprawny (i być może, jak mówią, najlepszy rodzaj poprawności!).NAN
w<math.h>
, co jest lepsze niż0.0/0.0
, bo0.0/0.0
to UB, gdy__STDC_IEC_559__
nie jest określona (Większość implementacji nie definiują__STDC_IEC_559__
, ale w większości implementacji0.0/0.0
będzie nadal działać)Właśnie odkryłem, że ta brzydka składnia może być „przydatna”, a przynajmniej bardzo przyjemna do zabawy, gdy chcesz poradzić sobie z tablicą indeksów odnoszących się do pozycji w tej samej tablicy. Może zastąpić zagnieżdżone nawiasy kwadratowe i uczynić kod bardziej czytelnym!
Oczywiście jestem całkiem pewien, że nie ma takiego przypadku w prawdziwym kodzie, ale i tak mnie to zainteresowało :)
źródło
i[a][a][a]
że myślisz, że albo jest wskaźnikiem do tablicy lub tablicą wskaźnika do tablicy lub tablicy ... ia
jest indeksem. Kiedy widzisza[a[a[i]]]
, myślisz, że a jest wskaźnikiem do tablicy lub tablicy ii
jest indeksem.Ładne pytanie / odpowiedź.
Chcę tylko zaznaczyć, że wskaźniki C i tablice nie są takie same , chociaż w tym przypadku różnica nie jest istotna.
Rozważ następujące deklaracje:
W
a.out
, symbola
znajduje się pod adresem, który jest początkiem tablicy, a symbolp
znajduje się pod adresem, w którym przechowywany jest wskaźnik, a wartość wskaźnika w tym miejscu pamięci jest początkiem tablicy.źródło
int a[10]
wskaźnik zwany „a”, który wskazywał na wystarczającą ilość miejsca na 10 liczb całkowitych w innym miejscu. Zatem a + i j + i miały tę samą formę: dodaj zawartość kilku lokalizacji pamięci. W rzeczywistości uważam, że BCPL było bez typu, więc były identyczne. Skalowanie typu size nie miało zastosowania, ponieważ BCPL był zorientowany wyłącznie na słowa (także na maszynach z adresowaniem słów).int*p = a;
zint b = 5;
tymi ostatnimi, „b” i „5” są liczbami całkowitymi, ale „b” jest zmienną, podczas gdy „5” jest wartością stałą. Podobnie „p” i „a” są adresami znaku, ale „a” jest stałą wartością.Dla wskaźników w C mamy
i również
Dlatego prawdą jest, że
a[5] == 5[a].
źródło
Nie odpowiedź, ale tylko trochę do myślenia. Jeśli klasa ma przeciążonego operatora indeksu / indeksu dolnego, wyrażenie
0[x]
nie będzie działać:Ponieważ nie mamy dostępu do klasy int , nie można tego zrobić:
źródło
class Sub { public: int operator[](size_t nIndex) const { return 0; } friend int operator[](size_t nIndex, const Sub& This) { return 0; } };
operator[]
powinna być niestatyczną funkcją składową z dokładnie jednym parametrem.” Byłem zaznajomiony z tym ograniczeniemoperator=
, nie sądziłem, aby miało to zastosowanie[]
.[]
operatora, nigdy więcej nie będzie równoważna ... jeślia[b]
jest równa*(a + b)
i zmienisz to, będziesz musiał również przeciążaćint::operator[](const Sub&);
iint
nie jest klasą ...Ma bardzo dobre wytłumaczenie w TUTORIAL NA WSKAŹNIKI I SZABLONY W C autorstwa Teda Jensena.
Ted Jensen wyjaśnił to jako:
źródło
Wiem, że odpowiedź na to pytanie, ale nie mogłam się powstrzymać przed udostępnieniem tego wyjaśnienia.
Pamiętam Zasady projektowania kompilatora, Załóżmy, że
a
jest toint
tablica o rozmiarzeint
2 bajtów, a adres podstawowya
to 1000.Jak
a[5]
będzie działać ->Więc,
Podobnie, gdy kod c zostanie podzielony na kod 3-adresowy,
5[a]
stanie się ->Więc w zasadzie oba stwierdzenia są skierowane do tej samej lokalizacji w pamięci, a więc
a[5] = 5[a]
.To wyjaśnienie jest również powodem, dla którego ujemne indeksy w tablicach działają w C.
tzn. jeśli uzyskam dostęp
a[-5]
, da mi toZwróci mi obiekt w lokalizacji 990.
źródło
W macierzy C ,
arr[3]
i3[arr]
są takie same, a ich odpowiadające oznaczenia wskaźnika są*(arr + 3)
na*(3 + arr)
. Ale wręcz przeciwnie[arr]3
lub[3]arr
jest niepoprawny i spowoduje błąd składniowy, ponieważ(arr + 3)*
i(3 + arr)*
nie są prawidłowymi wyrażeniami. Powodem jest to, że operator dereferencji powinien być umieszczony przed adresem podanym w wyrażeniu, a nie po adresie.źródło
w kompilatorze c
istnieją różne sposoby odwoływania się do elementu w tablicy! (NIE WSZYSTKO WEIRD)
źródło
Trochę historii. Wśród innych języków BCPL miał dość duży wpływ na wczesny rozwój C. Jeśli zadeklarowałeś tablicę w BCPL za pomocą czegoś takiego:
który faktycznie przydzielił 11 słów pamięci, a nie 10. Zwykle V był pierwszy i zawierał adres następnego słowa. Tak więc, w przeciwieństwie do C, nazewnictwo V poszło w to miejsce i odebrało adres elementu zeroeth tablicy. Zatem pośrednia macierz w BCPL, wyrażona jako
naprawdę musiałem to zrobić
J = !(V + 5)
(używając składni BCPL), ponieważ konieczne było pobranie V, aby uzyskać adres bazowy tablicy. ZatemV!5
i5!V
były synonimami. Jako niepotwierdzone spostrzeżenie, WAFL (Warwick Functional Language) został napisany w BCPL i, zgodnie z moją najlepszą pamięcią, miałem tendencję do używania tej drugiej składni zamiast pierwszej do uzyskiwania dostępu do węzłów używanych do przechowywania danych. Przyznaję, że pochodzi to gdzieś pomiędzy 35 a 40 lat temu, więc moja pamięć jest trochę zardzewiała. :)Innowacja rezygnacji z dodatkowego słowa pamięci i kompilatora wstawiającego adres bazowy tablicy, gdy została ona nazwana, pojawiła się później. Według artykułu z historii C zdarzyło się to mniej więcej w czasie, gdy struktury zostały dodane do C.
Zauważ, że
!
w BCPL był zarówno operatorem jednoargumentowym, jak i operatorem binarnym, w obu przypadkach działając pośrednio. tylko, że postać binarna zawierała dodanie dwóch operandów przed wykonaniem pośrednictwa. Biorąc pod uwagę zorientowaną na słowa naturę BCPL (i B), miało to naprawdę sens. Ograniczenie „wskaźnika i liczby całkowitej” stało się konieczne w C, gdy zyskało typy danych isizeof
stało się rzeczą.źródło
Cóż, ta funkcja jest możliwa tylko ze względu na obsługę języka.
Kompilator interpretuje
a[i]
jako*(a+i)
i wyrażenie5[a]
ocenia na*(5+a)
. Ponieważ dodawanie jest przemienne, okazuje się, że oba są równe. Stąd wyrażenie ocenia natrue
.źródło
W C.
Wskaźnik jest „zmienną”
nazwa tablicy to „mnemoniczny” lub „synonim”
p++;
jest prawidłowy, alea++
jest nieprawidłowya[2]
jest równe 2 [a], ponieważ wewnętrzna operacja na obu z nich to„Arytmetyka wskaźnika” obliczona wewnętrznie jako
*(a+3)
równa się*(3+a)
źródło
typy wskaźników
1) wskaźnik do danych
2) const wskaźnik do danych
3) const wskaźnik do const danych
a tablice są typu (2) z naszej listy
Kiedy definiujesz tablicę jednocześnie, jeden adres jest inicjowany w tym wskaźniku
Ponieważ wiemy, że nie możemy zmienić ani zmodyfikować stałej wartości w naszym programie, ponieważ powoduje to błąd przy kompilacji czas
The Główną różnicą znalazłem to ...
Możemy ponownie zainicjować wskaźnik za pomocą adresu, ale nie w tym samym przypadku z tablicą.
======
i wracając do pytania ...
a[5]
to nic, ale*(a + 5)
można to łatwo zrozumieć
a
- zawierając adres (ludzie nazywają go jako adres bazowy) tak jak (2) typ wskaźnika na naszej liście[]
- tym operatorem może być wymienny ze wskaźnikiem*
.więc w końcu ...
źródło