Dlaczego ujemne wskaźniki tablicowe mają sens?

14

Natknąłem się na dziwne doświadczenie w programowaniu C. Rozważ ten kod:

int main(){
  int array1[6] = {0, 1, 2, 3, 4, 5};
  int array2[6] = {6, 7, 8, 9, 10, 11};

  printf("%d\n", array1[-1]);
  return 0;
}

Podczas kompilacji i uruchamiania nie otrzymuję żadnych błędów ani ostrzeżeń. Jak powiedział mój wykładowca, indeks tablicy -1uzyskuje dostęp do innej zmiennej. Nadal jestem zdezorientowany, dlaczego do cholery język programowania ma taką możliwość? Chodzi mi o to, dlaczego zezwalać na ujemne wskaźniki tablicowe?

Mohammed Fawzan
źródło
2
Chociaż to pytanie jest motywowane C jako konkretnym językiem programowania, myślę, że można je rozumieć jako pytanie pojęciowe, które jest tu aktualne (jeśli ledwo).
Raphael
7
@Raphael Nie zgadzam się i uważam, że powinien on należeć do SO, tak czy inaczej jest to zachowanie niezdefiniowane w podręczniku (odwoływanie się do pamięci poza tablicą), a odpowiednie flagi kompilatora powinny ostrzec o tym
maniak ratchet
Zgadzam się z @ratchetfreak. Wydaje się, że jest to błąd kompilatora, ponieważ prawidłowy zakres indeksu to [0, 5]. Wszystko, co znajduje się na zewnątrz, musi być błędem kompilacji / środowiska wykonawczego. Zasadniczo wektory są szczególnym przypadkiem funkcji, których indeks pierwszego elementu zależy od użytkownika. Ponieważ kontrakt C polega na tym, że elementy zaczynają się od indeksu 0, dostęp do elementów ujemnych jest błędem.
Val
2
@Raphael C ma dwie osobliwości w stosunku do typowych języków z tablicami, które mają tutaj znaczenie. Jednym z nich jest to, że C ma podtablice, a odniesienie do elementu -1podtablicy jest całkowicie poprawnym sposobem na odniesienie się do elementu przed tą tablicą w większej tablicy. Po drugie, jeśli indeks jest nieprawidłowy, program jest nieprawidłowy, ale w większości implementacji dostaniesz ciche, złe zachowanie, a nie błąd poza zakresem.
Gilles „SO- przestań być zły”
4
@Gilles Jeśli o to chodzi w pytaniu, to rzeczywiście powinno tak być w przypadku Przepełnienia stosu .
Raphael

Odpowiedzi:

27

Operacja indeksowania tablic a[i]zyskuje na znaczeniu z następujących funkcji języka C.

  1. Składnia a[i]jest równoważna z *(a + i). Dlatego warto powiedzieć, 5[a]aby dostać się do piątego elementu a.

  2. Pointer-arytmetyka mówi, że biorąc pod uwagę wskaźnik pi liczbę całkowitą i, p + i wskaźnik pprzesuwa się o i * sizeof(*p)bajty

  3. Nazwa tablicy abardzo szybko zmienia się w wskaźnik do 0-tego elementua

W efekcie indeksowanie tablic jest szczególnym przypadkiem indeksowania wskaźników. Ponieważ wskaźnik może wskazywać dowolne miejsce w tablicy, każde dowolne wyrażenie, które wygląda, niep[-1] jest błędne podczas badania, a zatem kompilatory nie (nie mogą) traktować wszystkich takich wyrażeń jako błędów.

Twój przykład, a[-1]gdzie atak naprawdę jest nazwa tablicy, jest w rzeczywistości nieprawidłowy. IIRC, nie jest zdefiniowane, jeśli istnieje znacząca wartość wskaźnika jako wynik wyrażenia, o a - 1którym awiadomo, że jest wskaźnikiem do 0-tego elementu tablicy. Sprytny kompilator może to wykryć i oznaczyć jako błąd. Inne kompilatory mogą być nadal zgodne, pozwalając ci strzelać sobie w stopę, dając ci wskaźnik do losowego stosu.

