Czy ujemne indeksy tablicy są dozwolone w języku C?

115

Właśnie czytałem kod i odkryłem, że osoba używała arr[-2]dostępu do drugiego elementu przed arr, na przykład:

|a|b|c|d|e|f|g|
       ^------------ arr[0]
         ^---------- arr[1]
   ^---------------- arr[-2]

Czy to jest dozwolone?

Wiem, że arr[x]to to samo co *(arr + x). Tak arr[-2]jest *(arr - 2), co wydaje się OK. Co myślisz?

bodacydo
źródło

Odpowiedzi:

168

To jest poprawne. Od C99 §6.5.2.1 / 2:

Definicja operatora indeksu [] jest taka, że ​​E1 [E2] jest identyczne z (* ((E1) + (E2))).

Nie ma magii. To jest równoważność 1-1. Jak zawsze podczas wyłuskiwania wskaźnika (*), musisz mieć pewność, że wskazuje prawidłowy adres.

Matthew Flaschen
źródło
2
Zauważ również, że nie musisz wyłuskiwać wskaźnika, aby uzyskać UB. Zwykłe obliczenia somearray-2są niezdefiniowane, chyba że wynik mieści się w zakresie od początku somearraydo 1 po jego końcu.
RBerteig
34
W starszych książkach []były one określane jako cukier składniowy dla arytmetyki wskaźnikowej. Ulubionym sposobem na zmylenie początkujących jest pisanie 1[arr]- zamiast arr[1]- i obserwowanie, jak zgadują, co to ma znaczyć.
Dummy00001
4
Co dzieje się w systemach 64-bitowych (LP64), gdy masz 32-bitowy indeks int, który jest ujemny? Czy indeks powinien być promowany do 64-bitowego znaku int ze znakiem przed obliczeniem adresu?
Paul R
4
@Paul, z §6.5.6 / 8 (operatory addytywne), „Gdy wyrażenie, które ma typ całkowity jest dodawane lub odejmowane od wskaźnika, wynik ma typ operandu wskaźnika. Jeśli operand wskaźnika wskazuje na element obiektu tablicy, a tablica jest wystarczająco duża, wynik wskazuje na element przesunięty w stosunku do oryginalnego elementu w taki sposób, że różnica między indeksami wynikowymi i oryginalnymi elementami tablicy jest równa wyrażeniu całkowitemu. " Więc myślę, że będzie promowany i ((E1)+(E2))będzie (64-bitowym) wskaźnikiem z oczekiwaną wartością.
Matthew Flaschen,
@Matthew: dzięki za to - wygląda na to, że powinno działać tak, jak można się było spodziewać.
Paul R
63

Jest to poprawne tylko wtedy, gdy arrjest wskaźnikiem wskazującym na drugi element tablicy lub późniejszy element. W przeciwnym razie nie jest to poprawne, ponieważ miałbyś dostęp do pamięci poza granicami tablicy. Na przykład byłoby to błędne:

int arr[10];

int x = arr[-2]; // invalid; out of range

Ale to byłoby w porządku:

int arr[10];
int* p = &arr[2];

int x = p[-2]; // valid:  accesses arr[0]

Jednak użycie ujemnego indeksu dolnego jest niezwykłe.

James McNellis
źródło
Nie posunąłbym się do stwierdzenia, że ​​jest nieważny, tylko potencjalnie niechlujny
Matt Joiner
13
@Matt: kod w pierwszym przykładzie daje niezdefiniowane zachowanie.
James McNellis
5
To jest nieważne. W standardzie C jawnie ma niezdefiniowane zachowanie. Z drugiej strony, gdyby int arr[10];była częścią struktury z innymi elementami przed nią, arr[-2]mogłaby być dobrze zdefiniowana i można by określić, czy jest oparta offsetofitp.
R .. GitHub STOP HELPING ICE
4
Znalazłem to w sekcji K&R 5.3, pod koniec: If one is sure that the elements exist, it is also possible to index backwards in an array; p[-1], p[-2], and so on are syntactically legal, and refer to the elements that immediately precede p[0]. Of course, it is illegal to refer to objects that are not within the array bounds.Mimo to twój przykład jest lepszy, ponieważ pomaga mi go zrozumieć. Dzięki!
Qiang Xu
4
Przepraszam za nekromancję wątków, ale po prostu uwielbiam to, jak K&R są niejednoznaczni, co oznacza „nielegalne”. Ostatnie zdanie sprawia, że ​​brzmi to tak, jakby dostęp poza granicami generował błąd kompilacji. Ta książka jest trucizną dla początkujących.
Martin,
12

