Quando i puntatori devono essere controllati per NULL in C?


18

Riepilogo :

Una funzione in C dovrebbe sempre verificare per assicurarsi che non stia dereferenziando un NULLpuntatore? In caso contrario, quando è opportuno saltare questi controlli?

Dettagli :

Ho letto alcuni libri sulle interviste di programmazione e mi chiedo quale sia il grado appropriato di convalida dell'input per gli argomenti delle funzioni in C? Ovviamente qualsiasi funzione che accetta input da un utente deve eseguire la validazione, incluso il controllo di un NULLpuntatore prima di dereferenziarlo. Ma per quanto riguarda una funzione all'interno dello stesso file che non ti aspetti di esporre attraverso la tua API?

Ad esempio, nel codice sorgente di git appare quanto segue :

static unsigned short graph_get_current_column_color(const struct git_graph *graph)
{
    if (!want_color(graph->revs->diffopt.use_color))
        return column_colors_max;
    return graph->default_column_color;
}

Se *graphè NULLquindi un puntatore nullo verrà dereferenziato, probabilmente arrestando in modo anomalo il programma, ma probabilmente comportando qualche altro comportamento imprevedibile. D'altra parte la funzione è statice quindi forse il programmatore ha già convalidato l'ingresso. Non lo so, l'ho selezionato a caso perché era un breve esempio in un programma applicativo scritto in C. Ho visto molti altri posti in cui i puntatori vengono utilizzati senza controllare NULL. La mia domanda non è generalmente specifica per questo segmento di codice.

Ho visto una domanda simile posta nel contesto della gestione delle eccezioni . Tuttavia, per un linguaggio non sicuro come C o C ++ non esiste una propagazione automatica degli errori delle eccezioni non gestite.

D'altra parte ho visto un sacco di codice nei progetti open source (come nell'esempio sopra) che non fa alcun controllo dei puntatori prima di usarli. Mi chiedo se qualcuno abbia delle idee sulle linee guida per quando mettere i controlli in una funzione anziché supporre che la funzione sia stata chiamata con argomenti corretti.

Sono interessato a questa domanda in generale per la scrittura del codice di produzione. Ma sono anche interessato al contesto delle interviste di programmazione. Ad esempio, molti libri di testo di algoritmi (come CLR) tendono a presentare gli algoritmi in pseudocodice senza alcun controllo degli errori. Tuttavia, sebbene ciò sia utile per comprendere il nucleo di un algoritmo, non è ovviamente una buona pratica di programmazione. Quindi non vorrei dire a un intervistatore che stavo saltando il controllo degli errori per semplificare i miei esempi di codice (come potrebbe fare un libro di testo). Ma anche io non vorrei sembrare che produca codice inefficiente con un controllo degli errori eccessivo. Ad esempio, graph_get_current_column_coloravrebbe potuto essere modificato per verificare la presenza *graphdi null, ma non è chiaro cosa farebbe se *graphfosse null, a parte questo non dovrebbe dereferenziarlo.


7
Se stai scrivendo una funzione per un'API in cui i chiamanti non dovrebbero comprendere le viscere, questo è uno di quei posti in cui la documentazione è importante. Se si documenta che un argomento deve essere un puntatore valido, non NULL, il controllo diventa responsabilità del chiamante.
Blrfl,


Con il senno di poi dell'anno 2017, tenendo presente la domanda e la maggior parte delle risposte sono state scritte nel 2013, alcune delle risposte affrontano il problema dei comportamenti indefiniti che viaggiano nel tempo a causa dell'ottimizzazione dei compilatori?
rwong

Nel caso di chiamate API in attesa di argomenti puntatore validi, mi chiedo quale sia il valore del test solo per NULL? Qualsiasi puntatore non valido che viene negato sarebbe altrettanto cattivo di NULL e segfault lo stesso.
PaulHK,

Risposte:


15

Puntatori null non validi possono essere causati da errori del programmatore o da errori di runtime. Gli errori di runtime sono qualcosa che un programmatore non può risolvere, come un mallocerrore dovuto alla memoria insufficiente o alla rete che fa cadere un pacchetto o che l'utente inserisce qualcosa di stupido. Gli errori del programmatore sono causati da un programmatore che utilizza la funzione in modo errato.

