Come funziona il confronto dei puntatori in C? Va bene confrontare i puntatori che non puntano allo stesso array?


33

Nel capitolo 5 di K&R (The C Programming Language 2nd Edition) ho letto quanto segue:

Innanzitutto, i puntatori possono essere confrontati in determinate circostanze. Se pe qpunto di membri della stessa matrice, i rapporti poi, come ==, !=, <, >=correttamente, ecc lavoro.

Ciò sembra implicare che solo i puntatori che puntano allo stesso array possono essere confrontati.

Tuttavia, quando ho provato questo codice

    char t = 't';
    char *pt = &t;
    char x = 'x';
    char *px = &x;

    printf("%d\n", pt > px);

1 viene stampato sullo schermo.

Prima di tutto, ho pensato che sarei stato indefinito o qualche tipo o errore, perché pt e pxnon punta allo stesso array (almeno nella mia comprensione).

Inoltre è pt > pxperché entrambi i puntatori puntano a variabili memorizzate nello stack e lo stack cresce, quindi l'indirizzo di memoria di tè maggiore di quello di x? Qual è il motivo per cui pt > pxè vero?

Mi confondo di più quando viene introdotto malloc. Anche in K&R nel capitolo 8.7 è scritto quanto segue:

C'è ancora un presupposto, tuttavia, che i puntatori a diversi blocchi restituiti sbrkpossono essere confrontati in modo significativo. Ciò non è garantito dallo standard che consente il confronto dei puntatori solo all'interno di un array. Quindi questa versione di mallocè portatile solo tra macchine per le quali il confronto generale dei puntatori è significativo.

Non ho avuto problemi a confrontare i puntatori che puntavano allo spazio malloced sull'heap con i puntatori che puntavano a impilare le variabili.

Ad esempio, il seguente codice ha funzionato bene, con la 1stampa:

    char t = 't';
    char *pt = &t;
    char *px = malloc(10);
    strcpy(px, pt);
    printf("%d\n", pt > px);

Sulla base dei miei esperimenti con il mio compilatore, mi viene portato a pensare che qualsiasi puntatore può essere confrontato con qualsiasi altro puntatore, indipendentemente da dove puntino individualmente. Inoltre, penso che l'aritmetica del puntatore tra due puntatori vada bene, indipendentemente da dove puntino individualmente perché l'aritmetica sta semplicemente usando gli indirizzi di memoria archiviati dai puntatori.

Tuttavia, sono confuso da ciò che sto leggendo in K&R.

Il motivo che sto chiedendo è perché il mio prof. in realtà è diventata una domanda d'esame. Ha dato il seguente codice:

struct A {
    char *p0;
    char *p1;
};

int main(int argc, char **argv) {
    char a = 0;
    char *b = "W";
    char c[] = [ 'L', 'O', 'L', 0 ];

   struct A p[3];
    p[0].p0 = &a;
    p[1].p0 = b;
    p[2].p0 = c;

   for(int i = 0; i < 3; i++) {
        p[i].p1 = malloc(10);
        strcpy(p[i].p1, p[i].p0);
    }
}

Cosa valutano questi per:

  1. p[0].p0 < p[0].p1
  2. p[1].p0 < p[1].p1
  3. p[2].p0 < p[2].p1

La risposta è 0, 1e0 .

