È possibile inizializzare un puntatore C a NULL?


90

Ho scritto cose come

char *x=NULL;

supponendo che

 char *x=2;

creerebbe un charpuntatore all'indirizzo 2.

Ma, in The GNU C Programming Tutorial si dice che int *my_int_ptr = 2;memorizza il valore intero 2in qualsiasi indirizzo casuale si trovi my_int_ptrquando viene allocato.

Ciò sembrerebbe implicare che il mio char *x=NULLassegni qualunque sia il valore di NULLcast ad a chara un indirizzo casuale in memoria.

Mentre

#include <stdlib.h>
#include <stdio.h>

int main()
{
    char *x=NULL;

    if (x==NULL)
        printf("is NULL\n");

    return EXIT_SUCCESS;
}

infatti, stampa

è zero

quando lo compilo e lo eseguo, sono preoccupato di fare affidamento su un comportamento indefinito, o almeno sotto-specificato, e di dover scrivere

char *x;
x=NULL;

anziché.


72
C'è una differenza molto confusa tra cosa int *x = whatever;fa e cosa int *x; *x = whatever;fa. int *x = whatever;effettivamente si comporta come int *x; x = whatever;, no *x = whatever;.
user2357112 supporta Monica

78
Questo tutorial sembra aver sbagliato questa distinzione confusa.
user2357112 supporta Monica

51
Tanti tutorial di merda sul web! Smetti di leggere immediatamente. Abbiamo davvero bisogno di una lista nera SO dove possiamo vergognare pubblicamente libri schifosi ...
Lundin

9
@MM Il che non lo rende meno schifoso nell'anno 2017. Data l'evoluzione dei compilatori e dei computer dagli anni '80, è fondamentalmente la stessa cosa come se fossi un medico e leggessi libri di medicina scritti nel XVIII secolo.
Lundin

13
Non penso che questo tutorial si qualifichi come " The GNU C Programming Tutorial" ...
marcelm

Risposte:


114

È possibile inizializzare un puntatore C a NULL?

TL; DR Sì, molto.


L' affermazione effettiva fatta sulla guida si legge come

D'altra parte, se si utilizza solo l'assegnazione iniziale singola int *my_int_ptr = 2;, il programma tenterà di riempire il contenuto della posizione di memoria indicata da my_int_ptrcon il valore 2. Poiché my_int_ptrè pieno di spazzatura, può essere qualsiasi indirizzo. [...]

Ebbene, essi sono sbagliato, hai ragione.

