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 tę 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!
c
arrays
size
language-lawyer
pointer-arithmetic
janojlic
źródło
źródło
&a + 1
nie wskazuje na żaden prawidłowy obiekt, więc jest nieprawidłowy.*((*(&array + 1)) - 1)
bezpieczne jest użycie do uzyskania ostatniego elementu tablicy automatycznej? . tl; dr*(&a + 1)
odwołuje się do Undefined Behvaior(ptr)[x]
są takie same jak*((ptr) + x)
.Odpowiedzi:
Po dodaniu 1 do wskaźnika wynikiem jest położenie następnego obiektu w sekwencji obiektów typu wskazanego (tj. Tablicy). Jeśli
p
wskazuje naint
obiekt,p + 1
wskaże następnyint
w sekwencji. Jeślip
wskazuje na 5-elementową tablicęint
(w tym przypadku wyrażenie&a
), top + 1
wskaż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
&a
zwraca adresa
i ma typint (*)[5]
(wskaźnik do 5-elementowej tablicyint
). Wyrażenie&a + 1
zwraca adres następnej 5-elementowej tablicyint
następujących elementówa
, a także ma typint (*)[5]
. Wyrażenie*(&a + 1)
wyłuskuje wynik&a + 1
, w taki sposób, że zwraca adres pierwszegoint
następującego po ostatnim elemenciea
i ma typint [5]
, który w tym kontekście „rozpada się” na wyrażenie typuint *
.Podobnie, wyrażenie
a
„rozpada się” na wskaźnik do pierwszego elementu tablicy i ma typint *
.Zdjęcie może pomóc:
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 :C 2011 Online Draft , 6.5.6 / 9
źródło
size = (int*)(&a + 1) - a;
ten kod byłby całkowicie poprawny? : oTa linia ma największe znaczenie:
Jak widać, najpierw pobiera adres
a
i 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&a
jest wskaźnikiem do następnej tablicy po 5int
sekundacha
. Następnie ten kod wyłuskuje wynikowy wskaźnik i odejmuje od niegoa
(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,
xyz
który wskazujeint
typ i zawiera wartość(int *)160
. Po odjęciu dowolnej liczby odxyz
, C określa, że rzeczywista odejmowana kwotaxyz
jest liczbą razy większą niż rozmiar typu, na który wskazuje. Na przykład, jeśli odejmiesz5
odxyz
, wartośćxyz
wyniku będzie taka,xyz - (sizeof(*xyz) * 5)
jeśli arytmetyka wskaźnika nie będzie miała zastosowania.Ponieważ
a
jest to tablica5
int
typó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ędzie1
.Oto mały przykład, który pokazuje adresy i dlaczego jest to niezdefiniowane. Po lewej stronie znajdują się adresy:
Oznacza to, że kod odejmuje
a
od&a[5]
(luba+5
), dając5
.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.
źródło
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, coa
. 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.źródło
sizeof(array)/sizeof(array[0])
podaje liczbę elementów w tablicy.&a+1
definiuje. Jak zauważa John Bollinger,*(&a+1)
nie jest, ponieważ próbuje wyodrębnić obiekt, który nie istnieje.char [][5]
aschar 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 zsizeof array / sizeof array[0]
.