La regola generale che ho visto è che gli errori di runtime dovrebbero sempre essere controllati, ma gli errori del programmatore non devono essere controllati ogni volta. Diciamo che un programmatore idiota ha chiamato direttamente graph_get_current_column_color(0). Segfault la prima volta che viene chiamato, ma una volta risolto, la correzione viene compilata in modo permanente. Non è necessario controllare ogni volta che viene eseguito.

A volte, specialmente nelle librerie di terze parti, vedrai un assertcontrollo degli errori del programmatore anziché ifun'istruzione. Ciò consente di compilare i controlli durante lo sviluppo e di lasciarli fuori nel codice di produzione. Ho anche occasionalmente visto controlli gratuiti in cui la fonte del potenziale errore del programmatore è molto lontana dal sintomo.

Ovviamente, puoi sempre trovare qualcuno più pedante, ma la maggior parte dei programmatori C che conosco preferisce un codice meno ingombrante rispetto a un codice che è leggermente più sicuro. E "più sicuro" è un termine soggettivo. Un palese segfault durante lo sviluppo è preferibile a un sottile errore di corruzione nel campo.


La domanda è in qualche modo soggettiva, ma questa sembrava la risposta migliore per ora. Grazie a tutti coloro che hanno espresso la loro opinione su questa domanda.
Gabriel Southern,

1
In iOS, malloc non restituirà mai NULL. Se non trova memoria, chiederà prima all'applicazione di rilasciare memoria, quindi chiederà al sistema operativo (che chiederà ad altre app di liberare memoria e possibilmente ucciderle), e se non c'è ancora memoria ucciderà la tua app . Nessun controllo necessario.
gnasher729,

11

Kernighan & Plauger, in "Strumenti software", scrissero che avrebbero controllato tutto e, per condizioni che credevano che in realtà non sarebbero mai potute accadere, avrebbero interrotto con un messaggio di errore "Non può accadere".

Riferiscono di essere stati rapidamente umiliati dal numero di volte in cui hanno visto "Non può succedere" uscire sui loro terminali.

Dovresti SEMPRE controllare il puntatore per NULL prima di (tentare di) dereferenziarlo. SEMPRE . La quantità di codice duplicato verificando la presenza di NULL che non si verificano e il processore che "spreca", sarà più che pagato dal numero di arresti anomali che non è necessario eseguire il debug da nient'altro che un dump di arresto anomalo: se sei così fortunato.

Se il puntatore è invariante all'interno di un ciclo, è sufficiente controllarlo all'esterno del ciclo, ma è necessario "copiarlo" in una variabile locale limitata dall'ambito, per l'uso da parte del ciclo, che aggiunge le decorazioni const appropriate. In questo caso, DEVI assicurarti che ogni funzione chiamata dal corpo del loop includa le decorazioni const necessarie sui prototipi, ALL THE WAY DOWN. Se non lo fai, o non può (perché, ad esempio, il pacchetto un fornitore o un collega ostinata), quindi è necessario verificare la presenza di NULL ogni volta che potrebbe essere modificato , perché è vero che COL Murphy era un inguaribile ottimista, qualcuno È in corso per zappare quando non stai guardando.

Se ci si trova all'interno di una funzione e si suppone che il puntatore non sia NULL, è necessario verificarlo.

