W jaki sposób ten fragment kodu określa rozmiar tablicy bez użycia sizeof ()?

134

Przeglądając kilka pytań do wywiadu w C, znalazłem pytanie "Jak znaleźć rozmiar tablicy w C bez użycia operatora sizeof?", Z następującym rozwiązaniem. Działa, ale nie mogę zrozumieć, dlaczego.

#include <stdio.h>

int main() {
    int a[] = {100, 200, 300, 400, 500};
    int size = 0;

    size = *(&a + 1) - a;
    printf("%d\n", size);

    return 0;
}

Zgodnie z oczekiwaniami zwraca 5.

edycja: ludzie wskazali na odpowiedź, ale składnia nieco się różni, tj. metoda indeksowania

size = (&arr)[1] - arr;

więc uważam, że oba pytania są ważne i mają nieco inne podejście do problemu. Dziękuję wszystkim za ogromną pomoc i dokładne wyjaśnienie!

janojlic
źródło
13
Cóż, nie mogę tego znaleźć, ale wygląda na to, że ściśle rzecz biorąc, jest. Załącznik J.2 wyraźnie stwierdza: Operand jednoargumentowego operatora * ma niepoprawną wartość i jest niezdefiniowanym zachowaniem. Tutaj &a + 1nie wskazuje na żaden prawidłowy obiekt, więc jest nieprawidłowy.
Eugene Sh.
5
Powiązane: Czy *((*(&array + 1)) - 1)bezpieczne jest użycie do uzyskania ostatniego elementu tablicy automatycznej? . tl; dr *(&a + 1)odwołuje się do Undefined Behvaior
Spikatrix
@AlmaDobrze, składnia trochę się różni, czyli część indeksująca, więc uważam, że to pytanie samo w sobie jest aktualne, ale mogę się mylić. Dziękuję za zwrócenie uwagi!
janojlic
1
@janojlicz Są w zasadzie takie same, ponieważ (ptr)[x]są takie same jak *((ptr) + x).
SS Anne

Odpowiedzi:

135

Po dodaniu 1 do wskaźnika wynikiem jest położenie następnego obiektu w sekwencji obiektów typu wskazanego (tj. Tablicy). Jeśli pwskazuje na intobiekt, p + 1wskaże następny intw sekwencji. Jeśli pwskazuje na 5-elementową tablicę int(w tym przypadku wyrażenie &a), to p + 1wskaże następną 5-elementową tablicęint w sekwencji.

Odejmowanie dwóch wskaźników (pod warunkiem, że oba wskazują na ten sam obiekt tablicy lub jeden wskazuje jeden poza ostatni element tablicy) daje liczbę obiektów (elementów tablicy) między tymi dwoma wskaźnikami.

Wyrażenie &azwraca adres ai ma typ int (*)[5](wskaźnik do 5-elementowej tablicy int). Wyrażenie &a + 1zwraca adres następnej 5-elementowej tablicy intnastępujących elementów a, a także ma typ int (*)[5]. Wyrażenie *(&a + 1)wyłuskuje wynik &a + 1, w taki sposób, że zwraca adres pierwszego intnastępującego po ostatnim elemencie ai ma typ int [5], który w tym kontekście „rozpada się” na wyrażenie typu int *.

Podobnie, wyrażenie a„rozpada się” na wskaźnik do pierwszego elementu tablicy i ma typ int *.

Zdjęcie może pomóc:

int [5]  int (*)[5]     int      int *

+---+                   +---+
|   | <- &a             |   | <- a
| - |                   +---+
|   |                   |   | <- a + 1
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
+---+                   +---+
|   | <- &a + 1         |   | <- *(&a + 1)
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
+---+                   +---+

To są dwa widoki tego samego magazynu - po lewej stronie widzimy go jako sekwencję 5-elementowych tablic int, a po prawej jako sekwencję int. Pokazuję też różne wyrażenia i ich rodzaje.

Należy pamiętać, że wyrażenie *(&a + 1)powoduje niezdefiniowane zachowanie :

