Indicizzazione del puntatore


11

Attualmente sto leggendo un libro intitolato "Ricette numeriche in C". In questo libro, l'autore specifica in che modo alcuni algoritmi funzionano intrinsecamente meglio se avessimo indici che iniziano con 1 (non seguo interamente il suo argomento e non è questo il punto di questo post), ma C indicizza sempre i suoi array a partire da 0 Per ovviare a questo, suggerisce semplicemente di ridurre il puntatore dopo l'allocazione, ad esempio:

float *a = malloc(size);
a--;

Questo, dice, ti darà effettivamente un puntatore che ha un indice che inizia con 1, che sarà quindi libero con:

free(a + 1);

Per quanto ne so, tuttavia, questo è un comportamento indefinito dallo standard C. Questo è apparentemente un libro di grande reputazione all'interno della comunità HPC, quindi non voglio semplicemente ignorare ciò che sta dicendo, ma semplicemente decrementare un puntatore al di fuori dell'intervallo assegnato mi sembra molto impreciso. Questo comportamento è "permesso" in C? L'ho provato usando sia gcc che icc, ed entrambi questi risultati sembrano indicare che mi sto preoccupando per nulla, ma voglio essere assolutamente positivo.


3
quale standard C fai riferimento? Lo chiedo perché, per mio ricordo, "Ricette numeriche in C" è stato pubblicato negli anni '90, nei tempi antichi di K&R e forse ANSI C
moscerino


3
"L'ho testato usando sia gcc che icc, ed entrambi questi risultati sembrano indicare che mi sto preoccupando per niente ma voglio essere assolutamente positivo." Non dare mai per scontato che, poiché il compilatore lo consente, il linguaggio C lo consente. A meno che, ovviamente, tu non stia bene con il tuo codice in futuro.
Doval,

5
Senza voler essere snarky, "Ricette numeriche" è generalmente considerato un libro utile, rapido e sporco, non un paradigma di sviluppo software o analisi numerica. Consulta l'articolo di Wikipedia su "Ricette numeriche" per un riassunto di alcune critiche.
Charles E. Grant,

1
A parte questo, ecco perché indicizziamo da zero: cs.utexas.edu/~EWD/ewd08xx/EWD831.PDF
Russell Borogove

Risposte:


16

Hai ragione quel codice come

float a = malloc(size);
a--;

produce un comportamento indefinito, secondo lo standard ANSI C, sezione 3.3.6:

A meno che sia l'operando del puntatore che il risultato non puntino a un membro dello stesso oggetto array o a uno oltre l'ultimo membro dell'oggetto array, il comportamento non è definito

Per codice come questo, la qualità del codice C nel libro (quando l'ho usato alla fine degli anni '90) non era considerata molto alta.

Il problema con un comportamento indefinito è che, indipendentemente dal risultato prodotto dal compilatore, quel risultato è per definizione corretto (anche se è altamente distruttivo e imprevedibile).
Fortunatamente, pochissimi compilatori si sforzano effettivamente di causare comportamenti imprevisti per tali casi e l' mallocimplementazione tipica su macchine utilizzate per HPC ha alcuni dati di contabilità appena prima dell'indirizzo che restituisce, quindi il decremento in genere fornisce un puntatore a tali dati di contabilità. Non è una buona idea scrivere lì, ma solo creare il puntatore è innocuo su quei sistemi.

Basta essere consapevoli del fatto che il codice potrebbe interrompersi quando l'ambiente di runtime viene modificato o quando il codice viene trasferito in un ambiente diverso.


4
Esattamente, è possibile su un'architettura multi-bank che malloc potrebbe darti il ​​nono indirizzo in una banca e il decremento può causare una trappola della CPU con un underflow per uno.
Valità,

1
Non sono d'accordo sul fatto che sia "fortunato". Penso che sarebbe molto meglio se i compilatori emettessero codice che si bloccava immediatamente ogni volta che invocavi un comportamento indefinito.
David Conrad,