Per la dichiarazione, ( ignorando, per ora, il fatto che il puntatore alla conversione di numeri interi è un comportamento definito dall'implementazione )

int * my_int_ptr = 2;

my_int_ptrè una variabile (di tipo puntatore a int), ha un indirizzo proprio (tipo: indirizzo del puntatore a numero intero), stai memorizzando un valore di 2in quell'indirizzo .

Ora, my_int_ptressendo un tipo di puntatore, possiamo dire, punta al valore di "tipo" nella posizione di memoria indicata dal valore contenuto my_int_ptr. Quindi, stai essenzialmente assegnando il valore della variabile del puntatore, non il valore della posizione di memoria a cui punta il puntatore.

Quindi, per concludere

 char *x=NULL;

inizializza la variabile del puntatore xsu NULL, non il valore nell'indirizzo di memoria a cui punta il puntatore .

Questo è lo stesso di

 char *x;
 x = NULL;    

Espansione:

Ora, essendo rigorosamente conforme, un'affermazione come

 int * my_int_ptr = 2;

è illegale, in quanto implica la violazione dei vincoli. Per essere chiari,

  • my_int_ptr è una variabile puntatore, tipo int *
  • una costante intera, 2ha tipo int, per definizione.

e non sono tipi "compatibili", quindi questa inizializzazione non è valida perché viola le regole dell'assegnazione semplice, menzionate nel capitolo §6.5.16.1 / P1, descritte nella risposta di Lundin .

Nel caso qualcuno sia interessato a come l'inizializzazione è collegata a semplici vincoli di assegnazione, citando C11, capitolo §6.7.9, P11

L'inizializzatore per uno scalare deve essere una singola espressione, facoltativamente racchiusa tra parentesi graffe. Il valore iniziale dell'oggetto è quello dell'espressione (dopo la conversione); si applicano gli stessi vincoli e conversioni di tipo dell'assegnazione semplice, considerando il tipo dello scalare come versione non qualificata del suo tipo dichiarato.


@ Random832n Essi sono sbagliate. Ho citato la parte correlata nella mia risposta, per favore correggimi in caso contrario. Oh, e l'enfasi è intenzionale.
Sourav Ghosh

"... è illegale, poiché implica la violazione di vincoli. ... un intero letterale, 2 ha tipo int, per definizione." è problematico. Sembra che perché 2è un int, l'assegnazione è un problema. Ma è più di questo. NULLpuò anche essere un int, un int 0. È solo che char *x = 0;è ben definito e char *x = 2;non lo è. 6.3.2.3 Puntatori 3 (BTW: C non definisce un valore letterale intero , solo un valore letterale stringa e un valore letterale composto . 0È una costante intera )
chux - Reinstate Monica

@chux Hai ragione, ma non è vero char *x = (void *)0;, essere conforme? o è solo con altre espressioni che fornisce il valore 0?
Sourav Ghosh

10
@SouravGhosh: le costanti intere con valore 0sono speciali: si convertono implicitamente in puntatori nulli separatamente dalle normali regole per il cast esplicito di espressioni intere generali a tipi di puntatore.
Steve Jessop

1
Il linguaggio descritto dal Manuale di riferimento C del 1974 non consentiva alle dichiarazioni di specificare espressioni di inizializzazione e la mancanza di tali espressioni rende "l'uso dei mirror delle dichiarazioni" molto più pratico. La sintassi int *p = somePtrExpressionè IMHO piuttosto orribile poiché sembra che stia impostando il valore di *pma in realtà sta impostando il valore di p.
supercat

53

Il tutorial è sbagliato. In ISO C, int *my_int_ptr = 2;è un errore. In GNU C, significa lo stesso di int *my_int_ptr = (int *)2;. Questo converte il numero intero 2in un indirizzo di memoria, in qualche modo determinato dal compilatore.

Non tenta di memorizzare nulla nella posizione indirizzata da quell'indirizzo (se presente). Se continuassi a scrivere *my_int_ptr = 5;, proverebbe a memorizzare il numero 5nella posizione indirizzata da quell'indirizzo.


1
Non sapevo che la conversione da intero a puntatore fosse definita dall'implementazione. Grazie per l'informazione.
taskinoor

1
@taskinoor Si noti che c'è una conversione solo nel caso in cui la si forzi da un cast, come in questa risposta. Se non fosse per il cast, il codice non dovrebbe essere compilato.
Lundin

2
@taskinoor: Sì, le varie conversioni in C creano confusione. Questa domanda contiene informazioni interessanti sulle conversioni: C: Quando il casting tra tipi di puntatore non è un comportamento indefinito? .
sleske

17

Per chiarire perché il tutorial è sbagliato, int *my_int_ptr = 2;è una "violazione dei vincoli", è un codice che non è permesso compilare e il compilatore deve darti una diagnostica quando lo incontra.

Come per 6.5.16.1 Assegnazione semplice:

Vincoli

Uno dei seguenti deve valere:

  • l'operando sinistro ha un tipo aritmetico atomico, qualificato o non qualificato e l'operando destro ha un tipo aritmetico;
  • l'operando di sinistra ha una versione atomica, qualificata o non qualificata di un tipo di struttura o unione compatibile con il tipo di destra;
  • l'operando sinistro ha un tipo di puntatore atomico, qualificato o non qualificato e (considerando il tipo che l'operando sinistro avrebbe dopo la conversione di lvalue) entrambi gli operandi sono puntatori a versioni qualificate o non qualificate di tipi compatibili, e il tipo a cui punta la sinistra ha tutto i qualificatori del tipo indicato dalla destra;
  • l'operando sinistro ha un tipo di puntatore atomico, qualificato o non qualificato e (considerando il tipo che l'operando sinistro avrebbe dopo la conversione lvalue) un operando è un puntatore a un tipo di oggetto e l'altro è un puntatore a una versione qualificata o non qualificata di void, e il tipo puntato dalla sinistra ha tutti i qualificatori del tipo puntato dalla destra;
  • l'operando di sinistra è un puntatore atomico, qualificato o non qualificato e quello di destra è una costante di puntatore nullo; o
  • l'operando di sinistra ha il tipo atomic, qualificato o non qualificato _Bool e quello di destra è un puntatore.

In questo caso l'operando di sinistra è un puntatore non qualificato. Non viene menzionato da nessuna parte che l'operando giusto può essere un numero intero (tipo aritmetico). Quindi il codice viola lo standard C.

È noto che GCC si comporta male a meno che non si dica esplicitamente che è un compilatore C standard. Se compili il codice come -std=c11 -pedantic-errors, darà correttamente una diagnostica come deve fare.


4
votato per aver suggerito -pedantic-errori. Anche se probabilmente userò il relativo -Wpedantic.
fagricipni

2
Un'eccezione alla tua affermazione che l'operando di destra non può essere un numero intero: la sezione 6.3.2.3 dice: "Un'espressione di costante intera con il valore 0, o un'espressione simile al tipo void *, è chiamata costante puntatore nullo." Nota il penultimo punto dell'elenco nella tua citazione. Pertanto, int* p = 0;è un modo legale per scrivere int* p = NULL;. Sebbene quest'ultimo sia più chiaro e più convenzionale.
Davislor