Odpowiedź informatyki brzmi:

  • W C []operator jest zdefiniowany na wskaźnikach, a nie tablicach. W szczególności jest to zdefiniowane w kategoriach arytmetyki wskaźnika i dereferencji wskaźnika.

  • W C wskaźnik jest abstrakcyjnie krotką (start, length, offset)z warunkiem, że 0 <= offset <= length. Arytmetyka wskaźnika jest zasadniczo podnoszona arytmetyką na przesunięciu, z zastrzeżeniem, że jeśli wynik operacji narusza warunek wskaźnika, jest to wartość niezdefiniowana. Usunięcie odwołania ze wskaźnika dodaje dodatkowe ograniczenie, które offset < length.

  • C ma pojęcie, undefined behaviourktóre pozwala kompilatorowi konkretnie reprezentować tę krotkę jako pojedynczą liczbę i nie musi wykrywać żadnych naruszeń warunku wskaźnika. Każdy program, który spełnia semantykę abstrakcyjną, będzie bezpieczny z semantyką konkretną (stratną). Wszystko, co narusza semantykę abstrakcyjną, może zostać bez komentarza zaakceptowane przez kompilator i może zrobić z nim wszystko, co chce.

Hari
źródło
Spróbuj udzielić ogólnej odpowiedzi, a nie jednej w zależności od specyfiki konkretnego języka programowania.
Raphael
6
@ Raphael, pytanie było wyraźnie o C. Myślę, że odniosłem się do konkretnego pytania, dlaczego kompilator C może kompilować pozornie pozbawione znaczenia wyrażenie w ramach definicji C.
Hari
Pytania o C w szczególności są tutaj nie na temat; zanotuj mój komentarz do pytania.
Raphael
5
Uważam, że aspekt lingwistyki porównawczej pytania jest nadal przydatny. Wydaje mi się, że podałem dość „informatyczny” opis tego, dlaczego określona implementacja wykazywała określoną konkretną semantykę.
Hari
15

Tablice są po prostu ułożone jako ciągłe fragmenty pamięci. Dostęp do tablicy, taki jak [i], jest konwertowany na dostęp do adresu lokalizacji pamięciOf (a) + i. Ten kod a[-1]jest doskonale zrozumiały, po prostu odnosi się do adresu przed początkiem tablicy.

To może wydawać się szalone, ale jest wiele powodów, dla których jest to dozwolone:

  • sprawdzenie, czy indeks i do [-] mieści się w granicach tablicy, jest kosztowne.
  • niektóre techniki programowania wykorzystują ten fakt a[-1]. Na przykład, jeśli wiem, że atak naprawdę nie jest to początek tablicy, ale wskaźnik na środku tablicy, to a[-1]po prostu pobiera element tablicy, który znajduje się po lewej stronie wskaźnika.
Dave Clarke
źródło
6
Innymi słowy, prawdopodobnie nie należy go używać. Kropka. Nazywasz się Donald Knuth i próbujesz zapisać kolejne 17 instrukcji? Oczywiście, śmiało.
Raphael
Dzięki za odpowiedź, ale nie wpadłem na pomysł. BTW, będę go czytać raz za razem, dopóki nie zrozumiem .. :)
Mohammed Fawzan
2
@Raphael: Implementacja modelu obiektowego coli wykorzystuje pozycję -1 do przechowywania vtable: piumarta.com/software/cola/objmodel2.pdf . Zatem pola są przechowywane w dodatniej części obiektu, a vtable w negatywie. Nie pamiętam szczegółów, ale myślę, że ma to związek ze spójnością.
Dave Clarke
@ DeZéroToxin: Tablica jest tak naprawdę tylko lokalizacją w pamięci, a niektóre lokalizacje obok niej są logicznie częścią tablicy. Ale tak naprawdę tablica jest tylko wskaźnikiem.
Dave Clarke
1
@Raphael, a[-1]ma sens w niektórych przypadkach a, w tym konkretnym przypadku jest to po prostu nielegalne (ale nie złapane przez kompilator)
vonbrand
4