Brzmi dobrze. Byłoby to jednak rzadkim przypadkiem, gdy byś tego potrzebował.

Matt Joiner
źródło
9
Nie jest to takie rzadkie - jest bardzo przydatne np. Przy przetwarzaniu obrazu z operatorami sąsiedztwa.
Paul R
Po prostu potrzebowałem tego użyć, ponieważ tworzę pulę pamięci ze stosem i stertą [struktura / projekt]. Stos rośnie w kierunku wyższych adresów pamięci, sterta rośnie w kierunku niższych adresów pamięci. Spotkanie w środku.
JMI MADISON
8

To prawdopodobnie arrwskazywało na środek tablicy, a więc arr[-2]wskazywało na coś w oryginalnej tablicy bez przekraczania granic.

Igor Zevaka
źródło
7

Nie jestem pewien, na ile jest to wiarygodne, ale właśnie przeczytałem następujące zastrzeżenie dotyczące ujemnych wskaźników tablicy w systemach 64-bitowych (prawdopodobnie LP64): http://www.devx.com/tips/Tip/41349

Autor wydaje się mówić, że 32-bitowe indeksy tablicy int z 64-bitowym adresowaniem mogą skutkować złymi obliczeniami adresu, chyba że indeks tablicy jest jawnie promowany do 64 bitów (np. Przez rzutowanie ptrdiff_t). Właściwie widziałem błąd jego natury w wersji PowerPC gcc 4.1.0, ale nie wiem, czy jest to błąd kompilatora (tj. Powinien działać zgodnie ze standardem C99), czy poprawne zachowanie (tj. Indeks wymaga rzutowania na 64 bity do prawidłowego zachowania)?

Paul R.
źródło
3
To brzmi jak błąd kompilatora.
tbleher
2

Wiem, że na pytanie udzielono odpowiedzi, ale nie mogłem się powstrzymać przed podzieleniem się tym wyjaśnieniem.

Pamiętam zasady projektowania kompilatora, załóżmy, że a jest tablicą int, a rozmiar int to 2, a adres bazowy a to 1000.

Jak a[5]będzie działać ->

Base Address of your Array a + (index of array *size of(data type for array a))
Base Address of your Array a + (5*size of(data type for array a))
i.e. 1000 + (5*2) = 1010

To wyjaśnienie jest również powodem, dla którego indeksy ujemne w tablicach działają w C.

tzn. jeśli uzyskam dostęp a[-5], da mi

Base Address of your Array a + (index of array *size of(data type for array a))
Base Address of your Array a + (-5 * size of(data type for array a))
i.e. 1000 + (-5*2) = 990

Zwróci mi obiekt w lokalizacji 990. Dzięki tej logice możemy uzyskać dostęp do ujemnych indeksów w tablicy w C.

Ajinkya Patil
źródło
2

O tym, dlaczego ktoś miałby chcieć używać indeksów ujemnych, użyłem ich w dwóch kontekstach:

  1. Posiadanie tabeli liczb kombinatorycznych, która mówi ci, że grzebień [1] [- 1] = 0; zawsze możesz sprawdzić indeksy przed uzyskaniem dostępu do tabeli, ale w ten sposób kod wygląda czysto i działa szybciej.

  2. Umieszczenie centynela na początku stołu. Na przykład chcesz użyć czegoś takiego jak

     while (x < a[i]) i--;

ale powinieneś także sprawdzić, czy ijest pozytywny.
Rozwiązanie: uczynić go tak, że a[-1]jest -DBLE_MAXtak, że x&lt;a[-1]zawsze będzie fałszywa.

Santiago Egido Arteaga
źródło
0
#include <stdio.h>

int main() // negative index
{ 
    int i = 1, a[5] = {10, 20, 30, 40, 50};
    int* mid = &a[5]; //legal;address,not element there
    for(; i < 6; ++i)
    printf(" mid[ %d ] = %d;", -i, mid[-i]);
}
Rathinavelu Muthaliar
źródło
1
Chociaż ten kod może odpowiedzieć na pytanie, zapewnia dodatkowy kontekst dotyczący tego, dlaczego i / lub jak ten kod odpowiada, poprawia jego długoterminową wartość.
β.εηοιτ.βε
Python groovy ... mam je. Prosty przypadek użycia polega na tym, że można uzyskać dostęp do ostatniego elementu tablicy bez znajomości rozmiaru tablicy, co jest bardzo realnym wymaganiem w wielu sytuacjach projektu. Korzysta na tym również wiele DSL.
Rathinavelu Muthaliar