1
Il che rende int m = 1, n = 2 * 2, * p = 1 - 1, q = 2 - 1;legale anche l'offuscamento patologico .
Davislor

@Davislor che è coperto dal punto elenco 5 nella citazione standard in questa risposta (d'accordo sul fatto che il riepilogo in seguito probabilmente dovrebbe menzionarlo)
MM

1
@chux Credo che un programma ben formato dovrebbe convertire un intptr_tesplicitamente in uno dei tipi consentiti sul lato destro. Cioè, void* a = (void*)(intptr_t)b;è legale per il punto 4, ma (intptr_t)bnon è né un tipo puntatore compatibile, né a void*, né una costante puntatore nullo, e void* anon è né un tipo aritmetico né _Bool. Lo standard dice che la conversione è legale, ma non che sia implicita.
Davislor

15

int *my_int_ptr = 2

memorizza il valore intero 2 in qualsiasi indirizzo casuale si trovi in ​​my_int_ptr quando viene allocato.

Questo è completamente sbagliato. Se questo è effettivamente scritto, procurati un libro o un tutorial migliore.

int *my_int_ptr = 2definisce un puntatore intero che punta all'indirizzo 2. Molto probabilmente si verificherà un arresto anomalo se si tenta di accedere all'indirizzo 2.

*my_int_ptr = 2, cioè senza la intriga, memorizza il valore due in qualsiasi indirizzo casuale my_int_ptrstia puntando. Detto questo, puoi assegnare NULLa un puntatore quando è definito. char *x=NULL;è perfettamente valido C.

Modifica: mentre scrivevo questo articolo non sapevo che la conversione da intero a puntatore fosse un comportamento definito dall'implementazione. Si prega di vedere le buone risposte di @MM e @SouravGhosh per i dettagli.


1
È completamente sbagliato perché è una violazione dei vincoli, non per nessun altro motivo. In particolare, questo non è corretto: "int * my_int_ptr = 2 definisce un puntatore intero che punta all'indirizzo 2".
Lundin

@ Lundin: la tua frase "non per nessun altro motivo" è essa stessa sbagliata e fuorviante. Se risolvi il problema di compatibilità del tipo, ti resta il fatto che l'autore del tutorial sta travisando grossolanamente come funzionano le inizializzazioni e le assegnazioni del puntatore.
Gare di leggerezza in orbita

14

Molta confusione sui puntatori C deriva da una pessima scelta originariamente fatta riguardo allo stile di codifica, corroborata da una pessima scelta nella sintassi del linguaggio.

int *x = NULL;è corretto C, ma è molto fuorviante, direi addirittura privo di senso, e ha ostacolato la comprensione della lingua per molti principianti. Viene da pensare che in seguito potremmo fare il *x = NULL;che ovviamente è impossibile. Vedete, il tipo di variabile non lo è inte il nome della variabile no *x, né il *nella dichiarazione svolge alcun ruolo funzionale in collaborazione con il =. È puramente dichiarativo. Quindi, ciò che ha molto più senso è questo:

int* x = NULL;che è anche corretto C, anche se non aderisce allo stile di codifica originale K&R. Rende perfettamente chiaro che il tipo è int*, e la variabile pointer lo è x, quindi diventa chiaramente evidente anche a chi non lo sapesse che il valore NULLviene memorizzato x, che è un puntatore a int.

Inoltre, rende più facile derivare una regola: quando la stella è lontana dal nome della variabile allora è una dichiarazione, mentre la stella che è attaccata al nome è la dereferenziazione del puntatore.

Quindi, ora diventa molto più comprensibile che più in basso possiamo fare x = NULL;o, *x = 2;in altre parole, rende più facile per un principiante vedere come variable = expressionconduce a pointer-type variable = pointer-expressione dereferenced-pointer-variable = expression. (Per gli iniziati, con "espressione" intendo "valore".)

La scelta sfortunata nella sintassi della lingua è che quando si dichiarano le variabili locali si può dire int i, *p;quale dichiara un intero e un puntatore a un intero, quindi si induce a credere che *sia una parte utile del nome. Ma non lo è, e questa sintassi è solo un bizzarro caso speciale, aggiunto per comodità, e secondo me non sarebbe mai dovuto esistere, perché invalida la regola che ho proposto sopra. Per quanto ne so, in nessun'altra parte del linguaggio questa sintassi è significativa, ma anche se lo è, indica una discrepanza nel modo in cui i tipi di puntatore sono definiti in C.Ovunque altrove, nelle dichiarazioni di variabili singole, negli elenchi di parametri, nei membri della struttura, ecc. puoi dichiarare i tuoi puntatori come type* pointer-variableinvece di type *pointer-variable; è perfettamente legale e ha più senso.


int *x = NULL; is correct C, but it is very misleading, I would even say nonsensical,... devo accettare di non essere d'accordo. It makes one think.... smettila di pensare, leggi prima un libro di C, senza offesa.
Sourav Ghosh

^^ questo avrebbe avuto perfettamente senso per me. Quindi, suppongo sia soggettivo.
Mike Nakis

5
@SouravGhosh Come una questione di opinione penso che il C avrebbe dovuto essere progettato in modo che int* somePtr, someotherPtrdichiari due puntatori, infatti, scrivevo int* somePtrma questo porta al bug che descrivi.
fagricipni

1
@fagricipni Ho smesso di usare la sintassi della dichiarazione di variabili multiple per questo motivo. Dichiaro le mie variabili una per una. Se davvero li voglio sulla stessa riga, li separo con punto e virgola anziché virgole. "Se un posto è brutto, non andare in quel posto."
Mike Nakis

2
@fagricipni Beh, se avessi potuto progettare Linux da zero, avrei usato createinvece di creat. :) Il punto è che è così e dobbiamo modellarci per adattarci a questo. Alla fine, tutto si riduce alla scelta personale, d'accordo.
Sourav Ghosh