Se lo stai ricevendo da una funzione e si suppone che non sia NULL in uscita, dovresti verificarlo. malloc () è particolarmente noto per questo. (Nortel Networks, ora defunto, aveva uno standard di codifica scritto duro e veloce su questo. Ho dovuto eseguire il debug di un arresto in un punto, che ho rintracciato in malloc () restituendo un puntatore NULL e il codificatore idiota non si preoccupa di controllare prima di scrivergli, perché sapeva che aveva un sacco di memoria ... Ho detto alcune cose molto brutte quando l'ho finalmente trovato.)


8
Se sei in una funzione che richiede un puntatore non NULL, ma controlli comunque ed è NULL ... e poi?
detenere il

1
@detly o interrompi ciò che stai facendo e restituisci un codice di errore, oppure attiva un'asserzione
James

1
@James - non ci ho pensato assert, certo. Non mi piace l'idea del codice di errore se stai parlando di modificare il codice esistente per includere i NULLcontrolli.
detenere il

10
@detly non andrai molto lontano come sviluppatore C se non ti piacciono i codici di errore
James

5
@ JohnR.Strohm - questa è C, sono affermazioni o niente: P
detly

5

Puoi saltare il segno di spunta quando riesci a convincerti in qualche modo che il puntatore non può essere nullo.

Di solito, i controlli puntatore null sono implementati nel codice in cui è previsto che null appaia come indicatore che un oggetto non è attualmente disponibile. Null viene utilizzato come valore sentinella, ad esempio per terminare elenchi collegati o persino matrici di puntatori. È necessario che il argvvettore di stringhe passato mainsia terminato con null da un puntatore, analogamente a come una stringa è terminata da un carattere null: argv[argc]è un puntatore null e puoi fare affidamento su questo quando analizzi la riga di comando.

while (*argv) {
   /* process argument string *argv */
   argv++; /* increment to next one */
}

Quindi, le situazioni per il controllo di null sono quelle in cui a è un valore atteso. I controlli null implementano il significato del puntatore null, come l'interruzione della ricerca di un elenco collegato. Impediscono al codice di dereferenziare il puntatore.

In una situazione in cui un valore di puntatore nullo non è previsto dalla progettazione, non ha senso verificarlo. Se si presenta un valore di puntatore non valido, molto probabilmente apparirà non nullo, che non può essere distinto dai valori validi in alcun modo portatile. Ad esempio, un valore di puntatore ottenuto dalla lettura di un archivio non inizializzato interpretato come un tipo di puntatore, un puntatore ottenuto tramite una conversione ombreggiata o un puntatore incrementato fuori dai limiti.

Informazioni su un tipo di dati come graph *: questo potrebbe essere progettato in modo che un valore null sia un grafico valido: qualcosa senza bordi e senza nodi. In questo caso, tutte le funzioni che prendono un graph *puntatore dovranno occuparsi di quel valore, poiché è un valore di dominio corretto nella rappresentazione dei grafici. D'altra parte, a graph *potrebbe essere un puntatore a un oggetto simile a un contenitore che non è mai nullo se manteniamo un grafico; un puntatore nullo potrebbe quindi dirci che "l'oggetto grafico non è presente; non l'abbiamo ancora allocato, o l'abbiamo liberato; o questo attualmente non ha un grafico associato". Quest'ultimo uso di puntatori è un booleano / satellite combinato: il puntatore essendo non nullo indica "Ho questo oggetto gemello" e fornisce quell'oggetto.

Potremmo impostare un puntatore su null anche se non stiamo liberando un oggetto, semplicemente per dissociare un oggetto da un altro:

tty_driver->tty = NULL; /* detach low level driver from the tty device */

L'argomento più convincente che conosco è che un puntatore non può essere nullo in un determinato punto è racchiudere quel punto in "if (ptr! = NULL) {" e un corrispondente "}". Oltre a ciò, sei nel territorio di verifica formale.
John R. Strohm,

4

Vorrei aggiungere un'altra voce alla fuga.

Come molte altre risposte, dico io: non preoccuparti di controllare a questo punto; è responsabilità del chiamante. Ma ho una base su cui basarmi piuttosto che una semplice opportunità (e arroganza di programmazione in C).

Cerco di seguire il principio di Donald Knuth nel rendere i programmi il più fragili possibile. Se qualcosa va storto, averlo in crash grande , e fa riferimento a un puntatore nullo di solito è un buon modo per farlo. L'idea generale è un arresto anomalo o un ciclo infinito è molto meglio della creazione di dati errati. E attira l'attenzione dei programmatori!

Ma fare riferimento a puntatori null (specialmente per strutture di dati di grandi dimensioni) non causa sempre un arresto anomalo. Sospiro. È vero. Ed è qui che rientrano gli Assert. Sono semplici, possono arrestare immediatamente il programma (che risponde alla domanda "Cosa dovrebbe fare il metodo se rileva un valore nullo?") E può essere attivato / disattivato per varie situazioni (consiglio NON spegnerli, poiché è meglio che i clienti abbiano un arresto anomalo e visualizzino un messaggio criptico piuttosto che avere dati errati).

Sono i miei due centesimi.


1

In genere controllo solo quando viene assegnato un puntatore, che in genere è l'unica volta in cui posso effettivamente fare qualcosa al riguardo e possibilmente recuperare se non è valido.

Se, ad esempio, ottengo un handle per una finestra, verificherò che sia null giusto e poi e lì, e farò qualcosa per la condizione null, ma non verificherò che sia null ogni volta Uso il puntatore, in ogni funzione a cui è passato il puntatore, altrimenti avrei montagne di duplicati di codice di gestione degli errori.

Funzioni come graph_get_current_column_colorprobabilmente non sono assolutamente in grado di fare nulla di utile alla tua situazione se riscontra un puntatore errato, quindi lascerei controllare NULL per i suoi chiamanti.


1

Direi che dipende da quanto segue:

  1. L'utilizzo della CPU è fondamentale? Ogni controllo per NULL richiede un certo periodo di tempo.
  2. Quali sono le probabilità che il puntatore sia NULL? È stato appena usato in una funzione precedente. Il valore del puntatore potrebbe essere stato modificato.
  3. Il sistema è preventivo? Significato potrebbe accadere un cambio di attività e cambiare il valore? Potrebbe un ISR entrare e cambiare il valore?
  4. Quanto è strettamente accoppiato il codice?
  5. Esiste una sorta di meccanismo automatico che controlla automaticamente i puntatori NULL?

Il puntatore di utilizzo / probabilità della CPU è NULL Ogni volta che si controlla NULL, ci vuole tempo. Per questo motivo provo a limitare i miei controlli a dove il puntatore avrebbe potuto cambiare il suo valore.

Sistema preventivo Se il codice è in esecuzione e un'altra attività potrebbe interromperlo e potenzialmente modificare il valore che un controllo sarebbe utile.

Moduli strettamente accoppiati Se il sistema è strettamente accoppiato, avrebbe senso avere più controlli. Quello che intendo con questo è se ci sono strutture di dati condivise tra più moduli un modulo potrebbe cambiare qualcosa da sotto un altro modulo. In queste situazioni ha senso controllare più spesso.

Controlli automatici / Assistenza hardware L'ultima cosa da prendere in considerazione è se l'hardware su cui si sta eseguendo ha una sorta di meccanismo che può verificare la presenza di NULL. In particolare mi riferisco al rilevamento degli errori di pagina. Se il sistema ha il rilevamento degli errori di pagina, la CPU stessa può verificare gli accessi NULL. Personalmente trovo che questo sia il miglior meccanismo poiché funziona sempre e non fa affidamento sul programmatore per effettuare controlli espliciti. Ha anche il vantaggio di un sovraccarico praticamente pari a zero. Se questo è disponibile, lo consiglio, il debug è un po 'più difficile ma non eccessivamente.

Per verificare se è disponibile, creare un programma con un puntatore. Impostare il puntatore su 0 e quindi provare a leggerlo / scriverlo.


Non so se classificherei un segfault come eseguendo un controllo NULL automatico. Concordo sul fatto che avere una protezione della memoria della CPU sia di aiuto in modo che un processo non possa arrecare danni al resto del sistema ma non lo definirei protezione automatica.
Gabriel Southern,

1

A mio avviso, la convalida degli input (pre / post-condizioni, ad es.) È una buona cosa per rilevare errori di programmazione, ma solo se si traduce in errori rumorosi e odiosi, di tipo show che non possono essere ignorati. assertin genere ha questo effetto.

Tutto ciò che non è in grado di farlo può trasformarsi in un incubo senza squadre molto attentamente coordinate. E, naturalmente, idealmente tutti i team sono coordinati con molta attenzione e unificati secondo standard rigorosi, ma la maggior parte degli ambienti in cui ho lavorato è stata molto meno.

Ad esempio, ho lavorato con alcuni colleghi che credevano che si dovrebbe verificare religiosamente la presenza di puntatori null, quindi hanno cosparso un sacco di codice come questo:

void vertex_move(Vertex* v)
{
     if (!v)
          return;
     ...
}

... e talvolta proprio così senza nemmeno restituire / impostare un codice di errore. E questo era in una base di codice che aveva diversi decenni con molti plugin di terze parti acquisiti. Era anche una base di codice afflitta da molti bug e spesso bug che erano molto difficili da rintracciare fino alle cause alla radice poiché avevano la tendenza a bloccarsi in siti molto lontani dalla fonte immediata del problema.

E questa pratica è stata una delle ragioni per cui. È una violazione di una pre-condizione stabilita della move_vertexfunzione precedente passare un vertice nullo ad essa, eppure tale funzione l'ha accettata silenziosamente e non ha fatto nulla in risposta. Quindi quello che tendeva a succedere era che un plugin poteva avere un errore del programmatore che lo faceva passare null a detta funzione, solo per non rilevarlo, solo per fare molte cose in seguito, e alla fine il sistema avrebbe iniziato a sfaldarsi o andare in crash.

Ma il vero problema qui era l'incapacità di rilevare facilmente questo problema. Quindi una volta ho provato a vedere cosa sarebbe successo se avessi trasformato il codice analogico sopra in un assert, in questo modo:

void vertex_move(Vertex* v)
{
     assert(v && "Vertex should never be null!");
     ...
}

... e con mio orrore, ho scoperto che l'asserzione falliva a destra e a sinistra anche all'avvio dell'applicazione. Dopo aver risolto i primi siti di chiamata, ho fatto alcune cose in più e poi ho ricevuto un numero maggiore di errori di asserzione. Ho continuato fino a quando non ho modificato così tanto il codice che ho finito per ripristinare le mie modifiche perché erano diventate troppo invasive e mantenevano a malincuore quel controllo del puntatore null, documentando invece che la funzione consente di accettare un vertice null.

Ma questo è il pericolo, anche se nel peggiore dei casi, di non riuscire a rilevare facilmente le violazioni delle condizioni pre / post. È quindi possibile, nel corso degli anni, accumulare silenziosamente un carico di codice di codice che viola tali condizioni pre / post mentre si vola sotto il radar dei test. A mio avviso, un tale controllo del puntatore nullo al di fuori di un fallimento palese e odioso dell'asserzione può effettivamente fare molto, molto più danno che bene.

Per quanto riguarda la domanda essenziale su quando dovresti controllare i puntatori null, credo di affermare liberamente se è progettato per rilevare un errore del programmatore e non lasciare che taci e sia difficile da rilevare. Se non si tratta di un errore di programmazione e di qualcosa al di fuori del controllo del programmatore, come un errore di memoria insufficiente, ha senso verificare la presenza di null e utilizzare la gestione degli errori. Oltre a ciò si tratta di una domanda di progettazione e basata su quelle che le tue funzioni considerano valide condizioni pre / post.


0

Una pratica consiste nell'eseguire sempre il controllo null a meno che non sia già stato verificato; quindi se l'input viene passato dalla funzione A () a B () e A () ha già convalidato il puntatore e si è certi che B () non sia chiamato da nessun'altra parte, allora B () può fidarsi di A () per avere disinfettato i dati.


1
... fino a quando tra 6 mesi arriva qualcuno e aggiunge altro codice che chiama B () (probabilmente supponendo che chiunque abbia scritto B () abbia sicuramente verificato la presenza di NULL). Allora sei fregato, vero? Regola di base: se esiste una condizione non valida per l'input in una funzione, verificarla perché l'input è al di fuori del controllo della funzione.
Maximus Minimus

@ mh01 Se stai semplicemente distruggendo il codice casuale (es. facendo assunzioni e non leggendo la documentazione), non penso che ulteriori NULLcontrolli faranno molto. Pensaci: ora B()controlla NULLe ... cosa fa? Ritorno -1? Se il chiamante non verifica NULL, che fiducia puoi avere per gestire il -1caso del valore di ritorno?
detenere il

1
Questa è la responsabilità dei chiamanti. Ti occupi della tua responsabilità, che include non fidarti di input arbitrari / inconoscibili / potenzialmente non verificati che ti vengono dati. Altrimenti sei nella città dei poliziotti. Se il chiamante non verifica, il chiamante ha sbagliato; hai controllato, il tuo culo è coperto, puoi dire a chiunque abbia scritto al chiamante che almeno hai fatto le cose nel modo giusto.
Maximus Minimus
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.