Quando dovrebbero essere controllati i puntatori per NULL in C?

18

Riepilogo :

Se una funzione in C verifica sempre che non stia differenziando un puntatore NULL ? 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 richiede l'input da un utente deve eseguire la convalida, incluso il controllo di un puntatore NULL prima di dereferenziarlo. Ma che dire nel caso di una funzione all'interno dello stesso file che non ti aspetti di esporre tramite la tua API?

Ad esempio il seguente appare nel codice sorgente di git:

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 è NULL allora un puntatore nullo sarà dereferenziato, probabilmente causando il crash del programma, ma probabilmente con qualche altro comportamento imprevedibile. D'altra parte la funzione è static e quindi forse il programmatore ha già validato l'input. Non lo so, l'ho appena selezionato a caso perché era un breve esempio in un programma applicativo scritto in C. Ho visto molti altri posti dove i puntatori sono usati senza verificare NULL. La mia domanda è generale non specifica per questo segmento di codice.

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

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

Sono interessato a questa domanda in generale per la scrittura di codice di produzione. Ma sono anche interessato al contesto delle interviste di programmazione. Ad esempio, molti libri di testo sugli algoritmi (come CLR) tendono a presentare gli algoritmi in pseudocodice senza alcun controllo degli errori. Tuttavia, mentre questo è utile per comprendere il nucleo di un algoritmo, ovviamente non è 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 essere un libro di testo). Ma anche io non vorrei apparire per produrre codice inefficiente con eccessivo controllo degli errori. Ad esempio il graph_get_current_column_color potrebbe essere stato modificato per verificare *graph per null ma non è chiaro cosa farebbe se *graph fosse nullo, diverso da quello non dovrebbe dereferenziarlo.

    
posta Gabriel Southern 05.02.2013 - 23:55
fonte

8 risposte

15

I puntatori nulli non validi possono essere causati dall'errore del programmatore o dall'errore di runtime. Gli errori di runtime sono qualcosa che un programmatore non può correggere, come un fallimento di malloc a causa della poca memoria o del fatto che la rete lasci cadere un pacchetto o che l'utente inserisca 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 qualche programmatore idiota chiama direttamente graph_get_current_column_color(0) . Si 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, viene visualizzato un assert per verificare gli errori del programmatore anziché un'istruzione if . Ciò consente di compilare i controlli durante lo sviluppo e di lasciarli nel codice di produzione. Di tanto in tanto ho anche visto dei 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 preferiscono un codice meno ingombrante su un codice che è marginalmente più sicuro. E "più sicuro" è un termine soggettivo. Un palese segfault durante lo sviluppo è preferibile a un sottile errore di corruzione nel campo.

    
risposta data 06.02.2013 - 00:48
fonte
11

Kernighan & Plauger, in "Software Tools", ha scritto che avrebbero controllato tutto, e, per le condizioni in cui credevano che in realtà non sarebbero mai accadute, avrebbero abortito con un messaggio di errore "Can not happen".

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

Devi SEMPRE controllare il puntatore per NULL prima che tu tenti di dereferenziarlo. sempre . La quantità di codice che si duplica controllando i valori NULL che non si verificano e il processore esegue il ciclo "sprecato" sarà più che compensato dal numero di arresti anomali che non è necessario eseguire il debug da nient'altro che un crash dump - se sei fortunato.

Se il puntatore è invariato all'interno di un ciclo, è sufficiente controllarlo al di fuori del ciclo, ma dovresti quindi "copiarlo" in una variabile locale limitata all'ambito, per l'uso del ciclo, che aggiunge le decorazioni cost appropriate. In questo caso, DEVE garantire che ogni funzione chiamata dal corpo del loop includa le necessarie decorazioni const sui prototipi, ALL THE WAY DOWN. Se non lo fai, o non puoi (per es. Un pacco venditore o un ostinato collega), allora devi controllarlo per NULL OGNI VOLTA CHE POTREBBE ESSERE MODIFICATO , perché sicuramente come COL Murphy era un ottimista incurabile, qualcuno IS sta andando a zapparlo quando non stai guardando.

Se sei all'interno di una funzione e il puntatore dovrebbe essere non NULL in arrivo, dovresti verificarlo.