...
Jeśli wynik wskazuje jeden za ostatnim elementem obiektu tablicy, nie powinien być używany jako argument operacji jednoargumentowego operatora *, który jest oceniany.

C 2011 Online Draft , 6.5.6 / 9

John Bode
źródło
13
Tekst „nie należy używać” jest oficjalny: C 2018 6.5.6 8.
Eric Postpischil
@EricPostpischil: Czy masz link do wersji roboczej 2018 przed publikacją (podobnej do N1570.pdf)?
John Bode
1
@JohnBode: Ta odpowiedź ma łącze do Wayback Machine . Sprawdziłem oficjalny standard w zakupionym egzemplarzu.
Eric Postpischil
7
Więc jeśli ktoś napisałby size = (int*)(&a + 1) - a;ten kod byłby całkowicie poprawny? : o
Gizmo
@Gizmo prawdopodobnie pierwotnie tego nie napisali, ponieważ w ten sposób musisz określić typ elementu; oryginał został prawdopodobnie napisany jako makro do ogólnego stosowania w różnych typach elementów.
Leushenko
35

Ta linia ma największe znaczenie:

size = *(&a + 1) - a;

Jak widać, najpierw pobiera adres ai dodaje do niego jeden. Następnie wyłuskuje ten wskaźnik i odejmuje od niego oryginalną wartość a.

Arytmetyka wskaźnika w C powoduje, że zwraca liczbę elementów w tablicy lub 5. Dodanie jednego i &ajest wskaźnikiem do następnej tablicy po 5 intsekundach a. Następnie ten kod wyłuskuje wynikowy wskaźnik i odejmuje od niego a(typ tablicy, który rozpadł się na wskaźnik), podając liczbę elementów w tablicy.

Szczegółowe informacje na temat działania arytmetyki wskaźników:

Załóżmy, że masz wskaźnik, xyzktóry wskazuje inttyp i zawiera wartość (int *)160. Po odjęciu dowolnej liczby od xyz, C określa, że ​​rzeczywista odejmowana kwota xyzjest liczbą razy większą niż rozmiar typu, na który wskazuje. Na przykład, jeśli odejmiesz 5od xyz, wartość xyzwyniku będzie taka, xyz - (sizeof(*xyz) * 5)jeśli arytmetyka wskaźnika nie będzie miała zastosowania.

Ponieważ ajest to tablica 5 inttypów, wynikowa wartość będzie równa 5. Jednak nie zadziała to ze wskaźnikiem, tylko z tablicą. Jeśli spróbujesz tego za pomocą wskaźnika, wynik zawsze będzie 1.

Oto mały przykład, który pokazuje adresy i dlaczego jest to niezdefiniowane. Po lewej stronie znajdują się adresy:

a + 0 | [a[0]] | &a points to this
a + 1 | [a[1]]
a + 2 | [a[2]]
a + 3 | [a[3]]
a + 4 | [a[4]] | end of array
a + 5 | [a[5]] | &a+1 points to this; accessing past array when dereferenced

Oznacza to, że kod odejmuje aod &a[5](lub a+5), dając 5.

Zauważ, że jest to niezdefiniowane zachowanie i nie powinno być używane w żadnych okolicznościach. Nie oczekuj, że zachowanie tego będzie spójne na wszystkich platformach i nie używaj go w programach produkcyjnych.

SS Anne
źródło
27

Hmm, podejrzewam, że to coś, co nie zadziałałoby we wczesnych dniach C. Jest jednak sprytne.

Wykonując kroki pojedynczo:

  • &a pobiera wskaźnik do obiektu typu int [5]
  • +1 pobiera następny taki obiekt, zakładając, że istnieje ich tablica
  • * skutecznie konwertuje ten adres na wskaźnik typu do int
  • -a odejmuje dwa wskaźniki int, zwracając liczbę int wystąpień między nimi.

