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?


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.


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.

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 (


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.


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.


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.


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
};;

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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.