This is the italian translation of the FAQs of comp.lang.c. The english source is
Copyright 1990-1996 by Steve Summit. Content from the book _C Programming FAQs: Frequently Asked Questions_ is made available here by permission of the author and the publisher as a service to the community. It is intended to complement the use of the published text and is protected by international copyright laws. The content is made available here and may be accessed freely for personal use but may not be republished without permission. Except as noted otherwise, the C code in this article is public domain and may be used without restriction.
Questo testo è la traduzione italiana delle FAQ
(Frequently Asked Questions, domande frequenti) di comp.lang.c, che è il
più importante newsgroup sulla programmazione C. Questa è la
prima versione di questa traduzione italiana pubblicata in rete. Di conseguenza,
è probabile che contenga errori di battitura o di impaginazione o altri difetti.
Inoltre la bibliografia attualmente contiene titoli inglesi; conto di aggiornarla in
una prossima versione riportando le traduzioni italiane (se esistono) dei testi citati.
Per la segnalazione di errori di traduzione, suggerimenti, o altri commenti relativi
alla sola versione italiana, inviate mail a:
andrea@sosio.it
Copyright (originale):
La versione inglese di questo articolo è sotto copyright di Steve Summit. Estratti
del contenuto del libro "C Programming FAQs: Frequently Asked Questions" sono riportati su
permesso dell'autore e dell'editore come servizio alla comunità. Questo articolo è inteso
come complementare al testo pubblicato ed è protetto da leggi di copyright internazionale.
Il contenuto può essere usato liberamente per uso personale ma non ripubblicato senza
permesso.
Alcuni argomenti si ripresentano periodicamente in questo newsgroup. Si tratta di buone
domande, e le risposte non sono essere ovvie, ma ogni volta che ricorrono, molta larghezza
di banda e tempo di lettura è sprecato con risposte ripetute e noiose correzioni alle
risposte scorrette che vengono inevitabilmente pubblicate. Questo articolo, pubblicato
mensilmente, cerca di rispondere a queste domande comuni in modo definitivo e succinto,
affinchè la conversazione sulla rete possa spostarsi su argomenti più
costruttivi senza tornare continuamente a questioni già discusse.
Un articolo di newsgroup non può sostituire un esame attento di un vero tutorial o manuale
di riferimento. Chiunque sia abbastanza interessato al C da seguire questo newsgroup
dovrebbe anche leggere e studiare uno o più di tali manuali, preferibilmente più di una
volta. Alcuni libri e manuali di compilatori C sono sfortunatamente inadeguati; alcuni
addirittura contribuiscono a perpetuare alcuni dei miti che questo articolo si propone di
confutare. La bibliografia di questo articolo elenca molti libri di C degni di nota; vedi
anche le domande 18.9 e 18.10. In molte delle successive domande-risposte sono contenuti
riferimenti a questi libri, cosicché il lettore interessato possa ottenerne maggiori
informazioni.
Se avete una domanda sul C che non ha risposta in questo articolo, cercate di trovare una
risposta in uno dei libri citati, o chiedendo a colleghi esperti, prima di rivolgere la
domanda alla rete. Molte persone in rete sono contente di rispondere alle vostre domande,
ma il volume di risposte per ogni domanda, e il numero crescente di domande dovute
all'accesso di più persone alla rete, possono soffocare il newsgroup. Se avete domande o
commenti su questo articolo, per favore inviatele per posta e non pubblicate sul newsgroup
- lo scopo di questo articolo è quello di ridurre il traffico sulla rete, non di
aumentarlo.
Oltre a elencare le domande poste di frequente, questo articolo contiene anche risposte
pubblicate di frequente. Anche se sapete tutte le risposte, vale la pena darci un'occhiata
una volta ogni tanto, in modo che possiate evitare di sprecare tempo a rispondere a domande
già presenti in questo elenco.
Questa traduzione è basata sull'originale inglese aggiornato al 5 settembre 1996,
ed è stata realizzata nel gennaio 2001. Pertanto, questo documento potrebbe non
essere aggiornato, in particolare se state guardando una copia stampata o recuperata su
CD-ROM. Dovreste essere in grado di ottenere la copia più aggiornata della versione
originale (in inglese) via ftp anonima dai siti ftp.eskimo.com, rtfm.mit.edu, e ftp.uu.net
(vedi domande 18.16 e 20.40), oppure spedendo il messaggio "help" a
mail-server@rtfm.mit.edu. Poiché questa lista viene periodicamente modificata, i numeri
delle domande potrebbero non corrispondere a quelli di copie più vecchie o
più nuove in circolazione; fate attenzione quando vi riferite a elementi di questa
lista solo attraverso il numero.
Questo articolo è stato prodotto per essere distribuito gratuitamente. Non dovreste
pagare nessuno per averne una copia. Sono disponibili anche altre versioni di questo
documento. Vedete la domanda 20.40 a questo proposito. Questo articolo viene continuamente
migliorato. Il vostro input è benvenuto. Mandate i vostri commenti a scs@eskimo.com.
Le domande a cui si dà risposta in questo articolo sono suddivise nelle seguenti categorie:
(I numeri delle domande in ciascuna sezione non sono sempre continui perché sono
allineati con la versione su libro menzionata prima, che contiene più domande).
Se vi servono valori molto grandi (sopra 32767 o sotto -32767), usate i long. Altrimenti,
se lo spazio è molto importante (p.es., se ci sono grandi vettori o molte strutture),
usate gli short. Altrimenti, usate gli int. Se è importante avere un comportamento ben
definito in caso di overflow e i numeri negativi non vi servono, o volete evitare problemi
con i segni quando manipolate bit o byte, usate uno dei corrispondenti tipi unsigned.
(Comunque, fatte attenzione quando mescolate valori con e senza segno in una espressione).
Benché i tipi carattere possono essere usati come "piccoli interi", spesso questo comporta
più problemi che vantaggi, a causa dell'impredicibilità dell'estensione del
segno e la maggiore dimensione del codice. (Può essere utile usare unsigned char; vedi
domanda 12.1 per un problema correlato). Simili tradeoff fra spazio e tempo valgono quando
si sceglie fra float e double. Nessuna delle regole dette sopra vale se si prende l'indirizzo di una variabile e questo deve avere un particolare tipo.
Se per qualche motivo avere bisogno di dichiarare qualcosa con una dimensione "esatta" (solitamente l'unico buon motivo è quello di conformarsi a qualche formato di memorizzazione imposto dall'esterno, ma vedi anche la domanda 20.5), assicuratevi di incapsulare la scelta con una typedef appropriata.
1.4: Cosa dovrebbe essere il tipo a 64 bit per le nuove macchine a 64 bit?
Alcuni produttori di prodotti C per macchine a 64 bit supportano int lunghi 64 bit. Altri produttori temono che troppo del codice esistente assuma che gli int e i long abbiano la stessa dimensione, o che uno dei due sia a 32 bit, e introducono un nuovo tipo non standard a 64 bit, il long long (o _longlong).
I programmatori che vogliono scrivere codice portabile dovrebbero isolare i loro tipi a 64 bit con typedef appropriate. I produttori che si sentono in dovere di introdurre un nuovo tipo intero più lungo dovrebbero presentarlo come lungo "almeno 64 bit" (che è veramente un tipo nuovo, che il C tradizionale non ha) e non "esattamente 64 bit".
Riferimenti: ANSI Sez. F.5.6; ISO Sez. G.5.6.
1.7: Qual è il modo migliore di dichiarare e definire variabili globali?
Innanzitutto, anche se ci possono essere molte "dichiarazioni" (anche su diverse unità di traduzione) di una singola variabile o funzione "globale" (più precisamente, "extern"), ci deve essere esattamente una "definizione". (La definizione è la dichiarazione che effettivamente alloca spazio, ed eventualmente fornisce un valore di inizializzazione). La soluzione migliore consiste nel piazzare ogni definizione in un (unico) file .c, con una dichiarazione esterna in una file header (.h) che viene poi incluso ovunque serva la dichiarazione. Il file .c dovrebbe includere anch'esso il file header, in modo che il compilatore possa verificare che le dichiarazioni siano coerenti con la definizione.
Questa regola fornisce un alto livello di portabilità: è consistente con i requisiti dello standard ANSI, ed è anche consistente con la maggior parte dei compilatori e linker pre-ANSI. (I compilatori e linker sotto Unix di solito usano un modello comune che consente definizioni multiple, a patto che al massimo una comprenda una inizializzazione; questo comportamento è menzionato nello standard ANSI come una "una comune estensione". Un piccolo insieme di sistemi molto strani potrebbero richiedere una inzializzazione esplicita per distinguere una definizione da una dichiarazione esterna).
È possibile usare dei trucchi di preprocessore per far sì che una linea come
DEFINE(int, i);
debba essere inserita solo una volta in un unico header file, e si trasformi in una dichiarazione o in una definizione a seconda di come è definita una qualche macro, ma non è chiaro se ne valga la pena.
È particolarmente importante mettere dichiarazioni globali in header file se volete che il compilatore vi segnali eventuali dichiarazioni inconsistenti. In particolare, non mettete mai il prototipo di una funzione esterna in file .c: in generale non ne verrà controllata la consistenza con la definizione, e avere un prototipo inconsistente è peggio che non avere alcun prototipo.
Vedi anche le domande 10.6 e 18.8.
1.11: Che cosa significa "extern" nella dichiarazione di una funzione?
Può essere usato come indizio stilistico per indicare che la definizione della funzione si trova probabilmente in un altro file sorgente, ma da un punto di vista formale non c'è differenza fra
ma il compilatore mi segnala un errore. È illegale per una struttura C contenere un puntatore a se stessa?
Le strutture C possono sicuramente contenere puntatori a loro stesse; la trattazione e l'esempio in Sez. 6.5 di K&R lo chiariscono. Il problema con l'esempio riportato è che la typedef non è ancora stata definita nel punto in cui è dichiarato il campo "next". Per risolvere il problema, date prima un tag alla struttura ("struct node"). Quindi, dichiarate il campo "next" come un semplice "struct node *", o slegate la typedef dalla dichiarazione della struttura, o entrambe le cose. Una versione corretta è la seguente:
struct node {
char *item;
struct node *next;
};
typedef struct node *NODEPTR;
ed esistono almento tre altri modi corretti equivalenti per fare la stessa cosa.
Un problema simile, con soluzione simile, può sorgere quando si cerca di dichiarare con typedef una coppia di tipi struttura che contengono mutui riferimenti. Vedi anche la domanda 2.1.
1.21: Come dichiaro un array di N puntatori a funzioni che ritornano puntatori a funzioni che ritornano puntatori a caratteri?
A questa domanda si può rispondere in almeno tre modi:
1. char *(*(*a[N])())();
2. Costruite la dichiarazione in modo incrementale, usando le typedef:
typedef char *pc; /* puntatore a char */
typedef pc fpc(); /* funzione che ritorna puntatore a char */
typedef fpc *pfpc; /* pointer a quanto sopra */
typedef pfpc fpfpc(); /* funzione che ritorna quanto sopra */
typedef fpfpc *pfpfpc; /* puntatore a quanto sopra */
pfpfpc a[N]; /* array di quanto sopra */
3. Usate il programma cdecl, che traduce inglese in C e viceversa:
cdecl> declare a as array of pointer to function returning pointer to function returning pointer to char
char *(*(*a[])())()
cdecl può anche spiegare dichiarazioni complesse, aiutare a scrivere cast, e indicare in quale coppia di parentesi vanno messi gli argomenti (per dichiarazioni di funzioni complesse, come quella sopra). Versioni di cdecl si trovano nel volume 14 di comp.sources.unix (vedi domanda 18.16) e in K&R2.
Ogni buon libro di C dovrebbe spiegare come leggere queste dichiarazioni complesse "da dentro a fuori" per capirle ("la dichiarazione simula l'uso").
Le dichiarazioni di puntatori a funzioni negli esempi sopra non includono informazione sul tipo dei parametri. Quando i parametri hanno tipi complessi, le dichiarazioni possono diventare veramente confuse. (Le versioni moderne di cdecl possono aiutare anche a questo proposito).
Riferimenti: K&R2 Sez. 5.12; ANSI Sez. 3.5 e sgg. (specialmente Sez. 3.5.4); ISO Sez. 6.5 e sgg. (specialmente Sez. 6.5.4); H&S Sez. 4.5, Sez. 5.10.1.
1.22: Come faccio a dichiarare una funzione che torna un puntatore a una funzione dello stesso tipo? Sto costruendo una macchina a stati con una funzione per stato, ciascuna delle quali torna un puntatore alla funzione per lo stato successivo, ma non so come dichiarare le funzioni.
Non potete farlo direttamente. Una possibilità è scrivere una funzione che ritorna un puntatore a funzione generico, con qualche cast giudizioso per sistemare i tipi dei puntatori che vengono passati in giro. Altrimenti, fare che la funzione ritorni una struttura che contiene solo un puntatore a una funzione che ritorna una struttura dello stesso tipo.
1.25: Il mio compilatore si lamenta di una ridichiarazione illegale di una funzione, ma io la definisco solo una volta e la chiamo solo una volta.
Le funzioni che vengono chiamate senza che ci sia una dichiarazione nello scope (per esempio perché la prima chiamata precede la definizione) sono considerate dal compilatore come funzioni che tornano int (e senza informazione sul tipo dei parametri), cosa che può portare a delle discrepanze se la funzione è successivamente dichiarata o definita altrimenti. Le funzioni non int devono essere dichiarate prima di essere chiamate.
Un'altra possibile origine di questo problema è che la funzione abbia lo stesso nome di un'altra dichiarata in qualche header file. Vedi anche le domande 11.3 e 15.1.
Riferimenti: K&R1 Sez. 4.2; K&R2 Sez. 4.2; ANSI Sez. 3.3.2.2; ISO Sez. 6.3.2.2; H&S Sec. 4.7.
1.30: Cosa posso assumere con certezza a proposito del valore iniziale di variabili non esplicitamente inizializzate? Se le variabili globali contengono inizialmente uno "zero", questo significa che anche i puntatori contengono null e le variabili float lo zero float?
Le variabili non inizializzate con tempo di vita "static" (ovvero, quelle dichiarate al di fuori delle funzioni, e quelle dichiarate con la classe di memorizzazione static) è garantito che contengano inizialmente zero, come se il programmatore avesse scritto "=0". Di conseguenza, tali variabili sono implicitamente dichiarate al puntatore null (del tipo corretto, vedi anche sezione 5) se sono puntatori, e a 0.0 se sono float.
Le variabili con tempo di vita "automatic" (ovvero, le variabili locali senza la classe di memorizzazione static) all'inizio contengono spazzatura a meno che non siano esplicitamente inizializzate.(Non si può assumere nulla di utile sulla spazzatura).
Anche la memoria allocata dinamicamente con malloc() o realloc() è probabile che contenga spazzatura, e deve essere inizializzata dal programma chiamante in modo appropriato. La memoria ottenuta con calloc() ha tutti i bit a zero, ma questo non è sempre utile per i puntatori o i valori float (vedi la domanda 7.31, e la sezione 5).
1.31: Questo codice, preso da un libro, non compila:
f()
{
char a[] = "Hello, world!";
}
Probabilmente avete un compilatore pre-ANSI, che non consente l'inizializzazione di "aggregati automatici" (ovvero, array, strutture o unioni locali non static). Per ovviare al problema, si può rendere l'array globale o static (se non è necessario averne una copia nuova ad ogni chiamata), o rimpiazzarlo con un puntatore (se non è necessario scrivere nel vettore). (Potete sempre inizializzare variabili locali di tipo char* con l'indirizzo di costanti stringa, ma vedi anche la domanda 1.32 sotto). Se nessuna di questa condizioni vale, dovrete inizializzare l'array "a mano" usando la strcpy() quando viene chiamata f(). Vedi anche la domanda 11.29.
1.31a: Cosa c'è che non va in questa inizializzazione?
char *p = malloc(10);
Il mio compilatore si lamenta di un "inizializzatore non valido", o qualcosa del genere.
È la dichiarazione di una variabile static o globale? Le chiamate di funzione non sono ammesse nell'inizializzazione di tali variabili.
1.32: Che differenze ci sono fra le seguenti inizializzazioni?
Il mio programma va in crash se provo ad assegnare un nuovo valore a p[i].
Una costante stringa può essere usata in due modi leggermente diversi. Se è usata per inizializzare un vettore (come nella dichiarazione di char a[]), specifica il valore iniziale dei caratteri nell'array. In qualsiasi altro caso, viene trasformata in un array static di caratteri, privo di nome, che può essere allocato in memoria a sola lettura, per cui non puoi modificarlo. Nel contesto di un'espressione, l'array è convertito in un puntatore, come al solito (vedi sezione 6), per cui la seconda dichiarazione inizializza p facendolo puntatore al primo elemento dell'array senza nome.
(Allo scopo di compilare vecchio codice, alcuni compilatori hanno uno switch per controllare se le stringhe sono scrivibili o no).
1.34: Finalmente ho capito la sintassi per dichiarare puntatori a funzioni; ma come li inizializzo?
Usate qualcosa come
extern int func();
int (*fp)() = func;
Quando il nome di una funzione viene usato in un'espressione come questa, "degenera" in un puntatore (ovvero, viene implicitamente estratto il suo indirizzo), grosso modo come nel caso degli array.
Normalmente è necessaria una dichiarazione esplicita della funzione, poiché in questo caso non avviene la dichiarazione esterna implicita (non trattandosi di una chiamata a funzione).
2.1: Qual e' la differenza fra queste due inizializzazioni?
struct x1 { ... };
typedef struct { ... } x2;
La prima forma dichiara un "tag" di struttura, la seconda dichiara una "typedef". La differenza principale consiste nel fatto che successivamente ci si può riferire al primo tipo con "struct x1" e al secondo solo con "x2". In altri termini, la seconda dichiarazione è un tipo leggermente più astratto - chi la usa non deve necessariamente sapere che è una struttura, e la keyword struct non si usa quando se ne dichiarano istanze.
2.2: Perché il seguente codice non funziona?
struct x { ... };
x thestruct;
Il C non è il C++. Non vengono generate automaticamente typedef per i tag delle struct. Vedi anche la domanda 2.1 sopra.
2.3: Una struttura può contenere un puntatore a se stessa?
Assolutamente si. Vedi la domanda 1.14.
2.4: Qual è il modo migliore di implementare tipi opachi (astratti) in C?
Un buon modo è quello di far sì che i clienti usino puntatori a struttura (magari ulteriormente nascosti dietro typedefs) che puntano a tipi struttura che non sono definiti pubblicamente.
2.6: Mi sono imbattuto in codice che dichiarava una struttura come segue:
struct name {
int namelen;
char namestr[1];
};
e poi faceva qualche trucco di allocazione per far sì che il vettore namestr si comportasse come se avesse più elementi. È legale e portabile?
Si tratta di una tecnica diffusa, anche se Dennis Ritchie l'ha battezzata "un'ingiustificato intrallazzo con l'implementazione del C". Un'interpretazione ufficiale sostiene che non è strettamente conforme allo standard. (Una trattazione completa degli argomenti pro e contro la legalità della tecnica esula dagli scopi di questo articolo). Sembra che sia portabile a tutte le implementazioni note. (I compilatori che eseguono controlli accurati sui limiti degli array potrebbero generare delle warning).
Un'altra possibilità è quella di dichiarare l'elemento di dimensioni variabili molto grande, anziché molto piccolo; nel caso dell'esempio sopra:
...
char namestr[MAXSIZE];
...
dove MAXSIZE è più grande di qualsiasi stringa che sarà memorizzata nel vettore. Tuttavia, sembra che anche questa tecnica sia proibita da un'interpretazione rigorosa dello standard. Entrambe queste strutture "intrallazzose" devono essere usate con attenzione, perché il programmatore sa, a proposito della loro dimensione, più di quanto sappia il compilatore. (In particolare, in generale possono essere manipolate solo via puntatori).
Riferimenti: Rationale Sez. 3.5.4.2.
2.7: Ho sentito che le strutture si possono assegnare a variabili e possono essere passate a e da funzioni, ma K&R1 dice di no.
Ciò che K&R1 diceva era che le restrizioni sulle operazioni con strutture sarebbero state rimosse in una versione successiva del compilatore, e infatti l'assegnamento e il passaggio di strutture erano funzionanti nel compilatore di Ritchie già nel momento in cui K&R1 fu pubblicato. Anche se qualche vecchio compilatore non aveva queste operazioni, esse sono supportate da tutti quelli moderni, e fanno parte dello standard ANSI, ragion per cui si possono utilizzare senza alcuna remora.
(Si noti che quando una struttura è assegnata, passata, o ritornata, la copia è fatta in modo monolitico; qualsiasi cosa sia puntata da eventuali campi puntatore non viene copiata).
2.8: Perché non si possono confrontare le strutture?
Non c'è nessun buon modo per il compilatore di implementare il confronto di strutture che sia consistente con lo stile "a basso livello" del C. Un semplice confronto byte a byte potrebbe A simple byte-by-byte comparison could fallire a causa dei bit casuali presenti nei "buchi" inutilizzati delle strutture (questi buchi sono usati come riempitivo per mantenere corretto l'allineamento dei campi successivi; vedi domanda 2.12). Un confronto campo a campo potrebbe richiedere quantità inaccettabile di codice ripetitivo per grandi strutture.
Se avete bisogno di confrontare due strutture, dovrete scrivere una funzione che lo faccia, campo per campo.
2.9: Come sono implementati il passaggio e il ritorno di strutture?
Quando le strutture sono passate come argomenti alle funzioni, di solito viene fatta una "push" dell'intera struttura sullo stack, usando tante parole di memoria quante ne servono. (I programmatori spesso preferiscono passare puntatori a strutture, proprio per evitare questo costo.) Alcuni compilatori passano semplicemente un puntatore alla struttura, anche se potrebbero dover fare una copia locale per preservare la semantica del passaggio per valore.
Spesso le strutture vengono tornate dalle funzioni in una locazione puntata da un argomento extra "nascosto", fornito dal compilatore, che viene passato alla funzione. Alcuni vecchi compilatori usavano una speciale locazione statica per i ritorni delle funzioni, anche se ciò rendeva non-rientranti le funzioni che tornavano strutture, cosa vietata dall'ANSI C.
Riferimenti: ANSI Sez. 2.2.3; ISO Sez. 5.2.3.
2.10: Come faccio a passare valori costanti a funzioni con parametri di tipi struttura?
In C non si possono generare valori anonimi di tipi struttura. Dovrete usare una variabile temporanea di tipo struttura o una piccola funzione che costruisce una struttura. (gcc fornisce costanti di tipi struttura come estensione, e il meccanismo probabilmente verrà incluso in una futura revisione dello standard). Vedi anche la domanda 4.10.
2.11: Come faccio a leggere/scrivere strutture da/su file?
È relativamente banale scrivere una struttura su file usando fwrite():
fwrite(&somestruct, sizeof somestruct, 1, fp);
e una corrispondente fread() può essere usata per leggere la struttura da file. (Nel C pre-ANSI, si richiede un cast (char*) sul primo argomento. È comunque importante notare che la fwrite() riceve un puntatore a byte, non un puntatore a struttura). Comunque, i file dati scritti in questo modo *non* saranno portabili (vedi domande 2.12 e 20.5). Notate anche che se la struttura contiene puntatori, verrà scritto solo il loro valore, e questo molto probabilmente non sarà più valido quando la struttura verrà letta da file. Inoltre, notate che per migliorare la portabilità dovete usare il flag "b" nella fopen(); vedi domanda 12.38.
Una soluzione più portabile, anche se più laboriosa all'inizio, è quella di scrivere un paio di funzioni per scrivere e leggere la struttura campo per campo in un formato portabile (magari persino leggibile da umani).
Riferimenti: H&S Sez. 15.13.
2.12: Il mio compilatore lascia dei buchi nelle strutture, cosa che spreca spazio e impedisce di fare I/O binario su file dati esterni. Posso disattivare il riempimento o controllare in qualche altro modo l'allineamento dei campi delle strutture?
Il compilatore può fornire un'estensione per fornire questo controllo (probabilmente una clausola #pragma, vedi domanda 11.20), ma non c'è un metodo standard.
Vedi anche domanda 20.5.
Riferimenti: K&R2 Sez. 6.4; H&S Sez. 5.6.4.
2.13: Perché sizeof ritorna una dimensione maggiore di quella che mi aspetto per una struttura, come se ci fosse del riempimento in fondo?
Le strutture possono avere del riempimento ("padding") in fondo (così come all'interno) se questo è necessario per assicurare che l'allineamento sia preservato nel caso in cui venga allocato un array di strutture contigue. Anche quando la struttura non fa parte di un array, il riempimento al fondo rimane, in modo tale che sizeof ritorni sempre una dimensione consistente. Vedi domanda 2.12 sopra.
Riferimenti: H&S Sez. 5.6.7.
2.14: Come faccio a determinare l'offset in byte di un campo all'interno di una struttura?
L'ANSI C definisce la macro offsetof(), che dovrebbe essere usata a questo scopo se disponibile; vedi . Se non c'è, una possibile implementazione è
#define offsetof(type, mem) ((size_t)((char *)&((type *)0)->mem - (char *)(type *)0))
Questa implementazione non è portabile al 100%; alcuni compilatori potrebbero legittimamente rifiutarla.
2.15: Come faccio ad accedere a un campo di struttura per nome a runtime?
Costruite una tabella di nomi e offset, usando la macro offsetof(). L'offset di un campo b di una struttura a è
offsetb = offsetof(struct a, b)
Se structp è un puntatore a un'istanza di questa struttura, e il campo b è un intero (con l'offset calcolato come sopra), il valore di b può essere assegnato indirettamente con
*(int *)((char *)structp + offsetb) = value;
2.18: Questo programma funziona correttamente, ma va in core dump dopo aver terminato. Come mai?
struct list {
char *item;
struct list *next;
}
/* Programma principale: */
main(argc, argv)
{ ... }
Un punto e virgola mancante fa sì che main() sia dichiarata come funzione che torna una struttura. (Il collegamento è difficile da vedere a causa del commento in mezzo). Poiché le funzioni con valori struttura sono normalmente implementate usando un puntatore nascosto (vedi la domanda 2.9), il codice generato per il main cerca di accettare 3 argomenti, mentre gliene vengono passati solo 2 (in questo caso, dal codice di start-up del C). Vedi anche le domande 10.9 e 16.4.
Riferimenti: CT&P Sez. 2.3.
2.20: Si possono inizializzare le union?
Lo standard ANSI consente un inizializzatore per il primo membro di una unione. Non esiste un modo standard per inizializzare un qualsiasi altro membro (e sui compilatori pre-ANSI in generale l'inizializzazione di unioni è del tutto impossibile).
2.22: Qual è la differenza fra una enumerazione e un insieme di #define?
Al momento, la differenza è molto piccola. Benché molti avrebbero sperato altrimenti, lo standard C dice che le enumerazioni possono essere mischiate liberamente con altri tipi interi, senza errori. (Se questo fosse proibito a meno di cast espliciti, l'uso giudizioso delle enumerazioni potrebbe intercettare certi errori di programmazione.)
Alcuni vantaggi delle enumerazioni sono che i valori numerici sono assegnati automaticamente, che un debugger potrebbe essere in grado di mostrare i valori simbolici quando si esaminano variabili enumerazione, e che obbediscono allo scope a blocchi. (Un compilatore potrebbe anche generare warning non fatali quando le enumerazioni sono mischiate agli interi, poiché ció puó comunque essere considerato cattivo stile anche se non è strettamente illegale). Uno svantaggio è che il programmatore ha poco controllo su queste warning non fatali; alcuni programmatori sono anche disturbati dal fatto di non avere controllo sulla dimensione delle variabili di tipi enumerazione.
2.24: C'è un modo semplice per stampare i valori delle enumerazioni in forma simbolica?
No. Potete scrivere una piccola funzione che mappa una costante enumerativa in una stringa. (Se l'unico motivo per cui vi interessa è il debugging, un buon debugger dovrebbe stampare automaticamente le costanti enumerative in forma simbolica).
La sottoespressione i++ causa un effetto collaterale - modifica il valore di i - cosa che causa un comportamento indefinito perché i compare anche altrove nella stessa espressione. (Nota che anche se K&R dice letteramente che il comportamento dell'espressione è non specificato, lo standard C fa l'affermazione più forte che esso sia indefinito - vedi domanda 11.33).
stampa 49. Indipendentemente dall'ordine di valutazione, non dovrebbe stampare 56?
Anche se gli operatori di post-incremento e post-decremento eseguono le loro operazioni "dopo" aver restituito il valore precedente, il significato di questo "dopo" è spesso frainteso. Non è garantito che l'incremento o decremento sia eseguito immediatamente dopo aver fornito il valore precedente e prima che qualsiasi altra parte dell'espressione sia valutata. È solo garantito che l'aggiornamento sarà eseguito in qualche momento prima che l'espressione si consideri "finita" (prima del prossimo "punto di sequenza" nella terminologia ANSI; vedi domanda 3.8). Nell'esempio, il compilatore ha scelto di moltiplicare il valore vecchio per se stesso ed eseguire dopo entrambi gli incrementi.
Il comportamento di codice che contiene effetti collaterali molteplici e ambigui è sempre stato indefinito. (Un po' grossolanamente, si può dire che per "effetti collaterali molteplici e ambigui" si intende ogni combinazione di ++, --, =, +=, -=, ecc., all'interno di una singola espressione, che fa sì che lo stesso oggetto sia o modificato due volte oppure modificato e poi valutato. Questa definizione è imprecisa; vedi la domanda 3.8 per una più precisa, e la domanda 11.33 per il significato di "indefinito").
Non cercate neanche di scoprire come il vostro compilatore implementa queste cose (nonostante i poco saggi esercizi che si trovano in alcuni testi di C); come K&R saggiamente indicano, "se non sai come fanno i compilatori, questa ingenuità può proteggerti".
su vari compilatori. Alcuni hanno assegnato a i il valore 3, altri 4, uno ha assegnato 7. So che il comportamento è indefinito, ma come ha fatto a risultare 7?
Comportamento indefinito significa che può succedere qualsiasi cosa. Vedi le domande 3.9 e 11.33. (Anche, nota che né i++ né ++i sono la stessa cosa di i+1). Se volete incrementare i, usate i=i+1, i+=1, i++, o ++i, non qualche combinazione. Vedi anche la domanda 3.12.)
3.4: Posso usare parentesi esplicite per forzare l'ordine di valutazione che desidero? E se anche non lo faccio, l'ordine non viene determinato dalle precedenze?
Non in generale. La precedenza degli operatori e le parentesi esplicite impongono solo un ordine parziale sulla valutazione di espressioni. Nell'espressione
f() + g() * h()
anche se sappiamo che la moltiplicazione avverrà prima dell'addizione, niente dice quale delle tre funzioni sarà chiamata per prima.
Quando avete bisogno di assicurare che venga seguito un certo ordine nella valutazione di sottoespressioni, usate variabili temporanee esplicite e separate le istruzioni.
3.5: Ma come stanno le cose nel caso degli operatori && e ||? Vedo codice come
while((c = getchar()) != EOF && c != '\n') ...
Esiste una speciale eccezione per questi operatori (e per gli operatori ?: e virgola): è garantita la valutazione da sinistra a destra (grazie a un punto di sequenza intermedio, vedi la domanda 3.8). Ogni libro sul C dovrebbe dirlo chiaramente.
3.8: Come si leggono queste espressioni complesse? Cos'è un punto di sequenza?
Un punto di sequenza è il punto (alla fine di una espressione completa, o agli operatori ||, &&, ?:, o virgola, o subito prima di una chiamata di funzione) in cui la polvere si è posata ed è garantito che tutti gli effetti collaterali siano stati completati. Lo standard ANSI/ISO dice che
"Fra un punto di sequenza e il successivo il valore memorizzato in un oggetto deve essere
modificato al massimo una volta dalla valutazione di un'espressione. Inoltre, al valore precedente
si deve accedere solo per determinare il valore da memorizzare."
La seconda frase può essere difficile da comprendere. Dice che se si scrive in un oggetto in una espressione completa, tutti gli accessi a questo oggetto in quella espressione devono essere allo scopo di calcolare il valore che deve esservi scritto. Questa regola limita le espressioni legali a quelle in cui è possibile dimostrare che gli accessi in lettura a un oggetto precedono la modifica.
non sappiamo in quale cella di a[] viene scritto un valore, ma i viene incrementato di uno, giusto?
Sbagliato! Se un'espressione di un programma diventa indefinita, tutti i suoi aspetti diventano indefiniti. Vedi le domande 3.2, 3.3, 11.33, e 11.35.
3.12: Se non uso il valore dell'espressione, è meglio usare i++ o ++i per incrementare una variabile?
Poiché le due forme differiscono solo per quanto concerne il valore che restituiscono, sono del tutto equivalenti quando serve solo il loro effetto collaterale. (Comunque, la forma prefissa è preferita in C).
Secondo le regole di promozione dei tipi interi del C, la moltiplicazione viene eseguita usando l'aritmetica degli int, e il risultato potrebbe andare in overflow o essere troncato prima di essere promosso e assegnato al long int a sinistra. Usate un cast esplicito per forzare l'uso dell'aritmetica long:
long int c = (long int)a * b;
Notate che (long int)(a * b) non avrebbe l'effetto desiderato.
Un problema simile può sorgere quando si dividono due interi, assegnando il risultato a una variabile a virgola mobile.
3.16: Ho un'espressione complessa che devo assegnare a una di due variabili, scelta in base a una condizione. Posso usare codice come questo?
((condizione) ? a : b) = espressione_complessa;
No. L'operatore ?:, come la maggior parte degli operatori, restituisce un valore, e non si può assegnare a un valore. (In altri termini, ?: non restituisce un "l-value"). Se proprio volete, potete usare qualcosa come
*((condizione) ? &a : &b) = espressione_complessa;
anche se non è proprio elegante.
Riferimenti: ANSI Sez. 3.3.15; ISO Sez. 6.3.15; H&S Sez. 7.1.
4.2: Sto cercando di dichiarare un puntatore e allocare dello spazio, ma non funziona. Cosa c'e che non va in questo codice?
char *p;
*p = malloc(10);
Il puntatore che avete dichiarato è p, non *p. Per far puntare un puntatore da qualche parte, dovete usare il nome del puntatore:
p = malloc(10);
È quando si manipola la memoria puntata-da che si usa * come operatore di indirezione:
*p = 'H';
Vedi anche le domande 1.21, 7.1, e 8.3.
Riferimenti: CT&P Sez. 3.1.
4.3: *p++ incrementa p o ciò che è puntato da p?
Gli operatori unari come *, ++, e - sono tutti associativi da destra a sinistra. Perciò, *p++ incrementa p (e ritorna il valore puntato da p prima dell'incremento). Per incrementare il valore puntato da p, usate (*p)++ (o magari ++*p, se l'ordine degli effetti collaterali non conta).
4.5: Ho un puntatore char * che punta ad alcuni int, e voglio scorrerli. Perché
((int *)p)++;
non funziona?
In C, l'operatore di cast non significa "fai finta che questi bit siano di un tipo diverso, e trattali di conseguenza"; è un operatore di conversione, e per definizione restituisce un rvalue, che non può essere assegnato o incrementato con ++. (È una anomalia dei compilatori derivati da pcc, e un'estensione in gcc, il fatto che espressioni come quella sopra siano accettate). Dite quello che intendete:
p = (char *)((int *)p + 1);
oppure (poiché p è un char *) semplicemente
p += sizeof(int);
Se è possibile, dovreste sempre usare i tipi puntatore appropriati fin dall'inizio, invece di trattarli secondo un tipo diverso dal loro.
4.8: Ho una funzione che accetta un puntatore, e dovrebbe inizializzarlo:
void f(ip)
int *ip;
{
static int dummy = 5;
ip = &dummy;
}
Ma quando la chiamo nel modo seguente:
int *ip;
f(ip);
il puntatore del chiamante rimane immutato.
Siete sicuri che la funzione inizializzi quello che pensate debba inizializzare? Ricordate che gli argomenti in C sono passati per valore. La funzione chiamata ha cambiato solo la copia del puntatore che gli è stata passata. Dovrete passarle l'indirizzo del puntatore (cosicché la funzione dovrà accettare un puntatore a puntatore) o farle tornare il puntatore.
Vedi anche le domande 4.9 e 4.11.
4.9: Posso usare un puntatore void** per passare un generico puntatore per riferimento a una funzione?
Non in modo portabile. In C non esiste un tipo generico puntatore a puntatore. Il tipo void * si comporta come puntatore generico solo perché il C esegue conversioni automatiche quando altri tipi puntatori sono assegnati da o a void*; queste conversioni non possono essere eseguite (ovvero il corretto tipo di puntatori sottostante non è noto) se si tenta di dereferenziare un valore void** che punta a qualcosa che non sia un void*.
4.10: Ho una funzione
extern int f(int *);
che accetta un puntatore a un int. Come faccio a passare una costante per riferimento? Una chiamata come
f(&5);
sembra non funzionare.
Non si può fare direttamente. Occorre dichiare una variabile temporanea, e quindi passarne l'indirizzo alla funzione:
int five = 5;
f(&five);
Vedi anche le domande 2.10, 4.8, e 20.1.
4.11: C'è il "passaggio per riferimento" in C?
Non proprio. Rigorosamente parlando, il C usa sempre il "passaggio per valore". Si può simulare il passaggio per riferimento definendo funzioni che accettano puntatori, e usando l'operatore & quando si fa la chiamata; il compilatore fa la stessa cosa automaticamente quando si passa un vettore (passando, in realtà, un puntatore, vedi domanda 6.4), ma il C non ha niente che sia davvero equivalente al passaggio per riferimento formalmente detto o ai parametri per riferimento del C++. (Tuttavia, le macro con parametri del preprocessore in effetti forniscono una forma di "passaggio per nome").
4.12: Ho visto diversi modi per chiamare le funzioni attraverso puntatori. Com'è la storia?
Originariamente, un puntatore a funzione doveva essere "trasformato" in una "vera" funzione con l'operatore * (e un paio di parentesi extra per mantenere le precedenze corrette) prima di fare la chiamata:
int r, func(), (*fp)() = func;
r = (*fp)();
Si può anche sostenere che le funzioni sono sempre chiamate attraverso puntatori, e che i nomi "veri" delle funzioni degenerano implicitamente in puntatori (nelle espressioni, così come fanno nelle inizializzazioni; vedi la domanda 1.34). Questo ragionamento, diffuso da pcc e adottato nello standard ANSI, significa che
r = fp();
è legale e funziona correttamente sia che fp sia il nome della funzione o un puntatore a funzione. (L'uso è sempre stato non ambiguo; l'unica cosa che si può fare con un puntatore a funzione seguito da una lista di argomenti è chiamare la funzione puntata). Un * esplicito è ancora consentito (e consigliato nel caso in cui la portabilità verso i compilatori più vecchi sia importante).
La definizione del linguaggio stabilisce che per ciascun tipo puntatore, c'è un valore speciale (il puntatore nullo) che è distinguibile da tutti gli altri valori e che è garantito essere diverso da un puntatore a qualsiasi oggetto o funzione. In altri termini, l'operatore indirizzo-di (&) o una chiamata a malloc() con successo non restituiranno mai il puntatore nullo. (malloc() ritorna il puntatore nullo quando fallisce, e questo è un uso tipico dei puntatori nulli: come valore "speciale" con un altro significato come "non allocato" o "che non punta ancora a niente").
Un puntatore nullo è concettualmente diverso da un puntatore non inizializzato. Di un puntatore nullo si sa che non punta ad alcun oggetto o funzione; un puntatore non inizializzato può puntare ovunque. Vedi anche le domande 1.30, 7.1, e 7.31.
Come detto sopra, c'è un puntatore nullo per ogni tipo puntatore, e i valori interni dei puntatori nulli di diversi tipi potrebbero essere diversi. Anche se i programmatori non hanno bisogno di conoscere il valore interno di un puntatore nullo, il compilatore deve sempre sapere quale tipo di puntatore nullo è richiesto, in modo tale da poter distinguere laddove sia necessario (vedi domande 5.2, 5.5, e 5.6 sotto).
5.2: Come ottengo un puntatore nullo nei miei programmi?
Secondo la definizione del linguaggio, la costante 0 in un contesto puntatore è convertita in un puntatore nullo a tempo di compilazione. Quindi, in una inizializzazione, in un assegnamento, o in un confronto dove uno dei lati è una variabile o un'espressione di tipo puntatore, il compilatore può capire che una costante 0 sull'altro lato rappresenta un puntatore nullo, e generare il valore puntatore nullo del tipo corretto. Di conseguenza i seguenti frammenti sono perfettamente legali:
char *p = 0;
if(p != 0)
(Vedi anche la domanda 5.3.)
Comunque, un argomento passato a una funzione non è sempre riconoscibile come contesto puntatore, e il compilatore potrebbe non essere in grado di capire che uno 0 senza altre indicazioni *rappresenti* un puntatore nullo. Per generare un puntatore nullo nel contesto di una chiamata di funzione, potrebbe servire un cast esplicito, o un altro meccanismo che forzi il compilatore a riconoscere lo 0 come puntatore. Per esempio, la system call execl di Unix riceve una lista di argomenti di tipo char* di lunghezza variabile e terminata dal puntatore nullo, e si chiama correttamente così:
execl("/bin/sh", "sh", "-c", "date", (char *)0);
Se si omette il cast (char *), il compilatore non sa che deve passare un puntatore nullo, e passarebbe invece lo 0 intero. (Nota che molti manuali Unix contengono questo errore.)
Quando i prototipi di funzione sono in scope, il passaggio di argomenti diventa un "contesto di assegnamento", e la maggior parte dei cast si possono omettere senza danni, poiché il prototipo dice al compilatore che serve un puntatore, e di che tipo, consentendogli di convertire correttamente lo 0. Comunque, i prototipi di funzioni non possono fornire i tipi degli argomenti di liste di argomenti di lunghezza variabile, per cui i cast espliciti in questo caso sono comunque necessari. (Vedi anche la domanda 15.3.). La cosa più sicura è fare un cast di tutte le costanti puntatore nullo nelle chiamate di funzione: per proteggersi dalle funzioni vararg e da quelle senza prototipi; per consentire l'uso sporadico di compilatori non-ANSI; e per dimostrare di sapere quello che state facendo. (Incidentalmente, è anche una regola più facile da ricordare).
Riassumendo:
Cast non richiesto
Cast richiesto
inizializzazione
chiamata di funzione senza prototipo in scope
assegnamento
argomento variabile in chiamata di funzione vararg
confronto
chiamata di funzione con prototipo in scope, argomento fisso
5.3: Il confronto abbreviato "if(p)" per verificare se p è il puntatore nullo è valido? Cosa succede se la rappresentazione interna dei puntatori nulli non è zero?
Quando in C è necessario un valore booleano per un'espressione (nelle istruzioni if, while, for, e do, e con gli operatori &&, ||, !, e ?:), viene inferito un valore falso se l'espressione è uguale a 0, e vero altrimenti. In altre parole, ogni volta che si scrive
if(expr)
dove "expr" è una qualsiasi espressione, il compilatore essenzialmente agisce come se si fosse scritto
if((expr) != 0)
Sostituendo l'espressione banale "p" al posto di "expr," si ottiene che
if(p)
è equivalente a
if(p != 0)
e questo è un confronto di contesto, ragion per cui il compilatore può capire che lo 0 (anche se implicito) in effetti rappresenta la costante puntatore nullo. Non c'è nessun trucco; i compilatori effettivamente funzionano così, e generano identico codice per i due costrutti. La rappresentazione interna del puntatore nullo *non* è rilevante.
L'operatore di negazione booleana ! può essere descritto come segue:
!expr
è equivalente a
(expr)?0:1
e anche a
((expr) == 0)
il che consente di concludere che
if(!p)
è equivalente a
if(p == 0)
Qualcuno ritiene che le "abbreviazioni" come if(p), anche se perfettamente legali, siano cattivo stile (e qualcuno le considera buon stile; vedi la domanda 17.10).
Come questione di stile, molti programmatori preferiscono non avere degli 0 sparsi per i loro programmi. Di conseguenza, la macro NULL è stata definita via #define (in o ) con il valore 0, in alcuni casi con cast a (void*) (vedi anche la domanda 5.6). Un programmatore che voglia rendere esplicita la distinzione fra l'intero 0 e il puntatore nullo può usare NULL ogni volta che serve un puntatore NULL.
L'uso di NULL è solo una convenzione stilistica; il preprocessore ritrasforma NULL in 0, che viene poi trattato dal compilatore, in contesti puntatore, come indicato sopra. In particolare, potrebbe essere ancora necessario un cast prima di NULL (come prima di 0) per argomenti di chiamate di funzione. La tavola riassuntiva al termine della domanda 5.2 sopra si applica a NULL tanto quanto a 0 (un NULL senza cast è equivalente a uno 0 senza cast).
NULL dovrebbe essere usato solo per i puntatori; vedi la domanda 5.9.
5.5: Come dovrebbe essere definito NULL su una macchina che usa una sequenza di bit diversa da 0 come rappresentazione interna del puntatore nullo?
Nello stesso modo che sulle altre macchine: come 0 (o ((void *)0)).
Ogni volta che un programmatore richiede un puntatore nullo, scrivendo "0" o "NULL", è responsabilità del compilatore generare qualsiasi stringa di bit la macchina usi per i puntatori nulli. Perciò, una #define di NULL come 0 su una macchina per cui la rappresentazione interna dei puntatori nulli è diversa da zero è valido quanto su qualsiasi altra macchina: il compilatore deve sempre essere in grado di generare i puntatori nulli corretti per gli 0 che si trovano in contesti puntatore. Vedi anche le domande 5.2, 5.10, e 5.17.
Riferimenti: ANSI Sez. 4.1.5; ISO Sez. 7.1.6; Rationale Sez. 4.1.5.
5.6: Se NULL fosse definito come segue:
#define NULL ((char *)0)
questo farebbe funzionare le chiamate di funzioni che passano un NULL senza cast?
Non in generale. Il problema è che ci sono macchine che usano differenti rappresentazioni interne per puntatori a differenti tipi di dati. La definizione suggerita farebbe funzionare argomenti NULL senza cast nel caso di funzioni che richiedono puntatori a char, ma argomenti puntatore di altri tipi sarebbero ancora problematici, e costrutti legali come
FILE *fp = NULL;
potrebbero fallire.
Tuttavia, l'ANSI C consente la definizione alternativa
#define NULL ((void *)0)
per NULL. Oltre a far funzionare programmi potenzialmente scorretti (ma solo su macchine con puntatori omogenei, il che è di dubbia utilità), questa definizione può intercettare usi scorretti di NULL (per esempio, laddove si intendeva usare il carattere ASCII NUL; vedi la domanda 5.9).
Riferimenti: Rationale Sez. 4.1.5.
5.9: Se NULL e 0 sono equivalenti come costanti puntatore nulle, quale è meglio usare?
Molti programmatori pensano che NULL dovrebbe essere usato in tutti i contesti puntatore, per ricordare che il valore deve essere visto come puntatore. Altri ritengono che la confusione attorno a NULL e 0 viene soltanto aumentata nascondendo lo 0 dietro una macro, e preferiscono usare lo 0. Non c'è una risposta giusta. (Vedi anche le domande 9.2 e 17.10.) I programmatori C devono capire che NULL e 0 sono interscambiabili in contesti puntatore, e che uno 0 senza cast è perfettamente accettabile. Qualsiasi uso di NULL invece di 0 deve essere considerato solo un promemoria del fatto che si stanno usando puntatori: i puntatori non dovrebbero dipendere da ciò allo scopo di distinguere, o fare distinguere al compilatore, lo 0 int dal puntatore nullo.
Non si dovrebbe usare NULL dove è necessario un altro tipo di 0, anche se può funzionare, perché questo manda il messaggio sbagliato. (Inoltre, ANSI consente la definizione di NULL come ((void*)0), che non funziona del tutto in contesti non puntatore). In particolare, non si deve usare NULL quando si vuole indicare il carattere ASCII nullo (NUL). Se è proprio necessario, è meglio crearsi la propria macro:
5.10: Ma non sarebbe meglio usare NULL (invece di 0), nel caso il valore di NULL cambi, magari su una macchina con puntatori nulli internamente rappresentati diversi da zero?
No. (Può essere preferibile usare NULL, ma non per questo motivo.) Anche se le costanti simboliche sono usate spesso in luogo dei numeri perché i numeri potrebbero cambiare, questa non è la ragione per cui NULL si usa al posto di 0. Ancora una volta, il linguaggio garantisce che gli 0 nel codice sorgente (in contesti puntatore) generino puntatori nulli. NULL è solo usato come convenzione stilistica. Vedi le domande 5.5 e 9.2.
5.12: Uso la macro di preprocessore
#define Nullptr(type) (type *)0
per facilitarmi la costruzione di puntatori nulli del tipo corretto.
Questo trucco, anche se popolare e a prima vista attraente, non fa guadagnare molto. Non è necessario negli assegnamenti e nei confronti; vedi la domanda 5.2. Non fa neanche risparmiare battute. Il suo uso potrebbe suggerire al lettore che l'autore del programma zoppica sull'argomento dei puntatori nulli, cosicché potrebbe sembrare necessario ricontrollare la #define, le sue invocazioni, e tutti gli altri usi di puntatori. Vedi anche le domande 9.1 e 10.2.
5.13: È strano. C'è la garanzia che NULL sia 0, ma non che il puntatore nullo sia rappresentato con 0?
Quando si usa in modo informale il termine "nullo" o "NULL", si possono intendere diverse cose:
1. Il puntatore nullo concettuale, ovvero il concetto astratto presente nella definizione del linguaggio e definito nella domanda 5.1. Questo è implementato con...
2. La rappresentazione interna (o a tempo di esecuzione) di un puntatore nullo, che potrebbe, ma non necessariamente deve, essere con tutti i bit a 0, e che può essere diversa per diversi tipi puntatore. I valori effettivi dovrebbero interessare solo a chi scrive i compilatori. I programmatori C non li vedono mai, perché usano...
3. La costante puntatore nullo, che è uno 0 intero costante (vedi domanda 5.2). Questa costante è spesso nascosta dietro...
4. La macro NULL, che è definita da una #define come "0" o "((void *)0)" (vedi domanda 5.4). Infine, ma non c'entrano nulla, ci sono...
5. Il carattere nullo del codice ASCII (NUL), che ha in effetti tutti i bit a zero, ma non ha altra relazione con il puntatore nullo che nel nome; e...
6. La stringa nulla, che è un altro nome per la stringa vuota (""). Usare il termine "stringa nulla " può creare confusione in C, perché una stringa vuota è data da un carattere nullo ('\0'), ma non, da un puntatore nullo; e questo chiude il cerchio...
Questo articolo usa l'espressione "puntatore nullo" (in minuscolo) per il significato 1, il carattere "0" o l'espressione "costante puntatore nullo" per il senso 3, e la parola maiuscola NULL per il significato 4.
5.14: Come mai c'è tutta questa confusione riguardo ai puntatori nulli? Perché queste domande sono così ricorrenti?
I programmatori C tradizionalmente vogliono sapere più di quanto gli servirebbe a proposito dell'implementazione sulla macchina sottostante. Il fatto che i puntatori nulli siano rappresentati come 0 tanto nel codice sorgente quanto internamente su molte macchine, favorisce assunzioni incaute. L'uso di una macro di preprocessore (NULL) suggerisce erroneamente che il valore potrebbe cambiare in futuro, o essere diverso su qualche macchina bizzarra. Il costrutto "if(p == 0)" spesso viene erroneamente interpretato come una richiesta di conversione di p in un intero prima del confronto, anziché che come richiesta di conversione di 0 a un tipo puntatore. Infine, la distinzione fra i diversi usi del termine "null" o "nullo" (elencata nella domanda 5.13 sopra) è spesso trascurata.
Un buon modo per liberarsi di questa confusione è quello di immaginare che il C abbia una parola chiave (per esempio "nil", come il Pascal) come costante puntatore nullo. A quel punto il compilatore potrebbe trasformare "nil" nel tipo corretto di puntatore nullo qualora possa determinare tale tipo dal codice, o lamentarsi se non può. Ora, in effetti, in C la parola chiave per un puntatore nullo non è "nil" ma "0", il che va quasi altrettanto bene, eccetto che uno "0" in un contesto non-puntatore genera uno zero intero invece di un messaggio di errore, e se quello 0 doveva essere un puntatore nullo, il codice potrebbe non funzionare.
5.15: Sono confuso. Non capisco niente di tutta questa faccenda dei puntatori nulli.
Basta seguire queste due semplici regole:
1. Quando si vuole una costante puntatore nullo nel sorgente, usare "0" o "NULL".
2. Se "0" o "NULL" sono argomeni in una chiamata a funzione, fare il cast al tipo puntatore aspettato dalla funzione.
Il resto della discussione riguarda errori di comprensione di altre persone, la rappresentazione interna dei puntatori nulli (che non dovrebbe interessarci), e i raffinamenti dell'ANSI C. Se si sono capite le domande 5.1, 5.2, e 5.4, e si tengono presente le 5.3, 5.9, 5.13, e 5.14, si è a posto.
5.16: Vista tutta la confusione che c`è sui puntatori nulli, non sarebbe più semplice richiedere che siano rappresentati internamente come zeri?
Come minimo, questo sarebbe poco saggio perché imporrebbe dei vincoli inutili a quelle implementazioni che rappresenterebbero più naturalmente i puntatori nulli attraverso speciali pattern di bit, diversi da zero, per esempio perché tali valori causano trap hardware automatici per gli accessi non validi.
Inoltre, cosa si guadagnerebbe con un simile requisito? La comprensione dei puntatori nulli non richiede la conoscenza della rappresentazione interna, sia essa zero o no. Assumere che i puntatori nulli siano internamente zero non semplifica la scrittura del codice (fatta eccezione per un modo poco saggio di usare la calloc(); vedi la domanda 7.31). Puntatori interni a zero non ovvierebbero alla necessità di eseguire un cast nelle chiamate a funzione, perché la *dimensione* del puntatore potrebbe essere comunque diversa da quella di un int. (Se si usasse "nil" per richiedere puntatori nulli, come menzionato nella domanda 5.14, la necessità di assumere una rappresentazione interna a zero non nascerebbe neanche).
5.17: Seriamente, esistono effettivamente delle macchine che rappresentano i puntatori nulli con valori diversi da zero, o che usano diverse rappresentazioni interne per puntatori a diversi tipi?
La serie Prime 50 usava l'indirizzo segmento 07777, offset 0 per il puntatore nullo, almeno per PL/I. Modelli più recenti usavano segmento 0, offset 0 per il puntatore nullo C, cosa che introdusse la necessità di nuove istruzioni come TCNP (Test C Null Pointer), evidentemente come as a concessione per tutto il deplorevole codice C esistente che faceva assunzioni scorrette. È anche noto che macchine Prime più vecchie richiedevano puntatori a byte (char *) più grandi dei puntatori a parola (int *).
La serie Eclipse MV della Data General fornisce supporto architetturale per tre formati di puntatore (a parola, a byte e a bit), due dei quali sono usati dai compilatori C: i puntatori a byte per char * e void * e i puntatori a parola per tutto il resto.
Alcuni mainframe Honeywell-Bull usano il pattern di bit 06000 per i puntatori nulli (interni).
La serie Cyber 180 della CDC ha puntatori a 48 bit composti da anello, segmento e offset. Per la maggior parte degli utenti (nell'anello 11) il puntatore nullo è 0xB00000000000. Nelle vecchie macchine CDC in complemento a 1 era comune usare parole con tutti i bit a uno come valore speciale per tutti i tipi di dati, inclusi gli indirizzi non validi.
La vecchia serie HP 3000 usa uno schema di indirizzamento diverso per i byte e le parole; di conseguenza, come molte altre delle macchine citate sopra, usa per i char* e i void* una rappresentazione diversa da quella usata per gli altri puntatori.
La Symbolics Lisp Machine, che ha una architettura a tag, non ha neppure puntatori convenzionali numerici; usa la coppia (essenzialmente un handle