Nie jestem pewien, czy jest to całkowicie legalne (mam na myśli język - prawnik prawniczy - nie sprawdzi się w praktyce), biorąc pod uwagę niektóre operacje tego typu. Na przykład „wolno” odejmować dwa wskaźniki tylko wtedy, gdy wskazują na elementy w tej samej tablicy. *(&a+1)została zsyntetyzowana przez dostęp do innej tablicy, aczkolwiek tablicy nadrzędnej, więc w rzeczywistości nie jest wskaźnikiem do tej samej tablicy, co a. Ponadto, chociaż możesz zsyntetyzować wskaźnik znajdujący się za ostatnim elementem tablicy i możesz traktować dowolny obiekt jako tablicę składającą się z 1 elementu, operacja wyłuskiwania ( *) nie jest „dozwolona” na tym zsyntetyzowanym wskaźniku, mimo że nie zachowuje się w tym przypadku!

Podejrzewam, że we wczesnych dniach C (składnia K&R, ktoś?) Tablica rozpadała się na wskaźnik znacznie szybciej, więc *(&a+1)może zwracać tylko adres następnego wskaźnika typu int **. Bardziej rygorystyczne definicje współczesnego C ++ zdecydowanie pozwalają na istnienie wskaźnika do typu tablicy i znajomość rozmiaru tablicy, i prawdopodobnie standardy C poszły w ich ślady. Cały kod funkcji C przyjmuje tylko wskaźniki jako argumenty, więc widoczna techniczna różnica jest minimalna. Ale ja tylko zgaduję.

Tego rodzaju szczegółowe pytanie dotyczące legalności zwykle dotyczy interpretera języka C lub narzędzia typu lint, a nie skompilowanego kodu. Interpreter może zaimplementować tablicę 2D jako tablicę wskaźników do tablic, ponieważ jest jedna funkcja mniej czasu wykonywania do zaimplementowania, w którym to przypadku dereferencja +1 byłaby fatalna, a nawet gdyby zadziałała, dałaby błędną odpowiedź.

Inną możliwą słabością może być to, że kompilator C może wyrównać zewnętrzną tablicę. Wyobraź sobie, że byłaby to tablica 5 znaków ( char arr[5]), gdy program wykonuje &a+1, wywołuje zachowanie „tablica tablicy”. Kompilator może zdecydować, że tablica zawierająca 5 znaków ( char arr[][5]) jest w rzeczywistości generowana jako tablica zawierająca 8 znaków ( char arr[][8]), tak aby zewnętrzna tablica była ładnie wyrównana. Kod, który omawiamy, zgłosiłby teraz rozmiar tablicy jako 8, a nie 5. Nie mówię, że konkretny kompilator na pewno by to zrobił, ale może.

Gem Taylor
źródło
Słusznie. Jednak z trudnych do wyjaśnienia powodów wszyscy używają sizeof () / sizeof ()?
Gem Taylor
5
Większość ludzi to robi. Na przykład sizeof(array)/sizeof(array[0])podaje liczbę elementów w tablicy.
SS Anne
Kompilator C może wyrównać tablicę, ale nie jestem przekonany, że po wykonaniu tej czynności można zmienić typ tablicy. Wyrównanie byłoby bardziej realistycznie realizowane poprzez wstawienie bajtów wypełniających.
Kevin
1
Odejmowanie wskaźników nie jest ograniczone tylko do dwóch wskaźników w tej samej tablicy - wskaźniki mogą również znajdować się jeden za końcem tablicy. &a+1definiuje. Jak zauważa John Bollinger, *(&a+1)nie jest, ponieważ próbuje wyodrębnić obiekt, który nie istnieje.
Eric Postpischil
5
Kompilator nie może zaimplementować char [][5]as char arr[][8]. Tablica to tylko powtarzające się w niej obiekty; nie ma wypełnienia. Dodatkowo złamałoby to (nienormatywny) przykład 2 w C 2018 6.5.3.4 7, który mówi nam, że możemy obliczyć liczbę elementów w tablicy z sizeof array / sizeof array[0].
Eric Postpischil