6

Vorrei aggiungere qualcosa di ortogonale alle tante ottime risposte. In realtà, l'inizializzazione su NULLè tutt'altro che una cattiva pratica e può essere utile se quel puntatore può o non può essere utilizzato per memorizzare un blocco di memoria allocato dinamicamente.

int * p = NULL;
...
if (...) {
    p = (int*) malloc(...);
    ...
}
...
free(p);

Poiché secondo lo standard ISO-IEC 9899 free è un nop quando l'argomento è NULL, il codice sopra (o qualcosa di più significativo sulla stessa linea) è legittimo.


5
È ridondante eseguire il cast del risultato di malloc in C, a meno che il codice C non venga compilato anche come C ++.
gatto

Hai ragione, void*viene convertito secondo necessità. Ma avere un codice che funziona con un compilatore C e C ++ potrebbe avere dei vantaggi.
Luca Citi

1
@LucaCiti C e C ++ sono linguaggi diversi. Ci sono solo errori che ti aspettano se provi a compilare un file sorgente scritto per uno utilizzando un compilatore progettato per l'altro. È come provare a scrivere codice C che puoi compilare usando gli strumenti Pascal.
Evil Dog Pie

1
Buon Consiglio. Io (provo a) inizializzare sempre le mie costanti del puntatore a qualcosa. Nel C moderno, questo di solito può essere il loro valore finale e possono essere constpuntatori dichiarati in medias res , ma anche quando un puntatore deve essere mutabile (come quello usato in un ciclo o da realloc()), impostandolo per NULLcatturare i bug dove è stato usato prima è impostato con il suo valore reale. Sulla maggior parte dei sistemi, la dereferenziazione NULLcausa un segfault al punto di errore (sebbene ci siano eccezioni), mentre un puntatore non inizializzato contiene spazzatura e la scrittura su di esso danneggia la memoria arbitraria.
Davislor

1
Inoltre, è molto facile vedere nel debugger che un puntatore contiene NULL, ma può essere molto difficile distinguere un puntatore spazzatura da uno valido. Quindi è utile assicurarsi che tutti i puntatori siano sempre validi o NULL, dal momento della dichiarazione.
Davislor


1

Questo è corretto.

int main()
{
    char * x = NULL;

    if (x==NULL)
        printf("is NULL\n");

    return EXIT_SUCCESS;
}

Questa funzione è corretta per ciò che fa. Assegna l'indirizzo 0 al puntatore di caratteri x. Cioè, punta il puntatore x all'indirizzo di memoria 0.

Alternativa:

int main()
{
    char* x = 0;

    if ( !x )
        printf(" x points to NULL\n");

    return EXIT_SUCCESS;
}

La mia ipotesi su ciò che volevi è:

int main()
{
    char* x = NULL;
    x = alloc( sizeof( char ));
    *x = '2';

    if ( *x == '2' )
        printf(" x points to an address/location that contains a '2' \n");

    return EXIT_SUCCESS;
}

x is the street address of a house. *x examines the contents of that house.

"Assegna l'indirizzo 0 al puntatore di caratteri x." -> Forse. C non specifica il valore del puntatore, solo che char* x = 0; if (x == 0)sarà vero. I puntatori non sono necessariamente numeri interi.
chux - Ripristina Monica l'

Non "punta il puntatore x all'indirizzo di memoria 0". Imposta il valore del puntatore su un valore non valido non specificato che può essere verificato confrontandolo con 0 o NULL. L'operazione effettiva è definita dall'implementazione. Non c'è niente qui che risponda alla domanda reale.
Marchese di Lorne
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.