4
@DavidConrad: Allora C non è la lingua che fa per te. Gran parte del comportamento indefinito in C non può essere facilmente rilevato o solo con un grave calo delle prestazioni.
Bart van Ingen Schenau,

Stavo pensando di aggiungere "con uno switch di compilatore". Ovviamente non lo vorrai per un codice ottimizzato. Ma hai ragione, ed è per questo che ho rinunciato a scrivere C dieci anni fa.
David Conrad,

@BartvanIngenSchenau a seconda di cosa intendi per "grave calo delle prestazioni", esiste un'esecuzione simbolica per C (ad esempio clang + klee) e per i disinfettanti (asan, tsan, ubsan, valgrind ecc.) Che tendono ad essere molto utili per il debug.
Maciej Piechotka,

10

Ufficialmente, è un comportamento indefinito avere un punto puntatore all'esterno dell'array (tranne uno oltre la fine), anche se non è mai negato .

In pratica, se il tuo processore ha un modello di memoria piatta (al contrario di quelli strani come x86-16 ) e se il compilatore non ti dà un errore di runtime o un'ottimizzazione errata se crei un puntatore non valido, allora il codice funzionerà va bene.


1
Ha senso. Sfortunatamente, sono due troppi se è per i miei gusti.
WolfPack88

3
L'ultimo punto è IMHO il più problematico. Dato che i compilatori di questi tempi non lasciano semplicemente accadere qualunque cosa la piattaforma "naturalmente" faccia in caso di UB, ma gli ottimizzatori la stanno sfruttando in modo aggressivo , non ci giocherei così alla leggera.
Matteo Italia,

3

Innanzitutto, è un comportamento indefinito. Oggi alcuni compilatori di ottimizzazione diventano molto aggressivi nei confronti di comportamenti indefiniti. Ad esempio, poiché a-- in questo caso è un comportamento indefinito, il compilatore potrebbe decidere di salvare un'istruzione e un ciclo del processore e non decrementare a. Che è ufficialmente corretto e legale.

Ignorandolo, potresti sottrarre 1, o 2 o 1980. Ad esempio, se avessi dati finanziari per gli anni dal 1980 al 2013, potrei sottrarre il 1980. Ora se prendiamo float * a = malloc (size); c'è sicuramente una grande costante k tale che a - k è un puntatore nullo. In tal caso, ci aspettiamo davvero che qualcosa vada storto.

