Czy bezpieczne jest trzymanie wskaźnika poza zakresem bez wyłuskiwania go? [duplikować]
Czy w C jest bezpiecznie trzymać wskaźnik poza zakresem (bez wyłuskiwania go) dla dalszych działań arytmetycznych?
void f(int *array)
{
int *i = array - 1; // OOB
while(...) {
++i;
...
}
}
void g(int *array, int *end /* past-the-end pointer: OOB */)
{
while(array != end) {
...
++array;
}
}
Wyobrażam sobie ekstremalne przypadki, jeśli adres jest pierwszym ze wspomnień, czy ostatnim ...
Odpowiedzi
Przesuwanie wskaźnika do jednego elementu poza ostatni element jest dozwolone, ale przesuwanie dalej lub przesuwanie przed pierwszym elementem jest niedozwolone.
Cytat z N1570 6.5.6 Operatory addytywne (punkt 8):
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. Innymi słowy, jeśli wyrażenie P wskazuje na i-ty element obiektu tablicy, wyrażenia (P) + N (równoważnie, N + (P)) i (P) -N (gdzie N ma wartość n) punkt odpowiednio do i + n-tego i i-n-tego elementu obiektu tablicy, o ile istnieją. Co więcej, jeśli wyrażenie P wskazuje na ostatni element obiektu tablicy, wyrażenie (P) +1 wskazuje jeden za ostatni element obiektu tablicy, a jeśli wyrażenie Q wskazuje jeden za ostatni element obiektu tablicy, wyrażenie (Q) -1 wskazuje na ostatni element obiektu tablicy. Jeśli zarówno operand wskaźnika, jak i wynik wskazują na elementy tego samego obiektu tablicy lub jeden za ostatnim elementem obiektu tablicy, ocena nie spowoduje przepełnienia; w przeciwnym razie zachowanie jest niezdefiniowane. Jeśli wynik wskazuje jeden za ostatnim elementem obiektu tablicy, nie powinien być używany jako argument operacji jednoargumentowego operatora *, który jest oceniany.
Wskaźnik może wskazywać na jeden element za ostatnim elementem tablicy, a arytmetyka wskaźnika może być wykonywana między tym wskaźnikiem a wskaźnikiem do elementu tablicy.
Takiego wskaźnika nie można wyłuskać, ale można go użyć w arytmetyce wskaźników. Na przykład obowiązuje:
char arr[10];
char *p1, *p2;
p1 = arr + 10;
p2 = arr + 5;
int diff = p1 - p2;
printf("diff=%d\n", diff); // prints 5
Wskaźnik nie może wskazywać przed pierwszym elementem.
Jest to opisane w sekcji 6.5.6p8 standardu C :
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 indeksów wynikowych i oryginalnych elementów tablicy jest równa wyrażeniu całkowitemu. Innymi słowy, jeśli wyrażenie
P
wskazuje na i- ty element obiektu tablicy, wyrażenia(P)+N
(równoważnie,N+(P)
) i(P)-N
(gdzieN
ma wartość n ) wskazują, odpowiednio, i + n -ty i i-n -ty elementy obiektu tablicy, o ile istnieją. Ponadto, jeśli wyrażenieP
wskazuje na ostatni element obiektu tablicy, wyrażenie(P)+1
wskazuje o jeden za ostatnim elementem obiektu tablicy, a jeśli wyrażenieQ
wskazuje o jeden za ostatnim elementem obiektu tablicy, wyrażenie(Q)-1
wskazuje na ostatni element obiektu tablicy. Jeśli zarówno operand wskaźnika, jak i wynik wskazują na elementy tego samego obiektu tablicy lub jeden za ostatnim elementem obiektu tablicy, ocena nie spowoduje przepełnienia; w przeciwnym razie zachowanie jest niezdefiniowane. Jeśli wynik wskazuje jeden za ostatnim elementem obiektu tablicy, nie może być używany jako argument operacji jednoargumentowego*
operatora, który jest oceniany.
Zauważ, że pogrubiona część, która stwierdza, że można utworzyć wskaźnik wskazujący jeden element poza koniec tablicy, i nie ma nic, co pozwala wskazać dowolny punkt przed początkiem tablicy.
Jak zauważyli inni, możesz wskazać jedną przeszłość . Pamiętaj jednak, że NIE wolno wskazać jednego elementu przed pierwszym. Możesz więc zachować ostrożność, pisząc algorytmy, które przechodzą przez tablice wstecz. Ponieważ ten fragment jest nieprawidłowy:
void foo(int *arr, int *end) {
while(end-- != arr) { // Ouch, bad idea...
// Code
}
// Here, end has the value arr[-1]
}
Dzieje się tak, ponieważ gdy end
wskazuje ten sam element co arr
, warunek będzie fałszywy, ale po tym end
jest ponownie zmniejszany i wskaże jeden element przed tablicą, wywołując niezdefiniowane zachowanie.
Zwróć uwagę, że poza tym kod działa dobrze. Aby naprawić błąd, możesz to zrobić zamiast tego:
void foo(int *arr, int *end) {
while(end != arr) {
end--; // Move end-- to inside the loop, in the very beginning
// Code
}
// And here, end is equal to arr, which is perfectly fine
}
Kod w pętli będzie działał dokładnie tak, jak poprzednio. Jedyna różnica polega na tym end
, że ostatnim razem nie zostanie zmniejszona.