Jak wyjaśniają inne odpowiedzi, jest to niezdefiniowane zachowanie w C. Weź pod uwagę, że C zostało zdefiniowane (i jest najczęściej używane) jako „asembler wysokiego poziomu”. Użytkownicy C cenią go za jego bezkompromisową szybkość, a sprawdzanie rzeczy w czasie wykonywania jest (głównie) wykluczone ze względu na samą wydajność. Niektóre konstrukcje C, które wyglądają na nonsensowne dla ludzi pochodzących z innych języków, mają w C idealny sens, tak jak to a[-1]. Tak, to nie zawsze ma sens (

vonbrand
źródło
1
Podoba mi się ta odpowiedź. Podaje prawdziwy powód, dla którego jest to w porządku.
darxsys,
3

Można użyć takiej funkcji do napisania metod alokacji pamięci, które mają bezpośredni dostęp do pamięci. Jednym z takich zastosowań jest sprawdzenie poprzedniego bloku pamięci za pomocą ujemnego indeksu tablicy w celu ustalenia, czy dwa bloki można połączyć. Korzystałem z tej funkcji, gdy opracowałem menedżera pamięci trwałej.

Theron W Genaux
źródło
2

C nie jest mocno wpisany. Standardowy kompilator C nie sprawdzałby granic tablic. Inną rzeczą jest to, że tablica w C jest niczym innym jak ciągłym blokiem pamięci, a indeksowanie rozpoczyna się od 0, więc indeks -1 jest lokalizacją dowolnego wzorca bitów a[0].

Inne języki ładnie wykorzystują ujemne wskaźniki. W Pythonie a[-1]zwróci ostatni element, a[-2]zwróci element przedostatni i tak dalej.

saadtaame
źródło
2
Jak wiążą się silne indeksy typowania i tablic? Czy istnieją języki z typem naturali, w których indeksy tablicowe muszą być naturalsami?
Raphael
@Raphael O ile mi wiadomo, mocne pisanie oznacza, że ​​wychwytywane są błędy pisowni. Tablica jest typem, IndexOutOfBounds jest błędem, więc w silnie typowanym języku zostanie to zgłoszone, w C nie. O to mi chodziło.
saadtaame
W językach, które znam, indeksy tablicowe są typu int, a[-5]a zatem , bardziej ogólnie, int i; ... a[i] = ...;są poprawnie wpisywane. Błędy indeksu są wykrywane tylko w czasie wykonywania. Oczywiście sprytny kompilator może wykryć niektóre naruszenia.
Raphael
@ Rafael Mówię o typie danych tablicy jako całości, a nie o typach indeksów. To wyjaśnia, dlaczego C pozwala użytkownikom napisać [-5]. Tak, -5 jest poprawnym typem indeksu, ale jest poza zakresem i to jest błąd. W mojej odpowiedzi nie ma wzmianki o sprawdzaniu typu kompilacji ani typu wykonawczego.
saadtaame
1

W prostych słowach:

Wszystkie zmienne (w tym tablice) w C są przechowywane w pamięci. Załóżmy, że masz 14 bajtów „pamięci” i inicjujesz następujące:

int a=0;
int array1[6] = {0, 1, 2, 3, 4, 5};

Rozważ również wielkość int jako 2 bajty. Następnie hipotetycznie w pierwszych 2 bajtach zostanie zapisana liczba całkowita a. W następnych 2 bajtach zostanie zapisana liczba całkowita pierwszej pozycji tablicy (co oznacza tablicę [0]).

Następnie, gdy mówisz, że tablica [-1] przypomina odniesienie do liczby całkowitej zapisanej w pamięci, która znajduje się tuż przed tablicą [0], która w naszym przypadku jest hipotetycznie liczbą całkowitą a. W rzeczywistości nie jest to dokładnie sposób, w jaki zmienne są przechowywane w pamięci.

Dchris
źródło
0
//:Example of negative index:
//:A memory pool with a heap and a stack:

unsigned char memory_pool[64] = {0};

unsigned char* stack = &( memory_pool[ 64 - 1] );
unsigned char* heap  = &( memory_pool[ 0     ] );

int stack_index =    0;
int  heap_index =    0;

//:reserve 4 bytes on stack:
stack_index += 4;

//:reserve 8 bytes on heap:
heap_index  += 8;

//:Read back all reserved memory from stack:
for( int i = 0; i < stack_index; i++ ){
    unsigned char c = stack[ 0 - i ];
    //:do something with c
};;
//:Read back all reserved memory from heap:
for( int i = 0; i < heap_index; i++ ){
    unsigned char c = heap[ 0 + i ];
    //:do something with c
};;
JMI MADISON
źródło
Witamy w CS.SE! Szukamy odpowiedzi, które pochodzą z wyjaśnieniem lub opisem lektury. Nie jesteśmy witryną kodującą i nie chcemy odpowiedzi, które są tylko blokiem kodu. Zastanów się, czy możesz edytować swoją odpowiedź, aby podać tego rodzaju informacje. Dziękuję Ci!
DW