Ora prendi una grande struttura, diciamo un megabyte di dimensioni. Allocare un puntatore p che punta a due strutture. p - 1 potrebbe essere un puntatore nullo. p - 1 potrebbe finire (se uno struct è un megabyte e il blocco malloc è 900 KB dall'inizio dello spazio degli indirizzi). Quindi potrebbe essere senza malizia del compilatore che p - 1> p. Le cose potrebbero diventare interessanti.


1

... semplicemente decrementare un puntatore al di fuori dell'intervallo assegnato mi sembra molto impreciso. Questo comportamento è "permesso" in C?

Permesso? Sì. Buona idea? Non solitamente.

C è una scorciatoia per il linguaggio assembly e nel linguaggio assembly non ci sono puntatori, ma solo indirizzi di memoria. I puntatori di C sono indirizzi di memoria che hanno un comportamento laterale di incremento o decremento in base alla dimensione di ciò a cui indicano quando sottoposti all'aritmetica. Questo rende quanto segue bene dal punto di vista della sintassi:

double *p = (double *)0xdeadbeef;
--p;  // p == 0xdeadbee7, assuming sizeof(double) == 8.
double d = p[0];

Le matrici non sono davvero una cosa in C; sono solo puntatori a intervalli contigui di memoria che si comportano come array. L' []operatore è una scorciatoia per eseguire l'aritmetica e la dereferenziazione del puntatore, quindi in a[x]realtà significa *(a + x).

Esistono validi motivi per fare quanto sopra, come alcuni dispositivi I / O con un paio di doubles mappati in 0xdeadbee7e 0xdeadbeef. Pochissimi programmi dovrebbero farlo.

Quando si crea l'indirizzo di qualcosa, ad esempio utilizzando l' &operatore o chiamando malloc(), si desidera mantenere intatto il puntatore originale in modo da sapere che ciò a cui punta è effettivamente qualcosa di valido. Decrementare il puntatore significa che un po 'di codice errato potrebbe tentare di dereferenziarlo, ottenendo risultati errati, ostruendo qualcosa o, a seconda del proprio ambiente, commettendo una violazione della segmentazione. Questo è particolarmente vero con malloc(), perché hai messo l'onere su chi sta chiamando free()a ricordare di passare il valore originale e non una versione alterata che farà perdere tutto il diavolo.

Se hai bisogno di array basati su 1 in C, puoi farlo in sicurezza a spese dell'allocazione di un elemento aggiuntivo che non verrà mai utilizzato:

double *array_create(size_t size) {
    // Wasting one element, so don't allow it to be full-sized
    assert(size < SIZE_MAX);
    return malloc((size+1) * sizeof(double));
}

inline double array_index(double *array, size_t index) {
    assert(array != NULL);
    assert(index >= 1);  // This is a 1-based array
    return array[index];
}

Nota che questo non fa nulla per proteggere dal superamento del limite superiore, ma è abbastanza facile da gestire.


Addendum:

Alcuni capitoli e versetti della bozza del C99 (scusate, è tutto ciò a cui posso collegarmi):

§6.5.2.1.1 afferma che la seconda ("altra") espressione utilizzata con l'operatore di pedice è di tipo intero. -1è un numero intero, che rende p[-1]valido e quindi rende valido anche il puntatore &(p[-1]). Ciò non implica che l'accesso alla memoria in quella posizione produrrebbe un comportamento definito, ma il puntatore è ancora un puntatore valido.

§6.5.2.2 afferma che l'operatore di sottoscrizione dell'array valuta l'equivalente dell'aggiunta del numero di elemento al puntatore, pertanto p[-1]è equivalente a *(p + (-1)). È ancora valido, ma potrebbe non produrre comportamenti desiderabili.

§6.5.6.8 dice (enfasi mia):

Quando un'espressione con tipo intero viene aggiunta o sottratta da un puntatore, il risultato ha il tipo di operando puntatore.

... se l'espressione Ppunta iall'elemento -th di un oggetto array, le espressioni (P)+N(equivalentemente N+(P)) e (P)-N (dove Nha il valore n) puntano, rispettivamente, agli elementi i+n-th e i−n-th dell'oggetto array, purché esistano .

Ciò significa che i risultati dell'aritmetica del puntatore devono puntare a un elemento in un array. Non dice che l'aritmetica debba essere fatta tutta in una volta. Perciò:

double a[20];

// This points to element 9 of a; behavior is defined.
double d = a[-1 + 10];

double *p = a - 1;  // This is just a pointer.  No dereferencing.

double e = p[0];   // Does not point at any element of a; behavior is undefined.
double f = p[1];   // Points at element 0 of a; behavior is defined.

Consiglio di fare le cose in questo modo? Non lo so, e la mia risposta spiega perché.


8
-1 Una definizione di "consentito" che include il codice che lo standard C dichiara come generazione di risultati non definiti non è utile.
Pete Kirkham,

Altri hanno sottolineato che si tratta di un comportamento indefinito, quindi non dovresti dire che è "permesso". Tuttavia, il suggerimento di allocare un ulteriore elemento inutilizzato 0 è buono.
200_successo

Questo non è proprio giusto, per favore nota che questo è proibito dallo standard C.
Valità,

@PeteKirkham: non sono d'accordo. Vedi l'addendum alla mia risposta.
Blrfl,

4
@Blrfl 6.5.6 dello standard ISO C11 afferma in caso di aggiunta di un numero intero a un puntatore: "Se sia l'operando del puntatore che il risultato puntano a elementi dello stesso oggetto array, o uno oltre l'ultimo elemento dell'oggetto array , la valutazione non deve produrre un overflow; in caso contrario, il comportamento non è definito. "
Valità,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.