Obecnie czytam książkę zatytułowaną „Przepisy numeryczne w C”. W tej książce autor wyszczególnia, jak pewne algorytmy z natury działają lepiej, gdybyśmy mieli indeksy zaczynające się od 1 (nie do końca podążam za jego argumentem i nie o to chodzi w tym poście), ale C zawsze indeksuje swoje tablice zaczynające się od 0 Aby obejść ten problem, sugeruje po prostu zmniejszenie wskaźnika po alokacji, np .:
float *a = malloc(size);
a--;
To, jak mówi, da ci efektywnie wskaźnik, którego indeks zaczyna się od 1, a następnie zostanie zwolniony z:
free(a + 1);
O ile mi wiadomo, jest to niezdefiniowane zachowanie według standardu C. Jest to podobno bardzo renomowana książka w społeczności HPC, więc nie chcę po prostu lekceważyć tego, co mówi, ale po prostu zmniejszanie wskaźnika poza przydzielony zakres wydaje mi się bardzo szkicowe. Czy to „dozwolone” zachowanie w C? Przetestowałem to przy użyciu zarówno gcc, jak i icc, i oba te wyniki wydają się wskazywać, że nie martwię się niczym, ale chcę być absolutnie pozytywny.
Odpowiedzi:
Masz rację, że kod taki jak
daje niezdefiniowane zachowanie, zgodnie ze standardem ANSI C, sekcja 3.3.6:
W przypadku takiego kodu jakość kodu C w książce (kiedy go używałem pod koniec lat 90.) nie była uważana za bardzo wysoką.
Problem z niezdefiniowanym zachowaniem polega na tym, że bez względu na wynik kompilatora, wynik ten jest z definicji poprawny (nawet jeśli jest wysoce destrukcyjny i nieprzewidywalny).
Na szczęście bardzo niewiele kompilatorów stara się spowodować nieoczekiwane zachowanie w takich przypadkach, a typowa
malloc
implementacja na komputerach używanych do HPC ma pewne dane księgowe tuż przed zwróconym adresem, więc zmniejszenie zazwyczaj daje wskaźnik do tych danych księgowych. Pisanie tam nie jest dobrym pomysłem, ale samo tworzenie wskaźnika jest w tych systemach nieszkodliwe.Należy pamiętać, że kod może ulec awarii, gdy środowisko wykonawcze zostanie zmienione lub gdy zostanie przeniesiony do innego środowiska.
źródło
Oficjalnie jego niezdefiniowanym zachowaniem jest umieszczenie wskaźnika poza tablicą (z wyjątkiem jednego za końcem), nawet jeśli nigdy nie jest dereferencyjny .
W praktyce, jeśli Twój procesor ma płaski model pamięci (w przeciwieństwie do dziwnych, takich jak x86-16 ), a jeśli kompilator nie wyświetli błędu w czasie wykonywania lub niepoprawnej optymalizacji, jeśli utworzysz nieprawidłowy wskaźnik, kod będzie działał w porządku.
źródło
Po pierwsze, jest to niezdefiniowane zachowanie. Niektóre kompilatory optymalizujące stają się obecnie bardzo agresywne wobec niezdefiniowanych zachowań. Na przykład, ponieważ - w tym przypadku zachowanie jest niezdefiniowane, kompilator może zdecydować o zapisaniu instrukcji i cyklu procesora, a nie zmniejszeniu a. Co jest oficjalnie prawidłowe i zgodne z prawem.
Ignorując to, możesz odjąć 1, 2 lub 1980. Na przykład, jeśli mam dane finansowe za lata 1980–2013, mogę odjąć 1980. Teraz, jeśli weźmiemy zmiennoprzecinkowe * a = malloc (rozmiar); jest na pewno jakiś duży stała k takie, że a - k jest wskaźnikiem NULL. W takim przypadku naprawdę spodziewamy się, że coś pójdzie nie tak.
Teraz weź dużą strukturę, powiedzmy megabajt. Przydziel wskaźnik p wskazujący na dwie struktury. p - 1 może być wskaźnikiem zerowym. p - 1 może się zawijać (jeśli struct jest megabajtem, a blok malloc ma 900 KB od początku przestrzeni adresowej). Więc może być bez komplikacji kompilatora, że p - 1> p. Rzeczy mogą być interesujące.
źródło
Dozwolony? Tak. Dobry pomysł? Zazwyczaj nie.
C jest skrótem od języka asemblera, aw języku asemblera nie ma wskaźników, tylko adresy pamięci. Wskaźniki C są adresami pamięci, które mają boczne działanie polegające na zwiększaniu lub zmniejszaniu o rozmiar tego, na co wskazują, gdy są poddawane arytmetyki. To sprawia, że następujące elementy są w porządku z perspektywy składni:
Tablice nie są tak naprawdę w C; są tylko wskaźnikami do ciągłych zakresów pamięci, które zachowują się jak tablice.
[]
Operator jest skrótem robi wskaźnik arytmetyczne i wyłuskania, taka[x]
naprawdę oznacza*(a + x)
.Istnieją uzasadnione powody, aby zrobić powyższe, na przykład niektóre urządzenia I / O mające kilka
double
s mapowanych na0xdeadbee7
i0xdeadbeef
. Bardzo niewiele programów musiałoby to zrobić.Kiedy tworzysz adres czegoś, na przykład za pomocą
&
operatora lub połączeniamalloc()
, chcesz zachować oryginalny wskaźnik nietknięty, abyś wiedział, że to, co wskazuje, jest w rzeczywistości czymś ważnym. Zmniejszenie wskaźnika oznacza, że trochę błędnego kodu może próbować go wyrejestrować, uzyskać błędne wyniki, zablokować coś lub, w zależności od środowiska, popełnić naruszenie segmentacji. Jest to szczególnie prawdziwe w przypadkumalloc()
, ponieważ obciążysz każdego, kto dzwoni,free()
aby pamiętać o przekazaniu pierwotnej wartości, a nie jakiejkolwiek zmienionej wersji, która spowoduje, że wszyscy się uwolnią.Jeśli potrzebujesz tablic opartych na 1 w C, możesz to zrobić bezpiecznie kosztem przydzielenia jednego dodatkowego elementu, który nigdy nie będzie używany:
Zauważ, że nie ma to nic wspólnego z ochroną przed przekroczeniem górnej granicy, ale jest to dość łatwe w obsłudze.
Uzupełnienie:
Trochę rozdziału i wersetu z projektu C99 (przepraszam, to wszystko, co mogę link do):
§6.5.2.1.1 mówi, że drugie („inne”) wyrażenie używane z operatorem indeksu dolnego jest liczbą całkowitą.
-1
jest liczbą całkowitą, co czynip[-1]
poprawność, a zatem również poprawia wskaźnik&(p[-1])
. Nie oznacza to, że dostęp do pamięci w tym miejscu spowodowałby określone zachowanie, ale wskaźnik jest nadal prawidłowym wskaźnikiem.§6.5.2.2 mówi, że operator tablicy indeksów ocenia równoważność dodania numeru elementu do wskaźnika, a zatem
p[-1]
jest równoważny*(p + (-1))
. Nadal obowiązuje, ale może nie powodować pożądanego zachowania.W pkt 6.5.6.8 (podkreślenie moje):
Oznacza to, że wyniki arytmetyki wskaźnika muszą wskazywać element w tablicy. Nie mówi, że arytmetykę należy wykonać od razu. W związku z tym:
Czy polecam robić to w ten sposób? Nie wiem, a moja odpowiedź wyjaśnia, dlaczego.
źródło