Perché il comportamento non definito del puntatore fuori limite è aritmetico?

Il seguente esempio è tratto da Wikipedia .

int arr[4] = {0, 1, 2, 3}; int* p = arr + 5; // undefined behavior 

Se non ho mai dereferenziato p, allora perché arr + 5 solo comportamento indefinito? Mi aspetto che i puntatori si comportino come numeri interi, con l’eccezione che quando si effettua il deferenziamento il valore di un puntatore viene considerato come un indirizzo di memoria.

Questo perché i puntatori non si comportano come numeri interi. È un comportamento indefinito perché lo standard lo dice.

Tuttavia, sulla maggior parte delle piattaforms (se non tutte), non si verificherà un arresto anomalo o si incorre in comportamenti dubbi se non si dereferenzia l’array. Ma poi, se non lo elevi, qual è il punto di fare l’aggiunta?

Detto questo, si noti che un’espressione che va oltre la fine di un array è tecnicamente 100% “corretta” e garantisce di non arrestarsi in modo anomalo per §5.7 ¶5 delle specifiche C ++ 11. Tuttavia, il risultato di tale espressione non è specificato (è solo garantito che non sia un overflow); mentre qualsiasi altra espressione che va più di una volta i limiti dell’array è un comportamento esplicitamente indefinito .

Nota: ciò non significa che sia sicuro leggere e scrivere da un offset sovra-uno. Probabilmente modificherete i dati che non appartengono a quell’array e causeranno il danneggiamento dello stato / della memoria. Semplicemente non causerai un’eccezione di overflow.

La mia ipotesi è che sia così perché non è solo il dereferenziamento che è sbagliato. Anche l’aritmetica dei puntatori, il confronto dei puntatori, ecc. Quindi è più semplice dire che non fare questo invece di enumerare le situazioni in cui può essere pericoloso.

Il x86 originale può avere problemi con tali dichiarazioni. Su un codice a 16 bit, i puntatori sono 16 + 16 bit. Se si aggiunge un offset ai 16 bit inferiori, potrebbe essere necessario gestire l’overflow e modificare i 16 bit superiori. Era un’operazione lenta e meglio evitata.

Su quei sistemi, array_base+offset era garantito per non array_base+offset , se l’offset era nell’intervallo (<= dimensione dell'array). Ma array+5 avrebbe un overflow se la matrice contenesse solo 3 elementi.

La conseguenza di tale overflow è che hai un puntatore che non punta dietro l’array, ma prima. E potrebbe anche non essere RAM, ma hardware mappato in memoria. Lo standard C ++ non tenta di limitare ciò che accade se costruisci dei puntatori a componenti hardware casuali, cioè è un comportamento non definito su sistemi reali.

“Comportamento indefinito” non significa che deve bloccarsi su quella riga di codice, ma significa che non è ansible garantire il risultato. Per esempio:

 int arr[4] = {0, 1, 2, 3}; int* p = arr + 5; // I guess this is allowed to crash, but that would be a rather // unusual implementation choice on most machines. *p; //may cause a crash, or it may read data out of some other data structure assert(arr < p); // this statement may not be true // (arr may be so close to the end of the address space that // adding 5 overflowed the address space and wrapped around) assert(p - arr == 5); //this statement may not be true //the compiler may have assigned p some other value 

Sono sicuro che ci sono molti altri esempi da inserire qui.

Se arr trova proprio alla fine dello spazio di memoria della macchina, arr+5 potrebbe trovarsi al di fuori dello spazio di memoria, quindi il tipo di puntatore potrebbe non essere in grado di rappresentare il valore, ovvero potrebbe traboccare e l’overflow non è definito.

Alcuni sistemi, sistemi molto rari e non posso nominarne uno, causeranno trap quando aumenterai i limiti del passato in questo modo. Inoltre, consente un’implementazione che fornisce la protezione dei confini per esistere … anche se non riesco a pensarne una.

In sostanza, non dovresti farlo e quindi non c’è motivo di specificare cosa succede quando lo fai. Specificare cosa succede mette un onere ingiustificato sul fornitore di implementazione.