(Il mio professore include il disclaimer sull'esame che le domande riguardano un ambiente di programmazione versione Ubuntu Linux 16.04, 64-bit)

(nota del redattore: se SO consentisse più tag, quell'ultima parte avrebbe giustificato , e forse . Se il punto della domanda / classe fossero i dettagli di implementazione del sistema operativo di basso livello, piuttosto che portatili C.)


17
Si sono forse confondendo ciò che è valida in Ccon ciò che è al sicuro in C. Il confronto tra due puntatori con lo stesso tipo può sempre essere eseguito (verificando l'uguaglianza, ad esempio), tuttavia, utilizzando l'aritmetica e il confronto dei puntatori >ed <è sicuro solo se utilizzato all'interno di un determinato array (o blocco di memoria).
Adrian Mole il

13
Per inciso, si dovrebbe non essere di apprendimento C da K & R. Per cominciare, da allora la lingua ha subito molti cambiamenti. E, a dire il vero, il codice di esempio presente era di un'epoca in cui venivano valutate la terseness piuttosto che la leggibilità.
paxdiablo,

5
No, non è garantito che funzioni. In pratica può fallire su macchine con modelli di memoria segmentati. Vedi C ha un equivalente di std :: less da C ++? Sulla maggior parte delle macchine moderne, funzionerà nonostante UB.
Peter Cordes,

6
@Adam: Close, ma in realtà è UB (a meno che il compilatore utilizzato dall'OP, GCC, scelga di definirlo. Potrebbe). Ma UB non significa "esplode definitivamente"; uno dei possibili comportamenti per UB sta funzionando come previsto !! Questo è ciò che rende UB così cattivo; può funzionare correttamente in una build di debug e fallire con l'ottimizzazione abilitata, o viceversa, o interrompere in base al codice circostante. Il confronto con altri puntatori ti darà comunque una risposta, ma la lingua non definisce il significato di tale risposta (se non altro). No, è consentito l'arresto anomalo. È davvero UB.
Peter Cordes,

3
@Adam: Oh sì, non importa la prima parte del mio commento, ho letto male il tuo. Ma affermi che il confronto con altri puntatori ti darà ancora una risposta . Non è vero. Sarebbe un risultato non specificato , non UB completo. UB è molto peggio e significa che il tuo programma potrebbe segfault o SIGILL se l'esecuzione raggiunge quell'istruzione con quegli input (in qualsiasi momento prima o dopo che ciò avvenga effettivamente). (È plausibile solo su x86-64 se l'UB è visibile al momento della compilazione, ma in generale può succedere di tutto.) Parte del punto di UB è lasciare che il compilatore faccia ipotesi "non sicure" mentre genera asm.
Peter Cordes,

Risposte:


33

Secondo la norma C11 , gli operatori relazionali <, <=, >, e >=possono essere utilizzati solo su puntatori ad elementi dello stesso vettore o un oggetto struct. Questo è spiegato nella sezione 6.5.8p5:

Quando vengono confrontati due puntatori, il risultato dipende dalle posizioni relative nello spazio degli indirizzi degli oggetti indicati. Se due puntatori a tipi di oggetto puntano entrambi sullo stesso oggetto o entrambi puntano uno oltre l'ultimo elemento dello stesso oggetto array, si equivalgono. Se gli oggetti indicati sono membri dello stesso oggetto aggregato, i puntatori ai membri della struttura dichiarati successivamente confrontano maggiori dei puntatori con i membri dichiarati in precedenza nella struttura e i puntatori agli elementi dell'array con valori di indice più grandi confrontano maggiore dei puntatori agli elementi dello stesso array con valori di indice inferiori. Tutti i puntatori ai membri dello stesso oggetto unione sono uguali.

Si noti che eventuali confronti che non soddisfano questo requisito invocano comportamenti indefiniti , il che significa (tra le altre cose) che non si può dipendere dai risultati per essere ripetibili.

Nel tuo caso particolare, sia per il confronto tra gli indirizzi di due variabili locali sia tra l'indirizzo di un indirizzo locale e dinamico, l'operazione sembra "funzionare", tuttavia il risultato potrebbe cambiare apportando una modifica apparentemente non correlata al codice o addirittura compilando lo stesso codice con impostazioni di ottimizzazione diverse. Con un comportamento indefinito, solo perché il codice potrebbe bloccarsi o generare un errore non significa che lo farà .

Ad esempio, un processore x86 in esecuzione in modalità reale 8086 ha un modello di memoria segmentato che utilizza un segmento a 16 bit e un offset a 16 bit per creare un indirizzo a 20 bit. Quindi in questo caso un indirizzo non converte esattamente in un numero intero.

Gli operatori di uguaglianza ==e !=tuttavia non hanno questa limitazione. Possono essere utilizzati tra due puntatori a tipi compatibili o puntatori NULL. Quindi l'utilizzo ==o !=in entrambi i tuoi esempi produrrebbe un codice C valido.

Tuttavia, anche con ==e !=potresti ottenere alcuni risultati inaspettati ma ancora ben definiti. Vedi Può un confronto di uguaglianza di puntatori non correlati valutare vero? per maggiori dettagli su questo.

Per quanto riguarda la domanda d'esame formulata dal tuo professore, formula una serie di ipotesi errate:

  • Esiste un modello di memoria piatta in cui esiste una corrispondenza da 1 a 1 tra un indirizzo e un valore intero.
  • Che i valori del puntatore convertiti rientrino in un tipo intero.
  • Il fatto che l'implementazione tratti semplicemente i puntatori come numeri interi durante l'esecuzione di confronti senza sfruttare la libertà data da un comportamento indefinito.
  • Che uno stack sia utilizzato e che le variabili locali siano memorizzate lì.
  • Che un heap viene utilizzato per estrarre la memoria allocata da.
  • Che lo stack (e quindi le variabili locali) appaia a un indirizzo superiore rispetto all'heap (e quindi agli oggetti allocati).
  • Le costanti della stringa vengono visualizzate a un indirizzo inferiore rispetto all'heap.

Se dovessi eseguire questo codice su un'architettura e / o con un compilatore che non soddisfa questi presupposti, potresti ottenere risultati molto diversi.

Inoltre, entrambi gli esempi mostrano anche comportamenti indefiniti quando chiamano strcpy, poiché l'operando destro (in alcuni casi) punta a un singolo carattere e non a una stringa con terminazione nulla, risultando nella funzione che legge oltre i limiti della variabile data.


3
@Shisui Anche questo, non dovresti ancora dipendere dai risultati. I compilatori possono diventare molto aggressivi quando si tratta di ottimizzazione e useranno comportamenti indefiniti come un'opportunità per farlo. È possibile che l'utilizzo di un compilatore diverso e / o impostazioni di ottimizzazione diverse possano generare output diversi.
dbush,

2
@Shisui: in genere accadrà su macchine con un modello di memoria piatta, come x86-64. Alcuni compilatori per tali sistemi potrebbero persino definire il comportamento nella loro documentazione. In caso contrario, il comportamento "folle" può verificarsi a causa di UB visibile in fase di compilazione. (In pratica non credo che qualcuno lo voglia, quindi non è qualcosa che i compilatori mainstream cercano e "provano a rompere".)
Peter Cordes,

1
Come se un compilatore vedesse che un percorso di esecuzione porterebbe <tra mallocrisultato e una variabile locale (memorizzazione automatica, cioè stack), si potrebbe presumere che il percorso di esecuzione non sia mai preso e compila l'intera funzione in ud2un'istruzione (solleva un illegale -eccezione di istruzione che il kernel gestirà consegnando un SIGILL al processo). GCC / clang lo fanno in pratica per altri tipi di UB, come cadere dalla fine di una non voidfunzione. godbolt.org è in questo momento a quanto pare, ma prova a copiare / incollare int foo(){int x=2;}e nota la mancanza di unret
Peter Cordes,

4
@Shisui: TL: DR: non è C portatile, nonostante funzioni su Linux x86-64. Tuttavia, fare ipotesi sui risultati del confronto è semplicemente folle. Se non sei nel thread principale, il tuo stack di thread sarà stato allocato in modo dinamico utilizzando lo stesso meccanismo mallocutilizzato per ottenere più memoria dal sistema operativo, quindi non c'è motivo di presumere che i tuoi var locali (stack di thread) siano sopra mallocallocati dinamicamente Conservazione.
Peter Cordes,

2
@PeterCordes: ciò che è necessario è riconoscere vari aspetti del comportamento come "facoltativamente definiti", in modo tale che le implementazioni possano definirli o meno, a loro piacimento, ma devono indicare in modo verificabile (ad esempio macro predefinita) se non lo fanno. Inoltre, invece di caratterizzare che qualsiasi situazione in cui gli effetti di un'ottimizzazione sarebbero osservabili come "Comportamento indefinito", sarebbe molto più utile dire che gli ottimizzatori possono considerare alcuni aspetti del comportamento come "non osservabili" se indicano che essi fare così. Ad esempio, data int x,y;, un'implementazione ...
supercat

12

Il problema principale nel confrontare i puntatori con due matrici distinte dello stesso tipo è che le matrici stesse non devono essere posizionate in un particolare posizionamento relativo: una potrebbe finire prima e dopo l'altra.

Prima di tutto, ho pensato che sarei diventato indefinito o di qualche tipo o errore, perché pt e px non puntano allo stesso array (almeno nella mia comprensione).

No, il risultato dipende dall'implementazione e da altri fattori imprevedibili.

Inoltre pt> px perché entrambi i puntatori puntano a variabili memorizzate nello stack e lo stack si riduce, quindi l'indirizzo di memoria di t è maggiore di quello di x? È per questo che pt> px è vero?

Non c'è necessariamente uno stack . Quando esiste, non ha bisogno di crescere. Potrebbe crescere. Potrebbe essere non contiguo in qualche modo bizzarro.

Inoltre, penso che l'aritmetica del puntatore tra due puntatori vada bene, indipendentemente da dove puntino individualmente perché l'aritmetica sta semplicemente usando gli indirizzi di memoria archiviati dai puntatori.

Diamo un'occhiata alla specifica C , §6.5.8 a pagina 85 che discute gli operatori relazionali (cioè gli operatori di confronto che stai usando). Si noti che ciò non si applica al diretto !=o al ==confronto.

Quando si confrontano due puntatori, il risultato dipende dalle posizioni relative nello spazio degli indirizzi degli oggetti indicati. ... Se gli oggetti indicati sono membri dello stesso oggetto aggregato, ... i puntatori agli elementi della matrice con valori di indice più grandi confrontano i puntatori con elementi dello stesso array con valori di indice più bassi.

In tutti gli altri casi, il comportamento non è definito.

L'ultima frase è importante. Mentre taglio alcuni casi non correlati per risparmiare spazio, c'è un caso che è importante per noi: due array, non parte dello stesso oggetto struct / aggregate 1 , e stiamo confrontando i puntatori con quei due array. Questo è un comportamento indefinito .

Mentre il tuo compilatore ha appena inserito una sorta di istruzione macchina CMP (confronta) che confronta numericamente i puntatori e sei stato fortunato qui, UB è una bestia piuttosto pericolosa. Letteralmente può succedere di tutto: il compilatore potrebbe ottimizzare l'intera funzione, inclusi gli effetti collaterali visibili. Potrebbe generare demoni nasali.

1 È possibile confrontare i puntatori in due matrici diverse che fanno parte della stessa struttura, poiché ciò rientra nella clausola in cui le due matrici fanno parte dello stesso oggetto aggregato (la struttura).


1
Ancora più importante, con ted xessendo definito nella stessa funzione, non vi è alcun motivo per supporre che un compilatore indirizzato a x86-64 disponga i locali nel frame dello stack per questa funzione. Lo stack che cresce verso il basso non ha nulla a che fare con l'ordine di dichiarazione delle variabili in una funzione. Anche in funzioni separate, se uno potesse allinearsi all'altro, i locali della funzione "figlio" potrebbero ancora mescolarsi con i genitori.
Peter Cordes,

1
il compilatore potrebbe ottimizzare l'intera funzione compresi gli effetti collaterali visibili Non un'esagerazione: per altri tipi di UB (come cadere alla fine di un non voidfunzionamento) g ++ e clang ++ davvero fare che, in pratica: godbolt.org/z/g5vesB essi supponiamo che il percorso di esecuzione non sia preso perché conduce a UB e compilare tali blocchi di base in un'istruzione illegale. O per nessuna istruzione, cadendo in silenzio in qualsiasi asm è il prossimo se quella funzione fosse mai stata chiamata. (Per qualche motivo gccnon lo fa, solo g++).
Peter Cordes,

6

Quindi chiesto cosa

p[0].p0 < p[0].p1
p[1].p0 < p[1].p1
p[2].p0 < p[2].p1

Valuta a. La risposta è 0, 1 e 0.

Queste domande si riducono a:

  1. L'heap è sopra o sotto lo stack.
  2. È l'heap sopra o sotto la sezione letterale stringa del programma.
  3. come [1].

E la risposta a tutte e tre è "implementazione definita". Le domande del tuo prof sono false; l'hanno basato sul layout unix tradizionale:

<empty>
text
rodata
rwdata
bss
< empty, used for heap >
...
stack
kernel

ma diversi unici moderni (e sistemi alternativi) non sono conformi a tali tradizioni. A meno che non abbiano anteposto alla domanda "dal 1992"; assicurati di dare un -1 all'eval.


3
Implementazione non definita, non definita! Pensala in questo modo, la prima può variare tra le implementazioni, ma le implementazioni dovrebbero documentare come viene deciso il comportamento. Quest'ultimo significa che il comportamento può variare in qualsiasi modo e l'implementazione non deve dirti a squat :-)
paxdiablo

1
@paxdiablo: Secondo Rationale dagli autori dello Standard, "Comportamento indefinito ... identifica anche aree di possibile estensione della lingua conforme: l'implementatore può aumentare la lingua fornendo una definizione del comportamento ufficialmente indefinito". La motivazione dice inoltre "L'obiettivo è quello di dare al programmatore la possibilità di combattere per creare potenti programmi in C che sono anche altamente portatili, senza sembrare sminuire i programmi di C perfettamente utili che sembrano non essere portatili, quindi l'avverbio rigorosamente". Gli autori di compilatori commerciali lo comprendono, ma altri scrittori di compilatori no.
supercat

C'è un altro aspetto definito dall'implementazione; il confronto dei puntatori è firmato , quindi a seconda della macchina / sistema operativo / compilatore, alcuni indirizzi possono essere interpretati come negativi. Ad esempio, una macchina a 32 bit che posizionava lo stack su 0xc << 28, probabilmente mostrava le variabili automatiche a un indirizzo inferiore rispetto all'heap o alla rodata.
Mevets,

1
@mevets: lo standard specifica una situazione in cui la firma dei puntatori nei confronti sarebbe osservabile? Mi aspetto che se una piattaforma a 16 bit consenta oggetti maggiori di 32768 byte, ed arr[]è un tale oggetto, lo Standard imporrebbe un arr+32768confronto maggiore arranche se un confronto del puntatore firmato segnalasse diversamente.
supercat

Non lo so; lo standard C è in orbita nel nono cerchio di Dante, pregando per l'eutanasia. Il PO ha fatto specifico riferimento a K&R e a una domanda d'esame. #UB è detriti di un gruppo di lavoro pigro.
Mevets,

1

Su quasi tutte le piattaforme moderne in remoto, i puntatori e gli interi hanno una relazione di ordinamento isomorfa e i puntatori a oggetti disgiunti non sono interlacciati. La maggior parte dei compilatori espone questo ordinamento ai programmatori quando le ottimizzazioni sono disabilitate, ma lo Standard non fa distinzioni tra le piattaforme che hanno un tale ordinamento e quelle che non hanno e non richiedono che eventuali implementazioni espongano tale programmatore al programmatore anche su piattaforme che lo farebbero definirlo. Di conseguenza, alcuni autori di compilatori eseguono vari tipi di ottimizzazioni e "ottimizzazioni" basate sul presupposto che il codice non confronterà mai l'uso degli operatori relazionali sui puntatori con oggetti diversi.

Secondo la Rationale pubblicata, gli autori dello Standard intendevano che le implementazioni estendono il linguaggio specificando come si comporteranno in situazioni che lo Standard definisce come "Comportamento indefinito" (cioè dove lo Standard non impone requisiti ) quando ciò sarebbe utile e pratico , ma alcuni autori di compilatori preferirebbero presumere che i programmi non proveranno mai a beneficiare di nulla al di là di quanto richiesto dallo Standard, piuttosto che consentire ai programmi di sfruttare in modo utile comportamenti che le piattaforme potrebbero supportare senza costi aggiuntivi.

Non sono a conoscenza di compilatori progettati commercialmente che fanno qualcosa di strano con i confronti dei puntatori, ma mentre i compilatori passano al LLVM non commerciale per il loro back-end, sono sempre più propensi a elaborare codice senza senso il cui comportamento era stato specificato in precedenza compilatori per le loro piattaforme. Tale comportamento non è limitato agli operatori relazionali, ma può anche influire sull'uguaglianza / disuguaglianza. Ad esempio, anche se lo Standard specifica che un confronto tra un puntatore a un oggetto e un puntatore "appena passato" a un oggetto immediatamente precedente confronterà uguale, i compilatori basati su gcc e LLVM sono inclini a generare codice senza senso se i programmi eseguono tale confronti.

Come esempio di una situazione in cui anche il confronto di uguaglianza si comporta in modo insensato in gcc e clang, considera:

extern int x[],y[];
int test(int i)
{
    int *p = y+i;
    y[0] = 4;
    if (p == x+10)
        *p = 1;
    return y[0];
}

Sia clang che gcc genereranno un codice che restituirà sempre 4 anche se xè di dieci elementi, ylo segue immediatamente ed iè zero e il confronto è vero e p[0]viene scritto con il valore 1. Penso che ciò che accade sia che un passaggio di ottimizzazione riscrive la funzione come se *p = 1;fosse sostituita da x[10] = 1;. Quest'ultimo codice sarebbe equivalente se il compilatore fosse interpretato *(x+10)come equivalente *(y+i), ma sfortunatamente una fase di ottimizzazione a valle riconosce che un accesso a x[10]sarebbe definito solo se xavesse avuto almeno 11 elementi, il che renderebbe impossibile tale accesso y.

Se i compilatori riescono a ottenere quella "creatività" con lo scenario di uguaglianza dei puntatori, descritta dallo Standard, non mi fiderei loro di astenersi dal diventare ancora più creativi nei casi in cui lo Standard non imponga requisiti.


0

È semplice: confrontare i puntatori non ha senso poiché le posizioni di memoria per gli oggetti non sono mai garantite nello stesso ordine in cui sono state dichiarate. L'eccezione sono le matrici. & array [0] è inferiore a & array [1]. Questo è ciò che K&R sottolinea. In pratica, gli indirizzi dei membri di struct sono anche nell'ordine in cui li dichiari nella mia esperienza. Nessuna garanzia al riguardo .... Un'altra eccezione è se si confronta un puntatore per uguale. Quando un puntatore è uguale a un altro, sai che punta allo stesso oggetto. Qualunque cosa sia. Esame negativo se mi chiedi. A seconda di Ubuntu Linux 16.04, ambiente di programmazione versione 64-bit per una domanda di esame? Veramente ?


Tecnicamente, gli array non sono in realtà un'eccezione dal momento che non si dichiara arr[0], arr[1]ecc separatamente. Dichiara arrnel suo insieme quindi l'ordinamento dei singoli elementi dell'array è un problema diverso da quello descritto in questa domanda.
paxdiablo,

1
Gli elementi della struttura sono garantiti per essere in ordine, il che garantisce che si può usare memcpyper copiare una parte contigua di una struttura e influenzare tutti gli elementi in essa contenuti e non influenzare nient'altro. Lo standard è sciatto sulla terminologia per quanto riguarda i tipi di aritmetica dei puntatori che possono essere fatti con le strutture o l' malloc()archiviazione allocata. La offsetofmacro sarebbe piuttosto inutile se non si potesse fare lo stesso tipo di puntatore aritmetico con i byte di una struttura come con a char[], ma lo Standard non dice espressamente che i byte di una struttura sono (o possono essere usati come) un oggetto array.
supercat

-4

Che domanda provocatoria!

Anche la scansione superficiale delle risposte e dei commenti in questo thread rivelerà come emotiva la tua query apparentemente semplice e diretta.

Non dovrebbe essere sorprendente.

Indubbiamente, i malintesi sul concetto e sull'uso dei puntatori rappresentano una causa predominante di gravi errori nella programmazione in generale.

Il riconoscimento di questa realtà è prontamente evidente nell'ubiquità delle lingue progettate specificamente per affrontare e preferibilmente per evitare le sfide che i puntatori introducono del tutto. Pensa al C ++ e ad altri derivati ​​di C, Java e delle sue relazioni, Python e altri script - semplicemente come quelli più importanti e prevalenti, e più o meno ordinati in ordine di gravità del problema.

Sviluppare una comprensione più profonda dei principi sottostanti, pertanto, deve essere pertinente per ogni individuo che aspira all'eccellenza nella programmazione, specialmente a livello di sistemi .

Immagino che questo sia esattamente ciò che il tuo insegnante intende dimostrare.

E la natura di C lo rende un veicolo conveniente per questa esplorazione. Meno chiaramente dell'assemblaggio - sebbene forse più facilmente comprensibile - e ancora molto più esplicitamente dei linguaggi basati sull'astrazione più profonda dell'ambiente di esecuzione.

Progettato per facilitare la traduzione deterministica dell'intento del programmatore in istruzioni che le macchine possono comprendere, C è un linguaggio a livello di sistema . Sebbene classificato come di alto livello, appartiene davvero a una categoria "media"; ma poiché non esiste nulla del genere, la designazione di "sistema" deve essere sufficiente.

Questa caratteristica è in gran parte responsabile di renderla una lingua di scelta per i driver di dispositivo , il codice del sistema operativo e le implementazioni integrate . Inoltre, un'alternativa meritatamente favorita in applicazioni in cui l'efficienza ottimale è fondamentale; dove ciò significa la differenza tra sopravvivenza ed estinzione, e quindi è una necessità al contrario di un lusso. In tali casi, la convenienza attraente della portabilità perde tutto il suo fascino e optare per le prestazioni di scarsa lucentezza del minimo comune denominatore diventa un'opzione impensabilmente dannosa .

Ciò che rende C - e alcuni dei suoi derivati ​​- abbastanza speciale, è che consente ai suoi utenti il controllo completo - quando questo è ciò che desiderano - senza imporre loro le relative responsabilità quando non lo fanno. Tuttavia, non offre mai più del più sottile degli isolanti dalla macchina , pertanto un uso corretto richiede una comprensione approfondita del concetto di puntatori .

In sostanza, la risposta alla tua domanda è sublimemente semplice e soddisfacente in modo dolce - a conferma dei tuoi sospetti. Purché , tuttavia, si attribuisca il significato necessario a ogni concetto di questa affermazione:

  • Gli atti di esame, confronto e manipolazione dei puntatori sono sempre e necessariamente validi, mentre le conclusioni derivate dal risultato dipendono dalla validità dei valori contenuti, e quindi non è necessario.

Il primo è invariabilmente sicuro e potenzialmente adeguato , mentre il secondo può essere sempre corretto solo quando è stato stabilito come sicuro . Sorprendentemente - per alcuni - quindi stabilire la validità di quest'ultimo dipende e richiede il primo.

Naturalmente, parte della confusione deriva dall'effetto della ricorsione intrinsecamente presente nel principio di un indicatore - e dalle sfide poste dalla differenziazione del contenuto dall'indirizzo.

Hai ipotizzato abbastanza correttamente ,

Sono stato indotto a pensare che qualsiasi puntatore può essere confrontato con qualsiasi altro puntatore, indipendentemente da dove puntino individualmente. Inoltre, penso che l'aritmetica del puntatore tra due puntatori vada bene, indipendentemente da dove puntino individualmente perché l'aritmetica sta semplicemente usando gli indirizzi di memoria archiviati dai puntatori.

E diversi collaboratori hanno affermato: i puntatori sono solo numeri. A volte qualcosa di più vicino ai numeri complessi , ma ancora non più dei numeri.

L'acrimonia divertente in cui questa tesi è stata ricevuta qui rivela più sulla natura umana che sulla programmazione, ma rimane degno di nota ed elaborazione. Forse lo faremo più tardi ...

Come un commento inizia a suggerire; tutta questa confusione e costernazione deriva dalla necessità di discernere ciò che è valido da ciò che è sicuro , ma questa è una semplificazione eccessiva. Dobbiamo anche distinguere ciò che è funzionale e ciò che è affidabile , ciò che è pratico e ciò che potrebbe essere corretto , e ancora di più: ciò che è appropriato in una particolare circostanza da ciò che può essere proprio in un senso più generale . Per non parlare di; la differenza tra conformità e proprietà .

A questo scopo, abbiamo prima bisogno di apprezzare esattamente ciò che un puntatore è .

  • Hai dimostrato una presa salda sul concetto e, come alcuni altri, potresti trovare queste illustrazioni pignolosamente semplicistiche, ma il livello di confusione evidente qui richiede tale semplicità di chiarimento.

Come molti hanno sottolineato: il termine puntatore è semplicemente un nome speciale per ciò che è semplicemente un indice , e quindi niente di più di qualsiasi altro numero .

Ciò dovrebbe già essere evidente in considerazione del fatto che tutti i computer tradizionali contemporanei sono macchine binarie che necessariamente funzionano esclusivamente con e sui numeri . Il calcolo quantistico può cambiarlo, ma è altamente improbabile e non ha raggiunto la maggiore età.

Tecnicamente, come hai notato, i puntatori sono indirizzi più accurati ; un'ovvia intuizione che introduce naturalmente l'analogia gratificante della correlazione con gli "indirizzi" delle case, o trame su una strada.

  • In un modello di memoria piatta : l'intera memoria del sistema è organizzata in un'unica sequenza lineare: tutte le case della città si trovano sulla stessa strada e ogni casa è identificata in modo univoco dal solo numero. Deliziosamente semplice.

  • In schemi segmentati : un'organizzazione gerarchica di strade numerate viene introdotta sopra quella di case numerate in modo da richiedere indirizzi compositi.

    • Alcune implementazioni sono ancora più contorte e la totalità di "strade" distinte non deve necessariamente riassumere in una sequenza contigua, ma nulla di tutto ciò cambia nulla sul sottostante.
    • Siamo necessariamente in grado di scomporre ogni collegamento gerarchico in un'organizzazione piatta. Più complessa è l'organizzazione, più cerchi dovremo saltare per farlo, ma deve essere possibile. In effetti, questo vale anche per la "modalità reale" su x86.
    • Altrimenti la mappatura dei collegamenti alle posizioni non sarebbe biiettiva , in quanto l'esecuzione affidabile - a livello di sistema - richiede che DEVE esserlo.
      • più indirizzi non devono essere associati a posizioni di memoria singolari e
      • gli indirizzi singolari non devono mai essere associati a più posizioni di memoria.

Portandoci all'ulteriore svolta che trasforma l'enigma in un groviglio così affascinante e complicato . Sopra, era opportuno suggerire che i puntatori sono indirizzi, per ragioni di semplicità e chiarezza. Certo, questo non è corretto. Un puntatore non è un indirizzo; un puntatore è un riferimento a un indirizzo , contiene un indirizzo . Come la busta sfoggia un riferimento alla casa. Contemplare questo può portare a intravedere cosa si intendesse con il suggerimento di ricorsione contenuto nel concetto. Ancora; abbiamo solo così tante parole, e parlando del indirizzi dei riferimenti agli indirizzie così, blocca presto la maggior parte dei cervelli coneccezione del codice operativo non valida . E per la maggior parte, l'intento è prontamente raccolto dal contesto, quindi torniamo in strada.

I lavoratori delle poste in questa nostra città immaginaria sono molto simili a quelli che troviamo nel mondo "reale". È probabile che nessuno subisca un ictus quando parli o chiedi di un indirizzo non valido , ma ogni ultimo si opporrà quando gli chiedi di agire su tali informazioni.

Supponiamo che ci siano solo 20 case sulla nostra strada singolare. Fingi inoltre che un'anima fuorviante o dislessica abbia indirizzato una lettera, molto importante, al numero 71. Ora, possiamo chiedere al nostro corriere Frank se esiste un tale indirizzo e riferirà semplicemente e con calma: no . Possiamo anche aspettarci che per valutare la distanza al di fuori della strada questa posizione si troverebbe se ha fatto esistono: circa 2,5 volte oltre la fine. Niente di tutto ciò gli causerà alcuna esasperazione. Tuttavia, se dovessimo chiedergli di consegnare questa lettera o di ritirare un oggetto da quel luogo, è probabile che sia piuttosto sincero riguardo al suo dispiacere e al rifiuto di conformarsi.

I puntatori sono solo indirizzi e gli indirizzi sono solo numeri.

Verificare l'output di quanto segue:

void foo( void *p ) {
   printf(“%p\t%zu\t%d\n”, p, (size_t)p, p == (size_t)p);
}

Chiamalo su tutti i puntatori che desideri, validi o meno. Si prega di non inviare i vostri risultati se non riesce sulla vostra piattaforma, o il vostro (contemporaneo) compilatore si lamenta.

Ora, poiché i puntatori sono semplicemente numeri, è inevitabilmente valido confrontarli. In un certo senso, questo è esattamente ciò che il tuo insegnante sta dimostrando. Tutte le seguenti affermazioni sono perfettamente valide e appropriate! - C, e quando compilato verrà eseguito senza problemi , anche se nessuno dei due puntatori deve essere inizializzato e i valori in essi contenuti potrebbero pertanto non essere definiti :

  • Stiamo solo calcolando result esplicitamente per motivi di chiarezza e stampandolo per forzare il compilatore a calcolare quello che altrimenti sarebbe codice ridondante e morto.
void foo( size_t *a, size_t *b ) {
   size_t result;
   result = (size_t)a;
   printf(“%zu\n”, result);
   result = a == b;
   printf(“%zu\n”, result);
   result = a < b;
   printf(“%zu\n”, result);
   result = a - b;
   printf(“%zu\n”, result);
}

Naturalmente, il programma è mal formato quando a o b non è definito (leggi: non correttamente inizializzato ) al momento del test, ma questo è assolutamente irrilevante per questa parte della nostra discussione. Questi frammenti, come anche le seguenti affermazioni, sono garantiti - dallo "standard" - per compilare ed eseguire in modo impeccabile, nonostante la validità IN di qualsiasi puntatore coinvolto.

I problemi sorgono solo quando un puntatore non valido è dereferenziato . Quando chiediamo a Frank di ritirare o consegnare all'indirizzo non valido e inesistente.

Dato qualsiasi puntatore arbitrario:

int *p;

Mentre questa affermazione deve compilare ed eseguire:

printf(“%p”, p);

... come deve questo:

size_t foo( int *p ) { return (size_t)p; }

... i due seguenti, in netto contrasto, si compileranno ancora prontamente, ma falliranno nell'esecuzione a meno che il puntatore non sia valido - con il quale qui intendiamo semplicemente che fa riferimento a un indirizzo a cui è stato concesso l'accesso alla presente applicazione :

printf(“%p”, *p);
size_t foo( int *p ) { return *p; }

Quanto è sottile il cambiamento? La distinzione sta nella differenza tra il valore del puntatore - che è l'indirizzo e il valore dei contenuti: della casa in quel numero. Nessun problema si pone fino a quando il puntatore non viene referenziato ; fino a quando si tenta di accedere all'indirizzo a cui si collega. Nel tentativo di consegnare o ritirare il pacco oltre il tratto di strada ...

Per estensione, lo stesso principio si applica necessariamente ad esempi più complessi, inclusa la necessità di cui sopra per stabilire la validità richiesta:

int* validate( int *p, int *head, int *tail ) { 
    return p >= head && p <= tail ? p : NULL; 
}

Il confronto relazionale e l'aritmetica offrono un'utilità identica al test dell'equivalenza e sono equivalentemente validi - in linea di principio. Tuttavia , ciò che i risultati di tale calcolo sarebbe significare , è una questione del tutto diversa - e precisamente il problema affrontato dalle quotazioni hai incluso.

In C, un array è un buffer contiguo, una serie lineare ininterrotta di posizioni di memoria. Confronto e aritmetica applicati a puntatori che fanno riferimento a posizioni all'interno di un tale singolare serie così sono naturalmente e ovviamente significative in relazione sia l'una con l'altra, sia a questo "array" (che è semplicemente identificato dalla base). Lo stesso vale per ogni blocco allocato tramite malloc, o sbrk. Poiché queste relazioni sono implicite , il compilatore è in grado di stabilire relazioni valide tra loro e quindi può essere sicuro che i calcoli forniranno le risposte previste.

L'esecuzione di una ginnastica simile su puntatori che fanno riferimento a blocchi o matrici distinti non offrono tale utilità intrinseca e apparente . Tanto più che qualsiasi relazione esistente in un momento può essere invalidata da una riallocazione che segue, in cui è altamente probabile che cambi, può persino essere invertita. In tali casi il compilatore non è in grado di ottenere le informazioni necessarie per stabilire la fiducia che aveva nella situazione precedente.

È , tuttavia, come il programmatore, potrebbe avere una tale conoscenza! E in alcuni casi sono obbligati a sfruttarlo.

Vi sono quindi circostanze in cui ANCHE QUESTO è interamente VALIDO e perfettamente CORRETTO.

In effetti, questo è esattamente ciò che mallocdeve fare internamente quando arriva il momento di provare a fondere i blocchi recuperati, nella stragrande maggioranza delle architetture. Lo stesso vale per l'allocatore del sistema operativo, come quello dietro sbrk; se più ovviamente , frequentemente , su entità più disparate , di più criticamente - e rilevanti anche su piattaforme dove ciòmallocpotrebbe non essere. E quanti di questi non sono scritti in C?

La validità, la sicurezza e il successo di un'azione sono inevitabilmente la conseguenza del livello di comprensione su cui è premessa e applicata.

Nelle citazioni che hai offerto, Kernighan e Ritchie stanno affrontando un problema strettamente correlato, ma comunque separato. Stanno definendo i limiti del linguaggio e spiegando come è possibile sfruttare le capacità del compilatore per proteggerti rilevando almeno costrutti potenzialmente errati. Stanno descrivendo le lunghezze in cui il meccanismo è in grado - è progettato - di andare per aiutarvi nel vostro compito di programmazione. Il compilatore è il tuo servitore, tu sei il padrone. Un maestro saggio, tuttavia, è intimamente familiare con le capacità dei suoi vari servi.

In questo contesto, un comportamento indefinito serve a indicare un potenziale pericolo e la possibilità di danno; non implicare un destino imminente, irreversibile, o la fine del mondo come la conosciamo. Significa semplicemente che noi - "intendendo il compilatore" - non siamo in grado di fare congetture su ciò che questa cosa potrebbe essere o rappresentare e per questo motivo scegliamo di lavarci le mani. Non saremo ritenuti responsabili per eventuali disavventure che potrebbero derivare dall'uso o dal cattivo uso di questa struttura .

In effetti, dice semplicemente: "Oltre questo punto, cowboy : sei da solo ..."

Il tuo professore sta cercando di dimostrarti le sfumature più sottili .

Notate quale grande cura hanno preso nell'elaborare il loro esempio; e quanto è ancora fragile . Prendendo l'indirizzo di a, in

p[0].p0 = &a;

il compilatore è costretto ad allocare l'archiviazione effettiva per la variabile, piuttosto che metterlo in un registro. Essendo una variabile automatica, tuttavia, il programmatore non ha alcun controllo su dove viene assegnato e quindi incapace di formulare congetture valide su ciò che la seguirebbe. Ecco perché a deve essere impostato uguale a zero affinché il codice funzioni come previsto.

Semplicemente cambiando questa linea:

char a = 0;

a questo:

char a = 1;  // or ANY other value than 0

fa sì che il comportamento del programma diventi indefinito . Come minimo, la prima risposta sarà ora 1; ma il problema è molto più sinistro.

Ora il codice invita al disastro.

Sebbene sia ancora perfettamente valido e persino conforme allo standard , ora è mal formato e sebbene sicuro di essere compilato, potrebbe non riuscire nell'esecuzione per vari motivi. Per ora ci sono molti problemi - nessuno dei quali il compilatore è in grado di riconoscere.

strcpyinizierà all'indirizzo di ae proseguirà oltre per consumare - e trasferire - byte dopo byte, fino a quando non incontra un valore nullo.

Il p1puntatore è stato inizializzato su un blocco di esattamente 10 byte.

  • Se acapita di trovarsi alla fine di un blocco e il processo non ha accesso a ciò che segue, la lettura successiva - di p0 [1] - genererà un segfault. Questo scenario è improbabile sull'architettura x86, ma possibile.

  • Se l'area oltre l'indirizzo di a è accessibile, non si verificherà alcun errore di lettura, ma il programma non viene comunque salvato dalla sfortuna.

  • Se un byte zero capita che si verifichi entro dieci a partire dall'indirizzo di a, si può ancora sopravvivere, perché allora strcpysi ferma e almeno noi non subirà una violazione di scrittura.

  • Se è non violata per la lettura male, ma nessun byte zero si verifica in questo arco di 10, strcpycontinuerà e tentare di scrivere oltre il blocco allocato da malloc.

    • Se quest'area non è di proprietà del processo, il segfault dovrebbe essere immediatamente attivato.

    • La situazione ancora più disastrosa - e sottile - si presenta quando il blocco seguente è di proprietà del processo, poiché quindi l'errore non può essere rilevato, nessun segnale può essere generato e quindi può "apparire" ancora "funzionante" , mentre in realtà sovrascriverà altri dati, le strutture di gestione dell'allocatore o persino il codice (in determinati ambienti operativi).

Questo è il motivo per cui i bug relativi ai puntatori possono essere così difficili da rintracciare . Immagina che queste righe siano sepolte in profondità in migliaia di righe di codice intrinsecamente correlato, che qualcun altro ha scritto e che sei diretto a scavare.

Tuttavia , il programma deve ancora essere compilato, poiché rimane perfettamente valido e conforme agli standard C.

Questo tipo di errori, nessuno standard e nessun compilatore può proteggere gli incauti. Immagino che sia esattamente ciò che intendono insegnarti.

Le persone paranoiche cercano costantemente di cambiare la natura di C per smaltire queste possibilità problematiche e quindi salvarci da noi stessi; ma questo è disonesto . Questa è la responsabilità che siamo tenuti ad accettare quando scegliamo di perseguire il potere e ottenere la libertà che ci offre un controllo più diretto e completo della macchina. I promotori e gli inseguitori della perfezione nell'esecuzione non accetteranno mai niente di meno.

La portabilità e la generalità che rappresenta è una considerazione sostanzialmente separata e tutto ciò che lo standard cerca di affrontare:

Questo documento specifica il modulo e stabilisce l'interpretazione dei programmi espressa nel linguaggio di programmazione C. Its scopo è promuovere la portabilità , l'affidabilità, la manutenibilità e l'esecuzione efficiente dei programmi in linguaggio C su una varietà di sistemi di elaborazione .

Ecco perché è perfettamente corretto tenerlo distinto dalla definizione e specifiche tecniche della lingua stessa. Contrariamente a quanto molti credono che la generalità sia antitetica a eccezionale ed esemplare .

Concludere:

  • Esaminare e manipolare gli stessi puntatori è invariabilmente valido e spesso fruttuoso . L'interpretazione dei risultati, può o non può essere significativa, ma la calamità non è mai invitata fino a quando il puntatore non viene rimosso ; fino a quando non viene effettuato un tentativo di accesso all'indirizzo collegato.

Se ciò non fosse vero, programmare come lo conosciamo - e lo adoriamo - non sarebbe stato possibile.


3
Questa risposta è purtroppo intrinsecamente non valida. Non puoi ragionare su comportamenti indefiniti. Non è necessario effettuare il confronto a livello di macchina.
Antti Haapala,

6
Ghii, in realtà no. Se si guarda all'allegato J C11 e 6.5.8, l'atto di confronto stesso è UB. La dereferenziazione è un problema separato.
paxdiablo,

6
No, UB può ancora essere dannoso anche prima che un puntatore venga negato. Un compilatore è libero di ottimizzare completamente una funzione con UB in un singolo NOP, anche se questo ovviamente cambia il comportamento visibile.
nanofarad,

2
@Ghii, l'Allegato J (il bit che ho citato) è l'elenco delle cose che sono comportamenti indefiniti , quindi non sono sicuro di come questo sostenga il tuo argomento :-) 6.5.8 chiama esplicitamente il confronto come UB. Per il tuo commento a supercat, non c'è paragone in corso quando si stampa un puntatore, quindi probabilmente hai ragione che non si bloccherà. Ma non è questo ciò che l'OP stava chiedendo. 3.4.3è anche una sezione da considerare: definisce UB come comportamento "per il quale questo standard internazionale non impone requisiti".
paxdiablo,

3
@GhiiVelte, continui a dichiarare cose che sono semplicemente sbagliate, nonostante ciò ti sia stato segnalato. Sì, lo snippet che hai pubblicato deve essere compilato ma la tua tesi secondo cui viene eseguita senza intoppi è errata. Ti suggerisco di leggere effettivamente lo standard, in particolare (in questo caso) C11 6.5.6/9, tenendo presente che la parola "deve" indica un requisito L "Quando vengono sottratti due puntatori, entrambi devono puntare a elementi dello stesso oggetto array o uno oltre l'ultimo elemento dell'oggetto array ".
paxdiablo,

-5

I puntatori sono solo numeri interi, come tutto il resto in un computer. Puoi assolutamente confrontarli con< e >produrre risultati senza causare l'arresto anomalo di un programma. Detto questo, lo standard non garantisce che tali risultati abbiano alcun significato al di fuori dei confronti di array.

Nel tuo esempio di variabili allocate nello stack, il compilatore è libero di allocare tali variabili ai registri o agli indirizzi di memoria dello stack e in qualsiasi ordine lo scelga. Confronti come <e >quindi non saranno coerenti tra compilatori o architetture. Tuttavia, ==e !=non sono così limitati, confrontare l' uguaglianza dei puntatori è un'operazione valida e utile.


2
Lo stack di parole appare esattamente zero volte nello standard C11. E un comportamento indefinito significa che tutto può succedere (incluso l'arresto anomalo del programma).
paxdiablo,

1
@paxdiablo L'ho detto?
Nickelpro,

2
Hai menzionato le variabili allocate nello stack. Non c'è stack nello standard, questo è solo un dettaglio di implementazione. Il problema più serio con questa risposta è la tesi che puoi confrontare i puntatori senza possibilità di un arresto, è solo sbagliato.
paxdiablo,

1
@nickelpro: se si desidera scrivere un codice compatibile con gli ottimizzatori in gcc e clang, è necessario saltare attraverso molti sciocchi cerchi. Entrambi gli ottimizzatori cercheranno in modo aggressivo opportunità di trarre conclusioni su quali cose accederanno ai puntatori ogni volta che c'è un modo in cui lo Standard può essere distorto per giustificarli (e anche a volte quando non lo è). Dato int x[10],y[10],*p;, se il codice valuta y[0], quindi valuta p>(x+5)e scrive *psenza modificare pnel frattempo, e infine valuta di y[0]nuovo, ...
supercat

1
nickelpro, accetta di non essere d'accordo, ma la tua risposta è ancora fondamentalmente sbagliata. Ho paragonato il tuo approccio a quello delle persone che usano (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')invece isalpha()perché quale implementazione sensata avrebbe quei personaggi discontinui? La linea di fondo è che, anche se nessuna implementazione che conosci ha un problema, dovresti codificare il più possibile lo standard se apprezzi la portabilità. Apprezzo molto l'etichetta "standard maven", grazie per quello. Posso inserire il mio CV :-)
paxdiablo il
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.