Se lo si riceve da una funzione e si suppone che non sia NULL in uscita, è necessario verificarlo. malloc () è particolarmente noto per questo. (Nortel Networks, ora defunto, aveva uno standard di codifica scritto e veloce a questo proposito. Ho dovuto eseguire il debug di un crash a un certo punto, risalendo a malloc () restituendo un puntatore NULL e il codificatore idiota che non si preoccupava di controllare prima che ci scrivesse, perché sapeva solo che aveva un sacco di memoria ... Ho detto alcune cose molto brutte quando finalmente l'ho trovato.)

    
risposta data 06.02.2013 - 01:09
fonte
5

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

Solitamente, i controlli puntatori nulli sono implementati nel codice in cui è previsto che nullo appaia come un indicatore che un oggetto non è al momento disponibile. Null viene utilizzato come valore sentinella, ad esempio per terminare elenchi collegati o anche matrici di puntatori. Il vettore argv di stringhe passate in main deve essere terminato con null da un puntatore, analogamente al modo in cui una stringa viene terminata da un carattere null: argv[argc] è un puntatore nullo, e puoi fare affidamento su questo quando analizzando 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 previsto. I controlli null implementano il significato del puntatore nullo, ad esempio l'interruzione della ricerca di un elenco collegato. Impediscono al codice di dereferenziare il puntatore.

In una situazione in cui un valore del puntatore nullo non è previsto dalla progettazione, non c'è motivo di verificarlo. Se si verifica un valore di puntatore non valido, molto probabilmente apparirà non nullo, che non può essere distinto da valori validi in alcun modo portatile. Ad esempio, un valore del puntatore ottenuto dalla lettura della memoria non inizializzata interpretata come un tipo di puntatore, un puntatore ottenuto tramite una conversione ombreggiata o un puntatore incrementato oltre i limiti.

Informazioni su un tipo di dati come graph * : questo potrebbe essere progettato in modo che un valore nullo sia un grafico valido: qualcosa senza bordi e senza nodi. In questo caso, tutte le funzioni che prendono un puntatore graph * dovranno occuparsi di quel valore, poiché è un valore di dominio corretto nella rappresentazione di grafici. D'altra parte, un graph * potrebbe essere un puntatore a un oggetto simile a un contenitore che non è mai nullo se teniamo un grafico; un puntatore nullo potrebbe quindi dirci che "l'oggetto grafico non è presente, non lo abbiamo ancora assegnato, o lo abbiamo liberato, o questo al momento non ha un grafico associato". Quest'ultimo uso di puntatori è un booleano / satellite combinato: il puntatore non-null indica "Ho questo oggetto sorella", 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 */
    
risposta data 05.02.2013 - 23:59
fonte
4

Permettimi di aggiungere un'altra voce alla fuga.

Come molte altre risposte, dico - non preoccuparti di controllare a questo punto; è responsabilità del chiamante. Ma io ho una base su cui basarci piuttosto che su semplici opportunità (e sulla programmazione di arroganza).

Cerco di seguire il principio di Donald Knuth di rendere i programmi il più fragili possibile. Se qualcosa va storto, si blocca grande e fare riferimento a un puntatore nullo di solito è un buon modo per farlo. L'idea generale è un crash o un loop infinito è lontano migliore rispetto alla creazione di dati errati. E attira l'attenzione dei programmatori!

Ma fare riferimento ai puntatori nulli (specialmente per strutture di dati di grandi dimensioni) non causa sempre un arresto anomalo. Sospiro. È vero. Ed è qui che asseriscono gli Assert. Sono semplici, possono bloccare istantaneamente il tuo programma (che risponde alla domanda, "Cosa dovrebbe fare il metodo se incontra un null?"), E può essere attivato / disattivato per varie situazioni (io raccomando NON spegnendoli, poiché è meglio per i clienti avere un arresto anomalo e visualizzare un messaggio criptico piuttosto che avere dati non validi).

Sono i miei due centesimi.

    
risposta data 06.02.2013 - 05:21
fonte
1

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

Se ottengo un handle per una finestra, ad esempio, controllerò che sia nullo giusto e poi e lì, e fare qualcosa per la condizione nulla, ma non ho intenzione di verificare se è null ogni e ogni volta che uso il puntatore, in ogni funzione viene passato il puntatore, altrimenti avrei montagne di codice di gestione degli errori duplicato.

Funzioni come graph_get_current_column_color probabilmente non sono in grado di fare nulla di utile alla tua situazione se incontra un puntatore non valido, quindi lascerei il controllo di NULL ai suoi chiamanti.

    
risposta data 06.02.2013 - 00:32
fonte
1

Direi che dipende da quanto segue:

  1. L'utilizzo della CPU è critico? Ogni controllo per NULL richiede un po 'di tempo.
  2. Quali sono le probabilità che il puntatore sia NULL? È stato appena usato in una funzione precedente. Potrebbe essere stato modificato il valore del puntatore.
  3. Il sistema è preventivo? Il significato potrebbe cambiare un compito e cambiare il valore? Potrebbe venire un ISR e modificare il valore?
  4. Quanto è strettamente il codice?
  5. Esiste una sorta di meccanismo automatico che controllerà automaticamente i puntatori NULL?

Utilizzo della CPU / Puntatore di quote è NULL Ogni volta che controlli NULL ci vuole tempo. Per questo motivo provo a limitare i miei controlli a dove il puntatore avrebbe potuto avere il suo valore modificato.

Sistema preventivo Se il tuo codice è in esecuzione e un'altra attività potrebbe interromperla e potenzialmente cambiare il valore che sarebbe utile avere un controllo.

Moduli strettamente accoppiati Se il sistema è strettamente accoppiato allora avrebbe senso avere più controlli. Ciò che intendo è che se ci sono strutture 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 tenere in considerazione è se l'hardware su cui si sta eseguendo ha una sorta di meccanismo che può controllare NULL. Specificamente mi riferisco al rilevamento dei guasti di pagina. Se il sistema ha il rilevamento degli errori di pagina, la CPU stessa può controllare gli accessi NULL. Personalmente trovo che questo sia il miglior meccanismo dal momento che funziona sempre e non si affida al programmatore per effettuare controlli espliciti. Ha anche il vantaggio di praticamente zero spese generali. Se questo è disponibile lo raccomando, il debug è un po 'più difficile ma non eccessivamente.

Per verificare se è disponibile creare un programma con un puntatore. Imposta il puntatore su 0 e prova a leggere / scrivere.

    
risposta data 06.02.2013 - 04:32
fonte
1

A mio parere, la validazione degli input (pre / post-condizioni, ad esempio) è una buona cosa per rilevare errori di programmazione, ma solo se si traduce in errori fastidiosi e fastidiosi di tipo che non possono essere ignorati. assert ha in genere questo effetto.

Tutto ciò che non lo è può trasformarsi in un incubo senza squadre molto attentamente coordinate. E, naturalmente, idealmente tutte le squadre sono molto attentamente coordinate e unificate in base a standard severi, ma la maggior parte degli ambienti in cui ho lavorato non è andata molto bene.

Proprio come un esempio, ho lavorato con alcuni colleghi che credevano che bisognasse controllare religiosamente la presenza di puntatori nulli, quindi hanno sparpagliato un sacco di codice come questo:

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

... ea volte 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 a cause di root poiché avevano la tendenza a bloccarsi in siti lontani dalla fonte immediata del problema.

E questa pratica è stata una delle ragioni per cui. È una violazione di una pre-condizione stabilita della funzione move_vertex di cui sopra per passare ad un vertice nullo, tuttavia una tale funzione l'ha accettata silenziosamente e non ha fatto nulla in risposta. Quindi, quello che tendeva ad accadere era che un plugin poteva avere un errore del programmatore che lo faceva passare nullo a quella funzione, solo per non rilevarlo, solo per fare molte cose in seguito, e alla fine il sistema iniziava a scoppiare o a schiantarsi.

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

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

... e con mio orrore, ho trovato che l'asserzione falliva a destra e sinistra anche all'avvio dell'applicazione. Dopo aver sistemato i primi siti di chiamate, ho fatto altre cose e poi ottenuto un maggior carico di asserzioni. Ho continuato ad andare avanti fino a quando non ho modificato così tanto codice che ho finito per ripristinare i miei cambiamenti perché erano diventati troppo invadenti e malvolentieri hanno mantenuto quel controllo del puntatore nullo, documentando invece che la funzione consente di accettare un vertice nullo.

Ma questo è il pericolo, anche se nel peggiore dei casi, di non riuscire a rendere facilmente rilevabili le violazioni delle pre / post-condizioni. Puoi quindi, nel corso degli anni, accumulare silenziosamente un carico di codice che viola tali pre / post-condizioni mentre voli sotto il radar dei test. A mio avviso, tale puntatore nullo controlla al di fuori di un evidente e odioso errore di asserzione in realtà può fare molto, molto più male che bene.

Per quanto riguarda la questione essenziale su quando dovresti controllare i puntatori nulli, credo di affermare liberamente se è progettato per rilevare un errore del programmatore, e non lasciarlo andare in silenzio e difficile da rilevare. Se non si tratta di un errore di programmazione e qualcosa che va oltre il controllo del programmatore, come un errore di memoria insufficiente, allora ha senso verificare il null e utilizzare la gestione degli errori. Oltre a ciò, si tratta di una domanda di progettazione e basata su ciò che le vostre funzioni considerano condizioni pre / post valide.

    
risposta data 15.01.2018 - 06:29
fonte
0

Una pratica è quella di eseguire sempre il controllo null se non lo hai già controllato; quindi se l'input viene passato dalla funzione A () a B (), e A () ha già convalidato il puntatore e sei sicuro che B () non è chiamato da nessun'altra parte, quindi B () può fidarsi di A () per aver disinfettato i dati.

    
risposta data 06.02.2013 - 00:35
fonte

Leggi altre domande sui tag