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.



Le FAQ di comp.lang.c

Newsgroups: comp.lang.c,comp.lang.c.moderated,comp.answers,news.answers
Archive-name: C-faq/faq
Comp-lang-c-archive-name: C-FAQ-list
URL: http://www.eskimo.com/~scs/C-faq/top.html

[Ultima modifica 5 Settembre 1996]


Premessa

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).

1. Dichiarazioni e inizializzazioni

1.1: Come decido quale tipo di interi usare?

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.

Riferimenti: K&R1 Sez. 2.2, Sez. A4.2, Sez. B11 p. 257; ANSI Sez. 2.2.4.2.1, Sez. 3.1.2.5; ISO Sez. 5.2.4.2.1, Sez. 6.1.2.5; H&S Sez. 5.1, 5.2.

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.

Riferimenti: K&R1 Sez. 4.5; K&R2 Sez. 4.4; ANSI Sez. 3.1.2.2, Sez. 3.7, Sez. 3.7.2, Sez. F.5.11; ISO Sez. 6.1.2.2, Sez. 6.7, Sez. 6.7.2, Sez. G.5.11; Rationale Sez. 3.1.2.2; H&S Sez. 4.8, Sez. 9.2.3; CT&P Sez. 4.2.

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

extern int f();

e

int f();

Riferimenti: ANSI Sez. 3.1.2.2, Sez. 3.5.1; ISO Sez. 6.1.2.2, Sez. 6.5.1; Rationale Sez. 3.1.2.2; H&S Sez. 4.3, Sez. 4.3.1.

1.12: A cosa serve la parola chiave "auto"?

A niente, è arcaica. Vedi anche la domanda 20.37.

Riferimenti: K&R1 Sez. A8.1 p. 193; ANSI Sez. 3.1.2.4, Sez. 3.5.1; ISO Sez. 6.1.2.4, Sez. 6.5.1; H&S Sez. 4.3, Sez. 4.3.1.

1.14: Non riesco a dichiarare una lista. Ho provato con

typedef struct {
char *item;
NODEPTR next;
} *NODEPTR;

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.

Riferimenti: K&R1 Sez. 6.5; K&R2 Sez. 6.5; ANSI Sez. 3.5.2, Sez. 3.5.2.3, esempi; ISO Sez. 6.5.2, Sez. 6.5.2.3; H&S Sez. 5.6.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: 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).

Riferimenti: K&R1 Sez. 4.9; K&R2 Sez. 4.9 pp. 85-86; ANSI Sez. 3.5.7, Sez. 4.10.3.1, Sez. 4.10.5.3; ISO Sez. 6.5.7, Sez. 7.10.3.1, Sez. 7.10.5.3; H&S Sez. 4.2.8, Sez. 4.6, Sez. 4.6.2, Sez. 4.6.3, Sez. 16.1.

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?

char a[] = "string literal";
char *p = "string literal";

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).

Vedi anche le domande 1.31, 6.1, 6.2, e 6.8.

Riferimenti: K&R2 Sez. 5.5; ANSI Sez. 3.1.4, Sez. 3.5.7; ISO Sez. 6.1.4, Sez. 6.5.7; Rationale Sez. 3.1.4; H&S Sez. 2.7.4.

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).

Vedi anche le domande 1.25 e 4.12.


2. Strutture, unioni, ed enumerazioni

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).

Riferimenti: K&R1 Sez. 6.2; K&R2 Sez. 6.2; ANSI Sez. 3.1.2.5, Sez. 3.2.2.1, Sez. 3.3.16; ISO Sez. 6.1.2.5, Sez. 6.2.2.1, Sez. 6.3.16; H&S Sez. 5.6.2.

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.

Riferimenti: K&R2 Sez. 6.2; ANSI Sez. 4.11.4.1 nota 136; Rationale Sez. 3.3.9; H&S Sez. 5.6.2.

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.

Vedi domanda 2.15 per un suggerimento di uso.

Riferimenti: ANSI Sez. 4.1.5; ISO Sez. 7.1.6; Rationale Sez. 3.5.4.2; H&S Sez. 11.1.

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).

Riferimenti: K&R2 Sez. 6.8; ANSI Sez. 3.5.7; ISO Sez. 6.5.7; H&S Sez. 4.6.7.

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.

Riferimenti: K&R2 Sez. 2.3, Sec. A4.2; ANSI Sez. 3.1.2.5, Sez. 3.5.2, Sez. 3.5.2.2, Appendix E; ISO Sez. 6.1.2.5, Sez. 6.5.2, Sez. 6.5.2.2, Annex F; H&S Sez. 5.5, Sez. 5.11.2.

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).


3. Espressioni

3.1: Perché il seguente codice non funziona?

a[i] = i++;

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).

Riferimenti: K&R1 Sez. 2.12; K&R2 Sez. 2.12; ANSI Sez. 3.3; ISO Sez. 6.3.

3.2: Sul mio compilatore, il codice:

int i = 7;
printf("%d\n", i++ * i++);

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".

Riferimenti: K&R1 Sez. 2.12; K&R2 Sez. 2.12; ANSI Sez. 3.3; ISO Sez. 6.3; CT&P Sez. 3.7; PCS Sez. 9.5.

3.3: Ho sperimentato il codice

int i = 3;
i = i++;

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.

Riferimenti: K&R1 Sez. 2.12, Sez. A.7; K&R2 Sez. 2.12, Sec. A.7.

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.

Riferimenti: K&R1 Sez. 2.6, Sez. A7.11-12; K&R2 Sez. 2.6, Sez. A7.14-15; ANSI Sez. 3.3.13, Sez. 3.3.14, Sez. 3.3.15; ISO Sez. 6.3.13, Sez. 6.3.14, Sez. 6.3.15; H&S Sez. 7.7, Sez. 7.8, Sez. 7.12.1; CT&P Sez. 3.7.

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.

Vedi anche la domanda 3.9 sotto.

Riferimenti: ANSI Sez. 2.1.2.3, Sez. 3.3, App. B; ISO Sez. 5.1.2.3, Sez. 6.3, App. C; Rationale Sez. 2.1.2.3; H&S Sez. 7.12.1.

3.9: Quindi, dato

a[i] = i++;

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).

Vedi anche la domanda 3.3.

Riferimenti: K&R1 Sez. 2.8; K&R2 Sez. 2.8; ANSI Sez. 3.3.2.4, Sez. 3.3.3.1; ISO Sez. 6.3.2.4, Sez. 6.3.3.1; H&S Sez. 7.4.4, Sez. 7.5.8.

3.14: Perché il codice

int a = 1000, b = 1000;
long int c = a * b;

non funziona?

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.

Riferimenti: K&R1 Sez. 2.7; K&R2 Sez. 2.7; ANSI Sez. 3.2.1.5; ISO Sez. 6.2.1.5; H&S Sez. 6.3.4; CT&P Sez. 3.9 pp. 49-50.

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. Puntatori

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).

Riferimenti: K&R1 Sez. 5.1; K&R2 Sez. 5.1; ANSI Sez. 3.3.2, Sez. 3.3.3; ISO Sez. 6.3.2, Sez. 6.3.3; H&S Sez. 7.4.4, Sez. 7.5, Sez. 7.5.7, 7.5.8.

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.

Riferimenti: K&R2 Sez. A7.5; ANSI Sez. 3.3.4 (nota 14); ISO Sez. 6.3.4; Rationale Sez. 3.3.2.4; H&S Sez. 7.1.

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").

Vedi anche le domande 4.8 e 20.1.

Riferimenti: K&R1 Sez. 1.8, Sez. 5.2; K&R2 Sez. 1.8, Sez. 5.2; ANSI Sez. 3.3.2.2, specialmente nota 39; ISO Sez. 6.3.2.2; H&S Sez. 9.5.

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).

Vedi anche la domanda 1.34.

Riferimenti: K&R1 Sez. 5.12; K&R2 Sez. 5.11; ANSI Sez. 3.3.2.2; ISO Sez. 6.3.2.2; Rationale Sez. 3.3.2.2; H&S Sez. 5.8, Sez. 7.4.3.


5. Puntatori NULL

5.1: Che cos'è il puntatore nullo?

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).

Riferimenti: K&R1 Sez. 5.4; K&R2 Sez. 5.4; ANSI Sez. 3.2.2.3; ISO Sez. 6.2.2.3; Rationale Sez. 3.2.2.3; H&S Sez. 5.3.2.

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

Riferimenti: K&R1 Sez. A7.7, Sez. A7.14; K&R2 Sez. A7.10, Sez. A7.17; ANSI Sez. 3.2.2.3; ISO Sez. 6.2.2.3; H&S Sez. 4.6.3, Sez. 6.2.7.

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).

Vedi anche la domanda 9.2.

Riferimenti: K&R2 Sez. A7.4.7; ANSI Sez. 3.3.3.3, Sez. 3.3.9, Sez. 3.3.13, Sez. 3.3.14, Sez. 3.3.15, Sez. 3.6.4.1, Sez. 3.6.5; ISO Sez. 6.3.3.3, Sez. 6.3.9, Sez. 6.3.13, Sez. 6.3.14, Sez. 6.3.15, Sez. 6.6.4.1, Sez. 6.6.5; H&S Sez. 5.3.2.

5.4: Che cos'è NULL e come è definito?

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.

Riferimenti: K&R1 Sez. 5.4; K&R2 Sez. 5.4; ANSI Sez. 4.1.5, Sez. 3.2.2.3; ISO Sez. 7.1.6, Sez. 6.2.2.3; Rationale Sez. 4.1.5; H&S Sez. 5.3.2, Sez. 11.1.

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:

#define NUL '\0'

Riferimenti: K&R1 Sez. 5.4; K&R2 Sez. 5.4.

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 inesistente) come puntatore nullo C.

A seconda del modello di memoria usato, i processori della famiglia 8086 (PC compatibili) possono usare puntatori a dati a 16 bit e puntatori a funzione a 32 bit, o viceversa.

Alcune macchine Cray a 64 bit Cray rappresentano gli int* con i 48 bit inferiori di una parola; i char* usano, in più, anche i 16 bit superiori per indicare l'indirizzo di un byte all'interno di una parola.

Riferimenti: K&R1 Sez. A14.4.

5.20: Che cosa significa l'errore a tempo di esecuzione "null pointer assignment"? Come si fa a dargli la caccia?

Questo messaggio, che solitamente capita con i compilatori MSDOS (perciò, vedi la sezione 19), significa che si è cercato di scrivere in una locazione non valida (probabilmente l'offset 0 nel segmento dati di default) attraverso un puntatore nullo (probabilmente non inizializzato)

Un debugger potrebbe consentire di piazzare un breakpoint o un watchpoint o qualcos'altro nella locazione 0. In alternativa, si può scrivere un po' di codice che memorizza una copia di circa 20 byte dalla locazione 0, e periodicamente verifica che la memoria a quella locazione non sia cambiata. Vedi anche la domanda 16.8.


6. Vettori e puntatori

6.1: Ho messo la definizione di char a[6] in un file sorgente, e in un altro ho messo la dichiarazione extern char *a. Perché non ha funzionato?

La dichiatazione extern char *a, semplicemente, non concorda con la definizione. Il tipo puntatore-a- T non è la stessa cosa di vettore-di- T. La dichiarazione da usare è extern char a[].

Riferimenti: ANSI Sez. 3.5.4.2; ISO Sez. 6.5.4.2; CT&P Sez. 3.3, Sez. 4.5.

6.2: Ma ho sentito dire che char a[] è identico a char *a.

Assolutamente no. (Quello che avete sentito, se è corretto, riguarda i parametri formali delle funzioni; vedi la domanda 6.4.) I vettori non sono puntatori. La dichiatazione di vettore char a[6] richiede l'allocazione di un'area di memoria per 6 caratteri, la quale potrà essere poi essere usata col nome "a". Ovvero, c'è una locazione di nome "a" in cui possono sedersi 6 caratteri. La dichiarazione char *p, d'altro canto, richiede un luogo per contenere un puntatore, che potrà poi essere usato con il nome "p". Questo puntatore può puntare quasi ovunque: a qualsiasi carattere, o qualsiasi vettore contiguo di caratteri, o da nessuna parte (vedi anche le domande 5.1 e 1.30).

Come sempre, un disegno vale mille parole. Le dichiarazioni

char a[] = "hello"; char *p = "world";

inizializzano strutture dati che si presentano così:

    +---+---+---+---+---+---+
a:  | h | e | l | l | o |\0 |
    +---+---+---+---+---+---+
    +-----+     +---+---+---+---+---+---+
p:  |  *======> | w | o | r | l | d |\0 |
    +-----+     +---+---+---+---+---+---+

È importante comprendere che un costrutto come x[3] genera codice diverso a seconda che x sia un vettore o un puntatore. Date le dichiarazioni sopra, quando il compilatore vede l'espressione a[3], produce codice che parte alla locazione "a", si sposta di tre avanti, e preleva il carattere che si trova lì. Quando incontra l'espressione p[3], produce codice che parte alla locazione "p", preleva il valore puntatore che si trova lì, aggiunge tre al puntatore, e preleva il carattere puntato. In altre parole, a[3] significa "tre posti dopo (l'inizio de) l'oggetto che *si chiama* a", mentre p[3] significa "tre posti dopo l'oggetto *puntato da* p". Nell'esempio sopra, sia a[3] che p[3] risultano essere il carattere 'l', ma il compilatore ci arriva in modi diversi. (La differenza essenziale è che i valori di un vettore come a e di un puntatore come p sono calcolati in modi diversi *dovunque* compaiano in espressioni, indipendentemente dal fatto che siano o no indicizzati, come spiegato in maggiore dettaglio nella prossima domanda).

Riferimenti: K&R2 Sez. 5.5; CT&P Sez. 4.5.

6.3: Ma allora cosa si intende per "equivalenza di puntatori e vettori" in C?

Molta della confusione a proposito di vettori e puntatori in C può ricondursi a un fraintendimento di questa frase. Dire che vettori e puntatori sono "equivalenti" non significa che siano identici e neanche che siano interscambiabili.

L'"equivalenza" si riferisce alla seguente definizione chiave:

Un lvalue di tipo vettore-di-T che compare in una espressione degenera (con tre eccezioni) in un puntatore al suo primo elemento; il tipo del puntatore risultante è puntatore-a-T. (Le eccezioni sono quando il vettore è l'operando dell'operatore sizeof o dell'operatore &, oppure è un inizializzatore stringa costante per un vettore di caratteri.

Come conseguenza di questa definizione, il compilatore non applica l'operatore di indicizzazione di vettori [] in modo molto diverso per vettori e puntatori, dopo tutto. In un'espressione come a[i], il vettore degenera in un puntatore, secondo la regola sopra, ed è successivamente indicizzato così come accadrebbe a una variabile puntatore nell'espressione p[i] (anche se avverranno differenti accessi in memoria, come spiegato nella domanda 6.2). Se si assegna l'indirizzo di un vettore a un puntatore:

p = a;

successivamente p[3] e a[3] accederanno allo stesso elemento.

Vedi anche la domanda 6.8.

Riferimenti: K&R1 Sez. 5.3; K&R2 Sez. 5.3; ANSI Sez. 3.2.2.1, Sez. 3.3.2.1, Sez. 3.3.6; ISO Sez. 6.2.2.1, Sez. 6.3.2.1, Sez. 6.3.6; H&S Sez. 5.4.1.

6.4: Allora perché le dichiarazioni di vettori e puntatori sono interscambiabili come parametri formali di funzioni?

Si suppone che sia comodo.

Poiché i vettori degenerano immediatamente in puntatori, un vettore non è mai realmente passato a una funzione. Il fatto di consentire a parametri puntatore di essere dichiarati come vettori è semplicemente un modo di dare l'idea che sia il vettore a essere passato - il programmatore potrebbe volere anche enfatizzare che il parametro sarà trattato come un vettore convenzionale, o che verrà passato un vettore (strettamente parlando, il suo indirizzo). Per comodità, quindi, ogni parametro che "sembri" un vettore, per esempio

f(char a[]) { ... }

è trattato dal compilatore come se fosse un puntatore, dato che questo è ciò che la funzione comunque riceverebbe se venisse passato un vettore:

f(char *a) { ... }

Questa conversione vale solo nella dichiarazione di parametri formali di una funzione, e da nessun'altra parte. Se la conversione vi dà fastidio, evitatela; molti hanno concluso che la confusione che essa causa è predominante rispetto al vantaggio di avere una dichiarazione che "assomiglia" alla chiamata o all'uso del parametro all'interno della funzione.

Vedi anche la domanda 6.21.

Riferimenti: K&R1 Sez. 5.3, Sez. A10.1; K&R2 Sez. 5.3, Sez. A8.6.3, Sez. A10.1; ANSI Sez. 3.5.4.3, Sez. 3.7.1, Sez. 3.9.6; ISO Sez. 6.5.4.3, Sez. 6.7.1, Sez. 6.9.6; H&S Sez. 9.3; CT&P Sez. 3.3 pp. 33-4.

6.7: Come è possibile che un vettore sia un lvalue, se non ci si possono fare assegnamenti?

Lo standard ANSI definisce il concetto di "lvalue modificabile", e stabilisce che i vettori non lo sono.

Riferimenti: ANSI Sez. 3.2.2.1; ISO Sez. 6.2.2.1; Rationale Sez. 3.2.2.1; H&S Sez. 7.1.

6.8: Da un punto di vista pratico, qual è la differenza fra vettori e puntatori?

Per i vettori l'allocazione di spazio è automatica, ma non possono essere spostati o ridimensionati. Ai puntatori bisogna esplicitamente assegnare l'indirizzo di spazio allocato (magari usando malloc), ma è possibile riassegnarli (ovvero farli puntare ad altri oggetti) a piacere, e hanno molti altri usi oltre a quello di base per blocchi di memoria.

A causa della cosiddetta equivalenza fra vettori e puntatori (vedi domanda 6.3), i vettori e i puntatori spesso sembrano interscambiaabili, e in particulare un puntatore a un blocco di memoria allocato con malloc è spesso trattato esattamente come se fosse un vero vettore (e vi si può accedere usando []). (Comunque, attenzione con sizeof.) Vedi le domande 6.14 e 6.16.

Vedi anche le domande 1.32 e 20.14.

6.9: Qualcuno mi spiegato che i vettori sono in realtà semplicemente puntatori costanti.

È una semplificazione un po' eccessiva. Il nome di un vettore è "costante" nel senso che non è possibile assegnargli un valore, ma un vettore non è un puntatore, come mostrato dalla discussione e dai disegni nella domanda 6.2. Vedi anche le domande 6.3 e 6.8.

6.11: Ho trovato un pezzo di codice "scherzoso" che contiene l'espressione 5["abcdef"]. Come fa a essere legale in C?

Ebbene si, l'indicizzazione di vettori è commutativa in C. Questo fatto curioso deriva dalla definizione di indicizzazione basata su puntatori, che dice che a[e] è identica a *((a)+(e)), per qualsiasi coppia di espressioni a ed e, a patto che una di esse sia un'espressione puntatore e una sia intera. Questa strana commutatività è spesso menzionata nei testi di C come se fosse qualcosa di cui andare fieri, ma non ha alcuna applicazione utile eccetto nella Obfuscated C Contest (vedi domanda 20.36).

Riferimenti: Rationale Sez. 3.3.2.1; H&S Sez. 5.4.1, Sez. 7.4.1.

6.12: Se i vettori degenerano in puntatori, dato un vettore arr, che differenza c'è fra arr e &arr?

Il tipo.

Nel C Standard, &arr restituisce un puntatore all'intero vettore, di tipo puntatore-a-vettore-di-T. (Nel C pre-ANSI, la & in &arr di solito generava una warning ed era solitamente ignorata). Sotto tutti i compilatori C, un riferimento semplice a un vettore (senza la & esplicita), restituisce un puntatore al primo elemento del vettore, di tipo puntatore-a-T. (Vedi anche le domande 6.3, 6.13, e 6.18.)

Riferimenti: ANSI Sez. 3.2.2.1, Sez. 3.3.3.2; ISO Sez. 6.2.2.1, Sez. 6.3.3.2; Rationale Sez. 3.3.3.2; H&S Sez. 7.5.6.

6.13: Come si dichiara un puntatore a vettore?

Di solito, nessuno vuole dichiararlo. Quando si parla informalmente di un puntatore a un vettore, si intende di solito un puntatore al primo elemento del vettore.

Invece di un puntatore a un vettore, si dovrebbe provare a usare un puntatore a uno degli elementi del vettore. I vettori di tipo T degenerano in puntatori a T (vedi la domanda 6.3), il che è comodo; indicizzare o incrementare il puntatore risultante consente l'accesso ai singoli membri del vettore. I veri puntatori a vettori, quando vengono indicizzati o incrementati, si spostano di un intero vettore a ogni passo, e di solito sono utili al più quando si opera su vettori di vettori. (Vedi la domanda 6.18.)

Se si ha davvero bisogno di dichiarare un puntatore a un intero vettore, si deve usare qualcosa come "int (*ap)[N];" dove N è la dimensione del vettore. (Vedi anche la domanda 1.21.) Se la dimensione del vettore non è nota, si può, in linea di principio, ometterla; ma il tipo risultante, "puntatore a vettore di dimensione ignota", è inutile.

Vedi anche la domanda 6.12 sopra.

Riferimenti: ANSI Sez. 3.2.2.1; ISO Sez. 6.2.2.1.

6.14: Come faccio a decidere la dimensione di un vettore a run-time? Come si fa a creare vettori di dimensione variabile?

L'equivalenza fra vettori e puntatori (vedi domanda 6.3) consente di simulare un vettore in modo molto efficace usando un puntatore a un'area di memoria riservata con malloc(). Dopo aver eseguito

#include int *dynarray; dynarray = malloc(10 * sizeof(int));

(se la chiamata a malloc() ha successo), si può accedere a dynarray[i] (per i da 0 a 9) esattamente come se dynarray fosse un vettore convenzionale allocato staticamente (int a[10]). Vedi anche le domande 1.31a, 6.16, e 7.7.

6.15: Come si fa a dichiarare un vettore locale la cui dimensione si conformi di volta in volta a quella di un vettore passato alla funzione?

Non si può. Le dimensioni dei vettori devono essere costanti a tempo di compilazione. (Il gcc fornisce vettori parametrici come estensione.) È necessario usare malloc(), e ricordarsi di chiamare free() prima che la funzione ritorni. Vedi anche le domande 6.14, 6.16, 6.19, 7.22, e magari 7.32.

Riferimenti: ANSI Sez. 3.4, Sez. 3.5.4.2; ISO Sez. 6.4, Sez. 6.5.4.2.

6.16: Come si fa ad allocare dinamicamente un vettore multidimensionale?

Di solito è preferibile allocare un vettore di puntatori, e poi inizializzare ciascun puntatore con l'indirizzo di una "riga" allocata dinamicamente. Ecco un esempio a due dimensioni:

#include <stdlib.h>

int **array1 = (int **)malloc(nrrighe * sizeof(int *));
for(i = 0; i < nrrighe; i++)
  array1[i] = (int *)malloc(nrcolonne * sizeof(int));

(In un vero programma, naturalmente, bisognerebbe controllare tutti i valori di ritorno delle malloc).

Usando un po' di aritmetica dei puntatori esplicita, è possibile forzare l'allocazione contigua di tutto il contenuto del vettore, al prezzo di rendere più difficile eventuali riallocazioni successive di singole righe:

int **array2 = (int **)malloc(nrrighe * sizeof(int *));
array2[0] = (int *)malloc(nrows * nrcolonne * sizeof(int));
for(i = 1; i < nrrighe; i++)
  array2[i] = array2[0] + i * nrcolonne;

In entrambi i casi, si può accedere agli elementi del vettore dinamico usando indici apparentemente normali: arrayx[i][j] (per 0 <= i < NROWS e 0 <= j < NCOLUMNS).

Se la doppia indirezione implicata dagli schemi qui sopra fosse per qualche motivo inaccettabile, è possibile simulare un vettore bidimensionale con un singolo vettore unidimensionale allocato dinamicamente:

int *array3 = (int *)malloc(nrrighe * nrcolonne * sizeof(int));

Tuttavia, in questo caso sarà necessario fare a mano i calcoli per l'indicizzazione, accedendo all'elemento di posizione (i, j) con array3[i * ncolumns + j]. (Una macro potrebbe nascondere il calcolo esplicito, ma la sua invocazione potrebbe richiedere parentesi e virgole che comunque non sembrerebbero identiche alla sintassi dei vettori multidimensionali, e la macro avrebbe comunque bisogno di accedere anche ad almeno una delle due dimensioni. Vedi anche la domanda 6.19.)

Infine, si possono usare puntatori a vettori:

int (*array4)[NRCOLONNE] = (int (*)[NRCOLONNE])malloc(nrrighe * sizeof(*array4));

ma la sintassi comincia a diventare orribile e almeno una dimensione deve essere specificata a runtime.

Con tutte queste tecniche, naturalmente, può essere necessario ricordarsi di liberare i vettori (il che richiede diversi passi, vedi domanda 7.23) quando non servono più, e non è sempre possibile mischiare vettori allocati dinamicamente con vettori convenzionali, allocati staticamente (vedi domanda 6.20, e anche 6.18).

Tutte queste tecniche possono anche essere estese a vettori a tre o più dimensioni.

6.17: Ecco un bel trucco: se scrivo

int realarray[10];
int *array = &realarray[-1];

posso trattare "array" come se fosse un vettore che parte da 1.

Anche se questa tecnica è attraente (ed era usata nelle vecchie edizioni del libro "Numerical Recipes in C"), non è conforme agli standard. L'aritmetica dei puntatori è definita solo finché il puntatore punta all'interno di uno stesso blocco di memoria allocata o all'elemento "terminatore" immaginario che è posizionato una posizione dopo la memoria allocata; in tutti gli altri casi, il comportamento è indefinito, anche se il puntatore non viene dereferenziato. Il codice di cui sopra potrebbe fallire se, quando l'offset viene sottratto, venisse generato un indirizzo illegale (per esempio, perché l'indirizzo ha cercato di "piegarsi attorno" all'inizio di qualche segmento di memoria).

Riferimenti: K&R2 Sez. 5.3, Sez. 5.4, Sez. A7.7; ANSI Sez. 3.3.6; ISO Sez. 6.3.6; Rationale Sez. 3.2.2.3.

6.18: Il mio compilatore da errore se passo un vettore a due dimensioni a una funzione che si aspetta un puntatore a puntatore.

La regola per cui i vettori degenerano in puntatori (vedi domanda 6.3) non si applica ricorsivamente. Un vettore bidimensionale (ovvero, in C, un vettore di vettori) degenera in un puntatore a vettore, non in un puntatore a puntatore. I puntatori a vettori possono creare confusione, e vanno trattati con cautela; vedi anche la domanda 6.13. (La confusione è aumentata dal fatto che alcuni compilatori scorretti, come alcune vecchie versioni di pcc e di lint derivati da pcc, accettano impropriamente assegnamenti di vettori multidimensionali a puntatori multi-livello.)

Se si passa un vettore bidimensionale a una funzione:

int array[NRRIGHE][NRCOLONNE];
f(array);

la dichiarazione della funzione dev'essere compatibile:

f(int a[][NRCOLONNE])

oppure

f(int (*ap)[NRCOLONNE]) /* ap è un puntatore a un vettore */

Nella prima dichiarazione, il compilatore esegue la normale sostituzione implicita del parametro da "vettore di vettori" a "puntatore a puntatore" (vedi le domande 6.3 e 6.4); nella seconda forma la dichiarazione come puntatore è esplicita. Poiché la funzione chiamata non alloca spazio per il vettore, non ha bisogno di conoscerne le dimensioni complessive, cosicché il numero delle righe NROWS può essere omesse. La "forma" del vettore è comunque importante, ragion per cui il numero di colonne NCOLUMNS (e, nel caso di vettori a tre o più dimensioni, tutte le altre dimensioni aggiunte) devono essere conservate.

Se una funzione è dichiarata ricevere un puntatore a puntatore, probabilmente è privo di senso passarle direttamente un vettore bidimensionale.

Vedi anche le domande 6.12 e 6.15.

Riferimenti: K&R1 Sez. 5.10; K&R2 Sez. 5.9; H&S Sez. 5.4.3.

6.19: Come si fa a scrivere funzioni che accettano vettori a due dimensioni quando la "larghezza" non è nota a tempo di compilazione?

Non è semplice. Un modo consiste nel passare un puntatore all'elemento [0][0], insieme alle dimensioni, e simulare l'indicizzazione "a mano":

f2(int *aryp, int nrrighe, int nrcolonne)
{ ... ad array[i][j] accedo con aryp[i * nrcolonne + j] ... }

Questa funzione può essere chiamata passandole il vettore della domanda 6.18 nel seguente modo:

f2(&array[0][0], NRRIGHE, NRCOLONNE);

Si deve comunque notare che un programma che fa indicizzazione "a mano" di vettori multidimensionali non è strettamente conforme allo stardard ANSI; secondo l'interpretazione ufficiale, il comportamento che si ottiene accedendo a (&array[0][0])[x] non è definito per x >= NCOLUMNS.

gcc consente la dichiarazione di vettori locali con dimensione specificata dagli argomenti della funzione, ma è un'estensione non standard.

Se volete usare una funzione su vettori multidimensionali di diverse dimensioni, una soluzione consiste nel simulare tutti i vettori dinamicamente, come nella domanda 6.16.

Vedi anche le domande 6.18, 6.20, e 6.15.

Riferimenti: ANSI Sez. 3.3.6; ISO Sez. 6.3.6.

6.20: Come si fa a usare vettori multidimensionali allocati staticamente e dinamicamente in modo interscambiabile quando li si passa a funzioni?

Non esiste un unico metodo perfetto. Date le dichiarazioni

int array[NRRIGHE][NRCOLONNE];
int **array1; /* frastagliato */
int **array2; /* contiguo */
int *array3; /* "appiattito" */
int (*array4)[NRCOLONNE];

dove i puntatori sono inizializzati come nei frammenti di codice della domanda 6.16, e date le seguenti funzioni

f1(int a[][NRCOLONNE], int nrrighe, int nrcolonne);
f2(int *aryp, int nrrighe, int nrcolonne);
f3(int **pp, int nrrighe, int nrcolonne);

dove f1() accetta un vettore bidimensionale normale, f2() accetta un vettore bidimensionale "appiattito", e f3() accetta un vettore simulato via puntatore-a-puntatore (vedi anche le domande 6.18 e 6.19), le seguenti chiamate dovrebbero funzionare come ci si aspetta:

f1(array, NRRIGHE, NRCOLONNE);
f1(array4, nrrighe, NRCOLONNE);
f2(&array[0][0], NRRIGHE, NRCOLONNE);
f2(*array, NRRIGHE, NRCOLONNE);
f2(*array2, nrrighe, nrcolonne);
f2(array3, nrrighe, nrcolonne);
f2(*array4, nrrighe, NRCOLONNE);
f3(array1, nrrighe, nrcolonne);
f3(array2, nrrighe, nrcolonne);

Le seguenti due chiamate probabilmente funzionano sulla maggior parte dei sistemi, ma coinvolgono cast abbastanza discutibili, e funzionano solo se il ncolumns dinamico è uguale al NCOLUMNS statico:

f1((int (*)[NRCOLONNE])(*array2), nrrighe, nrcolonne);
f1((int (*)[NRCOLONNE])array3, nrrighe, nrcolonne);

Si deve notare, di nuovo, che passare a f2() &array[0][0] (o, equivalentamente, *array) non è strettamente conforme; vedi la domanda 6.19.

Chi capisce perché le chiamate elencate sopra funzionano e perché sono scritte in quel modo, e perché le combinazioni non elencate non funzionerebbero, ha una comprensione molto buona dei vettori e dei puntatori in C.

Invece di preoccuparsi di queste cose, un approccio all'uso di vettori multidimensionali è quello di farli *tutti* dinamici, come nella domanda 6.16. Se non ci sono vettori statici multidimensionali - cioè se tutti i vettori sono allocati come array1 o array2 della domanda 6.16 -si possono scrivere tutte le funzioni come f3().

6.21: Come mai sizeof non riporta correttamente la dimensione di un vettore quando questo è passato come parametro a una funzione?

Il compilatore finge che il parametro vettore sia dichiarato come puntatore (vedi domanda 6.4), e sizeof riporta la dimensione del puntatore.

Riferimenti: H&S Sez. 7.5.2.


7. Allocazione della memoria

7.1: Perché questo frammento non funziona?

char *answer;
printf("Scrivi qualcosa:\n");
gets(answer);
printf("Hai scritto \"%s\"\n", answer);

La variabile puntatore answer, che è passata a gets() come locazione in cui memorizzare la risposta, non è stata inizializzata a puntare ad alcuna area di memoria valida. In altre parole, non sappiamo dove answer punti. (Poiché le variabili locali non sono inizializzate, e solitamente contengono spazzatura, non è neppure garantito che answer sia inizialmente il puntatore nullo. Vedi le domande 1.30 e 5.1.)

Il modo più semplice di correggere il programma è quello di usare un vettore locale, invece di un puntatore, e lasciare che sia il compilatore a occuparsi dell'allocazione di memoria:

#include <stdio.h>
#include <string.h>

char answer[100], *p;
printf("Scrivi qualcosa:\n");
fgets(answer, sizeof answer, stdin);
if((p = strchr(answer, '\n')) != NULL)
  *p = '\0';
printf("Hai scritto \"%s\"\n", answer);

Questo esempio usa anche fgets() invece di gets(), in modo che il vettore non possa essere scritto oltre l'estremità. (Vedi domanda 12.23. Sfortunatamente per questo esempio, fgets non rimuove automaticamente il '\n' finale, come fa gets()). Sarebbe anche possibile usare malloc() per allocare il buffer answer.

7.2: Non riesco a far funzionare strcat(). Ho provato

char *s1 = "Hello, ";
char *s2 = "world!";
char *s3 = strcat(s1, s2);

ma ho avuto uno strano risultato.

Come nel caso della domanda 7.1, il problema principale in questo caso è che lo spazio per il risultato della concatenazione non è correttamente allocato. Il C non fornisce un tipo stringa gestito automaticamente. I compilatori C allocano memoria soltanto per oggetti esplicitamente menzionati nel sorgente; nel caso delle stringhe, questi possono essere vettori di caratteri o stringhe letterali. Il programmatore deve predisporre spazio sufficiente per il risultato di operazioni a tempo di esecuzione come la concatenazione di stringhe, di solito dichiarando vettori, o chiamando malloc().

strcat() non esegue alcuna allocazione; la seconda stringa è appesa in coda alla prima. Quindi, un modo per risolvere il problema consiste nel dichiarare la prima stringa come vettore:

char s1[20] = "Hello, ";

Poiché strcat() ritorna il valore del suo primo argomento (s1, in questo caso), la variabile è superflua.

La chiamata a strcat() nella domanda in effetti ha due problemi: la string letterale puntata da s1, oltre a non essere abbastanza grande da contenere testo concatenato, non è neppure necessariamente scrivibile. Vedi la domanda 1.32.

Riferimenti: CT&P Sez. 3.2.

7.3: Ma il man dice che strcat accetta due char* come argomenti. Come faccio a sapere che devo allocare qualcosa?

In generale, quando si usano i puntatori si deve sempre considerare il problema dell'allocazione di memoria, se non altro per accertarsi che il compilatore la esegua per noi. Se la documentazione di una funzione di libreria non menziona esplicitamente l'allocazione di memoria, solitamente è implicito che se ne debba occupare il chiamante.

La sezione "synopsis" in testa ai manuali alla Unix o alla ANSI può essere fuorviante. I frammenti di codice presentati sono più vicini alla definizione della funzione usata dall'implementatore che alle invocazioni del chiamante. In particolare, molte funzioni che accettano puntatori (per esempio, a strutture o stringhe) devono essere in effetti chiamate passando l'indirizzo di un oggetto (una struttura, o un vettore - vedi le domande 6.3 e 6.4). Altri esempi comuni sono time() (vedi domanda 13.12) e stat().

7.5: Ho una funzione che dovrebbe tornare una stringa, ma quando torna al chiamante, la stringa tornata contiene spazzatura.

Bisogna assicurarsi che la memoria puntata sia correttamente allocata. Il puntatore tornato dovrebbe puntare a un buffer allocato staticamente, o a un buffer passato dal chiamante, o a memoria ottenuta con malloc(), ma non a un vettore locale (automatico). In altre parole, mai fare qualcosa come

char *itoa(int n)
{
  char retbuf[20]; /* SBAGLIATO */
  sprintf(retbuf, "%d", n);
  return retbuf; /* SBAGLIATO */
}

Una soluzione (imperfetta, specialmente se la funzione è chiamata ricorsivamente, o se più valori di ritorno sono necessari contemporaneamente), sarebbe dichiarare il buffer da usare per il ritorno come

static char retbuf[20];

Vedi anche le domande 12.21 e 20.1.

Riferimenti: ANSI Sez. 3.1.2.4; ISO Sez. 6.1.2.4.

7.6: Perché la chiamata a malloc() mi genera in compilazione il messaggio "warning: assignment of pointer from integer lacks a cast"?

Manca l'inclusione di <stdlib.h>, o un'altra dichiarazione appropriata di malloc(). Vedi anche la domanda 1.25.

Riferimenti: H&S Sez. 4.7.

7.7: Perché in certi programmi il valore tornato da malloc() viene accuratamente convertito con un cast al tipo di puntatore allocato?

Prima che lo Standard ANSI/ISO introducesse il puntatore generico void*, queste conversioni erano necessarie per evitare warning (e in alcuni casi indurre conversioni) quando si assegnava fra tipi puntatore incompatibili.

Nello Standard ANSI/ISO questi cast non sono più necessari, e anzi la pratica attuale li scoraggia perché possono sopprimere warning importanti nel caso in cui malloc() non sia dichiarata correttamente; vedi domanda 7.6 sopra.

Riferimenti: H&S Sez. 16.1.

7.8: Ho visto codice come il seguente:

char *p = malloc(strlen(s) + 1);
strcpy(p, s);

Non dovrebbe essere malloc((strlen(s) + 1) * sizeof(char))?

Non è mai necessario moltiplicare per sizeof(char), perché sizeof(char), per definizione, è esattamente 1. (D'altra parte, moltiplicare per sizeof(char) non fa male, e in alcuni casi può aiutare perché introduce il tipo size_t nell'espressione) Vedi anche la domanda 8.9.

Riferimenti: ANSI Sez. 3.3.3.4; ISO Sez. 6.3.3.4; H&S Sez. 7.5.2.

7.14: Ho sentito che alcuni sistemi operativi non allocano effettivamente la memoria richiesta da malloc() fino a quando il programma non cerca effettivamente di usarla. È legale?

È dificile a dirsi. Lo Standard non dice che un sistema possa farlo, ma non dice neanche esplicitamente che non possa.

Riferimenti: ANSI Sez. 4.10.3; ISO Sez. 7.10.3.

7.16: Sto allocando un grosso vettore per certi calcoli numerici, usando

double *array = malloc(300 * 300 * sizeof(double));

malloc() non ritorna null, ma il programma si comporta in modo strano, come se stesse sovrascrivendo la memoria, o come se malloc() non stia allocando tutta la memoria che ho richiesto, o qualcosa del genere.

Si noti che 300 x 300 è 90000, numero che non sta in un int a 16 bit, anche prima di multiplicarlo per sizeof(double) (vedi domanda 1.1). Quando si ha bisogno di allocare così tanta memoria, bisogna fare attenzione. Se size_t (il tipe accettato da malloc()) è un tipo a 32 bit sulla macchina, ma int è a 16 bit, si può risolvere scrivendo 300 * (300 * sizeof(double)) (vedi domanda 3.14). Altrimenti, occorrerà spezzare la struttura dati in pezzi più piccoli, o usare una macchina a 32 bit, o usare routine di allocazione della memoria non standard. Vedi anche la domanda 19.23.

7.17: Ho 8 mega di memoria nel mio PC. Perché riesco a fare la malloc() solo di 640K o giù di lì?

Nell'architettura segmentata dei PC-compatibili, può essere difficile usare più di 640K in modo trasparente. Vedi anche la domanda 19.23.

7.19: Il mio programma si pianta, apparentemente da qualche parte durante malloc(), ma non ci vedo niente di sbagliato.

Sfortunatamente, è molto facile corrompere le strutture dati interne di malloc, e i problemi che ne conseguono possono essere difficili da eliminare. La fonte di problemi più comune consiste di scrivere in una zona allocata con malloc() più di quanto questa possa contenere; un bug particolarmente comune è fare malloc(strlen(s)) invece di strlen(s) + 1. Altri problemi possono coinvolgere l'uso di puntatori a memoria liberata con la free, fare la free più volte sullo stesso puntatore, fare la free su puntatori non ottenuti con malloc, o cercare di fare la realloc su un puntatore nullo (vedi la domanda 7.30).

Vedi anche le domande 7.26, 16.8, e 18.2.

7.20: Non si può usare la memoria allocata dinamicamente dopo averla rilasciata con free, giusto?

Giusto. Alcuni vecchi manuali per malloc() dicevano che il contenuto della memoria liberata era "lasciato indisturbato", ma questa garanzia non è mai stata universale e non è richiesta dallo Standard.

Pochi programmatori userebbero deliberatamente il contenuto di memoria liberata, ma è facile farlo per sbaglio. Si consideri il seguente codice (corretto) per liberare una lista a link singolo:

struct list *listp, *nextp;
for(listp = base; listp != NULL; listp = nextp) {
nextp = listp->next;
free((void *)listp);
}

e si pensi a cosa accadrebbe se si fosse usata la più ovvia espressione di ciclo, listp = listp->next, senza il puntatore temporaneo nextp.

Riferimenti: K&R2 Sez. 7.8.5; ANSI Sez. 4.10.3; ISO Sez. 7.10.3; Rationale Sez. 4.10.3.2; H&S Sez. 16.2; CT&P Sez. 7.10.

7.21: Perché i puntatori non diventano nulli dopo la free()? Quanto è pericoloso usare (assegnare, confrontare) un valore di puntatore dopo che è stata fatta la free()?

Quando si chiama la free(), la memoria puntata dal puntatore passato viene rilasciata, ma il valore del puntatore nel chiamante rimane probabilmente immutato, perché la semantica "passaggio per valore" del C significa che le funzioni chiamate non cambiano mai in modo permanente il valore dei loro argomenti (Vedi anche la domanda 4.8.)

Un valore puntatore che è stato liberato, strettamente parlando, è non valido, e qualsiasi uso di un valore non valido, anche se non derefenziato, può teoricamente causare dei problemi, anche se da un punto di vista pratico, la maggior parte delle implementazioni probabilmente non genererano eccezioni per usi innocui di puntatori non validi.

Riferimenti: ANSI Sez. 4.10.3; ISO Sez. 7.10.3; Rationale Sez. 3.2.2.3.

7.22: Quando chiamo malloc() per allocare memoria su un puntatore locale, devo poi esplicitamente liberarla con free()?

Si. Si ricordi che un puntatore è una cosa diversa da ciò a cui punta. Le variabili locali vengono deallocate quando una funzione ritorna, ma nel caso di una variabile puntatore questo significa che viene deallocato il puntatore, *non* ciò a cui punta. La memoria allocata con malloc() persiste sempre finché non viene esplicitamente rilasciata. In generale, per ogni chiamata a malloc(), ci dovrebbe essere una chiamata corrispondente a free().

7.23: Sto allocando strutture che contengono puntatori ad altri oggetti allocati dinamicamente. Quando libero la struttura, devo anche invocare free() su ognuno di questi puntatori secondari?

Si. In generale, bisogna fare in modo che ogni puntatore tornato da malloc() sia passato a free(), esattamente una volta. Una buona regola pratica è verificare di essere in grado di identificare, per ogni chiamata a malloc() in un programma, la chiamata a free() che libera la memoria da essa allocata.

Vedi ache la domanda 7.24.

7.24: È necessario liberare tutta la memoria allocata prima dell'uscita dal programma?

Non dovrebbe esserlo. Un vero sistema operativo certamente rilascia tutta la memoria quando un programma termina. Ciononostante, si dice che alcuni personal computer non recuperino la memoria in modo affidabile, e tutto ciò che si può inferire dallo Standard ANSI/ISO è che questo è un problema di "qualità dell'implementazione".

Riferimenti: ANSI Sez. 4.10.3.2; ISO Sez. 7.10.3.2.

7.25: Ho un programma che richiede con malloc(), e successivamente rilascia con free(), molta memoria, ma la sua occupazione di memoria (secondo quanto riportato da ps) sembra non ridursi con la free.

La maggior parte delle implementazioni di malloc/free non restituiscono la memoria liberata al sistema operativo, ma la rendono semplicemente disponibile per malloc() successive.

7.26: Come fa free() a sapere quanti byte liberare?

L'implementazione di malloc/free ricorda la dimensione di ogni blocco di memoria allocato e rilasciato, perciò non è necessario ricordarle la dimensione da liberare.

7.27: Quindi si può interrogare la libreria per sapere quanto è grande un blocco allocato?

Non in modo portabile.

7.30: È legale passare un puntatore nullo come primo argomento a realloc()? A cosa serve?

L'ANSI C sancisce la legalità di questo uso (e della realloc(..., 0), che libera), anche se molte implementazioni meno recenti non lo supportano, ragion per cui potrebbe non essere completamente portabile. Passare un puntatore inizialmente nullo a realloc() può semplificare la scrittura di un algoritmo di allocazione incrementale che si auto-inizializza.

Riferimenti: ANSI Sez. 4.10.3.4; ISO Sez. 7.10.3.4; H&S Sez. 16.3.

7.31: Qual è la differenza fra calloc() e malloc()? C'è da fidarsi a usare il riempimento-a-zero di calloc? La free() funziona su memoria allocata con calloc(), o serve una cfree()?

calloc(m, n) è essenzialmente equivalente a

p = malloc(m * n); memset(p, 0, m * n);

Il riempimento è a tutti bit a zero, e quindi *non* garantisce valori puntatori nulli (vedi sezione 5) o valori zero a virgola mobile. È corretto usare free() per liberare la memoria allocata da calloc().

Riferimenti: ANSI Sez. da 4.10.3 a 4.10.3.2; ISO Sez. da 7.10.3 a 7.10.3.2; H&S Sez. 16.1, Sez. 16.2; PCS Sez. 11.

7.32: Cos'è alloca() e perché se ne sconsiglia l'uso?

alloca() alloca memoria che viene automaticamente liberata quando la funzione che ha chiamato alloca() ritorna. In altri termini, la memoria allocata da alloca è locale allo "stack frame" o contesto di una particolare funzione.

alloca() non può essere scritta in modo portabile, ed è difficile da implementare su macchine senza uno stack convenzionale. Il suo uso è problematico (e l'implementazione banale su una macchina basata su stack fallisce) quando il suo valore di ritorno è passato direttamente a un'altra funzione, come in fgets(alloca(100), 100, stdin).

Per queste ragioni, alloca() non appartiene allo Standard e, anche se utile, non deve essere usata in prorami che devono essere ampiamente portabili.

Vedi anche la domanda 7.22.

Riferimenti: Rationale Sez. 4.10.3.


8. Caratteri e stringhe

8.1: Perché

strcat(string, '!');

non funziona?

C'è una differenza molto concreta fra i caratteri e le stringhe, e strcat() concatena stringhe.

I caratteri sono rappresentati in C da piccoli interi che corrispondono al loro valore nel set dei caratteri (vedi anche la domanda 8.6 sotto). Le stringhe sono rappresentate da vettori di caratteri; normalmente si manipola un puntatore al primo carattere del vettore. Non è mai corretto usare un carattere laddove serve una stringa, o viceversa. Per appendere un ! a una stringa, si usi

strcat(string, "!");

Vedi anche le domande 1.32, 7.2, e 16.6.

Riferimenti: CT&P Sez. 1.5.

8.2: Devo controllare se una stringa contiene un particolare valore. Perché questo codice non funziona?

char *string;
...
if(string == "qualcosa") {
nbsp;nbsp;...
}

Le stringhe in C sono rappresentate come vettori di caratteri, e in C non è possibile manipolare (assegnare, confrontare, eccetera) interi vettori. L'operatore == nel frammento di codice sopra confronta due puntatori - il valore del puntatore variabile string e un puntatore alla stringa letterale "value" - per verificare se sono uguali, ovvero se puntano alla stessa cosa (nello stesso posto). Probabilmente questo non è vero, perció il confronto non ha mai successo.

Per confrontare due stringhe, si usa solitamente la funzione di libreria strcmp():

if(strcmp(string, "value") == 0) {
nbsp;nbsp;...
}

8.3: Se si può scrivere

char a[] = "Hello, world!";

perché non si può anche scrivere

char a[14];
a = "Hello, world!";

Le stringhe sono vettori, e non si possono assegnare direttamente. Si usi, invece, strcpy():

strcpy(a, "Hello, world!");

Nota del traduttore: in C, in generale, il fatto che nell'inizializzazione di variabili si usi il simbolo "=" fa pensare che una inizializzazione sia una sorta di abbinamento di dichiarazione e assegnamento, e che pertanto la si possa sempre spezzare in queste due parti. Questo non è vero; le inizializzazioni seguono regole diverse da quelle degli assegnamenti in moltissimi casi. (Per inciso, l'apprendimento del C++ chiarisce ulteriormente questa differenza).

Vedi anche le domande 1.32, 4.2, e 7.2.

8.6: Come si fa a ottenere il valore numerico che corrisponde a un carattere nel set di caratteri, o viceversa?

In C, i caratteri sono rappresentati da piccoli interi che corrispondono al loro valore (nel set di caratteri della macchina), quindi non serve una routine di conversione: se hai il carattere, hai il suo valore.

8.9: Penso che il mio compilatore abbia qualcosa che non va: ho notato che sizeof('a') è 2 e non 1 (ovvero, non è sizeof(char)).

Forse è sorprendente, ma le costanti carattere in C sono di tipo int, perció sizeof('a') è sizeof(int) (anche se questo è diverso in C++). Vedi anche la domanda 7.8.

Riferimenti: ANSI Sez. 3.1.3.4; ISO Sez. 6.1.3.4; H&S Sez. 2.7.3.


9. Espressioni booleane

9.1: Qual è il tipo giusto da usare per i valori booleani in C? Perché non è un tipo standard? È meglio usare delle #define o una enumerazione per i valori vero e falso?

Il C non fornisce un tipo booleano standard in parte perché sceglierne uno comporta una scelta fra spazio e tempo che può essere fatta nel modo migliore solo dal programmatore. (Usare un int può andare più veloce, mentre usare un char può risparmiare spazio. I tipi più piccoli possono comunque rendere più grosso e/o più lento il codice generato, se richiedono molte conversioni a e da int.)

La scelta fra usare due #define o una enumerazione per i valori vero e falso è arbitrario e non molto interessante (vedi anche le domande 2.22 e 17.10). Si usi una qualsiasi delle seguenti soluzioni

#define VERO 1
#define FALSO 0

enum bool {falso, vero};

o semplicemente 1 e 0, purché si faccia la scelta in modo consistente nell'intero programma o progetto. (Una enumerazione può essere preferibile se il vostro debugger mostra i nomi delle costanti enumerative quando si esaminano le variabili.)

Alcuni preferiscono varianti come

#define TRUE (1==1)
#define FALSE (!TRUE)

o definiscono macro "di appoggio" come

#define Istrue(e) ((e) != 0)

Queste tecniche non fanno guadagnare nulla (vedi la domanda 9.2 sotto; vedi anche le domande 5.12 e 10.2).

9.2: Non è pericoloso usare una #define di TRUE come 1, visto che qualsiasi valore diverso da zero viene considerato "vero" in C? Cosa succede se un operatore logico o relazionale "ritorna" qualcosa che non sia 1?

È vero che qualsiasi valore diverso da zero viene considerato vero in C, ma questo vale solo "in input", ovvero laddove è atteso un valore booleano. Quando un valore booleano è generato da un operatore predefinito, è garantito che esso sia o 1 o 0. Quindi, il test

if((a == b) == TRUE)

funzionerebbe come previsto (se TRUE è 1), ma è chiaramente stupido. In generale, test espliciti di confronto con TRUE e FALSE sono sconsigliabili, perché alcune funzioni di libreria (tra cui isupper(), isalpha(), ecc.) ritornano, in caso di successo, un valore diverso da zero che *non è* necessariamente 1. (Inoltre, se uno pensa che "if((a == b) == TRUE)" sia meglio di "if(a == b)", perché fermarsi qui? Perché non usare "if(((a == b) == TRUE) == TRUE)"?). Una buona regola pratica è usare TRUE e FALSE (o simili) solo negli assegnamenti a variabili booleane o parametri di funzione, o come valore di ritorno da una funzione booleana, ma mai in un confronto.

Le macro di preprocessore TRUE e FALSE (e, naturalmente, NULL) sono usate per leggibilità del codice, non perché i valori effettivi possono cambiare. (Vedi anche le domande 5.3 e 5.10.)

D'altra parte, valori e definizioni booleane possono evidentemente generare confusione, e alcuni programmatori pensano che TRUE e FALSE peggiorino solo le cose. (Vedi anche la domanda 5.9.)

Riferimenti: K&R1 Sez. 2.6, Sez. 2.7; K&R2 Sez. 2.6, Sez. 2.7, Sez. A7.4.7, Sez. A7.9; ANSI Sez. 3.3.3.3, Sez. 3.3.8, Sez. 3.3.9, Sez. 3.3.13, Sez. 3.3.14, Sez. 3.3.15, Sez. 3.6.4.1, Sez. 3.6.5; ISO Sez. 6.3.3.3, Sez. 6.3.8, Sez. 6.3.9, Sez. 6.3.13, Sez. 6.3.14, Sez. 6.3.15, Sez. 6.6.4.1, Sez. 6.6.5; H&S Sez. 7.5.4, Sez. 7.6.4, Sez. 7.6.5, Sez. 7.7, Sez. 7.8, Sez. 8.5, Sez. 8.6; "Ciò che la tartaruga disse ad Achille".

9.3: if(p), dove p è un puntatore, è una condizione valida?

Si. Vedi domanda 5.3.


10. Preprocessore

10.2: Ecco un paio di belle macro:

#define begin {
#define end }

Che ne pensate?

Blah.

Vedi anche sezione 17.

10.3: Come si fa a scrivere una macro generica per scambiare due valori?

Non c'è una buona risposta a questa domanda. Se i valori sono interi, si potrebbe usare un trucco ben noto che usa l'OR esclusivo, ma questo non funziona per numeri in virgola mobile o puntatori, o se i due valori sono la stessa variabile (e la "ovvia" implementazione supercompressa per tipi interi a^=b^=a^=b è illegale a causa dei molteplici effetti collaterali; vedi domanda 3.2). Se la macro deve funzionare con valori di qualsiasi tipo (ciò che solitamente si desidera), non può usare una variabile temporanea, poiché non sa di che tipo deve essere (e anche se lo sapesse le risulterebbe piuttosto difficile darle un nome), e il C standard non fornisce un operatore typeof.

La migliore soluzione è probabilmente lasciar perdere, a meno che si sia disposti a passarle il tipo come terzo argomento.

10.4: Qual è il modo migliore di scrivere una macro a più istruzioni (statement)?

Solitamente si vuole scrivere una macro che possa essere invocata come se fosse una singola chiamata di funzione. Ciò significa che il "chiamante" dovrà fornire il punto e virgola finale, che può quindi non esserci nel corpo della macro. Di conseguenza il corpo della macro non può essere un'istruzione composta racchiusa fra graffe, perché ne risulterebbero errori di sintassi se fosse invocata (apparentemente come istruzione singola, ma con un punto e virgola extra) come ramo if di una istruzione if/else con una clausola else esplicita.

La soluzione tradizionale è quindi usare

#define MACRO(arg1, arg2) do { \
/* declarations */ \
stmt1; \
stmt2; \
/* ... */ \
} while(0) /* (niente ; ) */

Quando il chiamante appende un punto e virgola, l'espansione diventa una singola istruzione indipendentemente dal contesto. (Un compilatore ottimizzante rimuoverà ogni condizione "morta" o i rami della condizione costante 0, anche se lint potrebbe protestare.)

Se tutte le istruzioni della macro desiderata sono espressioni semplici, senza dichiarazioni né cicli, un'altra tecnica consiste nello scrivere una singola espressione parentesizzata usando uno o più operatori virgola. (Come esempio, si veda la prima macro DEBUG() nella domanda 10.26.) Questa tecnica consente anche il "ritorno" di un valore.

Riferimenti: H&S Sez. 3.3.2; CT&P Sez. 6.3.

10.6: Sto spezzando un programma in più file sorgente per la prima volta, e mi chiedo cosa devo mettere nei file .c e cosa nei file .h. (Cosa significa ".h", a proposito?)

Come regola generale, nei file di intestazione, o "header" (.h) vanno le seguenti cose:
  • definizioni di macro (#define)
  • dichiarazioni di strutture, unioni, e enumerazioni
  • dichiarazioni di typedef
  • dichiarazioni di funzioni esterne (vedi anche la domanda 1.11)
  • dichiarazioni di variabili globali
È particolarmente importante mettere una dichiarazione o definizione in un header file se questa deve essere condivisa da più altri file. (In particolare, mai mettere prototipi di funzioni esterne nei file .c. Vedi anche la domanda 1.7.)

D'altra parte, quando una definizione o dichiarazione deve rimanere privata a un file sorgente, va benissimo lasciarla lì.

Vedi anche le domande 1.7 e 10.7.

Riferimenti: K&R2 Sez. 4.5; H&S Sez. 9.2.3; CT&P Sez. 4.6.

10.7: È accettabile che un header file ne includa (#include) un altro?

È una questione di stile, e suscita pertanto un considerevole dibattito. Molti credono che gli #include file "annidati" dovrebbero essere evitati; la prestigiosa Indian Hill Style Guide (vedi domanda 17.9) li critica impietosamente; possono rendere più difficile trovare le definizioni; possono portare a errori di definizioni multiple se un file viene incluso più volte; e rendono la manutenzione manuale dei makefile molto complessa. D'altra parte, rendono possibile un uso modulare degli header file (un header file può includere ciò che gli serve, anziché richiedere che questo sia fatto da chi lo include); un tool come grep (o un file di tag) rende possibile trovare le definizioni dovunque siano; un trucco molto apprezzato del genere

#ifndef HFILENAME_USED
#define HFILENAME_USED
...contenuti del file header...
#endif

(dove un diverso nome di macro "parentesizzante" è usato per ogni header file) rende "idempotenti" gli header file cosicché possono essere inclusi più volte senza problemi; e i tool di manutenzione automatica dei makefile (che sono comunque necessari nei grandi progetti; vedi la domanda 18.1) consentono di gestire facilmente le dipendenze anche nel caso di inclusioni annidate. Vedi anche la domanda 17.10.

Riferimenti: Rationale Sez. 4.1.2.

10.8: Dov'è che vengono cercati gli header file?

Il comportamento esatto è definito dall'implementazione (il che significa che ci si aspetta che sia documentato; vedi la domanda 11.33). Solitamente, gli header nominati con la sintassi <> vengono cercati in uno o più luoghi standard. Gli header nominati con la sintassi "" vengono prima cercati nel "direttorio corrente" e poi (se non vengono trovati) nei posti standard.

Tradizionalmente (specialmente sotto i compilatori Unix), si assume che il direttorio corrente sia quello che contiene il file in cui c'è la direttiva #include. Sotto altri compilatori, comunque, il direttorio corrente (se esiste) è quello in cui è stato invocato il compilatore. Controllate la documentazione del compilatore.

Riferimenti: K&R2 Sez. A12.4; ANSI Sez. 3.8.2; ISO Sez. 6.8.2; H&S Sez. 3.4.

10.9: Ricevo strani errori di sintassi sulla prima dichiarazione di un file, ma sembra a posto.

Forse manca un punto e virgola alla fine dell'ultima dichiarazione nell'ultimo header file che viene incluso. Vedi anche le domande 2.18, 11.29, e 16.2a.

10.11: Sembra che mi manchi un header file di sistema . C'è qualcuno che potrebbe spedirmene una copia?

Gli header standard esistono, in parte, per fornire le definizioni appropriate per uno specifico compilatore, sistema operativo, e processore. Non si può semplicemente prendere una copia degli header file di qualcun altro e aspettarsi che funzionino, a meno che questo qualcuno usi esattamente lo stesso ambiente. Chiedete al produttore del compilatore perché il file non è stato fornito (o di spedirvene una copia).

10.12: Come si fa a costruire espressioni #if di preprocessore che confrontino stringhe?

Non si può fare direttamente; l'aritmetica degli #if di preprocessore usa solo interi. Si possono comunque definire con #define un po' di costanti manifeste, e usarle per implementare le condizioni.

Vedi anche la domanda 20.17.

Riferimenti: K&R2 Sez. 4.11.3; ANSI Sez. 3.8.1; ISO Sez. 6.8.1; H&S Sez. 7.11.1.

10.13: L'operatore sizeof funziona anche nelle direttive #if?

No. La precompilazione avviene durante una fase della compilazione precedente all'analisi dei nomi dei tipi. Invece di sizeof, si consideri la possibilità di usare le costanti predefinite contenute in <limits.h>, se è utile, o magari uno script di "configurazione". (Ancora meglio, si cerchi di scrivere codice che si adatta aumaticamente alle dimensioni dei tipi.)

Riferimenti: ANSI Sez. 2.1.1.2, Sez. 3.8.1; ISO Sez. 5.1.1.2, Sez. 6.8.1; H&S Sez. 7.11.1.

10.14: Posso usare una #ifdef in una linea #define, allo scopo di dare due definizioni alternative di qualcosa?

No. Non si può, per così dire, fare girare il preprocessore "su se stesso". Ciò che si può fare è usare due linee #define separate, incluse o escluse nel sorgente sulla base di una #ifdef.

Riferimenti: ANSI Sez. 3.8.3, Sez. 3.8.3.4; ISO Sez. 6.8.3, Sez. 6.8.3.4; H&S Sez. 3.2.

10.15: Esiste qualcosa di simile a una #ifdef per le typedef?

Sfortunatamente no. Potrebbe essere necessario creare una serie di macro di preprocessore (p.es., MIO_TIPO_DEFINITO) che memorizzano se è stata introdotta una certa typedef. (Vedi anche la domanda 10.13.)

Riferimenti: ANSI Sez. 2.1.1.2, Sez. 3.8.1; ISO Sez. 5.1.1.2, Sez. 6.8.1; H&S Sez. 7.11.1.

10.16: Come si può fare a usare una espressione #if per capire se una macchina è big-endian o little-endian?

Probabilmente non si può. (L'aritmetica del preprocessore usa solo interi lunghi, e non esiste il concetto di indirizzo.) Sei sicuro di avere bisogno di conoscere esplicitamente la "endianità" della macchina? Normalmente è meglio scrivere codice che non se ne preoccupa. Vedi anche la domanda 20.9

Riferimenti: ANSI Sez. 3.8.1; ISO Sez. 6.8.1; H&S Sez. 7.11.1.

10.18: Ho ereditato del codice che contiene decisamente troppe #ifdef's per i miei gusti. Posso preprocessare il codice in modo da lasciare un solo insieme di compilazione condizionale, senza eseguire la normale precompilazione che mi espanderebbe anche tutte le #include e le #define?

Si trovano in giro dei programmi chiamati unifdef, rmifdef, e scpp ("selective C preprocessor") che fanno proprio questo. Vedi la domanda 18.16.

10.19: Come faccio ad avere una lista di tutti gli identificatori predefiniti con #define?

Non c'è un modo standard, anche se è un desiderio molto comune. Se la documentazione del compilatore non dice nulla, il modo più conveniente è probabilmente quello di estrarre le stringhe scrivibili dall'eseguibile di compilatore o preprocessore con qualcosa come la utility Unix strings. Attenzione perché molti degli identificatori predefiniti con #define che si trovano nei compilatori (p.es. "unix") non sono standard (perché entrano in conflitto con lo spazio dei nomi dell'utente) e stanno venendo rimossi o rinominati.

10.20: Ho del vecchio codice che cerca di costruire degli identificatori con una macro come

#define Paste(a, b) a/**/b

ma non funziona più.

Il fatto che i commenti sparissero completamente e potessero quindi essere usati per "incollare" dei token era una caratteristica non documentata di alcuni vecchi compilatori (in particolare quello di John Reiser). L'ANSI (come anche K&R1) afferma che i commenti sono rimpiazzati con spazi bianchi. Comunque, poiché si è dimostrato che la necessità di incollare token è reale, l'ANSI ha introdotto un operatore ben definito per farlo, ##, che può essere usato come segue:

#define Paste(a, b) a##b

Vedi anche la domanda 11.17.

Riferimenti: ANSI Sez. 3.8.3.3; ISO Sez. 6.8.3.3; Rationale Sez. 3.8.3.3; H&S Sez. 3.3.9.

10.22: Come mai la macro

#define TRACE(n) printf("TRACE: %d\n", n)

causa la warning "macro replacement within a string literal"? Sembra che espanda

TRACE(count);

con

printf("TRACE: %d\count", count);

Vedi domanda 11.18.

10.23: Incontro dei problemi a usare argomenti macro dentro stringhe letterali, usando l'operatore #.

Vedi le domande 11.17 e 11.18.

10.25: C'è un trucco che vorrei fare con il compilatore e non capisco come farlo.

Il preprocessore del C non è "general purpose". (Si noti anche che non è garantito che sia disponibile come programma separato.) Anziché forzarlo a fare qualcosa di strano, sarebbe meglio scrivere il proprio tool di precompilazione personalizzato. Si può facilmente far si che una utility come make applichi un tool non standard automaticamente.

(Se si vuole preprocessare qualcosa di diverso dal C, si può usare un preprocessore general-purpose. Uno un po' vecchio disponibile sulla maggior parte degli Unix è m4.)

10.26: Come si fa a scrivere una macro con un numero variabile di argomenti?

Un trucco abbastanza diffuso è quello di definire e invocare la macro con un singolo "argomento" parentesizzato, che nell'espansione della macro si trasforma nell'intera lista di argomenti (parentesi incluse), per una funzione come printf:

#define DEBUG(args) (printf("DEBUG: "), printf args)

if(n != 0) DEBUG(("n is %d\n", n));

L'ovvio svantaggio è che il chiamante deve sempre ricordarsi di usare le parentesi extra.

gcc ha una estensione che consente a una pseudofunzione realizzata con una macro di accettare un numero variabile di argomenti, ma non è standard. Altre possibili soluzioni consistono nell'usare macro diverse a seconda del numero degli argomenti (DEBUG1, DEBUG2, ecc.) o giocare con le virgole:

#define DEBUG(args) (printf("DEBUG: "), printf(args))
#define _ ,

DEBUG("i = %d" _ i)

Solitamente è meglio usare una funzione vera, che può ricevere un numero variabile di argomenti in un modo ben definito. Vedi le domande 15.4 e 15.5. (Se hai proprio bisogno di una sostituzione di macro, prova a usare una funzione più una macro non-pseudofunzione, p.es. #define printf myprintf .)


11. Lo Standard ANSI/ISO

11.1: Cos'è lo standard "ANSI C"?

Nel 1983, l'American National Standards Institute (ANSI) ha dato commissione a un comitato, lo X3J11, di standardizzare il linguaggio C. Al termine di un lungo e arduo processo, con anche molte revisioni pubbliche, il lavoro del comitato è stato ratificato come ANS X3.159-1989 il 14 Dicembre 1989, e pubblicato nella primavera del 1990. In linea di massima, lo standard ANSI ha standardizzato ciò che era pratica diffusa, con alcune aggiunte tratte dal C++ (soprattutto, i prototipi di funzione) e supporto per set di caratteri internazionali (inclusa la controverse sequenze trigrafiche). Lo standard ANSI formalizza anche le routine delle librerie di supporto a run-time.

Più recentemente, lo standard è stato adottato come standard internazionale come ISO/IEC 9899:1990, e questo standard ISO ha sostituito il predecessore X3.159 anche all'interno degli Stati Uniti (dove è attualmente noto come ANSI/ISO 9899-1990 [1992]). Le sezioni sono numerate diversamente (in sostanza, le sezioni da 5 a 7 dell'ISO corrispondono all'incirca alle vecchie sezioni da 2 a 4 dell'ANSI). In quanto ISO, lo standard è soggetto a revisione continua attraverso il rilascio di "Technical Corrigenda" e "Normative Addenda".

Nel 1994, Il Technical Corrigendum 1 ha modificato lo Standard in circa 40 punti, nella maggior parte dei casi con piccole correzioni or chiarimenti. Più recentemente, il Normative Addendum 1 ha aggiunti circa 50 pagine di nuovo materiale, che principalmente specifica nuove funzioni di libreria per l'internalizzazione. La produzione di Technical Corrigenda è un processo continuo, e se ne aspetta una nuova nella seconda metà del 1995. Inoltre, sia l'ANSI che l'ISO richiedono una revisione periodica dei loro standard. Questo processo è cominciato nel 1995, e probabilmente produrrà uno standard completamente rivisto (battezzato "C9X" in quanto si assume che sia completato nel 1999).

Lo standard ANSI originale comprendeva un "Rationale" che spiegava molte delle sue decisioni e discuteva molti punti sottili, alcuni dei quali non sono trattati qui. (Il Rationale non era parte dello standard X3.159-1989, ma era incluso solo a titolo informativo, e non è diventato parte dello standard ISO).

11.2: Dove posso trovare una copia dello Standard?

Si deve contattare l'istituto degli standard della propria nazione, o l'ISO a Ginevra:

ISO Sales
Case Postale 56
CH-1211 Ginevra 20
Svizzera

(o vedere alla URL http://www.iso.ch, o consultare la FAQ di comp.std.internat, Standards.Faq).

Il libro "Annotated ANSI C Standard", con annotazioni di Herbert Schildt, contiene gran parte del testo dell'ISO 9899; è edito da Osborne/McGraw-Hill, ISBN 0-07-881952-0. Questo libro costa meno dello standard, e qualcuno ha suggerito che la differenza rifletta il valore delle annotazioni: sono piene di errori e omissioni, e mancano alcune pagine dello standard stesso. Molte persone sulla rete consigliano di ignorare del tutto le annotazioni. Una revisione delle annotazioni ("annotated annotations") di Clive Feather si trova sul web all'indirizzo http://www.lysator.liu.se/c/schildt.html .

Il testo del Rationale (non lo standard competo) si può scaricare via ftp anonima da ftp.uu.net (vedi domanda 18.16) dal direttorio doc/standards/ansi/X3.159-1989, ed è anche disponibile sul web all'indirizzo http://www.lysator.liu.se/c/rat/title.html. Il Rationale è stato anche stampato dalla Silicon Press, ISBN 0-929306-07-4.

Vedi anche la domanda 11.2a sotto.

11.2a: Dove posso trovare informazioni sugli aggiornamenti allo standard?

Si possono trovare notizie sui siti http://www.lysator.liu.se/c/index.html e http://www.dmk.com/ .

11.3: Il mio compilatore ANSI si lamenta di un "mismatch" quando compila:

extern int func(float);

int func(x)
float x;
{ ...
}

Avete mischiato la dichiarazione di prototipo, nel nuovo stile, "extern int func(float);" con la definizione della funzione, nel vecchio stile, "int func(x) float x;". Solitamente mischiare i due stili è innocuo (vedi domanda 11.4), ma non in questo caso.

Il vecchio C (e l'ANSI C, in assenza di prototipi, o in liste di argomenti a lunghezza variabile; vedi domanda 15.2) "allarga" alcuni argomenti quando vengono passati a funzioni. I float sono promossi a double, e i char e gli short int sono promossi a int. (Per le funzioni vecchio stile, i valori sono automaticamente riconvertiti ai tipi ridotti corrispondenti all'interno del corpo della funzione chiamata, se sono dichiarati in quel modo lì.)

Il problema si può risolvere usando la sintassi nuova in modo consistente anche nella definizione:

int func(float x) { ... }

o cambiando la dichiarazione di prototipo nuovo stile in modo che concordi con la definizione vecchio stile:

extern int func(double);

(In questo caso, sarebbe più chiaro cambiare la definizione vecchio stile in modo che usi anch'essa double, se l'indirizzo del parametro non viene letto.)

Può anche essere più sicuro evitare del tutto i tipi "stretti" (char, short int, e float) per gli argomenti di funzione e i tipi tornati.

Vedi anche la domanda 1.25.

Riferimenti: K&R1 Sez. A7.1; K&R2 Sez. A7.3.2; ANSI Sez. 3.3.2.2, Sez. 3.5.4.3; ISO Sez. 6.3.2.2, Sez. 6.5.4.3; Rationale Sez. 3.3.2.2, Sez. 3.5.4.3; H&S Sez. 9.2, Sez. 9.4.

11.4: Si possono mischiare la sintassi nuova e quella vecchia per le funzioni?

Farlo è perfettamente legale, a patto di state attenti (vedi in particolare la domanda 11.3). Si noti comunque che la vecchia sintassi è stata dichiarata obsoleta, per cui prima o poi potrebbe essere non più supportata.

Riferimenti: ANSI Sez. 3.7.1, Sez. 3.9.5; ISO Sez. 6.7.1, Sez. 6.9.5; H&S Sez. 9.2.2, Sez. 9.2.5.

11.5: Perché la dichiarazione

extern f(struct x *p);

mi da una warning oscura del tipo "struct x introduced in prototype scope"?

Per una anomalia delle normali regole di scope a blocchi del C, una struttura dichiarata (o anche solo menzionata) per la prima volta in un prototipo non può essere compatibile con altre strutture dichiarate nello stesso file sorgente (va fuori scope alla fine del prototipo).

Per risolvere il problema, si può anteporre al prototipo la dichiarazione vaga

struct x;

che colloca una dichiarazione (incompleta) della struct x nello scope di file, in modo che si possa essere certi che tutte le successive dichiarazioni che coinvolgono struct x siano riferita alla stessa struct x.

Riferimenti: ANSI Sez. 3.1.2.1, Sez. 3.1.2.6, Sez. 3.5.2.3; ISO Sez. 6.1.2.1, Sez. 6.1.2.6, Sez. 6.5.2.3.

11.8: Non capisco come mai non posso usare valori dichiarati const negli inizializzatori e nelle dimensioni dei vettori, come in

const int n = 5;
int a[n];

Il qualificatore const in realtà significa "a sola lettura"; un oggetto qualificato in questo modo è un oggetto a tempo di esecuzione che non può essere assegnato (normalmente). Il valore di un oggetto qualificato come const quindi non è una espressione costante nel vero senso del termine. (Su questo punto il C differisce dal C++). Se si ha bisogno di una vera costante a tempo di compilazione, si deve usare una #define di preprocessore (o magari una enum).

Riferimenti: ANSI Sez. 3.4; ISO Sez. 6.4; H&S Secz. 7.11.2,7.11.3.

11.9: Qual è la differenza fra "const char *p" e "char * const p"?

"char const *p" dichiara un puntatore a un carattere costante (non si può modificare il carattere); "char * const p" dichiara un puntatore costante a un carattere (variabile) (non si può modificare il puntatore).

Queste dichiarazioni vanno lette "da dentro a fuori" per capirle; vedi anche la domanda 1.21.

Riferimenti: ANSI Sez. 3.5.4.1 esempi; ISO Sez. 6.5.4.1; Rationale Sez. 3.5.4.1; H&S Sez. 4.4.4.

11.10: Perché non posso passare un char ** a una funzione che si aspetta un const char **?

Si può usare un puntatore-a-T (per qualsiasi tipo T) laddove sia richiesto un puntatore-a-const-T. Tuttavia, la regola (che è un'eccezione esplicita) non è applicata ricorsivamente, ma solo al primo livello. Si devono usare cast espliciti (e.g. (const char **) in questo caso) quando si assegnano (o passano) puntatori con qualificatori discordandi a qualsiasi livello di indirezione diverso dal primo.

Riferimenti: ANSI Sez. 3.1.2.6, Sez. 3.3.16.1, Sez. 3.5.3; ISO Sez. 6.1.2.6, Sez. 6.3.16.1, Sez. 6.5.3; H&S Sez. 7.9.1.

11.12: Posso dichiarare il main() come void, per zittire questi fastidiosi messaggi "main returns no value"?

No. Il main() deve essere dichiarato come funzione con int come valore di ritorno, e che prende o zero o due argumenti, del tipo corretto. Se si chiama exit() ma si ricevono ancora warning, potrebbe essere necessario introdurre un'istruzione return ridondante (o qualche genere di direttiva "not reached", se è disponibile).

Dichiarare una funzione come void non serve semplicemente a evitare delle warning: può anche causare una diversa sequenza di invocazione/ritorno dalla funzione, incompatibile con quella attesa dal chiamante (nel caso del main, il codice di startup del sistema a tempo di esecuzione del C).

(Si noti che questa discussione a proposito del main() si applica solo ad implementazioni "ospiti"; niente di tutto ciò vale per le implementazioni "libere", che potrebbero persino non avere un main(). Comunque, le implementazioni libere sono relativamente rare, e se ne usate una, probabilmente lo sapete. Se non avete mai sentito questa distinzione, probabilmente state usando un'implementazione ospitata, e le regole di cui sopra valgono).

Riferimenti: ANSI Sez. 2.1.2.2.1, Sez. F.5.1; ISO Sez. 5.1.2.2.1, Sez. G.5.1; H&S Sez. 20.1; CT&P Sez. 3.10.

11.13: E cosa mi dite del terzo argomento del main, envp?

È una estensione non-standard (anche se comune). Qualora si abbia proprio bisogno di accedere all'ambiente in modi non previsti dalla funzione standard getenv(), la variabile globale environ è probabilmente più consigliabile (anche se è ugualmente non-standard).

Riferimenti: ANSI Sez. F.5.1; ISO Sez. G.5.1; H&S Sez. 20.1.

11.14: Sono convinto che la dichiarazione void main() non possa darmi alcun problema, visto che chiamo exit() invece di usare l'istruzione return, e comunque il mio sistema operativo ignora lo stato di uscita/ritorno del programma.

Non conta se main ritorna qualcosa oppure no, o se qualcuno guarda lo stato di ritorno; il problema è che se main è dichiarato male, il suo chiamante (il codice di startup a tempo di esecuzione del C) potrebbe persino non essere in grado di chiamarlo correttamente (a causa di potenziali conflitti nelle convenzioni di chiamata; vedi 11.12). Può darsi che il tuo sistema operativo ignori lo stato di uscita, e che void main() per te funzioni, ma non è portabile e non è corretto.

11.15: Il libro che sto usando, "Programmazione C per il perfetto idiota", usa sempre void main().

Forse l'autore del libro si identifica con il pubblico a cui si rivolge. È sorprendente quanti libri usano void main() negli esempi. Sono tutti sbagliati.

11.16: È vero che exit(status) è equivalente a tornare status con return dal main()?

Si e no. Lo Standard dice che sono equivalenti. Comunque, alcuni sistemi più vecchi e non conformi potrebbero avere problemi con una delle due forme. Inoltre, non ci si può aspettare che return dal main() funzioni se sono necessari dei dati locali al main durante la fase di "cleanup"; vedi anche la domanda 16.4. (Infine, le due forme sono ovviamente non equivalenti nel caso di chiamate ricorsive a main().)

Riferimenti: K&R2 Sez. 7.6; ANSI Sez. 2.1.2.2.3; ISO Sez. 5.1.2.2.3.

11.17: Sto cercando di usare l'operatore di "stringhizzazione" ANSI `#' per inserire il valore di una costante simbolica in un messaggio, ma continua a stringhizzare il nome della macro invece del suo valore.

Per forzare il fatto che una macro sia espansa oltre che stringhizzata si può usare qualcosa di simile alla seguente procedura a due passi:

#define Str(x) #x
#define Xstr(x) Str(x)
#define OP plus
char *opname = Xstr(OP);

Questo codice rende opname uguale a "plus" anziché "OP".

Una circonlocuzione equivalente è necessaria quando si usa l'operatore di unione di token ## se si vogliono concatenare i valori (anziché i nomi) di due macro.

Riferimenti: ANSI Sez. 3.8.3.2, Sez. 3.8.3.5 esempio; ISO Sez. 6.8.3.2, Sez. 6.8.3.5.

11.18: Cosa significa il messaggio "warning: macro replacement within a string literal"?

Alcuni compilatori/preprocessori pre-ANSI interpretavano le definzioni di macro come

#define TRACE(var, fmt) printf("TRACE: var = fmt\n", var)

in un modo per cui le invocazioni come

TRACE(i, %d);

erano espanse come

printf("TRACE: i = %d\n", i);

In altre parole, i parametri della macro erano espansi anche dentro le stringhe letterali e le costanti carattere. L'espansione di macro non è definita cosí da K&R o dallo standard. Quando si vuole proprio trasformare gli argomenti di macro in stringhe, si può usare il nuovo operatore di preprocessore #, insieme alla concatenazione di stringhe letterali (un'altra innovazione ANSI):

#define TRACE(var, fmt) \
printf("TRACE: " #var " = " #fmt "\n", var)

Vedi anche la domanda 11.17 sopra.

Riferimenti: H&S Sez. 3.3.8.

11.19: Ricevo strani errori all'interno di linee che ho tagliato fuori con #ifdef

Sotto ANSI C, il testo all'interno di una #if, #ifdef, o #ifndef "spenta" deve comunque essere composto da "token di preprocessore validi." Questo significa che i caratteri " e ' devono essere a coppie come nel codice C normale, e che le coppie devono trovarsi sulla stessa riga. Di conseguenza, commenti in linguaggio naturale e pseudocodice dovrebbero comunque essere scritti fra i delimitatori di commento "ufficiali" /* e */. (Ma vedi la domanda 20.20, e anche 10.25.)

Riferimenti: ANSI Sez. 2.1.1.2, Sez. 3.1; ISO Sez. 5.1.1.2, Sez. 6.1; H&S Sez. 3.2.

11.20: Cosa sono le #pragma e a cosa servono?

La direttiva #pragma fornisce un'unica, ben definita "via di fuga" che può essere usata per qualsiasi genere di controllo ed estensione (non portabile) specifico di una implementazione: controllo della visualizzazione del sorgente, impacchettamento di strutture, soppressione delle warning (come i vecchi commenti /* NOTREACHED */ per lint), ecc.

Riferimenti: ANSI Sez. 3.8.6; ISO Sez. 6.8.6; H&S Sez. 3.7.

11.21: Cosa significa "#pragma once"? L'ho trovato in diversi file di include.

È un'estensione implementata da alcuni preprocessori che aiuta a rendere idempotenti gli header file; è equivalente al trucco #ifndef citato nella domanda 10.7, ma meno portabile.

11.22: È legale la dichiarazione char a[3] = "abc";? Cosa significa?

È legale in ANSI C (e magari in alcuni sistemi pre-ANSI), anche se è utile solo in rari casi. Dichiara un vettore di dimensione 3, inizializzato con i tre caratteri 'a', 'b', e 'c', *senza* il terminatore '\0'. L'array quindi non è una vera stringa C e non può essere usata con strcpy, printf %s, ecc.

Nella maggior parte dei casi, si dovrebbe lasciare che il compilatore conti gli inizializzatori quando si inizializzano vettori (nel caso dell'inizializzatore "abc", naturalmente, la dimensione calcolata sarà 4).

Riferimenti: ANSI Sez. 3.5.7; ISO Sez. 6.5.7; H&S Sez. 4.6.4.

11.24: Perché non posso eseguire operazioni aritmetiche su un puntatore void*?

Il compilatore non conosce la dimensione degli oggetti puntati. Prima di eseguire operazioni aritmetiche, si converta il puntatore o a char * o al tipo puntatore che si vuole manipolare (ma vedi anche la domanda 4.5).

Riferimenti: ANSI Sez. 3.1.2.5, Sez. 3.3.6; ISO Sez. 6.1.2.5, Sez. 6.3.6; H&S Sez. 7.6.2.

11.25: Qual è la differenza fra memcpy() e memmove()?

Per memmove() ci sono delle garanzie per quanto riguarda il comportamento nel caso in cui la sorgente e la destinazione siano sovrapposte. memcpy() non dà garanzie del genere, e può di conseguenza essere implementabile in modo più efficiente. Quando si è in dubbio, è più sicuro usare memmove().

Riferimenti: K&R2 Sez. B3; ANSI Sez. 4.11.2.1, Sez. 4.11.2.2; ISO Sez. 7.11.2.1, Sez. 7.11.2.2; Rationale Sez. 4.11.2; H&S Sez. 14.3; PCS Sez. 11.

11.26: Cosa dovrebbe fare malloc(0)? Tornare un puntatore nullo o un puntatore a 0 byte?

Lo standard ANSI/ISO dice che può fare entrambe le cose, il comportamento è definito dall'implementazione (vedi la domanda 11.33).

Riferimenti: ANSI Sez. 4.10.3; ISO Sez. 7.10.3; PCS Sez. 16.1.

11.27: Come mai lo standard ANSI non garantisce più di 6 caratteri case-insensitive significativi per gli identificatori esterni?

Il problema è rappresentato dai linker più vecchi, che non sono sotto il controllo della'ANSI/ISO né degli sviluppatori di compilatori. La limitazione è solo che per gli identificatori sono *significativi* solo i primi sei caratteri, non che siano vincolati a un massimo di sei caratteri di lunghezza. Questa limitazione è seccante, ma certamente non insopportabile, ed è marcata nello Standard come "obsolescente", ovvero una revisione futura probabilmente la rilasserà.

Questa concessione ai linker restrittivi attuali doveva necessariamente fatta, per quanto molti vi si siano opposti in modo veemente). (Il Rationale nota che mantenerla è stato "estremamente doloroso.") Se non siete d'accordo, o avete pensato un trucco attraverso il quale un compilatore soffocato da un linker restrittivo potrebbe fornire al programmatore l'illusione di più caratteri significativi negli identificatori esterni, leggete l'eccellente sezione 3.1.2 nel Rationale X3.159 (vedi domanda 11.1), che discute vari schemi del genere e spiega perché non potevano essere imposti dallo standard.

Riferimenti: ANSI Sez. 3.1.2, Sez. 3.9.1; ISO Sez. 6.1.2, Sez. 6.9.1; Rationale Sez. 3.1.2; H&S Sez. 2.5.

11.29: Il mio compilatore rifiuta i programmi più semplici, segnalando ogni tipo di errori di sintassi.

Forse è un compilatore pre-ANSI, incapace di accettare prototipi di funzione e simili.

Vedi anche le domande 1.31, 10.9, 11.30, e 16.2a.

11.30: Perché alcune routine della libreria standard ANSI/ISO risultano indefinite, anche se ho un compilatore ANSI?

È possibile che un compilatore accetti la sintassi ANSI, ma non abbia file di include o librerie run-time compatibili ANSI. (In effetti, questa situazione è piuttosto comune quando si usano compilatori non commerciali come gcc.) Vedi anche le domande 11.29, 13.25, e 13.26.

11.31: Esitono tool per convertire i programmi in C vecchio stile in ANSI C, o viceversa, o per generare automaticamente prototipi?

Due programmi, protoize e unprotoize, fanno le conversioni nelle due direzioni fra definizioni e dichiarazioni di funzione prototipate e vecchio stile. (Questi programmi *non* eseguono una traduzione a tutto campo fra il C "classico" e l'ANSI.) Questi programmi sono parte della distribuzione FSF del compilatore C della GNU; vedi la domanda 18.3.

Il programma unproto (/pub/unix/unproto5.shar.Z su ftp.win.tue.nl) è un filtro che si pone fra il preprocessore e il passo successivo del compilatore, convertendo al volo la maggior parte dell'ANSI C in C tradizionale.

Il pacchetto GhostScript della GNU contiene un piccolo programma chiamato ansi2knr.

Convertire l'ANSI C al vecchio stile richiede attenzione in quanto questa conversione non sempre può essere fatta in automatico in modo sicuro. L'ANSI C ha caratteristiche complesse nuove che non c'erano nel C di K&R. Occorre soprattutto fare attenzione alle chiamate di funzioni prototipate; ci sarà probabilmente bisogno di inserire cast esplicit. Vedi anche le domande 11.3 e 11.29.

Esistono molti generatori di prototipi, spesso realizzati come modifiche a lint. Un programma chiamato CPROTO è stato pubblicato su comp.sources.misc nel marzo 1992. C'è un altro programma chiamato "cextract." Molti produttori forniscono semplici utility come queste insieme ai loro compilatori. Vedi anche la domanda 18.16. (Comunque si deve stare attenti quando si generano prototipi per vecchie funzioni con parametri "stretti"; vedi la domanda 11.3.)

Infine, siete proprio sicuri di aver bisogno di convertire tanto codice vecchio in C ANSI? La sintassi vecchio stile è ancora accettabile (a parte per le funzioni con un numero variabile di argomenti; vedi la sezione 15), e una conversione affrettata può introdurre facilmente errori. (Vedi la domanda 11.3.)

11.32: Perché il compilatore Frobozz Magic, che dice di essere conforme ANSI, non accetta questo codice? Sono certo che sia ANSI, perché gcc lo accetta.

Molti compilatori forniscono alcune estensioni non standard, gcc più di altri. È certo che il codice che viene rifiutato non utilizzi qualcuna di queste estensioni? Non è quasi mai una buona idea fare esperimenti con un compilatore particolare per determinare proprietà di un linguaggio; lo standard applicabile può consentire delle variazioni, e il compilatore può essere sbagliato. Vedi anche la domanda 11.35.

11.33: Sembra che qualcuno ci tenga a distingere fra comportamento "definito dall'implementazione", "non specificato", e "non definito". Qual è la differenza?

In breve: "definito dall'implementazione" significa che una implementazione deve scegliere un comportamento e documentarlo. "Non specificato" significa che una implementazione dovrebbe scegliere un comportamento, ma non è necessario che lo documenti. "Non definito" significa che può accadere assolutamente qualsiasi cosa. In nessuno dei tre casi lo standard impone requisiti sul comportamento; nei primi due casi in alcuni casi suggerisce un piccolo insieme di comportamenti possibili (e può richiedere che se ne scelga uno).

Si noti che poiché lo standard non impone alcun requisito sul comportamento di un compilatore che si trovi a trattare un caso di comportamento indefinito, il compilatore può fare assolutamente qualsiasi cosa. In particolare, non c'è garanzia che il resto del programma si comporterà normalmente. È pericoloso pensare di poter tollerare un comportamento indefinito in un programma; vedi la domanda 3.2 per un esempio relativamente semplice.

Se si è interessati a scrivere codice portabile, si possono ignorare le distinzioni, poiché si vorrà evitare codice che dipenda da uno qualsiasi dei tre comportamenti.

Vedi anche le domande 3.9 e 11.34.

Riferimenti: ANSI Sez. 1.6; ISO Sez. 3.10, Sez. 3.16, Sez. 3.17; Rationale Sez. 1.6.

11.34: Sono stupito del fatto che lo standard ANSI lasci così tanti aspetti indefiniti. Non sarebbe compito di uno standard standardizzare queste cose?

Il fatto che certi costrutti si comportassero in un modo dipendente dal compilatore o dall'hardware è sempre stata una caratteristica del C. Questa imprecisione è voluta e spesso consente ai compilatori di generare codice più efficiente per i casi comuni, senza dover appesantire tutti i programmi con codice aggiuntivo per assicurare un comportamento ben definito nei casi che sono considerati meno ragionevoli. Quindi, lo Standard si limita a rendere ufficiale quella che è la pratica esistente.

Uno standard per un linguaggio di programmazione può essere visto come un patto fra gli utenti del linguaggio e i costruttori di compilatori. Il patto stabilisce le caratteristiche del linguaggio che i costruttori di compilatori si impegnano a fornire, e che gli utenti possono assumere. Esso stabilisce anche le regole che gli utenti si impegnano a seguire e che i costruttori di compilatori possono assumere siano seguite. Finché entrambe le parti tengono fede al patto, i programmi dovrebbero funzionare. Se una qualsiasi delle parti tradisce uno qualsiasi dei propri impegni, non è più garantito che funzioni nulla.

Vedi anche la domanda 11.35.

Riferimenti: Rationale Sez. 1.1.

11.35: La gente continua a sostenere che il comportamento di i = i++ è indefinito, ma io l'ho provato su un compilatore conforme ANSI, e ho ottenuto il risultato che mi aspettavo.

Un compilatore può fare qualsiasi cosa nei casi di comportamento indefinito (e anche, con qualche limitazione, nei casi di comportamento definito dall'implementazione o non specificato), incluso, ovviamente, fare quello che ti aspetti. Ciononostante, non è saggio scrivere codice che dipende da questo. Vedi anche le domande 11.32, 11.33, e 11.34.


12. Stdio

12.1: Cosa c'è che non va in questo codice?

char c;
while((c = getchar()) != EOF) ...

La variabile usata per ricevere il valore di ritorno di getchar deve essere un int. La getchar() può tornare un qualsiasi valore char, oppure EOF. Se si passa il valore di ritorno di getchar a un char, può succedere che un carattere normale sia scorrettamente interpretato come EOF, oppure che l'EOF sia alterato (specialmente se il tipo char è senza segno) e quindi non riconosciuto.

Riferimenti: K&R1 Sez. 1.5; K&R2 Sez. 1.5.1; ANSI Sez. 3.1.2.5, Sez. 4.9.1, Sez. 4.9.7.5; ISO Sez. 6.1.2.5, Sez. 7.9.1, Sez. 7.9.7.5; H&S Sez. 5.1.3, Sez. 15.1, Sez. 15.6; CT&P Sez. 5.1; PCS Sez. 11.

12.2: Perché il codice

while(!feof(infp)) {
   fgets(buf, MAXLINE, infp);
   fputs(buf, outfp);
}

copia due volte l'ultima riga?

In C, la fine del file viene segnalata dopo che una routine di input ha cercato di leggere, e ha raggiunto l'end-of-file. (In altre parole, l'I/O in C è diverso dal Pascal). Normalmente, si dovrebbe controllare direttamente il valore di ritorno della routine di input (in questo caso fgets()); spesso usare feof()è inutile.

Riferimenti: K&R2 Sez. 7.6; ANSI Sez. 4.9.3, Sez. 4.9.7.1, Sez. 4.9.10.2; ISO Sez. 7.9.3, Sez. 7.9.7.1, Sez. 7.9.10.2; H&S Sez. 15.14.

12.4: Il prompt e l'output intermedio del mio programma non appaiono sempre a schermo, specialmente se metto l'output in pipe attraverso un altro programma.

È bene usare una fflush(stdout) esplicita ogni volta che è necessario che l'output sia visibile. Molti meccanismi del C cercano di eseguire fflush() per voi, "al momento giusto," ma in generale funzionano in modo corretto solo quando stdout è un terminale interattivo terminal. (Vedi anche la domanda 12.24.)

Riferimenti: ANSI Sez. 4.9.5.2; ISO Sez. 7.9.5.2.

12.5: È possibile leggere un carattere alla volta, senza aspettare che l'utente digiti invio?

Vedi domanda 19.1.

12.6: Come faccio a stampare il carattere '%' in una printf? Ho provato \%, ma non funziona.

Basta raddoppiare: %% . \% non può funzionare, perché il backslash \ è il carattere di escape del compilatore, mentre il nostro problema qui è che il % è il carattere di escape della printf.

Vedi anche la domanda 19.17.

Riferimenti: K&R1 Sez. 7.3; K&R2 Sez. 7.2; ANSI Sez. 4.9.6.1; ISO Sez. 7.9.6.1.

12.9: Qualcuno mi ha detto che è scorretto usare %lf con printf(). Come mai printf() usa %f per i double, se scanf() usa %lf?

È vero che il %f della printf funziona sia con i float che con i double. A causa delle "promozioni di default degli argomenti" (che si applicano nelle liste di argomenti a lunghezza variabile come quella della printf, indipendentemente dal fatto che i prototipi siano in scope), i valori di tipo float sono promossi a double, e la printf() quindi riceve sempre e solo valori double. (printf() accetta invece %Lf, per i long double.) Vedi anche le domande 12.13 e 15.2.

Riferimenti: K&R1 Sez. 7.3, Sez. 7.4; K&R2 Sez. 7.2, Sez. 7.4; ANSI Sez. 4.9.6.1, Sez. 4.9.6.2; ISO Sez. 7.9.6.1, Sez. 7.9.6.2; H&S Sez. 15.8, Sez. 15.11; CT&P Sez. A.1.

12.10: Come si fa a implementare un campo a larghezza variabile con printf? Ovvero, invece di scrivere per esempio %8d, voglio che la dimensione sia specificata a run time.

printf("%*d", dimensione, n) fa questo. Vedi anche la domanda 12.15.

Riferimenti: K&R1 Sez. 7.3; K&R2 Sez. 7.2; ANSI Sez. 4.9.6.1; ISO Sez. 7.9.6.1; H&S Sez. 15.11.6; CT&P Sez. A.1.

12.11: Come si fa a stampare numeri con punti che separano le migliaia? Come si fa a formattare i numeri che rappresentano somme di denaro?

Le routine in <locale.h> forniscono alcuni strumenti di base per queste operazions, ma non c'è un modo standard per fare nessuna delle due cose. (L'unica cosa che printf() fa riguardo a una configurazione locale speciale è cambiare il carattere di punto decimale.)

Riferimenti: ANSI Sez. 4.4; ISO Sez. 7.4; H&S Sez. 11.6.

12.12: Perché la chiamata scanf("%d", i) non funziona?

Gli argomenti passati a scanf() devono essere sempre puntatori. Per sistemare il framento citato, sostituirlo con scanf("%d", &i) .

12.13: Perché il codice:

double d;
scanf("%f", &d);

non funziona?

A differenza di printf(), scanf() usa %lf per i valori di tipo double, e %f solo per i float. Vedi anche la domanda 12.9.

12.15: Come si fa a specificare un campo di dimensione variable nella stringa di formato della scanf()?

Non si può; un asterisco in una scanf() sopprime l'assegnamento. Potrebbe essere possibile usare la "stringhizzazione" e la concatenazione di stringhe ANSI per ottenere all'incirca la stessa cosa, o costruire la stringa di formato a run time.

12.17: Se leggo un numero da tastiera con scanf ("%d\n", &n), sembra bloccarsi finché l'utente non inserisce una linea extra di input.

Sorprendentemente, \n in una stringa di formato della scanf *non* significa che si aspetti un newline, bensì che devono essere letti e scartati tutti i caratteri di spazio bianco. Vedi anche la domanda 12.20.

Riferimenti: K&R2 Sez. B1.3; ANSI Sez. 4.9.6.2; ISO Sez. 7.9.6.2; H&S Sez. 15.8.

12.18: Leggo un numero con scanf %d e poi una stringa con gets(), ma il compilatore sembra saltare la chiamata a gets()!

La scanf %d non consuma il newline in fondo a una riga. Se il numero in input è immediatamente seguito da un newline, questo verrà acquisito dalla gets().

Come regola generale, non si dovrebbero alternare chiamate a scanf() con chiamate a gets() (o a qualsiasi altra routine di input); il trattamento particolare che la scanf fa dei newline conduce quasi sempre a problemi. Conviene usare la scanf() per leggere tutto o non usarla affatto.

Vedi anche le domande 12.20 e 12.23.

Riferimenti: ANSI Sez. 4.9.6.2; ISO Sez. 7.9.6.2; H&S Sez. 15.8.

12.19: Pensavo che per usare scanf() fosse più sicuro controllare il suo valore di ritorno per assicurarsi che l'utente abbia digitato i valori numerici che mi aspetto, ma ogni tanto sembra che entri in un ciclo infinito.

Quando scanf() cerca di leggere numeri, qualsiasi carattere non numerico incontrato abortisce la lettura, e i caratteri sono lasciati nello stream di input. Di conseguenza, a meno di non prendere speciali provvedimenti, input non numerico inaspettato "manda in palla" scanf(): non riesce a superare i caratteri non validi e raggiungere eventuali dati validi successivi. Se l'utente digita un carattere come `x' in risposta a un formato numerico come %d o %f, un programma che semplicemente richieda il valore e riprovi la stessa scanf() riincontrerà lo stesso `x'.

Vedi anche la domanda 12.20.

Riferimenti: ANSI Sez. 4.9.6.2; ISO Sez. 7.9.6.2; H&S Sez. 15.8.

12.20: Perché tutti sconsigliano di usare scanf()? Cosa si dovrebbe usare al suo posto?

La scanf() ha molti problemi - si vedano 12.17, 12.18, e 12.19. Inoltre, il formato %s ha gli stessi problemi della gets() (vedi domanda 12.23) - è difficile guarantire che il buffer di ricezione non vada in overflow. Più in generale, la scanf() è progettata per input relativamente strutturato e formattato (il suo nome infatti sta per "scan formatted"). Se si fa attenzione, è possibile capire se ha avuto successo o no, ma è possibile capire solo approssimativamente dove ha fallito, e non è possibile per niente capire come o perché. È quasi impossibile fare una gestione degli errori decente con scanf(); di solito è molto più facile leggere intere linee (con fgets() o simili) e poi elaborarle, usando sscanf() o altre tecniche. (Funzioni come strtol(), strtok(), e atoi() spesso sono utili; vedi anche la domanda 13.6.) Se si usa sscanf(), ci si assicuri di verificarne il valore di ritorno per assicurarsi che siano stati letti tutti i valori attesi. Inoltre, se si usa %s, ci si assicuri di prevenire l'overflow del buffer.

(scanf può essere adeguata se viene utilizzata per leggere input prodotto automaticamente da un programma, per esempio un file dati realizzato con fprintf).

Riferimenti: K&R2 Sez. 7.4.

12.21: Come si fa a capire quanto deve essere grande il buffer in una chiamata a sprintf? Come si fa a evitare l'overflow del buffer?

Non ci sono (ancora) buone risposte a queste eccellenti domande, e questo probabilmente rappresenta il più grosso problema della libreria stdio tradizionale.

Quando la stringa di formato usata da sprintf() è nota e relativamente semplice, di solito si può predire la dimensione del buffer ad hoc. Se il formato consiste di uno o due %s, si può contare i caratteri fissi nella stringa di formato (o farli contare da sizeof) e aggiungere il risultato della chiamata a strlen sulle stringhe da inserire. Il numero dei caratteri prodotti da %d non può superare

((sizeof(int) * CHAR_BIT + 2) / 3 + 1) /* +1 per il '-' */

(CHAR_BIT è definito in <limits.h>), anche se questo calcolo può essere troppo pessimista (calcola il numero di caratteri necessari per la rappresentazione in base 8 di un numero; è garantito che un'espansione in base 10 richieda lo stesso o meno spazio). Se la stringa di formato è più complessa, o addirittura non è nota fino a runtime, predire la dimensione necessaria per il buffer diventa difficile come reimplementare sprintf da capo, e di conseguenza è facile commettere errori (e lo si sconsiglia). Una soluzione estrema che viene talvolta suggerita consiste nell'usare fprintf per stampare lo stesso testo su un file temporaneo, e controllare il return di fprintf o la dimensione del file (ma vedi la domanda 19.12, e attenzione agli errori di scrittura). Se esiste la possibilità che il buffer non sia sufficientemente grande, probabilmente non vorrete chiamare sprintf senza qualche garanzia che il buffer non trabocchi e non venga sovrascritta qualche altra area di memoria. Se la stringa di formato è nota, si può limitare l'espansione di %s usando %Ns dove N è un qualche numero, oppure %.*s (vedi anche la domanda 12.10). Molte stdio (inclusa quella della GNU e di 4.4BSD) forniscono la ovvia funzione snprintf, che può essere usata così:

snprintf(buf, dimensione_buffer, "Hai scritto\"%s\"", answer);

e speriamo che una futura revisione del C ANSI/ISO includerà questa funzione.

12.23: Perché tutti sconsigliano di usare gets()?

Diversamente da fgets(), a gets() non viene comunicata la dimensione del buffer in cui deve leggere, e pertanto non è possibile prevenire l'overflow del buffer. Come regola generale, usate sempre fgets(). Vedi la domanda 7.1 per un frammento di codice che mostra come sostituire gets() con fgets().

Riferimenti: Rationale Sez. 4.9.7.2; H&S Sez. 15.7.

12.24: Perché errno contiene ENOTTY dopo una chiamata a printf()?

Molte implementazioni del package stdio adattano leggermente il loro comportamento se stdout è il terminale. Per capirlo, queste implementazioni eseguono qualche operazione che fallisce (producendo ENOTTY) se stdout non è un terminale. Anche se l'operazione di output termina con successo, errno contiene ancora ENOTTY. (Si noti che un programma dovrebbe controllare il contenuto di errno solo se viene riportato un errore; in caso contrario, non è garantito che errno contenga 0).

Riferimenti: ANSI Sez. 4.1.3, Sez. 4.9.10.3; ISO Sez. 7.1.4, Sez. 7.9.10.3; CT&P Sez. 5.4 p. 73; PCS Sez. 14.

12.25: Che differenza c'è fra fgetpos/fsetpos e ftell/fseek? A cosa servono fgetpos() e fsetpos()?

ftell() e fseek() usano il tipo long int per rappresentare l'offset (la posizione) all'interno di un file, e quindi possono trattare al massimo offset di circa 2 milioni (2^31-1). Le più recenti funzioni fgetpos() e fsetpos(), invece, usano una speciale typedef, fpos_t, per rappresentare gli offset. Il tipo dietro questa typedef, se scelto opportunamente, può rappresentare offset arbitrariamente grandi, cosicché fgetspos() e fsetpos() possono essere usati con file arbitrariamente grandi. Vedi anche la domanda 1.4.

Riferimenti: K&R2 Sez. B1.6; ANSI Sez. 4.9.1, Sez. 4.9.9.1, 4.9.9.3; ISO Sez. 7.9.1, Sez. 7.9.9.1, 7.9.9.3; H&S Sez. 15.5.

12.26: Come faccio a eseguire il flush dell'input pendente in modo che i caratteri digitati in anticipo dall'utente non siano letti al prossimo prompt? fflush(stdin) funziona?

fflush() vale solo per stream di output. Poiché lo scopo di fflush è quello di completare la scrittura dei caratteri bufferizzati (non quello di scartarli), scartare l'input non ancora letto non sarebbe neanche il suo analogo per stream di input.

Non esiste un modo standard per scartare caratteri non letti da uno stream di input stdio, né questo servirebbe a molto, perché i caratteri non letti possono anche accumularsi in altri buffer a livello di sistema operativo. Si possono leggere e scartare caratteri fino al '\n', o usare la funzione flushinp() di curses, o usare qualche tecnica dipendente dal sistema. Vedi anche le domande 19.1 and 19.2.

Riferimenti: ANSI Sez. 4.9.5.2; ISO Sez. 7.9.5.2; H&S Sez. 15.2.

12.30: Sto cercando di aggiornare un file usando la modalità di fopen "r+", leggendo una stringa, e riscrivendola modificata, ma non funziona.

È necessario chiamare fseek() prima di scrivere, sia per riposizionarsi all'inizio della stringa da sovrascrivere, sia perché fra una lettura e una scrittura, nelle modalità di lettura/scrittura "+", è sempre necessaria una fseek o una fflush. Bisogna anche ricordarsi di sovrascrivere i caratteri con lo stesso numero di caratteri di rimpiazzo, e che la sovrascrittura in modalità testo può troncare il file nel punto in cui si scrive. Si veda anche la domanda 19.14.

Riferimenti: ANSI Sez. 4.9.5.3; ISO Sez. 7.9.5.3.

12.33: È possibile redirigere stdin o stdout su file da dentro un programma?

Si, usando freopen() (ma vedi anche la domanda 12.34).

Riferimenti: ANSI Sez. 4.9.5.4; ISO Sez. 7.9.5.4; H&S Sez. 15.2.

12.34: Una volta che ho usato freopen(), come faccio a recuperare lo stdout (o stdin) originale?

Non c'e' un modo soddisfacente. Se hai bisogno di ritornare indietro, la soluzione migliore consiste nel non usare freopen() del tutto. È meglio fare output (o input) su una variabile di tipo stream, la quale può essere assegnata e riassegnata a piacimento, lasciando indisturbati lo stdout e stdin originali.

12.38: Come posso leggere correttamente un file binario? Mi capita di vedere che i valori 0x0a e 0x0d creino problemi, e sembra anche che venga raggiunta prematuramente la fine del file (EOF) se i dati contengono il valore 0x1a.

Quando si legge un file binario, bisognerebbe usare la modalità "rb" di fopen() per assicurarsi che non avvengano le trasformazioni applicati ai file di testo. Analogamente, se si scrive su un file binario, bisogna usare "wb".

Si noti che la distinzione fra file binario e file di testo ha luogo all'apertura del file: una volta che il file è aperto, le funzioni di I/O che si utilizzano non contano. Vedi anche la domanda 20.5.

Riferimenti: ANSI Sez. 4.9.5.3; ISO Sez. 7.9.5.3; H&S Sez. 15.2.1.


13. Funzioni di libreria

13.1: Come si fa a convertire numeri in stringhe (il contrario di atoi)? C'è una funzione itoa?

Basta usare sprintf(). (Non preoccupatevi che sia esageratamente potente, che sprechi tempo di esecuzione o spazio: in pratica funziona bene). Si vedano gli esempi nella risposta alla domanda 7.5, e la domanda 12.21

Ovviamente sprintf() può anche essere usata per convertire long o numeri floating-point in stringhe (usando %ld o %f).

Riferimenti: K&R1 Sez. 3.6; K&R2 Sez. 3.6.

13.2: Perché strncpy() non piazza sempre il terminatore '\0' in fondo alla stringa di destinazione?

Inizialmente, strncpy() è stata progettata per gestire una struttura dati ora obsoleta, la stringa di dimensione fissata, che non terminava necessariamente con '\0'. (Una stranezza correlata di strncpy() è che riempie gli spazi inutilizzati della stringa destinazione di caratteri '\0', fino alla lunghezza specificata). È risaputo che strncpy() è un po' contorta quando la si usa in altri contesti, perché è spesso necessario aggiungere il '\0' finale a mano. Si può ovviare a questo problema usando strncat() invece di strncpy(): se la stringa di destinazione è vuota, strncat() fa ciò che vorreste da strncpy(). Un'altra possibilità è sprintf (dest, "%.*s", n, sorgente).

Quando si copiano byte qualsiasi (anziché stringhe) memcpy() è solitamente meglio di strncpy().

13.5: Perché alcune versioni di toupper() si comportano in modo strano se ricevono una lettera maiuscola? Perché alcuni programmi chiamano islower() prima di toupper()?

Le versioni vecchie di toupper() e tolower() non funzionavano sempre su argomenti che non richiedevano conversione (cioè su cifre, punteggiatura, o lettere già maiuscole o minuscole). Nel C Standard ANSI/ISO, si garantisce che queste funzioni operino correttamente su qualsiasi argomento.

Riferimenti: ANSI Sez. 4.3.2; ISO Sez. 7.3.2; H&S Sez. 12.9; PCS.

13.6: Come si fa a suddividere una stringa in campi separati da spazi bianchi? Come si fa a simulare il procedimento con cui vengono passati argc e argv al main?

L'unica routine standard disponibile per "tokenizzare" una stringa è strtok(), anche se può essere complessa da usare e potrebbe non fare tutto ciò che vi serve. (Per esempio, non può gestire le stringhe fatte da più parole fra virgolette come singolo token).

Riferimenti: K&R2 Sez. B3; ANSI Sez. 4.11.5.8; ISO Sez. 7.11.5.8; H&S Sez. 13.7; PCS.

13.7: Devo scrivere del codice per gestire espressioni regolari e wild characters.

Occorre innanzitutto capire bene la differenza fra le espressioni regolari classiche (varianti delle quali sono usate da utility Unix come ed e grep) e i wild characters per i nomi di file (varianti dei quali sono usati da quasi tutti i sistemi operativi).

Ci sono moltissimi package per la gestione di espressioni regolari. La maggior parte usano un paio di funzioni, una per "compilare" l'espressione regolare, e una per "eseguirle" (cioè verificare se c'è un match fra una data stringa e l'espressione). Cercate header file con nomi come o , e funzioni chiamate regcmp/regex, regcomp/regexec, o re_comp/re_exec. (Tali funzioni possono essere contenute in una libreria regexp). Un package molto popolare e free, realizzato da Henry Spencer, è disponibile presso ftp.cs.toronto.edu in pub/regexp.shar.Z o in altri archivi. Il GNU project include un package chiamato rx. Vedi anche la domanda 18.16.

Il matching di wildcard per i nomi di file (talvolta detto "globbing") si fa in modi diversi su diversi sistemi. Su Unix, le wildcards sono automaticamente espanse dalla shell prima che un processo venga invocato, pertanto è raro che un programma se ne debba occupare esplicitamente. Sotto i compilatori MS-DOS, spesso esiste uno speciale file oggetto che può essere linkato a un programma per espandere le wildcard durante la costruzione di argv. Molti sistemi (inclusi MS-DOS e VMS) forniscono servizi di sistema per elencare o aprire i file specificati usando wildcard. Verificate la documentazione del vostro compilatore o delle vostre librerie. Si vedano anche le domande 19.20 e 20.3.

13.8: Sto cercando di ordinare un vettore di stringhe con qsort(), usando strcmp() come funzione di confronto, ma non funziona.

Con "vettore di stringhe" si intende probabilmente un "vettore di puntatori a char". Gli argomenti di qsort sono puntatori agli oggetti da ordinare, e quindi, in questo caso, puntatori a puntatori a char. strcmp(), però, accetta puntatori semplici a char; pertanto non può essere usata direttamente. Si scriva una funzione di confronto intermedia come questa:

/* compare strings via pointers */
int pstrcmp(const void *p1, const void *p2)
{
   return strcmp(*(char * const *)p1, *(char * const *)p2);
}

Gli argomenti della funzione di confronto sono espressi come "puntatori generici", const void *. Riconvertendoli in ciò che "sono realmente" (char**) e dereferenziandoli, si ottengono i char* che possono essere passati a strcmp(). (Nel caso di un compilatore pre-ANSI, i parametri puntatore devono essere dichiarati come char* invece che come void*, e i const vanno tolti).

(Non fatevi ingannare dalla discussione in K&R2 Sez. 5.11, che non parla della qsort della libreria Standard).

Riferimenti: ANSI Sez. 4.10.5.2; ISO Sez. 7.10.5.2; H&S Sez. 20.5.

13.9: Ora sto cercando di ordinare con qsort un vettore di strutture. La mia funzione di confronto prende in ingresso puntatori a strutture, ma il compilatore mi dà una warning dicendo che la funzione è del tipo sbagliato per qsort(). Quale cast devo fare per zittirlo?

I cast vanno fatti nella funzione di confronto, che deve essere dichiarato come funzione che accetta "puntatori generici" (const void*) come discusso nella domanda 13.8. La funzione di confronto potrebbe essere come segue:

int mystructcmp(const void *p1, const void *p2)
{
   const struct mystruct *sp1 = p1;
   const struct mystruct *sp2 = p2;
   /* confronta sp1->qualcosa e sp2->qualcosa ... */
}

(Le conversioni da puntatori generici a puntatori a struct avvengono all'inizializzaizone di sp1 e sp2; il compilatore esegue le conversioni implicitamente perché p1 e p2 sono puntatori a void. In un compilatore pre-ANSI si richiederebbero puntatori a char e cast espliciti. Vedi anche 7.7).

Se, d'altra parte, si vogliono ordinare puntatori a strutture, sarà necessaria un'ulteriore indirezione, come nel caso di 13.8: sp1 = *(struct mystruct **)p1.

In generale, non è una buona idea inserire dei cast per "zittire il compilatore". Le warning del compilatore, di solito, cercano di dirvi qualcosa, e a meno che non sappiate esattamente cosa state facendo, ignorarle o imbavagliarle è a vostro rischio. Vedi anche la domanda 4.9.

Riferimenti: ANSI Sez. 4.10.5.2; ISO Sez. 7.10.5.2; H&S Sez. 20.5.

13.10: Come si fa a ordinare una lista?

Normalmente è più facile tenere ordinata una lista mentre la si costruisce (o usare un albero). Gli algoritmi di inserimento ordinato o ordinamento con "merge" si prestano perfettamente all'uso con liste. Se volete usare una funzione della libreria standard, si può allocare un vettore di puntatori temporaneo, far sì che i puntatori puntino agli elementi della lista, eseguire qsort(), e ricostruire la lista in base al vettore.

Riferimenti: Knuth Sez. 5.2.1, Sez. 5.2.4; Sedgewick Sez. 8, Sez. 12.

13.11: Posso ordinare più dati di quelli che ci stanno in memoria?

Si, con un algoritmo di "ordinamento esterno". Algoritmi di questo genere sono descritti in Knuth, volume 3. L'idea fondamentale è quella di ordinare parte dei dati (tanti quanti ce ne stanno in memoria), scrivere i dati ordinati su file temporaneo, e poi unire i file con un ordinamento con "merge". Può darsi che il vostro sistema operativo fornisca una utility di ordinamento general purpose, e in tal caso, potete provare a invocarla da programma; si vedano le domande 19.27 e 19.30.

Riferimenti: Knuth Sez. 5.4; Sedgewick Sez. 13.

13.12: Come faccio ad accedere alla data o all'ora corrente in un programma C?

Basta usare le funzioni time(), ctime(), e/o localtime(). (Queste funzioni esistono da anni, e appartengono allo standard ANSI). Qui c'è un esempio:

#include <stdio.h>
#include <time.h>

main()
{
   time_t now;
   time(&now);
   printf("Oggi è %.24s.\n", ctime(&now));
   return 0;
}

Riferimenti: K&R2 Sez. B10; ANSI Sez. 4.12; ISO Sez. 7.12; H&S Sez. 18.

13.13: So che la routine di libreria localtime() converte un time_t in una struttura struct tm, e che ctime() converte un time_t in una stringa stampabile. Come si fa l'operazione inversa di convertire una struct tm o una stringa in un time_t?

Il C ANSI comprende una routine di libreria, mktime(), che converte una struct tm in un time_t.

Convertire una stringa in time_t è più complesso, a causa della grande varietà di formati esistenti per le date e ore. Alcuni sistemi forniscono una funzione strptime() che è sostanzialmente il contrario di strftime(). Altre routine molto usate sono partime() (distribuita con il package RCS) e getdate() (e alcune altre della distribuzione C news). Vedi 18.16.

Riferimenti: K&R2 Sez. B10; ANSI Sez. 4.12.2.3; ISO Sez. 7.12.2.3; H&S Sez. 18.4.

13.14: Come faccio a sommare N giorni a una data? Come faccio a calcolare la differenza fra due date?

Le funzioni standard ANSI/ISO mktime() e difftime() aiutano in entrambi i casi. mktime() accetta date non-normalizzate, perciò è facile prendere una struct tm, aggiungere o sottrarre dal campo tm_mday e chiamare mktime() per normalizzare anno, mese, e giorno (e, incidentalmente, convertire in un time_t). difftime() calcola la differenza, in secondi, fra due valori time_t; mktime() può essere usato per calcolare i valori time_t per le due date.

La garanzia che queste soluzioni funzionino c'è solo per date nel range che può essere rappresentato da time_t. Il campo tm_mday è un int, perciò distanza di più di circa 32,736 giorni possono causare overflow. Si noti anche che in caso di cambiamenti di ora legale, i giorni non sono lunghi 24 ore (perciò non bisogna assumere che la divisione per 86400 sia esatta).

Un altro approccio a entrambi i problemi consiste nell'usare "giorni giuliani". Implementazioni di routine per i giorni giuliani si trovano nel file JULCAL10.ZIP negli archivi Simtel/Oakland (vedi 18.16) e nell'articolo "Date conversions" citato nella Bibliografia.

Vedi anche 13.13, 20.31, e 20.32.

Riferimenti: K&R2 Sez. B10; ANSI Sez. 4.12.2.2,4.12.2.3; ISO Sez. 7.12.2.2,7.12.2.3; H&S Sez. 18.4,18.5; David Burki, "Date Conversions".

13.15: Devo generare numeri casuali.

La libreria standard ha un generatore di numeri casuali: rand(). L'implementazione sul vostro sistema potrebbe non essere perfetta, ma scriverne una migliore non è comunque facile.

Se proprio avete bisogno di implementare un generatore di numeri casuali, c'è abbondanza di materiale al riguardo in letteratura; si veda la Bibliografia. C'è anche un vasto numero di package in rete: cercate r250, RANLIB, e FSULTRA (vedi 18.16).

Riferimenti: K&R2 Sez. 2.7, Sez. 7.8.7; ANSI Sez. 4.10.2.1; ISO Sez. 7.10.2.1; H&S Sez. 17.7; PCS Sez. 11; Knuth Vol. 2 Cap. 3; Park and Miller, "Random Number Generators: Good Ones are hard to Find".

13.16: Come posso ottenere un numero casuale in un certo range?

Il metodo ovvio

rand() % N

(che torna numeri da 0 a N-1) è un po' debole, perchè i bit bassi dei numeri prodotti da alcuni generatori non sono molto random. (Vedi 13.18). Un metodo migliore è qualcosa come

(int)((double)rand() / ((double)RAND_MAX + 1) * N)

Se non volete usare i numeri in virgola mobile, si può usare

rand() / (RAND_MAX / N + 1)

Entrambi i metodi richiedono che si conosca RAND_MAX (definita nei compilatore ANSI in ), e assumono che N sia molto minore di RAND_MAX.

(Si noti, tra l'altro, che RAND_MAX è una *costante* che vi dice qual è il range fissato della funzione rand(). Non è possibile cambiare il valore di RAND_MAX, e non c'è modo di richiedere che rand() ritorni numeri in un altro range.)

Se usate un generatore di numeri casuali che torna valori in virgola mobile fra 0 e 1, tutto ciò che dovete fare per ottenere numeri da 0 a N-1 è moltiplicare per N il valore generato.

Riferimenti: K&R2 Sez. 7.8.7; PCS Sez. 11.

13.17: Ogni volta che eseguo il mio programma, rand() ritorna la stessa sequenza di numeri casuali.

Bisogna chiamare srand() per inizializzare il generatore di numeri pseudocasuali con un valore iniziale casuale ("seme"). Semi molto usati sono l'ora del giorno, o il tempo che passa prima che l'utente prema un tasto (anche se la pressione di tasti è difficile da determinare in modo portabile; vedi 19.37). (Si noti anche che di solito non serve chiamare srand() più di una volta in un programma; in particolare, non chiamate srand() prima di ogni rand() allo scopo di ottenere numeri "davvero" casuali.

Riferimenti: K&R2 Sez. 7.8.7; ANSI Sez. 4.10.2.2; ISO Sez. 7.10.2.2; H&S Sez. 17.7.

13.18: Ho bisogno di un valore casuale vero/falso, sto usando rand()%2, ma alterna 0, 1, 0, 1, 0...

I generatori di numeri pseudocasuali peggiori non sono molto random nei bit bassi. Occorre in tal caso cercare di usare i bit alti: vedi la domanda 13.16.

Riferimenti: Knuth Sez. 3.2.1.1.

13.20: Come si fa a generare numeri casuali con distribuzione normale o gaussiana?

Questo è un metodo, inventato da Box e Muller, e raccomandato da Knuth:

#include <stdlib.h>
#include <math.h>

double gaussrand()
{
   static double V1, V2, S;
   static int phase = 0;
   double X;

   if(phase == 0) {
     do {
       double U1 = (double)rand() / RAND_MAX;
       double U2 = (double)rand() / RAND_MAX;

       V1 = 2 * U1 - 1;
       V2 = 2 * U2 - 1;
       S = V1 * V1 + V2 * V2;
     } while(S >= 1 || S == 0);

     X = V1 * sqrt(-2 * log(S) / S);
   } else
     X = V2 * sqrt(-2 * log(S) / S);
   phase = 1 - phase;
   return X;
}

Si veda la versione estesa di questo listato (domanda 20.40) per ulteriori idee.

Riferimenti: Knuth Sez. 3.4.1; Box e Muller, "A Note on the Generation of Random Normal Deviates"; Press et al., _Numerical Recipes in C_ Sez. 7.2.

13.24: Sto cercando di fare il porting di un vecchio programma. Ho errori di tipo "undefined external" per le seguenti funzioni: index, rindex, bcopy, bcmp, bzero.

Tutte queste routine sono obsolete. index e rindex sono state sostituite da strchr e strrchr. Invece di bcopy, usare memmove, scambiando il primo e il secondo argomento (vedi anche 11.25). Invece di bcmp usare memcmp; invece di bzero, usare memset, con 0 come secondo argomento.

Al contrario, se vi capita di usare un vecchio sistema che non ha le funzioni strchr, strrchr, memmove, eccetera, è possibile implementarle attraverso le funzioni vecchie (con le sostituzioni opposte).

Riferimenti: PCS Sez. 11.

13.25: Continuo ad avere errori per funzioni di libreria non definite, anche se sto facendo #include di tutti i file header.

In generale, un file header contiene solo dichiarazioni. In alcuni casi (specialmente se le funzioni non sono standard) può essere necessario richiedere esplicitamente il link delle librerie corrette (includere il file header non causa automaticamente il link delle librerie). Si vedano anche le domande 11.30, 13.26, e 14.3

13.26: Continuo ad avere errori per funzioni di libreria non definite, anche se sto esplicitamente richiedendo il link delle librerie.

Molti linker eseguono una sola passata sulla lista di file oggetto e librerie specificate, e risolvono solo i riferimenti che risultano non definiti per i file esaminati in precedenza. Quindi, l'ordine in cui le librerie vengono elencate è significativo; solitamente, le librerie devono comparire per ultime. (Per esempio, sotto Unix, mettete le opzioni -l verso la fine della linea di comando). Vedi anche 13.28.

13.28: Cosa significa quando il linker dice che _end è indefinito?

È una stranezza dei vecchi linker Unix. Si ottiene il messaggio solo quando ci sono anche altre cose indefinite - correggendo gli altri errori, il messaggio riguardo a _end sparirà (vedi anche 13.25 e 13.26).


14. Virgola mobile

14.1: Perché quando assegno, per esempio, 3.1 a una variabile float, la printf stampa il suo valore come 3.0999999?

La maggior parte dei compilatori usano la base 2 per i numeri in virgola mobile, come per gli interi. In base 2, 1/1010 (cioè 1/10) è periodico: 0.0001100110011... A seconda della cura con cui sono state scritte le routine di conversione da binario a decimale (come quelle usate da printf), si possono osservare discrepanze quando numeri non rappresentabili esattamente in base 2 (specialmente se a virgola mobile in bassa precisione) vengono assegnati o letti e stampati (ovvero, convertiti dalla base 10 alla base 2 e viceversa). Vedi anche la domanda 14.6.

14.2: Sto cercando di calcolare radici quadrate, ma ottengo numeri assurdi.

Assicuratevi di aver incluso <math.h>, e di avere correttamente dichiarato le funzioni che tornano double. (Un'altra routine di libreria a cui stare attenti è atof(), dichiarata in <stdlib.h>.) Vedi anche 14.3.

Riferimenti: CT&P Sez. 4.5.

14.3: Sto cercando di eseguire alcuni semplici calcoli trigonometrici, e ho incluso , ma ho ancora l'errore "undefined: sin".

Assicurati di stare effettivamente linkando la libreria. Per esempio, sotto Unix, solitamente si deve compilare con l'opzione -lm, posta alla fine della linea di comando. Vedi anche 13.25 e 13.26.

14.4: I miei calcoli in virgola mobile si comportano in modo strano e danno risultati diversi su diverse macchine.

Prima di tutto vedi 14.2

Se il problema non è così semplice, ricorda che il formato in virgola mobile fornisce solo un'approssimazione della reale aritmetica dei numeri reali. Underflow, perdita cumulativa di precisione e altre anomalie sono comuni.

Non assumete che i risultati dei calcoli in virgola mobile siano esatti, e specialmente non assumere di poter confrontare due numeri in virgola mobile correttamente. (Non usare neanche strani "fattori fuzzy"; vedi 14.5).

Questi problemi non sono più gravi per il C di quanto siano per altri linguaggi. Molti aspetti dell'aritmetica floating point sono definiti dallo standard con l'espressione "secondo le regole del processore" (vedi anche 11.34) perché altrimenti il compilatore, su una macchina che non ha il modello "giusto", sarebbe costretto a fare simulazioni troppo costose.

In questo articoloo è impossibile elencare tutte le numerosissime trappole associate all'uso dei numeri in virgola mobile, o le soluzioni a questi problemi. Un buon libro di programmazione numerica dovrebbe fornire delle indicazioni di base; vedi anche i riferimenti.

Riferimenti: Kernighan e Plauger, _The Elements of Programming Style_ Sez. 6; Knuth, vol. 2 cap. 4; David Goldberg, "What Every Computer Scientist Should Know about Floating-Point Arithmetic".

14.5: Visto che normalmente non si può verificare l'uguaglianza esatta di due numeri in virgola mobile, c'è almeno un buon modo per verificare se sono "abbastanza vicini"?

Poiché l'accuratezza assoluta dei valori in virgola mobile varia, per definizione, a seconda della loro grandezza assoluta, il miglior modo di confrontare due valori è quello di usare una soglia di accuratezza che dipenda anch'essa dalla grandezza dei numeri stessi. Anziché fare

double a, b;
...
if(a == b) /* SBAGLIATO */

si può usare qualcosa come

#include <math.h>

if(fabs(a - b) <= epsilon * fabs(a))

per un epsilon scelto opportunamente (non zero!)

Riferimenti: Knuth Sez. 4.2.2.

14.6: Come si arrotondano i numeri?

Il modo più semplice è qualcosa come

(int)(x + 0.5)

Questa tecnica non funziona correttamente per numeri negativi (per tener conto dei quali si potrebbe usare qualcosa come (int)(x < 0 ? x - 0.5 : x + 0.5)).

14.7: Come mai il C non ha un'operatore di elevamento a potenza?

Perché pochi processori hanno un'operazione di elevamento a potenza. Il C ha una funzione pow(), dichiarata in <math.h>, anche se, nel caso di esponenti piccoli, positivi e interi è solitamente meglio eseguire esplicitamente le moltiplicazioni.

Riferimenti: ANSI Sez. 4.5.5.1; ISO Sez. 7.5.5.1; H&S Sez. 17.6.

14.8: Sembra che nel mio <math.h> manchi la #define M_PI.

Quella costante (che sembrerebbe dover contenere il valore pi greco, con precisione dipendente dalla macchina), non è standard. Se vi serve pi greco, dovrete fare una vostra #define, o calcolarlo con 4*atan(1.0).

Riferimenti: PCS Sez. 13.

14.9: Come faccio a verificare se una variabile contiene IEEE NaN o altri valori speciali?

Molti sistemi con implementazioni sofisticate dell'aritmetica in virgola mobile secondo lo standard IEEE forniscono qualche meccanismo per gestire tali valori in modo pulito (p.es. costanti predefinite o funzioni come isnan(), dichiarate come estensioni non-standard in o magari in un header nonstandard come o ). Si sta lavorando per standardizzare formalmente questi meccanismi. Un test brutale ma funzionante per verificare se un valore è NaN è

#define isnan(x) ((x) != (x))

anche se i compilatori non aderenti allo standard IEEE potrebbero eliminare il test come "ottimizzazione".

Un'altra possibilità è quella di formattare il valore in questione usando sprintf(): su molti sistemi questo genera stringhe come "NaN" o "Inf".

Vedi anche 19.39.

14.11: C'è un buon modo per implementare numeri complessi in C?

La cosa ovvia è definire una tipo struct e delle funzioni aritmetiche per manipolare strutture di quel tipo. Vedi anche 2.7, 2.10 e 14.12.

14.12: Sto cercando codice per calcolare trasformate di Fourier, algebra delle matrici (moltiplicazione, inversione, ecc.), arimetica complessa...

Ajay Shah gestisce un archivio di software numerico free; viene aggiornato periodicamente e archiviato nel direttorio comp.lang.c presso rtfm.mit.edu (vedi 20.40). Vedi anche 18.13, 18.15c e 18.16.

14.13: Ho dei problemi con un programma Turbo C che va in crash dicendo qualcosa come "floating point formats not linked."

Alcuni compilatori per piccoli computer, incluso quello della Borland (e il compilatore per PDP-11 di Ritchie) non linkano il supporto per i numeri in virgola mobile se non sembra che sia necessario. In particolare, la versione non in virgola mobile di printf e scanf risparmiano spazio se si elimina il codice per gestire %e, %f e %g. Il metodo euristico usato da Borland per scoprire se il programma usa numeri in virgola mobile è imperfetto, e a volte il programmatore deve aggiungere una chiamata esplicita a una routine di libreria come sqrt() (ma va bene una qualsiasi che usi i numeri in virgola mobile) per forzare il caricamento del supporto per i numeri in virgola mobile (vedi le FAQ di comp.os.msdos.programmer per ulteriori informazioni.)


<15. Liste di argomenti a lunghezza variabile

15.1: Ho sentito dire che per usare printf() si deve includere <stdio.h>. Perché?

In modo che ci sia in scope un prototipo di printf().

Un compilatore può usare una sequenza di chiamata speciale per le funzioni con liste di argomenti di lunghezza variabile (in particolare se le liste di argomenti di lunghezza variabile sono meno efficienti di quelle a lunghezza fissa). Quindi, un prototipo con i puntini "...", come quello di printf() (che indica una lista di argomenti di lunghezza variabile) deve essere in scope quando si chiama una funzione "varargs", in modo da dire al compilatore di usare il meccanismo di chiamata per varargs.

Riferimenti: ANSI Sez. 3.3.2.2, Sez. 4.1.6; ISO Sez. 6.3.2.2, Sez. 7.1.7; Rationale Sez. 3.3.2.2, Sez. 4.1.6; H&S Sez. 9.2.4, Sez. 9.6.

15.2: Com'è possibile che il %f della printf() vada bene sia per numeri float che per numeri double? Non sono tipi diversi?

Nella parte a lunghezza variabile di una lista di argomenti a lunghezza variabile, si applicano le "promozioni di default" degli argomenti: char e short int sono promossi a int, e float è promosso a double. (Queste sono le stesse promozioni che si applicano a chiamate di funzione senza prototipo in scope, anche note come chiamate "vecchio stile"; vedi 11.3). Perciò, il formato %f di printf opera in effetti solo con double (e analogamente, i formati %c o %hd operano solo con int). Vedi anche le domande 12.9 e 12.13.

Riferimenti: ANSI Sez. 3.3.2.2; ISO Sez. 6.3.2.2; H&S Sez. 6.3.5, Sez. 9.4.

15.3: Ho avuto un problema frustrante che ho scoperto essere causato dalla chiamata

printf("%d", n);

dove n in effetti era un long. Pensavo che i prototipi di funzione ANSI servissero a proteggere da questo genere di mancata corrispondenza dei tipi degli argomenti.

Quando una funzione accetta un numero variabile di argomenti, il suo prototipi non fornisce (non può fornire) informazione circa il numero e il tipo di tali argomenti variabili. Quindi, le protezioni normali *non* si applicano nella parte di lunghezza variabile della lista di argomenti: il compilatore non può eseguire le conversioni automatiche o (in generale) fornire warning circa l'incompatibilità di tipo.

Vedi anche le domande 5.2, 11.3, 12.9, e 15.2.

15.4: Come si fa a scrivere una funzione che riceve un numero variabile di argomenti?

Si devono usare gli strumenti dichiarati in <stdarg.h>.

Questo è un esempio di funzione che concatena un numero arbitrario di stringhe in memoria allocata con malloc:

#include <stdlib.h> /* perr malloc, NULL, size_t */
#include <stdarg.h> /* per la roba va_qualcosa */
#include <string.h> /* per strcat ecc. */

char *vstrcat(char *first, ...)
{
   size_t len;
   char *retbuf;
   va_list argp;
   char *p;

   if(first == NULL)
     return NULL;

   len = strlen(first);

   va_start(argp, first);

   while((p = va_arg(argp, char *)) != NULL)
     len += strlen(p);

   va_end(argp);

   retbuf = malloc(len + 1); /* +1 per lo \0 finale */

   if(retbuf == NULL)
     return NULL; /* errore */

   (void)strcpy(retbuf, first);

   va_start(argp, first); /* ricomincia; 2a lettura */

   while((p = va_arg(argp, char *)) != NULL)
     (void)strcat(retbuf, p);

   va_end(argp);

   return retbuf;
}

L'uso è come segue

char *str = vstrcat("Ciao, ", "mondo!", (char *)NULL);

Si noti il cast sull'ultimo argomento; vedi 5.2 e 15.3. (Si noti che il chiamante deve eseguire una free sulla memoria allocata.)

Vedi anche 15.7.

Riferimenti: K&R2 Sez. 7.3, Sez. B7; ANSI Sez. 4.8; ISO Sez. 7.8; Rationale Sez. 4.8; H&S Sez. 11.4; CT&P Sez. A.3; PCS Sez. 11, Sez. 13.

15.5: Come si fa a scrivere una funzione che riceve una stringa di formato e un numero variabile di argomenti, come printf(), e passa gli argomenti alla printf() stessa per farle fare parte del lavoro?

Si usano vprintf(), vfprintf(), o vsprintf().

Ecco una routine error() che stampa un messaggio di errore, preceduto dalla stringa "error: " e terminato con un newline:

#include <stdio.h>
#include <stdarg.h>

void error(char *fmt, ...)
{
   va_list argp;
   fprintf(stderr, "error: ");
   va_start(argp, fmt);
   vfprintf(stderr, fmt, argp);
   va_end(argp);
   fprintf(stderr, "\n");
}

Vedi anche 15.7.

Riferimenti: K&R2 Sez. 8.3, Sez. B1.2; ANSI Sez. 4.9.6.7, 4.9.6.8, 4.9.6.9; ISO Sez. 7.9.6.7, 7.9.6.8, 7.9.6.9; H&S Sez. 15.12; PCS Sez. 11.

15.6: Come si fa a scrivere una funzione che riceve una stringa di formato e un numero variabile di argomenti, come scanf(), e passa gli argomenti alla scanf() stessa per farle fare parte del lavoro?

Sfortunatamente, vscanf e simili non sono standard. Dovete arrangiarvi.

15.7: Ho un compilatore pre-ANSI, senza <stdarg.h>. Che cosa posso fare?

C'è un header più vecchio, <varargs.h>, che offre all'incirca le stesse funzionalità.

Riferimenti: H&S Sez. 11.4; CT&P Sez. A.2; PCS Sez. 11, Sez. 13.

15.8: Come si fa a capire con quanti argomenti è stata chiamata una funzione?

Questa informazione non è disponibile con mezzi portabili. Alcuni sistemi strani forniscono la funzione non standard nargs(), ma il suo uso è discutibile, perché tipicamente ritorna il numero di word passate, non il numero di argomenti. (Le strutture, i long int e i valori a virgola mobile solitamente occupano più word).

Una funzione che riceve un numero variabile di argomenti deve essere in grado di capire quanti sono gli argomenti dagli argomenti stessi. Le funzioni come printf() lo fanno esaminando gli specificatori di formato (%d e simili) nella stringa di formato (questo è il motivo per cui falliscono se la stringa di formato non è coerente con gli argomenti passati. Un'altra tecnica comune, applicabile quando gli argomenti sono tutti dello stesso tipo, è quella di usare un valore "sentinella" (per esempio, 0 o -1, o un puntatore null con cast appropriato) alla fine della lista (vedi gli esempi di execl() e vstrcat() in 5.2 e 15.4). Infine, se i tipi degli argomenti sono noti, si può passare un contatore esplicito che dice quanti sono (anche se questo è solitamente una seccatura per il chiamante che deve generarlo).

Riferimenti: PCS Sez. 11.

15.9: Il mio compilatore non mi lascia dichiarare la funzione

int f(...)
{
...
}

senza argomenti fissati.

Il C standard richiede che almeno uno degli argomenti sia fissato, in parte perché questo possa essere passato a va_start(). Vedi anche 15.10.

Riferimenti: ANSI Sez. 3.5.4, Sez. 3.5.4.3, Sez. 4.8.1.1; ISO Sez. 6.5.4, Sez. 6.5.4.3, Sez. 7.8.1.1; H&S Sez. 9.2.

15.10: Ho una funzione varargs che accetta un parametro float. Come mai

va_arg(argp, float)

non funziona?

Nella parte a lunghezza variabile delle liste di argomenti, si applicano le vecchie "promozioni di default degli argomenti": i sono sempre promossi a double, e char e short int sono promossi a int. Quindi, non è mai corretto invocare va_arg(argp, float); si dovrebbe usare va_arg(argp, double). Analogamente, si deve usare va_arg(argp, int) per recuperare argomenti che erano originariamente char, short, o int. (Per motivi analoghi, l'ultimo argomento "fissato", passato a va_start(), non dovrebbe essere espandibile.) Vedi anche 11.3 e 15.2.

Riferimenti: ANSI Sez. 3.3.2.2; ISO Sez. 6.3.2.2; Rationale Sez. 4.8.1.2; H&S Sez. 11.4.

15.11: Non riesco a usare va_arg() per estrarre un argomento di tipo puntatore a funzione.

I trucchi di riscrittura dei tipi usati dalla macro va_arg() non funzionano molto bene con tipo troppo complessi, come i puntatori a funzione. Tuttavia, se si usa una typedef per il tipo in questione tutto funzionerà bene. Vedi anche 1.21.

Riferimenti: ANSI Sez. 4.8.1.2; ISO Sez. 7.8.1.2; Rationale Sez. 4.8.1.2.

15.12: Come si fa a scrivere una funzione che riceve un numero variabile di argomenti e li passa a un'altra funzione che riceve un numero variabile di argomenti?

In generale, non si può. Idealmente, bisognerebbe innanzitutto scrivere una versione dell'altra funzione che riceva un puntatore a va_list (analogamente a vfprintf(); vedi 15.5). Se gli argomenti devono essere passati direttamente come argomenti veri e propri, o se non è possibile riscrivere la seconda funzione (in altre parole, se la seconda funzione deve ricevere un numero di argomenti variabile e non una va_list) non c'è nessuna soluzione portabile. (Si potrebbe forse risolvere il problema usando il linguaggio assembly della macchina; vedi anche 15.13).

15.13: Come si fa a chiamare una funzione passando una lista di argomenti costruita a runtime?

Non esiste un modo sicuro e portabile per farlo. Se siete curiosi, chiedete al curatore di questa lista, che ha un po' di idee strambe da proporvi...

Invece di una vera lista di argomenti, si può pensare di passare un array di puntatori generici (void*). La funzione chiamata può attraversare l'array, nello stesso modo in cui main() attraversa argv. (Ovviamente questo funziona solo se avete il controllo delle funzioni chiamate).

(Vedi anche 19.36.)


16. Strani problemi

16.2a: Ricevo errori di sintassi privi di senso, e pare che buona parte del mio programma non venga compilata.

Controllare commenti non chiusi o errori nell'uso delle direttive #if/#ifdef/#ifndef/#else/#endif, ricordandosi di controllare anche nei file header. (Vedi anche 2.18, 10.9, e 11.29.)

16.2b: Cosa c'è di sbagliato in questa chiamata di funzione? Sembra che il compilatore la salti del tutto.

Per caso si presenta così:

myprocedure;

Il C ha solo funzioni, e le chiamate di funzione richiedono sempre una lista di argomenti fra parentesi, anche quando tale lista è vuota. Usate

myprocedure();

16.3: Ho un programma che va in crash prima ancora di partire! (Se lo esamino step by step con un debugger, muore prima della prima istruzione del main().)

Probabilmente ci sono uno o più array locali molto grandi (un Kbyte o di più). Molti sistemi hanno stack di dimensione fissata, e quelli che eseguono automaticamente l'allocazione dinamica dello stack (p.es. Unix) possono comportarsi in modo strano quando lo stack cerca di crescere molto in un colpo solo. Di solito è preferibile dichiarare gli array grandi come statici o globali (a meno che, naturalmente, non ve ne serva uno nuovo per ogni chiamata ricorsiva, nel qual caso conviene allocarli dinamicamente con malloc(); vedi anche 1.31).

(Vedi anche 11.12, 16.4, 16.5, e 18.4.)

16.4: Ho un programma che sembra funzionare correttamente, ma va in crash mentre termina, *dopo* l'ultima istruzione del main(). Come è possibile?

Potrebbe esserci un errore nella dichiarazione del main() (vedi 2.18 e 10.9), o buffer locali passati a setbuf() o setvbuf(), o problemi nelle funzioni di cleanup registrate presso atexit(). Vedi anche 7.5 e 11.16.

Riferimenti: CT&P Sez. 5.3.

16.5: Questo programma funziona correttamente su una macchina, ma dà risultati strani su un'altra. Ancora più stranamente, i sintomi cambiano se si aggiungono o tolgono stampe di debug...

Ci sono tantissime cose che potrebbero stare succedendo. Ecco un elenco delle cose più comuni che vale la pena di controllare:
  • variabili locali non inizializzate (vedi anche 7.1)
  • overflow di interi, specialmente su macchine a 16-bit, specialmente relativi a un risultato intermedio in operazioni come a * b / c (vedi anche 3.14)
  • ordine di valutazione non definito (vedi da 3.1 a 3.4)
  • mancata dichiarazione di funzioni esterne, specialmente quelle che tornano qualcosa di diverso da int, o hanno argomenti "stretti" o variabili (vedi 1.25, 11.3, 14.2, e 15.1)
  • dereferenziazione di puntatori null (vedi 5)
  • uso scorretto di malloc/free: assunzioni circa il fatto che la memoria allocata da malloc contenga zeri, o che la memoria liberata con free resti accessibile, free multiple, corruzione dello heap (vedi anche 7.19 a 7.20)
  • problemi con i puntatori in generale (vedi 16.8)
  • mancata corrispondenza fra la stringa di formato di printf() e gli argomenti, specialmente cercare di stampare dei long usando %d (vedi 12.9)
  • cercare di allocare più memoria di quella che può essere contata da un unsigned int, specialmente su macchine con memoria limitata (vedi 7.16 e 19.23)
  • problemi con i margini degli array, specialmente per piccoli buffer temporanei, magari usati per costruire stringhe con sprintf() (vedi 7.1 e 12.21)
  • assunzioni sbagliate sulla mappatura delle typedef, in particolare problemi con il size_t di numeri in virgola mobile (vedi 14.1 e 14.4)
  • ogni cosa che vi sia sembrata furba per sfruttare le caratteristiche specifiche del vostro compilatore
Un uso appropriato dei prototipi può intercettare molti di questi problemi; lint può rivelarne altri. Vedi anche 16.3, 16.4, e 18.4.

16.6: Perché questo codice:

char *p = "hello, world!";
p[0] = 'H';

va in crash?

Le costanti stringa non sono necessariamente modificabili, tranne quando (regola pratica) sono usate come inizializzatori di array. Provare così:

char a[] = "hello, world!";

Vedi anche 1.32.

Riferimenti: ANSI Sez. 3.1.4; ISO Sez. 6.1.4; H&S Sez. 2.7.4.

16.8: Cosa significano "segmentation violation", "segmentation fault", e "Bus error"?

In generale indicano che il vostro programma ha cercato di accedere ad aree di memoria illegali, a seguito della corruzione dello stack o dell'uso scorretto di puntatori. Cause probabili sono l'overflow di array locali (automatici, e quindi allocati sullo stack); uso di puntatori null (vedi 5.2 e 5.20) o di puntatori non inizializzati, male allineati, o comunque impropriamente allocati (vedi 7.1 e 7.2); corruzione dello heap (vedi 7.19); e argomenti sbagliati per le funzioni, specialmente se ci sono di mezzo puntatori; due esempi sono scanf() (vedi 12.12) e fprintf() (assicurarsi che il primo argomento sia FILE *).

Vedi anche 16.3 e 16.4.


17. Stile

17.1: Qual'è il migliore stile di indentazione per il C?

K&R forniscono lo stile più diffuso, benché, al tempo stesso, essi forniscano anche una buona scusa per ignorarlo:

"La posizione delle graffe è meno importante, anche se la gente ci si appassiona. Abbiamo scelto uno di molti stili comunemente usati. Scegliete lo stile che volete, e poi usatelo in modo consistente."

È più importante che l'indentazione sia consistente (con se stessa, e con il resto del codice) piuttosto che sia "perfetta". Se il vostro ambiente (per esempio, l'azienda in cui lavorate) non suggerisce un particolare stile, e non vi va di invertarvene uno, copiate K&R. (Pro e contro di vari stili di indentazione e posizionamento delle graffe sono stati dibattuti alla morte, ma non vale la pena di entrare nel merito in questo articolo. Si veda anche la Indian Hill Style Guide).

La qualità ambigua del "buon stile" comprende molto più che semplici dettagli di indentazione; non perdete tempo formattando il testo se lo fate a discapito di questioni più rilevanti.

Si veda anche la domanda 10.6.

Riferimenti: K&R1 Sez. 1.2; K&R2 Sez 1.2.

17.3: Questo è un bel trucco per verificare se due stringhe sono uguali:

if(!strcmp(s1, s2))

È buono stile?

Non è stile particolarmente buono, ma è molto diffuso. Il test ha successo se le stringhe sono uguali, ma la presenza del ! ("not") potrebbe dare al lettore l'idea opposta.

Un'opzione migliore è:

#define Streq(s1, s2) (strcmp((s1), (s2)) == 0)

Le opinioni sullo stile, come quelle sulla religione, possono essere dibattute all'infinito. Anche se il buon stile è un obiettivo più che degno di essere perseguito, non può essere codificato in modo rigoroso. Vedi anche 17.10.

17.4: Perché alcuni scrivono if(0 == x) invece di (x == 0)?

È un trucco per difendersi dall'errore comune di scrivere

if(x = 0)

Se ci si abitua a scrivere la costante a sinistra, il compilatore segnalerà l'errore nel caso scriviate

if(0 = x)

Evidentemente, per qualcuno è più semplice ricordarsi di invertire il test che ricordarsi di usare "==".

Riferimenti: H&S Sez. 7.6.5.

17.5: Ho letto un programma in cui c'era un cast (void) prima di ogni printf(). Come mai?

printf() ritorna un valore, sebbene i programmi che lo usano sono molto pochi. Poiché alcuni compilatori (e lint) danno warning circa valori di ritorno ignorati, un cast a (void) serve a dire, "Si, ho deciso di ignorare questo valore di ritorno, ma continua a segnalarmi altri valori ignorati per errore". È comune fare cast di questo tipo nelle chiamate a strcpy() e strcat(), perché il valore di ritorno non serve mai a nessuno.

Riferimenti: K&R2 Sez. A6.7; Rationale Sez. 3.3.4; H&S Sez. 6.2.9, Sez. 7.13.

17.8: Cos'è la "Notazione Ungherese"? Vale le pena impararla?

La notazione ungherese è una convenzione sulla scelta degli identificatori, inventata da Charles Simonyi, che fa sì che il nome di una variabile fornisca informazioni sul suo tipo (e in alcuni casi l'uso che se ne vuole fare). È amata in alcuni ambienti e disprezzata in altri. Il suo più grande vantaggio è che rende ovvi i tipi delle variabili; il suo più grande svantaggio è che non è detto che valga la pena o sia vantaggioso mettere il tipo di una variabile nel suo nome.

Riferimenti: Simonyi and Heller, "The Hungarian Revolution" .

17.9: Dove posso procurarmi la "Indian Hill Style Guide" e altri standard di codifica?

Molti documenti si possono ottenere via ftp anonima da:
  • cs.washington.edu, directory pub/cstyle.tar.Z (guida Indian Hill aggiornata)
  • ftp.cs.toronto.edu, directory doc/programming
  • ftp.cs.umd.edu, directory pub/style-guide
Potrete anche leggere _The Elements of Programming Style_, _Plum Hall Programming Guidelines_, e _C Style: Standards and Guidelines_; vedi la Bibliografia. (Il libro _Standards and Guidelines_ in effetti non è una guida allo stile, ma un insieme di linee guida per la selezione e la creazione di guide allo stile).

Vedi anche 18.9.

17.10: Alcuni sostengono che i goto sono male e non vanno usati per nessun motivo. Non è una posizione un po' estrema?

Lo stile di programmazione, come lo stile con cui si scrive, assomiglia a un'arte, e non può essere codificato con regole inflessibili, anche se le discussioni sullo stile sembrano essere quasi sempre incentrate su regole di questo tipo.

Nel caso del goto, si è capito molto tempo fa che l'uso incontrollato del goto porta a programmi "spaghetti" non gestibili. Tuttavia, bandire il goto non conduce immediatamente alla buona programmazione: un programmatore non strutturato può costruire un labirinto bizantino anche senza goto (perempio sostituendolo con cicli innestati in modi strani e variabili booleane di controllo). La maggior parte delle osservazioni e delle "regole" circa il buon stile funzionano meglio come linee guida che come regole, e se i programmatori capiscono cosa si vuole ottenere con quella regola anziché seguirla pedissequamente. Evitare certi costrutti senza sapere perché può causare altrettanti problemi.

Inoltre, molte opinioni a proposito dello stile di programmazione sono per l'appunto opinioni. Di solito è futile farsi trascinare in "guerre di stile" perché su molti argomenti (come quelli trattati in 9.2, 5.3, 5.9, e 10.7), i contendendi non riescono mai a concordare, concordare di discordare, o smettere di dibattere.


18. Tool e altre risorse

18.1: Ho bisogno di un....

  • generatore di riferimenti incrociati: cflow, cxref, calls, cscope, xscope, o ixfw
  • un tool per abbellire o stampare elegantemente codice C: cb, indent, GNU indent, o vgrind
  • un tool di controllo delle versioni o configuration management: RCS o SCCS
  • un offuscatore di codice C: obfus, shroud, o opqcp
  • un generatore di dipendenze per make: makedepend, oppure cc -M o cpp -M
  • tool per misurare metriche del codice: ccount, Metre, lcount, o csize, or vedi URL http://www.qucis.queensu.ca/Software-Engineering/Cmetrics.html; c'è anche package venduto da McCabe and Associates
  • un contatore di linee di codice: si può fare (alla buona) con l'utility standard Unix wc, e un po' meglio con grep -c ";"
  • un tool che assiste nelle dichiarazioni C: cdecl, vedi 1.21
  • un generatore di prototipi: vedi 11.31
  • un tool per cercare errori legati a malloc: vedi 18.2
  • un preprocessore C "selettivo": vedi 10.18
  • tool di traduzione fra linguaggi: vedi 11.31 e 20.26
  • verificatori C (lint): vedi 18.7
  • un compilatore C!: vedi 18.3
(Questa lista non è assolutamente completa; se sapete di altri tool non menzionati, vi saremmo grati se contattaste il gestore di questa lista).

Altre liste di tool, con commenti, si trovano nei newsgroup Usenet comp.compilers e comp.software-eng .

Vedi anche 18.16 e 18.3.

18.2: Esiste un tool che aiuti nella ricerca di problemi legati alla malloc()?

Esistono numerosi package dedicati. Uno famoso è "dbmalloc" di Conor P. Cahill, pubblicato nel 1992 presso comp.sources.misc, volume 32. Altri sono "leak," disponibile nel volume 27 degli archivi comp.sources.unix; JMalloc.c e JMalloc.h nella collezione "Snippets"; e MEMDEBUG, reperibile presso ftp.crpht.lu nel direttorio pub/sources/memdebug. Vedi anche 18.16.

Esistono anche molti prodotti commerciali per il debug che possono fornire un grandissimo aiuto nella ricerca di problemi relativi a malloc e altri problemi complessi: "Bounds-Checker for DOS" della Nu-Mega Technologies (P.O. Box 7780, Nashua, NH 03060-7780, USA, 603-889-2386); "CodeCenter" (una volta chiamato Saber-C) della Centerline Software (10 Fawcett Street, Cambridge, MA 02138, USA, 617-498-3000); "Insight" della ParaSoft Corporation (2500 E. Foothill Blvd., Pasadena, CA 91107, USA, 818-792-9941, insight@parasoft.com); "Purify" della Pure Software (1309 S. Mary Ave., Sunnyvale, CA 94087, USA, 800-224-7873, http://www.pure.com, info-home@pure.com); "Final Exam Memory Advisor" della PLATINUM Technology (precedentemente distribuito come "Sentinel" dalla AIB Software) (1815 South Meyers Rd., Oakbrook Terrace, IL 60181, USA, 630-620-5000, 800-442-6861, info@platinum.com, www.platinum.com); "ZeroFault" di The Kernel Group (1250 Capital of Texas Highway South, Building Three, Suite 601, Austin, TX 78746, 512-433-3333, http://www.tkg.com, zf@tkg.com.

18.3: C'è un compilatore free o poco costoso?

Un compilatore free e di qualità molto famoso è quello della FSF-GNU, gcc. È disponibile via ftp anonima presso prep.ai.mit.edu (direttorio pub/gnu), e presso molti altri siti ftp della FSF. Una versione MSDOS, djgpp, può essere scaricata da ftp.delorie.com (pub/djgpp), o da uno dei numerosi mirror SimTel (p.es. ftp.simtel.net, pub/simtelnet/gnu/djgpp, e ftp.coast.net, SimTel/vendors/djgpp).

C'è un compilatore shareware chiamato PCC, scaricabile come PCC12C.ZIP .

Un altro compilatore molto economico per MS-DOS è "Power C" della Mix Software (1132 Commerce Drive, Richardson, TX 75801, USA, 214-783-6001).

Un altro compilatore sviluppato recentemente è lcc, disponibile via ftp anonima da ftp.cs.princeton.edu, in pub/lcc/.

Un compilatore shareware per MS-DOS C è disponibile presso ftp.hitech.com.au/hitech/pacific. La registrazione è opzionale per uso non commerciale.

Gli archivi associati a comp.compilers contengono molte informazioni su compilatori, interpreti, grammatiche, ecc. (per molti linguaggi). Gli archivi di comp.compilers (che includono una FAQ), sono mantenuti dal moderatore, John R. Levine, e si trovano presso iecc.com. Un'altra lista di compilatori e risorse simili è mantenuta da Mark Hopkins, Steven Robenalt e David Muir Sharnoff, presso ftp.idiom.com nel direttorio pub/compilers-list/. (Vedi anche il direttorio comp.compilers nell'archivio news.answers presso rtfm.mit.edu e ftp.uu.net; vedi 20.40.)

Vedi anche 18.16.

18.4: Ho appena terminato questo programma, e si comporta in modo strano. C'è qualcosa di sbagliato?

Prima di tutto esegui lint (magari con le opzioni -a, -c, -h, -p o altre). Molti compilatori sono in effetti mezzi compilatori, in quanto non diagnosticano quei problemi nel sorgente che non precludono la generazione di codice.

Vedi anche 16.5, 16.8 e 18.7.

Riferimenti: Ian Darwin, _Checking C Programs with lint_ .

18.5: Come posso far zittire la "warning: possible pointer alignment problem" data da lint per ogni chiamata a malloc()?

Il problema è che la versione tradizionale di lint non sa, e non gli si può dire, che la malloc() "ritorna un puntatore a uno spazio allineato in modo opportuno per la memorizzazione di qualsiasi tipo di oggetto". È possibile fornire una pseudoimplementazione di malloc() usando una #define dentro #ifdef lint, che zittisce questa warning, ma se non si fa attenzione questo sopprimerà anche messaggi relativi a invocazioni veramente scorrette. È probabilmente più semplice ignorare il messaggio, magari filtrandolo via in modo automatico con grep -v. (Ma non bisogna prendere l'abitudine di ignorare troppi messaggi di lint, altrimenti prima o poi capiterà di ignorarne uno importante).

18.7: Come posso procurarmi un lint ANSI-compatibile?

Ci sono due prodotti noti come PC-Lint e FlexeLint (in versione "shrouded source", compilabili su quasi qualsiasi sistema) disponibili presso

Gimpel Software
3207 Hogarth Lane
Collegeville, PA 19426 USA
(+1) 610 584 4261
gimpel@netaxs.com

Il lint di Unix System V release 4 è ANSI-compatibile, ed è disponibile separatamente (insieme ad altri tool C) dagli UNIX Support Labs o dai rivenditori di System V.

Un altro lint ANSI-compatibile (che può anche eseguire verifiche formali di più alto livello) è LCLint, disponibile via ftp anonima da larch.lcs.mit.edu, in pub/Larch/lclint/.

Se non si ha lint, si tenga presente che molti compilatori moderni tentano di diagnosticare la maggior parte dei problemi diagnosticati da lint. (Molti raccomandano gcc -Wall -pedantic .)

18.8: I prototipi ANSI rendono obsoleto lint?

No. Innanzitutto, i prototipi funzionano solo se ci sono e sono corretti; un prototipo scorretto è anche peggio che nessun prototipo. Secondo, lint verifica la consistenza di più file sorgente, e verifica le dichiarazioni dei dati oltre che delle funzioni. Infine, un programma indipendente come lint è quasi sempre più scrupoloso nel forzare pratiche di programmazione compatibili e portabili rispetto a qualsiasi compilatore (che è comunque particolare, dipendente dall'implementazione, ed arricchito di estensioni).

Se proprio volete usare i prototipi invece di lint per le verifiche di consistenza fra file, assicuratevi di usare prototipi correttamente nei file header. Vedi 1.7 e 10.6.

18.9: Ci sono manuali di C e risorse simili su Internet?

Molti: "Notes for C programmers" di Christopher Sawtell è disponibile presso svr-ftp.eng.cam.ac.uk, in misc/sawtell_C.shar e presso garbo.uwasa.fi, in /pc/c-lang/c-lesson.zip. "C for Programmers" di Tim Love è disponibile presso svr-ftp.eng.cam.ac.uk nel direttorio misc e, in html, presso http://club.eng.cam.ac.uk/help/tpl/languages/C/teaching_C/teaching_C.html. I tutorial "The Coronado Enterprises C" sui mirror Simtel in pub/msdos/c e sul web alla URL http://www.swcp.com/~dodrill. Un tutorial di Rick Rowe presso ftp.netcom.com col nome pub/rowe/tutorde.zip o presso ftp.wustl.edu col nome pub/MSDOS_UPLOADS/programming/c_language/ctutorde.zip. Ovviamente c'è un corso sul web presso http://www.strath.ac.uk/CC/Courses/CCourse/CCourse.html. Martin Brown pubblica materiali su un corso C sul web presso http://www-isis.ecs.soton.ac.uk/computing/c/Welcome.html. Su alcune macchine Unix si può provare a digitare "learn C" al prompt di shell. Infine, l'autore di questa FAQ insegna in un corso di C e ha iniziato a pubblicare sul web le proprie dispense, alla URL http://www.eskimo.com/~scs/cclass/cclass.html .

[Non ho personalmente visto tutti questi tutorial, e so che almeno uno di essi contiene vari errori. Con l'eccezione di quello con il mio nome, non posso garantire per il loro contenuto. Inoltre, questo tipo di informazione diventa rapidamente non aggiornata; gli indirizzi potrebbero non essere più funzionanti quando leggete questo documento].

Molti di questi tutorial, e molto altro materiale sul C, sono disponibili sul web presso http://www.lysator.liu.se/c/index.html .

Vinit Carpenter gestisce una lista di risorse per imparare C e C++, pubblicata presso comp.lang.c e comp.lang.c++ e archiviata negli stessi siti in cui è archiviato questo documento (vedi 20.40) o sul web presso http://www.cyberdiem.com/vin/learn.html .

Vedi anche 18.10 e 18.15c.

18.10: Qual è un buon libro per imparare il C?

Ci sono troppi libri sul C perché io possa elencarli; è impossibile averli letti tutti e poterli giudicare. Molti pensano che il migliore sia il primo _The C Programming Language_, di Kernighan e Ritchie ("K&R," attualmente alla seconda edizione). Ci sono opinioni diverse circa il fatto che K&R sia adatto come primo libro di programmazione; molti di noi hanno imparato il C su quel libro, e l''anno imparato bene; alcuni, tuttavia, pensano che sia un po' troppo clinico come primo libro per chi non ha background di programmazione. Molte annotazioni e errata corrige per K&R sono disponibili in rete, vedi p.es.

http://www.csd.uwo.ca/~jamie/.Refs/.Footnotes/C-annotes.html
http://www.eskimo.com/~scs/cclass/cclass.html
http://www.lysator.liu.se/c/c-errata.html#main

Un manuale di riferimento eccellente è _C: A Reference Manual_ di Samuel P. Harbison e Guy L. Steele, attualmente alla quarta edizione.

Anche se non è adatta a insegnare il C da zero, questa FAQ è stata pubblicata come libro; vedi la Bibliografia.

Mitch Wright gestisce una bibliografia annotata di libri sul C e Unix; è disponibile via ftp anonima presso ftp.rahul.net, in pub/mitch/YABL/.

La Association of C and C++ Users (ACCU) gestisce un vasto insieme di recensioni bibliografiche di titoli sul C/C++ presso http://bach.cis.temple.edu/accu/bookcase e http://www.accu.org/accu .

Il curatore di questa FAQ gestisce una collezione di risposte a domande, disponibile su richiesta. Vedi anche 18.9.

18.13: Dove possono trovare il codice sorgente delle librerie standard C?

Una fonte (anche se non di pubblico dominio) è _The Standard C Library_ di P.J. Plauger (vedi Bibliografia). Implementazioni di tutta o parte della libreria C sono disponibili come parte dei progetti NetBSD e GNU (anche Linux). Vedi anche 18.15c e 18.16.

18.14: Ho bisogno di codice per eseguire parsing e valutazione di espressioni.

Due package disponibili sono "defunc," pubblicato presso comp.sources.misc nel dicembre 1993 (V41 i32,33), presso alt.sources nel gennaio 1994, e disponibile presso sunsite.unc.edu in pub/packages/development/libraries/defunc-1.3.tar.Z, e "parse," presso lamont.ldgo.columbia.edu. Alternative includono l'interprete di S-Lang, disponibile via ftp anonima presso amy.tch.harvard.edu in pub/slang, e lo shareware Cmm ("C-minus-minus" o "C meno la parte difficile"). Vedi 18.16.

C'è del codice di parsing/valutazione anche in _Software Solutions in C_ (cap. 12).

18.15: Dove possono trovare una grammatice BNF o YACC per il C?

La grammatica definitiva è naturalmente quella nello standard ANSI; vedi 11.2. Un'altra è fornita da Jim Roskind (assieme a una grammatica per il C++) in pub/c++grammar1.1.tar.Z presso ics.uci.edu. Una versione scarna ma funzionante della grammatica ANSI (realizzata da Jeff Lee) è disponibile presso ftp.uu.net (vedi 18.16) in usenet/net.sources/ansi.c.grammar.Z (assieme a un esaminatore lessicale). Il compilatore FSF GNU contiene a grammar, così come l'appendice a K&R2.

Gli archivi di comp.compilers forniscono ulteriori informazioni sulle grammatiche; vedi 18.3.

Riferimenti: K&R1 Sez. A18; K&R2 Sez. A13; ANSI Sez. A.2; ISO Sez. B.2; H&S appendice B.

18.15a: Qualcuno può fornirmi una test suite per compilatori C?

Plum Hall (precedentemente a Cardiff, NJ; ora alle Hawaii) ne vende una; altri pacchetti sono RoadTest(tm) di Ronald Guilmette (collegarsi in ftp a netcom.com, e scaricare pub/rfg/roadtest/announce.txt, per informazioni) e l'Automated Compiler Performance Analysis Tool di Nullstone (vedi http://www.nullstone.com). La distribuzione di gcc della FSF GNU include un test-tortura-C che verifica molti problemi comuni dei compilatori. Il "test della paranoia" di Kahan disponibile presso netlib/paranoia su netlib.att.com, mette a dura prova le capacità di calcolo in virgola mobile di un compilatore C.

18.15c: Dove posso trovare collezioni di frammenti di codice ed esempi utili?

Gli "Snippet" di Bob Stout sono disponibili presso ftp.brokersys.com nella directory pub/snippets o sul web presso http://www.brokersys.com/snippets/ .

La "publib" di Lars Wirzenius è disponibile presso ftp.funet.fi nel direttorio pub/languages/C/Publib/.

Vedi anche 14.12, 18.9, 18.13, e 18.16.

18.16: Dove e come posso procurarmi copie di tutto questo software free?

Siccome il numero dei programmi, il numero dei siti che li pubblicano, e il numero di persone che vi accedono, crescono, questa domanda è facile e difficile allo stesso tempo.

Ci sono molti siti pubblici, come ftp.uu.net, archive.umich.edu, oak.oakland.edu, sumex-aim.stanford.edu, e wuarchive.wustl.edu, che hanno enormi quantità di software e altre risorse, free. Il sito di distribuzione centrale per il progetto GNU è prep.ai.mit.edu. Questi siti famosi tendono a diventare sempre più carichi e difficili da raggiungere, ma ci sono anche molti siti "mirror" che cercano di smistare parte del traffico.

Su Internet, il modo tradizionale per scaricare software da un sito è via ftp anonima. Per chi non ha accesso ftp, esistono anche molti server che inviano per posta file scaricati da ftp server. Sempre di più, il web sta comunque diventano lo strumento usato per annunciare, organizzare, e persino trasferire file di dati. Ci sono probabilmente anche metodi di accesso ancora più moderni.

Questa era la parte facile della risposta. La parte difficile sono i dettagli - è veramente impossibile conoscere o elencare tutti i siti o tutti i modi di accedere a essi. Se avete accesso alla rete, probabilmente siete in grado di procurarvi informazioni più aggiornate su siti e metodi di accesso di quanto non possiate ricavare da questo documento.

L'altro aspetto difficile della domanda, è spiegarvi almeno come *trovare* questi siti. C'è molto lavoro che sta venendo svolto in quest'area, e probabilmente ogni giorno nasce un nuovo meccanismo di indicizzazione e ricerca. Uno dei primi era "archie": per ogni programma o risorsa disponibile sulla rete, se ne conoscevate il nome, l'archie server vi diceva su quali siti ftp anonimi lo avevano. Potreste disporre ancora di un comando "archie" sul vostro sistema, o altrimenti potete mandare una mail "help" a archie@archie.cs.mcgill.ca per avere informazioni.

Se avete accesso a Usenet, leggete gli articoli pubblicati sui newgroup comp.sources.unix e comp.sources.misc newsgroups, in particolare quelli che descrivono le politiche di archiviazione e come accedere agli archivi, due dei quali sono ftp://gatekeeper.dec.com/pub/usenet/comp.sources.unix/ e ftp://ftp.uu.net/usenet/comp.sources.unix/. Il newsgroup comp.archives contiene numerosi annunci circa la disponibilità di varie risorse via ftp anonima. Infine, il newsgroup comp.sources.wanted è un posto appropriato per richiedere codice sorgente, ma prima di farlo leggere la *loro* FAQ ("How to find sources").

Vedi anche 14.12, 18.13, e 18.15c.


19. Caratteristiche dipendenti dal sistema

19.1: Posso leggere un carattere dalla tastiera senza aspettare che l'utente prima "invio"? Posso fare in modo che i caratteri digitati non siano visualizzati sullo schermo?

Non c'è un modo standard o portabile per fare questo genere di cose in C. Concetti come "schermo" e "tastiera" non sono neanche menzionati nello Standard, che parla soltanto di I/O di "flussi" (stream) di caratteri.

A qualche livello, l'input da tastiera è solitamente bufferizzato e passato al programma una linea per volta. In questo modo il sistema operativo può consentire l'editing della linea di input (backspace, delete, ecc) in modo consistente, senza richiedere che sia fornito da ogni singolo programma indipendentemente. Quando l'utente è soddisfatto e preme "invio" (o un tasto equivalente) la linea digitata viene passata al programma. Anche se il programma legge l'input un carattere alla volta (con getchar() o simili), la prima chiamata si blocca finché l'utente ha concluso l'intera linea, e a quel punto diventano disponibili tutti i caratteri che costituiscono la linea stessa, cosicché un certo numero di richieste di caratteri (p.es. chiamate a getchar()) sono soddisfatte in rapida successione.

Se un programma vuole leggere ogni carattere immediatamente, quello che occorre fare dipende dal particolare luogo in cui sta avvenendo la bufferizzazione delle righe e dalle modalità disponibili per disabilitarla. Sotto alcuni sistemi (p.es. MS-DOS e VMS in alcune modalità), esistono un insieme di chiamate di input speciali per bypassare la bufferizzazione. Sotto altri sistemi (p.es. Unix e VMS in altre modalità), la parte del sistema operativo responsabile per la serializzazione dell'input (chiamata spesso "terminal driver") deve essere posta in una modalità che disabilita la bufferizzazione in linea, dopodiché le normali chiamate (read(), getchar(), ecc.) ritorneranno i caratteri letti immediatamente. Infine, alcuni sistemi (specialmente vecchi sistemi batch) eseguono la gestione dell'input su processori periferici che non possono essere controllati in alcun modo dal C.

Quindi, quando si vuole fare input un carattere alla volta (o disabilitare l'eco, che è un problema analogo), la tecnica da usare dipende dal sistema, e potrebbe non esserci nessun modo per farlo. Poiché comp.lang.c è orientato ai problemi per cui il C fornisce supporto, in generale è consigliabile che rivolgiate eventuali domande in questo senso a newsgroup specifici sui vari sistemi, come comp.unix.questions o comp.os.msdos.programmer, o che leggiate le FAQ di tali gruppi. Si noti che la risposta può addirittura variare in diverse versioni di uno stesso sistema. Siccome comunque queste domande compaiono frequentemente su questo newsgroup, ecco alcune soluzioni per i casi più comuni:

Alcune versioni della libreria curses contengono funzioni che si chiamano cbreak(), noecho(), e getch() che fanno quanto richiesto. In particolare, se l'obiettivo è quello di leggere una breve password disabilitando l'eco a video, provate getpass(). Sotto Unix, si può usare ioctl() per manipolare la modalità del terminal driver (CBREAK o RAW nelle versioni "classiche"; ICANON, c_cc[VMIN] e c_cc[VTIME] su System V o sistemi POSIX; ECHO funziona su tutte le versioni), oppure system() e il comando stty. (Per ulteriori dettagli, vedi e tty(4) nelle versioni classiche, e termio(4) in System V, o e termios(4) su POSIX). In MS-DOS, usate getch() o getche(), o i corrispondenti interrupt della BIOS. Sotto VMS, provate a usare le routine di Screen Management (SMG$), o curses, o fate delle $QIO a basso livello con il codice IO$_READVBLK (e magari IO$M_NOECHO e altri) per ottenere un carattere alla volta. (Si può anche settare le modalità "un carattere alla volta" o "pass through" nel terminal driver VMS). Su altri sistemi, dovrete arrangiarvi.

(Nota a margine: usare setbuf() o setvbuf() per rendere stdin non bufferizzato *non* funziona, in generale, per consentire l'input un carattere alla volta).

Se state cercando di scrivere un programma portabile, è consigliabile scrivere tre funzioni che (1) settano il terminal driver o il sistema di input in modalità "un carattere alla volta"; (2) leggono i caratteri; (3) riportano il terminale allo stato iniziale. (Idealmente, queste funzioni dovrebbero essere, un giorno, parte dello standard C). La versione estesa di queste FAQ (vedi 20.40) contiene esempi di funzioni di questo tipo per molti sistemi operativi comuni.

Vedi anche 19.2.

Riferimenti: PCS Sez. 10, Sez. 10.1; POSIX Sez. 7.

19.2: Posso sapere se ci sono dei caratteri in attesa di essere letti (e quanti sono)? In alternativa, posso fare una lettura che non si blocchi se non ci sono caratteri pendenti?

Anche queste sono cose dipendenti dal sistema. Alcune versioni di curses hanno una funzione nodelay(). A seconda del sistema usato, potreste anche poter usare "I/O non bloccante" o una chiamata di sistema chiamata "select" o "poll", o il FIONREAD ioctl, o c_cc[VTIME], o kbhit(), o rdchk(), o l'opzione O_NDELAY di open() o fcntl(). Vedi anche 19.1.

19.3: Come faccio a mostrare una barra indicatrice di progresso, come quelle che si vedono durante i trasferimenti di file?

Finalmente, questo è qualcosa che si può fare abbastanza portabilmente. Stampare il carattere '\r', su quasi tutti i sistemi, causa un ritorno carrello (carriage return) senza line feed, in modo tale che potete riscrivere la linea corrente. Il carattere '\b' è un backspace, e normalmente sposta il cursore di una posizione a sinistra.

Riferimenti: ANSI Sez. 2.2.2; ISO Sez. 5.2.2.

19.4: Come faccio a cancellare lo schermo? Come si stampano scritte in reverse? Come si sposta il cursore in una determinata posizione x,y dello schermo?

Dipende dal tipo di terminale (o display). Occorre usare una libreria come termcap, terminfo, o curses, o qualche routine specifica del sistema.

Per cancellare lo schermo, una soluzione quasi portabile è stampare il carattere form-feed ('\f'). Ancora più portabile è scrivere una sequenza di newline in modo da far scomparire tutto via scroll. Come ultima risorsa, si può usare system() (vedi domanda 19.27) per invocare il comando di cancellazione dello schermo del sistema operativo.

Riferimenti: PCS Sez. 5.1.4, Sez. 5.1.5.

19.5: Come faccio a leggere i tasti freccia? E i tasti funzione?

Terminfo, alcune versioni di termcap, e alcune versioni di curses supportano questi tasti non-ASCII. Di solito, un tasto speciale corrisponde a una sequenze multicarattere (che solitamente inizia con ESC, '\033'); il parsing può essere non facilissimo. (curses fa il parsing per voi, se chiamate, prima, keypad()).

Sotto MSDOS, se si riceve un carattere che vale 0 (*non* '0'), questo è in realtà una flag che indica che il prossimo carattere sarà un tasto speciale. Si consulti una guida alla programmazione su DOS per un elenco dei codici di tastiera. (In breve: le frecce su, sinistra, destra e giù sono 72, 75, 77, e 80; i tasti funzione vanno da 59 a 68).

Riferimenti: PCS Sez. 5.1.4.

19.6: Come gestisco il mouse?

Consultate la documentazione del vostro sistema, o chiedete a un newsgroup specifico (leggendo le FAQ prima). La gestione del mouse è completamente diversa su X Windows, MS-DOS, Macintosh, e su ogni altro sistema.

Riferimenti: PCS Sez. 5.5 pp. 78-80.

19.7: Come si fa a fare I/O sulla porta seriale (comm)?

Dipende dal sistema. Sotto Unix, di solito si apre, legge e scrive un file device sotto /dev, e si usano le facility del termional driver per configurarne le caratteristiche. (Vedi anche 19.1 e 19.2). Sotto MS-DOS, si può usare lo stream predefinito stdaux, o un file speciale come COM1, o interrupt primitive del BIOS, oppure (se vi servono delle prestazioni decenti) un certo numero di pacchetti per l'I/O seriale guidato da interrupt. Molti raccomandano il libro _C Programmer's Guide to Serial Communications_ di Joe Campbell.

19.8: Come si fa a dirigere l'output sulla stampante?

Sotto Unix, si può usare popen() (vedi 19.30) per mandare l'output ai programmi lp o lpr, o su un file speciale come /dev/lp. Sotto MS-DOS, si scrive sullo stream predefinito (non standard) stdprn, oppure si aprono i file speciali PRN o LPT1.

Riferimenti: PCS Sez. 5.3.

19.9: Come faccio a inviare sequenze di escape per controllare un terminale o un'altra periferica?

Se siete in grado di spedire caratteri alla periferica (vedi 19.8), è facile spedire anche sequenze di escape. In ASCII, ESC ha codice 033 (27 decimale), perciò un'istruzione come

fprintf(ofd, "\033[J");

invia la sequenza ESC [ J .

19.10: Come faccio a usare la grafica?

Una volta, Unix aveva un piccolo grazioso insieme di routine di disegno indipendenti da device, descritte in plot(3) e plot(5), ma ormai sono in disuso.

Se programmate per MS-DOS, probabilmente vi conviene usare librerie conformi agli standard VESA o BGI.

Se state cercando di comandare uno specifico plotter, di solito questo si controlla spedendogli sequenze di escape; vedi 19.9. Il rivenditore può fornire una libreria invocabile da C, o potreste trovarne una in rete.

Se state programmando per un sistema a finestre (Macintosh, X windows, Microsoft Windows), dovrete usare le sue facility; cercate nella documentazione o nei newsgroup o nelle FAQ.

Riferimenti: PCS Sez. 5.4.

19.11: Come si fa a verificare se esiste un determinato file? Voglio avvertire l'utente se il file di input richiesto non esiste.

È incredibilmente difficile risolvere questo problema in modo affidabile e portabile. Ogni test può essere invalidato se il file viene creato o cancellato (p.es. da altri processi) nell'arco di tempo fra la vostra richiesta e il momento in cui cercate di aprirlo.

Tre routine che servono a questo scopo sono stat(), access(), e fopen(). (Per fare il test usando fopen(), aprite in lettura e chiudete subito dopo; il fallimento di fopen indica solitamente, ma non sempre, il fatto che il file non esiste). Di queste, solo fopen() è largamente portabile, e access(), se esiste, deve essere usata con attenzione se il programma usa il set-UID di Unix.

Anziché tentare di prevedere se una data operazione, come l'apertura di un file, avrà successo, è spesso preferibile provare a farla, verificare il valore di ritorno, e segnalare se fallisce. (Ovviamente, questo approccio non funziona se si sta cercando di evitare di sovrascrivere un file esistente, a meno di non avere a disposizione qualcosa come l'opzione O_EXCL, che fa proprio questo).

Riferimenti: PCS Sez. 12; POSIX Sez. 5.3.1, Sez. 5.6.2, Sez. 5.6.3.

19.12: Come posso determinare le dimensioni di un file prima di leggerlo?

Se per dimensione del file si intende il numero di caratteri che potrete leggere dal file, è difficile o impossibile determinare questo numero in modo esatto.

Sotto Unix, la chiamata stat() fornisce una risposta esatta. Molti altri sistemi forniscono una variante di stat() che però danno solo una risposta approssimata. Potete usare fseek() per posizionarvi alla fine del file e poi usare ftell(), oppure provare fstat(), ma ci possono essere dei problemi: fstat() non è portabile, e solitamente da la stessa risposta di stat(); ftell() non è garantito che torni il numero di byte se non si tratta di un file binario. Alcuni sistemi forniscono routine come filesize() o filelength(), ma neanche queste sono portabili.

Siete certi di aver bisogno di determinare le dimensioni del file in anticipo? Poiché il modo più accurato di determinare la dimensione di un file così come sarà percepita dal programma C consiste nell'aprirlo e leggerlo, se possibile è meglio far si che il programma misuri le dimensioni del file durante la lettura.

Riferimenti: ANSI Sez. 4.9.9.4; ISO Sez. 7.9.9.4; H&S Sez. 15.5.1; PCS Sez. 12; POSIX Sez. 5.6.2.

19.12a: Come si fa a leggere la data di ultima modifica di un file?

La funzione Unix e POSIX è stat(), che è fornita anche da molti altri sistemi (vedi 19.12).

19.13: Come posso accorciare un file senza spostarlo, e senza riscriverlo completamente?

I sistemi BSD forniscono ftruncate(), altri forniscono chsize(), e alcuni forniscono un'opzione (magari non documentata) F_FREESP per fcntl. Sotto MS-DOS, talvolta si può usare (fd, "", 0). Comunque non esiste una soluzione portabile, né un modo per rimuovere blocchi all'inizio del file. Vedi anche 19.14.

19.14: Posso inserire o cancellare una linea (o un record) nel mezzo di un file?

A meno di riscrivere il file, probabilmente no. (Invece di cancellare i record, considerate la possibilità di marcarli come cancellati). Vedi anche le domande 12.30 e 19.13.

19.15: Posso ricostruire il nome del file dato uno stream o un descrittore di file?

In generale, no. Sotto Unix, per esempio, si richiederebbe in teoria una scansione dell'intero disco (che tra l'altro richiederebbe permessi speciali), e comunque l'operazione fallirebbe se il descrittore fosse connesso a una pipe o riferito a un file cancellato (e potrebbe dare una risposta scorretta per un file con link multipli). È meglio ricordare il nome dei file quando li aprite (magari con una funzione che "incapsula" fopen).

19.16: Posso cancellare un file?

La funzione della libreria standard è remove(). (Quindi questa è una delle poche domande in questa sezione per le quali la risposta *non* è dipendente dal sistema). Sui vecchi sistemi Unix pre-ANSI, potreste non avere remove(), nel qual caso si può usare unlink().

Riferimenti: K&R2 Sez. B1.1; ANSI Sez. 4.9.4.1; ISO Sez. 7.9.4.1; H&S Sez. 15.15; PCS Sez. 12; POSIX Sez. 5.5.1, Sez. 8.2.4.

19.17: Posso aprire un file attraverso il suo path esplicito? La chiamata

fopen("c:\newdir\file.dat", "r")

fallisce.

Il file che avete richiesto - che ha un carattere '\n' e un carattere '\f' nel nome - probabilmente non esiste, e non è quello che volevate aprire.

Nelle costanti carattere e nei letterali stringa, il backslash \ è un carattere di escape, che dà significato speciale al carattere che lo segue. Al fine di far sì che eventuali \ siano effettivamente considerati parte del nome del file da fopen() o qualsiasi altra routine, essi devono essere raddoppiati, che è la convenzione adottata dal C per consentire al programmatore di indicare proprio il carattere \.

fopen("c:\\newdir\\file.dat", "r");

In alternativa, sotto MS-DOS, pare che anche gli slash (/) siano accettati come separatori di directory, per cui potete usare

fopen("c:/newdir/file.dat", "r");

(Si noti, tra l'altro, che i nomi di file header nelle #include non sono letterali stringa, per cui in tal caso non occorre preoccuparsi dei backslash).

19.18: Ho l'errore "Too many open files". Posso aumentare il numero massimo consentito di file contemporaneamente aperti?

Normalmente ci sono due limitazioni sul numero di file aperti contemporaneamente: il numero di descrittori di file (o "file handles") disponibili nel sistema operativo, e il numero di strutture FILE disponibili nella libreria stdio. Entrambi dovrebbero essere abbastanza. Sotto MSDOS, si può controllare il numero di file handle con una linea in CONFIG.SYS. Alcuni compilatori comprendono delle istruzioni (e in alcuni casi qualche file sorgente) che consentono di aumentare il numero di strutture FILE di stdio.

19.20: Come si fa a leggere il contenuto di un direttorio in C?

Un modo è usare le routine opendir() e readdir() routines, che fanno parte dello standard POSIX e sono disponibili sulla maggior parte delle versioni di Unix. Ne esistono implementazioni anche per MS-DOS, VMS, e altri sistemi. (MS-DOS ha anche le routine FINDFIRST e FINDNEXT che fanno più o meno la stessa cosa). readdir() ritorna solo i nomi dei file; se vi servono altre informazioni, provate a chiamare stat(). Per fare il matching dei nomi di file con pattern che includono wild characters, vedi 13.7.

Riferimenti: K&R2 Sez. 8.6; PCS Sez. 13; POSIX Sez. 5.1; Schumacher, _Software Solutions in C_ Sez. 8.

19.22: Come si fa a ricavare la quantità di memoria disponibile in C?

Il sistema operativo dovrebbe avere una routine che ritorna questa informazione, ma questa è diversa su ogni sistema.

19.23: Come si fa ad allocare vettori o strutture di dimensioni superiori a 64K?

Un'implementazione ragionevolmente ben fatta dovrebbe darvi accesso a tutta la memoria disponibile in modo trasparente. Se non siete così fortunati, dovrete riconsiderare il modo in cui usate la memoria nel vostro programma, o usare una qualche tecnica dipendente dal sistema.

Tuttavia, 64K sono un bel po' di memoria. Indipendentemente dalla quantità di memoria disponibile, richiedere l'allocazione di spazi di memoria contigua così grande è piuttosto pretenzioso. (Lo Standard C non garantisce che sia possibile avere strutture dati singole di dimensioni superiori a 32K). Di solito, è più ragionevole usare strutture dati che non richiedono l'allocazione di memoria contigua. Nel caso di vettori multidimensionali allocati dinamicamente, si possono usare puntatori a puntatori, come illustrato in 6.16. Invece di un enorme vettore di strutture si può usare una lista, o un vettore di puntatori a strutture.

Se state usando un sistema PC-compatible (basato sull'8086), e vi scontrate con un limite come 64K o 640K, prendete in considerazione l'idea di usare un modello di memoria "grande", o la memoria espansa o estesa, o varianti di malloc come halloc() or farmalloc(), o un compilatore "piatto" ("flat") a 32-bit (p.es. djgpp, vedi 18.3), o qualche genere di estensore DOS, o un altro sistema operativo.

Riferimenti: ANSI Sez. 2.2.4.1; ISO Sez. 5.2.4.1.

19.24: Cosa significa l'errore "DGROUP data allocation exceeds 64K", e cosa posso farci? Pensavo che l'uso di un modello grande significasse che posso usare più di 64K di dati!

Anche con modelli di memoria larghi, senbra che i compilatori MS-DOS piazzino alcuni tipi di dati (stringhe e alcune variabili globali o statiche inizializzate) in un segmento dati di default; è proprio questo il segmento che dà problemi di overflow. Usate meno dati globali, oppure, se vi siete già limitati a una quantità ragionevole (e il problema è dovuto a qualcosa come il numero di stringhe), potreste cercare di convincere il compilatore a non usare troppo il segmento dati di default. Alcuni compilatori piazzano in tale segmento solo oggetti piccoli e c'è un modo (p.es. per i compilatori Microsoft l'opzione /Gt) per configurare la soglia di dimensione in base al quale un dato viene considerato "piccolo".

19.25: Come faccio ad accedere a un certo indirizzo della memoria (p.es. per usare un device mappato in memoria, o la memoria grafica)?

Assegnate l'indirizzo a un puntatore del tipo appropriato (usando un cast esplicito per assicurare al compilatore che intendete proprio eseguire questa conversione non portabile):

unsigned int *magicloc = (unsigned int *)0x12345678;

A questo punto, *magicloc punta alla locazione desiderata. (Sotto MS-DOS, potete trovare una macro come MK_FP() che è utile per operare con segmenti e offset).

Riferimenti: K&R1 Sez. A14.4; K&R2 Sez. A6.6; ANSI Sez. 3.3.4; ISO Sez. 6.3.4; Rationale Sez. 3.3.4; H&S Sez. 6.2.7.

19.27: Come faccio a invocare un altro programma (un eseguibile standalone, o un comando di sistema operativo) da un programma C?

Con la funzione di libreria system(), che fa proprio questo. Si noti che il valore di ritorno di system è, nella migliore delle ipotesi, lo stato di uscita del comando (anche se anche questo non è garantito), e solitamente non ha nulla a che vedere con l'output del comando. Si noti anche che system() accetta una singola stringa che rappresenta il comando da invocare; se dovete costruire una linea di comando complessa, potete usare sprintf. Vedi anche 19.30.

Riferimenti: K&R1 Sez. 7.9; K&R2 Sez. 7.8.4, Sez. B6; ANSI Sez. 4.10.4.5; ISO Sez. 7.10.4.5; H&S Sez. 19.2; PCS Sez. 11.

19.30: Posso invocare un altro programma o un comando e intercettare il suo output?

Unix e altri sistemi forniscono una routine popen(), che apre uno stream stdio su una pipe connessa al processo che esegue il comando, in modo che l'output possa essere letto da un programma (o che l'input possa essere inviato da programma). (Ricordatevi comunque di chiamare anche pclose()).

Se non potete usare popen(), potreste poter usare system(), mandando l'output su un file che poi potete aprire e leggere.

Se siete su Unix e popen() non è sufficiente, potete imparare a usare pipe(), dup(), fork(), e exec().

(Una cosa che non dovrebbe funzionare, per inciso, è freopen()).

Riferimenti: PCS Sez. 11.

19.31: Come si fa, da programma, a scoprire il pathname completo dell'eseguibile del programma stesso?

argv[0] può contenere tutto il pathname o parte di esso, oppure anche non contenere nulla. Se il nome c'è ma è incompleto, potreste riuscire a duplicare la logica di ricerca di path dell'interprete del linguaggio dei comandi per localizzare l'eseguibile. Comunque, nessuna soluzione funziona di sicuro.

Riferimenti: K&R1 Sez. 5.11; K&R2 Sez. 5.10; ANSI Sez. 2.1.2.2.1; ISO Sez. 5.1.2.2.1; H&S Sez. 20.1.

19.32: Come faccio a collocare automaticamente i file di configurazione di un programma nello stesso direttorio in cui si trova l'eseguibile?

Non è semplice; vedi anche 19.31. Anche se trovate un modo per farlo, vi conviene considerare la possibilità di rendere configurabile il direttorio ausiliario del programma, per esempio attraverso una variabile d'ambiente. (Consentire la collocazione variabile dei file di configurazione di un programma è particolarmente importante se il programma deve essere usato da molte persone, p.es. in un sistema multiutente).

19.33: Un processo può cambiare una variabile d'ambiente del suo chiamante?

Può essere possibile o non esserlo. Diversi sistemi operativi implementano la funzionalità dei nomi globali/valori alla Unix in modi diversi. Il fatto che l'ambiente possa essere alterato da un programma, e, se si, come, dipende dal sistema.

Sotto Unix, un processo può modificare il proprio ambiente (alcuni sistemi forniscono a questo scopo le funzioni setenv() o putenv()), e l'ambiente modificato è solitamente passato ai processi figli, ma non è propagato all'indietro al processo padre.

19.36: Posso leggere un file oggetto ed eseguire qualcuna delle sue routine?

Potrebbe essere possibile usando un linker o loader dinamico. Occorrerebbe allocare un po' di spazio con malloc e leggervi l'oggetto, ma questo richiede conoscenze approfondite circa il formato dei file eseguibili, la rilocazione, ecc. Sotto Unix BSD, si può usare system() e ld -A per fare il linking. Molte versioni di SunOS e System V hanno la libreria -ldl che consente il caricamento dinamico di file oggetto. Sotto VMS, usate LIB$FIND_IMAGE_SYMBOL. La GNU fornisce un package chiamato "dld". Vedi anche 15.13.

19.37: Come si implementa un delay, o si misura il tempo di risposta dell'utente, con precisione sotto il secondo?

Sfortunatamente non c'è un modo portabile. Unix V7 e sistemi derivati forniscono una routine molto utile, ftime(), con risoluzione al millisecondo, ma non è stata inclusa in System V e POSIX. Altre routine che potreste cercare sono clock(), delay(), gettimeofday(), msleep(), nap(), napms(), nanosleep(), setitimer(), sleep(), times(), e usleep(). (La routine chiamata wait(), comunque, *non* serve a questo scopo, almeno non sotto Unix). Le chiamate select() e poll() (se disponibili) possono essere usate per implementare ritardi semplici. Su MS-DOS è possibile riprogrammare il timer di sistema e i suoi interrupt.

Di tutte le routine citate, solo clock() fa parte dello standard ANSI. La differenza fra due chiamate successive a clock() restituisce il tempo intercorso, e se CLOCKS_PER_SEC è maggiore di 1, tale differenza sarà con risoluzione inferiore al secondo. Tuttavia, clock() fornisce il tempo di processore usato dal programma, e su un sistema multitasking questo può essere molto diverso dal tempo reale.

Se state cercando di implementare un ritardo e tutto ciò che avete a disposizione è una funzione che ritorna l'ora, potete implementare un'attesa che consuma tempo di CPU (un loop di "busy wait"), ma questo è accettabile solo su una macchina singolo-utente e singolo-task, poiché sarebbe molto dannoso per gli altri processi. Su un sistema operativo multitasking, assicuratevi di usare una chiamata che faccia addormentare il processo mentre attende, per esempio sleep(), select(), o pause() usata insieme ad alarm() o setitimer().

Per attese molto brevi, si può essere tentati di fare un loop vuoto come:

long int i;
for(i = 0; i < 1000000; i++)
;

ma non fatelo! Primo, il vostro bel loop di attesa smetterà di funzionare il mese prossimo quando uscirà un processore più veloce. Peggio ancora, un compilatore intelligente si accorgerà che il loop non fa nulla e lo eliminerà come ottimizzazione.

Riferimenti: H&S Sez. 18.1; PCS Sez. 12; POSIX Sez. 4.5.2.

19.38: Come si fa a intercettare o ignore interrupt di tastiera come control-C?

Il passo fondamentale consiste nel chiamare signal(). Per ignorare il segnale di interrupt:

#include <signal.h>
signal(SIGINT, SIG_IGN);

oppure, per causare il trasferimento del controllo alla funzione func() in caso di interrupt:

extern void func(int);
signal(SIGINT, func);

Su un sistema multi-tasking come Unix, è meglio usare una tecnica leggermente più raffinata:

extern void func(int);
if(signal(SIGINT, SIG_IGN) != SIG_IGN)
   signal(SIGINT, func);

Il test e la chiamata in più assicurano che un interrupt di tastiera dal foreground non interromperà inavvertitamente un programma in esecuzione in background (e chiamate a signal() scritte così non fanno male su nessun sistema).

Su alcuni sistemi, la gestione degli interrupt di tastiera dipende anche dalla modalità del sottosistema di input da terminale; vedi 19.1. Su alcuni sistemi, si può controllare la ricezione di interrupt di tastiera solo quando il programma sta facendo input, e la gestione degli interrupt di tastiera, di conseguenza, può dipendere dalle routine di input utilizzate (e dal fatto che ci siano o no delle routine di input attive). Sui sistemi MS-DOS, le possono essere usate anche le funzioni setcbrk() o ctrlbrk().

Riferimenti: ANSI Sez. 4.7,4.7.1; ISO Sez. 7.7,7.7.1; H&S Sez. 19.6; PCS Sez. 12; POSIX Sez. 3.3.1, 3.3.4.

19.39: Come gestitsco in modo pulito le eccezioni floating-point?

Su molti sistemi, si può definire una funzione matherr() che viene invocata quando si verificano certi errori floating-point, come errori nelle routine di <math.h>. Può anche darsi che si possa usare signal() (vedi 19.38) per intercettare SIGFPE. Vedi anche 14.9.

Riferimenti: Rationale Sez. 4.5.1.

19.40: Come si fa a... usare le socket? fare programmazione di rete? scrivere applicazioni client/server?

Tutte queste domande sono al di fuori dello scopo di questa lista e hanno molto più a che vedere con le funzionalità di networking utilizzabili in C. Alcuni buoni libri su questo argomento sono i tre volumi di Douglas Comer _Internetworking with TCP/IP_ e W. R. Stevens, _UNIX Network Programming_. Ci sono anche molte informazioni in giro sulla rete.

19.40b: Come si eseguono chiamate a BIOS? Come si scrivono gli ISR? Come si creano i TSR?

Si tratta di concetti specifici di particolari sistemi (principalmente PC compatibili con MSDOS). Si possono trovare le informazioni necessarie nei newsgroup specifici come comp.os.msdos.programmer (vedi le FAQ); un'altra risorsa eccellente è la lista sugli interrupt di Ralf Brown.

19.41: Ma non posso usare tutte queste funzioni non-standard, dipendenti dal sistema, perché il mio programma deve essere compatibile ANSI!

Sei sfortunato. Può darsi che tu non abbia capito i requisiti che ti sono stati proposti, o che ti abbiano proposto requisiti impossibili da ottenere. Lo standard ANSI/ISO semplicemente non definisce alcun modo di fare queste cose; è lo standard di un linguaggio, non di un sistema operativo. Uno standard internazionale che affronta molti di questi argomenti è POSIX (IEEE 1003.1, ISO/IEC 9945-1), e molti sistemi operativi (non solo Unix) attualmente hanno interfacce di programmazione POSIX-compatibili.

Al più, è possibile (e consigliabile) far si che la maggior parte di un programma sia ANSI-compatibile, delegando la funzionalità dipendente dal sistema a poche funzioni, raccolte in pochi file, che possono essere riscritte in modo diverso per ogni sistema sul quale si vuole portare l'applicazione.


20. Miscellanea

20.1: Come si fa a tornare più valori da una funzione?

Si possono passare alla funzione puntatori a locazioni di memoria in cui depositare i valori, oppure farle tornare una struttura che contenga i valori, oppure (con attenzione) considerare l'uso di variabili globali. Vedi anche 2.7, 4.8, e 7.5.

20.3: Come si accede ad eventuali argomenti sulla linea di comando?

Sono puntati dal vettore argv passato al main. Vedi anche 13.7 e 19.20.

Riferimenti: K&R1 Sez. 5.11; K&R2 Sez. 5.10; ANSI Sez. 2.1.2.2.1; ISO Sez. 5.1.2.2.1; H&S Sez. 20.1; PCS Sez. 5.6, Sez. 11, Appendice F; Schumacher, _Software Solutions in C_ Sez. 4.

20.5: Come faccio a creare un file i dati che possa essere letto su altre macchine con diverse dimensioni delle parole, ordinamento dei byte, o formato floating-point?

La soluzione più portabile consiste nell'usare file di testo (solitamente ASCII), scritti con fprintf() e letti con fscanf() o simili. (Un suggerimento simile si applica anche ai protocolli di rete). Siate scettici con chi dice che i file di testo sono troppo grandi, o che leggerli e scriverli è troppo lento. Non soltanto l'efficienza dei file di testo è, in pratica, accettabile: i vantaggi che si hanno nello scambiarli fra macchine, e manipolarli con tool standard, possono essere enormi.

Se dovete per forza usare un formato binario, potete migliorare la portabilità, e sfruttare librerie di I/O pre-esistenti, usando formati standard come XDR di Sun (RFC 1014), l'ASN.1 di OSI (descritta in CCITT X.409 e ISO 8825 "Basic Encoding Rules"), CDF, netCDF, o HDF. Vedi anche 2.12 e 12.38.

Riferimenti: PCS Sez. 6.

20.6: Se ho una variabile di tipo char * che punta al nome di una funzione, posso chiamare quella funzione?

Si può farlo, per esempio, mantenendo una tabella di corrispondenza fra nomi e puntatori a funzione:

int func(), anotherfunc();

struct { char *name; int (*funcptr)(); } symtab[] = {
   "func",   func,
   "anotherfunc",   anotherfunc,
};

In questo modo, dato un nome, si può ricavare il puntatore attraverso il quale chiamare la funzione. Vedi anche 2.15 e 19.36.

Riferimenti: PCS Sez. 11.

20.8: Come si implementano insiemi o vettori di bit?

Conviene usare vettori di char o int, con un po' di macro per accedere al bit all'indice desiderato. Ecco alcune semplici macro per vettori di char:

#include <limits.h> /* for CHAR_BIT */

#define BITMASK(b) (1 << ((b) % CHAR_BIT))
#define BITSLOT(b) ((b) / CHAR_BIT)
#define BITSET(a, b) ((a)[BITSLOT(b)] |= BITMASK(b))
#define BITTEST(a, b) ((a)[BITSLOT(b)] & BITMASK(b))

(Se non disponete di <limits.h>, provate a usare 8 invece di CHAR_BIT.)

Riferimenti: H&S Sez. 7.6.7.

20.9: Come si fa a determinare se l'ordine dei byte di una macchina è big-endian o little-endian?

Un modo consiste nell'usare un puntatore:

int x = 1;
if(*(char *)&x == 1)
   printf("little-endian\n");
else printf("big-endian\n");

Si può anche usare una union.

Vedi anche 10.16.

Riferimenti: H&S Sez. 6.1.2.

20.10: Come si fa a convertire gli interi a binario o esadecimale?

La domanda fa pensare che stiate facendo un po' di confusione. Gli interi sono memorizzati in binario, anche se per la maggior parte degli usi non è scorretto pensarli come ottali, decimali, o esadecimali, secondo quanto risulta più comodo. La base che si utilizza nel riferirsi a un numero conta solo quando si legge o si scrive il numero da/a il mondo esterno.

Nel codice sorgente, una base non decimale è indicata con uno 0 iniziale (per ottale) o 0x (esadecimale). In I/O, la base di un numero formattato è controllata, nella famiglia di funzioni printf/scanf, dallo specificatore di formato (%d, %o, %x, ecc.); nelle funzioni strtol() e strtoul() tale controllo è svolto dal terzo argomento. Durante l'I/O binario, comunque, la base diventa comunque non attinente.

Per maggiori informazioni a proposito dello I/O "binario" vedi 2.11. Vedi anche 8.6 e 13.1.

Riferimenti: ANSI Sez. 4.10.1.5,4.10.1.6; ISO Sez. 7.10.1.5, 7.10.1.6.

20.11: Posso usare costanti in base 2 (p.es., qualcosa come 0b101010)? C'è un formato di printf() per il binario?

La risposta a entrambe le domande è no. Puoi convertire rappresentazioni stringa in base 2 a interi con strtol().

20.12: Qual è il modo più efficiente di contare il numeri di bit a 1 in un dato valore numerico?

Molti problemi di manipolazione di bit, come questo, possono essere risolti velocemente usando tavole di lookup (ma vedi anche 20.13).

20.13: Qual'è il modo migliore per rendere efficiente un programma?

Scegliere un buon algoritmo, implementarlo con cura, e assicurarsi che il programma non faccia nulla che non sia necessario. Per esempio, il ciclo di copia di caratteri più ottimizzato del modo è più lento del codice che non copia i caratteri del tutto.

Quando ci si preoccupa dell'efficienza, è importante mantenere le cose nella giusta prospettiva. Innanzitutto, anche se l'efficienza è un argomento che riscuote grande interesse, non sempre è importante come la gente sembra credere. La maggior parte del codice della maggior parte dei programmi non è "time-critical". Se il codice non è time-critical, di solito è molto più importante che sia scritto bene e portabilmente, piuttosto che sia massimamente efficiente. (Ricordatevi che i computer sono molto, molto veloci, e che codice che sembra inefficiente può essere efficientemente compilabile, e girare senza alcun ritardo visibile).

È notorialmente difficile predire dove saranno i "punti caldi" di un programma. Se l'efficienza è importante, si devono usare programmi di profiling per determinare a quali porzioni di codice prestare maggior attenzione.Spesso, il tempo di calcolo reale è soprattutto rallentato da attività periferiche come I/O e allocazione di memoria, che possono essere velocizzate usando tecniche di buffering e caching.

Anche nei casi in cui il codice sia effettivamente time-critical, una delle tecniche di ottimizzazione meno efficace è dedicarsi ai dettagli. Molti dei famosi "trucchi di programmazione efficiente" (p.es. sostituire le moltiplicazioni per potenze di due con operatori di shift bit a bit) sono usati automaticamente persino dai compilatori più semplici. Avere la mano troppo pesante nelle ottimizzazioni può rendere il codice così confuso da degradare addirittura le presentazioni, e ridurne o annullarne la portabilità (il codice potrebbe inoltre andare veloce su una macchina e lento su altre). In ogni caso, deformare il codice solitamente produce al più miglioramenti lineari nelle presentazioni; i veri vantaggi stanno quasi sempre nella scelta di algoritmi migliori.

Per ulteriori informazioni sui pro e contro in materia di efficienza, e per buoni consigli su come migliorare l'efficienza quando è importante farlo, vedi il cap. 7 di Kernighan e Plauger, _The Elements of Programming Style_, e Jon Bentley, _Writing Efficient Programs_.

20.14: È vero che i puntatori sono più veloci dei vettori? Quanto è lenta la chiamata di funzione? ++i è più veloce di i = i + 1?

La risposta a queste e simili domande dipendono dal processore e dal compilatore. Se dovete per forza avere una risposta, dovrete eseguire dei test accurati. (Spesso le differenze sono così piccole che ci vogliono centinaia di migliaia di iterazioni per notarle. Se disponibile, controllate l'assembly generato dal compilatore, per vedere se due alternative non sono per caso compilate in codice identico).

"Normalmente" è più veloce attraversare un grande vettore usando puntatori piuttosto che indici, ma su alcuni processori è vero l'opposto.

Le chiamate di funzione sono ovviamente più lente del codice in linea, ma sono così utili per la modularità e la chiarezza del codice che vale quasi sempre la pena di usarle.

Prima di riscrivere espressioni come i = i + 1, ricordatevi che avete a che fare con un compilatore, non una calcolatrice programmabile. Qualsiasi compilatore decente genererà lo stesso codice per ++i, i += 1, e i = i + 1. Il motivo per usare ++i o i += 1 invece di i = i + 1 riguarda lo stile, non l'efficienza. (Vedi anche 3.12).

20.17: Si possono usare stringhe come etichette dei rami degli switch?

Non direttamente. In alcuni casi, si può usare una funzione che mappa stringhe su codici interi, ed eseguire lo switch su tali codici. Altrimenti, naturalmente, si può usare strcmp() e una normale catena di if/else. Vedi anche le domande 10.12, 20.18, e 20.29.

Riferimenti: K&R1 Sez. 3.4; K&R2 Sez. 3.4; ANSI Sez. 3.6.4.2; ISO Sez. 6.6.4.2; H&S Sez. 8.7.

20.18: Si possono avere etichette dello switch che non siano costanti semplici (p.es., range o espressioni?)

No. L'istruzione switch è stata progettata per essere semplice da compilare, e per questo motivo le etichette degli switch sono limitate a espressioni intere singole costanti. Potete attaccare più etichette alla stessa sezione, cosa che permette di esprimere piccoli range elencando esplicitamente tutti i casi.

Se volete operare una scelta in base a range qualsiasi o espressioni non costanti, dovrete usare una catena di if/else.

Vedi anche 20.17.

Riferimenti: K&R1 Sez. 3.4; K&R2 Sez. 3.4; ANSI Sez. 3.6.4.2; ISO Sez. 6.6.4.2; Rationale Sez. 3.6.4.2; H&S Sez. 8.7.

20.19: È vero che le parentesi del return sono opzionali?

Si.

Molto tempo fa, agli albori del C, erano obbligatorie, e poiché molti hanno imparato il C in quell'epoca, e hanno scritto codice che è ancora in uso, l'idea che siano obbligatorie è molto diffusa.

(Tra l'altro, le parentesi sono opzionali anche nel caso dell'operatore sizeof, sotto certe condizioni).

Riferimenti: K&R1 Sez. A18.3; ANSI Sez. 3.3.3, Sez. 3.6.6; ISO Sez. 6.3.3, Sez. 6.6.6; H&S Sez. 8.9.

20.20: Perché i commenti C non si possono annidare? Come faccio a commentare codice che contiene commenti? I commenti sono legali dentro le stringhe?

I commenti C non sono annidabili principalmente perché non lo sono i commenti di PL/1, dai quali derivano quelli del C. Di conseguenza, è meglio "commentare" grandi sezioni di codice usando #ifdef o #if0 (ma vedi anche 11.19).

Le sequenze /* e */ non hanno alcun significato speciale nelle stringhe, e di conseguenza non introducono commenti, perché un programma (per esempio uno che generi codice C come output) potrebbe volerle stampare.

Si noti anche che i commenti //, alla C++, al momento non sono legali in C, quindi non è una buona idea usarli nel codice C (anche se molti compilatori li supportano come estensione).

Riferimenti: K&R1 Sez. A2.1; K&R2 Sez. A2.2; ANSI Sez. 3.1.9, Appendice E; ISO Sez. 6.1.9; Rationale Sez. 3.1.9; H&S Sez. 2.2; PCS Sez. 10.

20.24: Come mai il C non permette funzioni annidate?

Non è semplice implementare le funzioni annidate in modo che abbiano accesso alle variabili locali della funzione esterna, ragion per cui esse sono state escluse come semplificazione. (gcc le consente, come estensione). Per molti usi delle funzioni annidate (p.es., funzioni di confronto per qsort), una soluzione funzionante anche se un po' intricata consiste nell'usare funzioni adiacenti dichiarate static, e far comunicare le funzioni, se necessario, con variabili anch'esse static. (Una soluzione più pulita, anche se non supportata da qsort, consiste nel passare in giro un puntatore a una struttura che contiene il contesto necessario).

20.25: Come si fa a invocare funzioni FORTRAN (C++, BASIC, Pascal, Ada, LISP) dal C o viceversa?

La risposta dipende completamente dalla macchina e dalle sequenze di chiamata dei compilatori che usate, e potrebbe persino non essere possibile. Leggete molto attentamente la documentazione del vostro compilatore; a volte c'è una "guida alla programmazione multi-linguaggio", anche se le tecniche per il passaggio dei parametri e per assicurare una corretta inizializzazione a runtime sono spesso esoteriche. Maggiori informazioni si possono trovare in FORT.gz di Glenn Geers, disponibile via ftp anonima presso suphys.physics.su.oz.au nel direttorio src.

<cfortran.h>, un header file C, semplifica l'interfacciamento C/FORTRAN su molte macchine. È disponibile via ftp anonima presso zebra.desy.de (131.169.2.244).

In C++, un modificatore "C" nella dichiarazione di una funzione esterna indica che la funzione deve essere chiamata usando le convenzioni di chiamata del C.

Riferimenti: H&S Sez. 4.9.8.

20.26: Esistono programmi per convertire Pascal o FORTRAN (o LISP, Ada, awk, "vecchio" C, ...) in C?

Ci sono molti programmi free:
  • p2c: traduttore da Pascal a C, scritto da Dave Gillespie, pubblicato su comp.sources.unix nel marzo 1990 (Volume 21); disponibile via ftp anonima presso csvax.cs.caltech.edu, file pub/p2c-1.20.tar.Z
  • ptoc: altro traduttore da Pascal a C, scritto in Pascal (comp.sources.unix, Volume 10, patch in Volume 13?)
  • f2c: traduttore da FORTRAN a C sviluppato congiuntamente da persone di Bell Labs, Bellcore, e Carnegie Mellon. Per saperne di più, inviate una mail "send index from f2c" a netlib@research.att.com o research!netlib. (È anche disponibile via ftp anonima presso netlib.att.com, nella directory netlib/f2c.)
Il gestore di queste FAQ ha anche diversi altri prodotti commerciali, inclusi alcuni per linguaggi meno famosi.

Vedi anche 11.31 e 18.16.

20.27: È vero che il C++ è un superset del C? Posso cioè usare un compilatore C++ per compilare codice C?

Il C++ deriva dal C, ed è ampiamente basato su di esso, ma ci sono alcuni costrutti legali in C che non sono legali in C++. Al contrario, il C ANSI ha ereditato molte caratteristiche del C++, tra cui prototipi e const, per cui nessuno dei due è realmente un subset o un superset dell'altro; tra l'altro, definiscono anche in modo diverso il significato di alcuni costrutti comuni. Ciononostante, la maggior parte dei programmi C sono compilato correttamente da un ambiente C++, e molti compilatori recenti compilano sia C che C++, fornendo due diverse modalità. Vedi anche 8.9 e 20.20.

Riferimenti: H&S Sez. 1.1.5, Sez. 2.8, Sez. 4.9.

20.28: Ho bisogno di una strcmp "approssimata" che mi dica se due stringhe sono "quasi" uguali.

Alcune informazioni utili e algoritmi per il matching approssimato di stringhe, nonché una bella bibliografia, si trovano in Sun Wu e Udi Manber, "AGREP -- A Fast Approximate Pattern-Matching Tool."

Un altro approccio utilizza l'algoritmo "soundex", che mappa parole di suono simili negli stessi codici. Soundex è pensato per scoprire nomi con suono simile (per i servizi di assistenza telefonica tipo "12"), ma può essere usato per parole qualsiasi.

Riferimenti: Knuth Sez. 6 Volume 3; Wu e Manber, "AGREP -- A Fast Approximate Pattern-Matching Tool" .

20.29: Che cos'è l'hashing?

L'hashing è un procedimento per mappare stringhe su interi, di solito scelti in un piccolo range. Una funzione hash mappa una stringa (o una qualche altra struttura dati) a un numero in un certo range, il quale può essere usato come indice di un vettore, o allo scopo di eseguire confronti ripetuti. (Ovviamente, se il numero delle stringhe possibili è maggiore del numero degli interi, il mapping non sarà 1 a 1. Gli algoritmi che usano l'hashing sono perciò solitamente dotati di un meccanismo per gestire le "collisioni"). Sono state inventate molte funzioni di hashing e algoritmi correlati; un trattamento completo di questo argomento sarebbe troppo complesso ai fini di questa lista.

Riferimenti: K&R2 Sez. 6.6; Knuth Sez. 6.4 Volume 3; Sedgewick Sez. 16.

20.31: How can I find the day of the week given the date?

Si usino mktime() o localtime() (vedi 13.13 e 13.14, ma attenzione agli aggiustamenti dovuti all'ora legale, DST in inglese, se tm_hour è 0) o la congruenza di Zeller (vedi le FAQ di sci.math) o questo elegante algoritmo di Tomohiko Sakamoto:

dayofweek(y, m, d) /* 0 = Sunday */
int y, m, d; /* 1 <= m <= 12, y > 1752 or so */
{
   static int t[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
   y -= m < 3;
   return (y + y/4 - y/100 + y/400 + t[m-1] + d) % 7;
}

(Copyright 1993, Tomohiko Sakamoto)

Vedi anche 13.14 e 20.32.

Riferimenti: ANSI Sez. 4.12.2.3; ISO Sez. 7.12.2.3.

20.32: Il 2000 sarà bisestile? Il test (year % 4 == 0) è sufficiente per identificare gli anni bisestili?

Si e no rispettivamente. L'espressione corretta per il test sui bisestili nell'attuale calendario gregoriano è la seguente:

year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)

Vedi un buon almanacco astronomico o un altro riferimento per dettagli. (Per anticipare un eterno dibattito: chi sostiene che esista una regola addizionale relativa agli anni multipli di 4000 ha torto). Vedi anche 13.14.

20.34: Un bel quiz: si può scrivere un programma che stampa in output il proprio codice sorgente?

In effetti è alquanto difficile scrivere un programma portabile che si auto-stampa, in particolare per via delle virgolette e altre difficoltà legate al set dei caratteri.

Questo è un esempio classico (che normalmente si presenta su una singola linea, anche se si "sistema" da solo la prima volta che viene eseguito):

char*s="char*s=%c%s%c;main(){printf(s,34,s,34);}";
main(){printf(s,34,s,34);}

(Questo programma, come molti del genere, non include <stdio.h>, e assume che le virgolette " abbiano valore 34, come avviene in ASCII.)

20.35: Che cos'è il "Duff's Device" ("marchingegno di Duff")?

È un loop di copia di byte terribilmente intricato, inventato da Tom Duff quando lavorava alla Lucasfilm. Nella sua forma "classica", si presenta così:

register n = (count + 7) / 8; /* assumo che count > 0 */
switch (count % 8)
{
    case 0: do { *to = *from++;
    case 7: *to = *from++;
    case 6: *to = *from++;
    case 5: *to = *from++;
    case 4: *to = *from++;
    case 3: *to = *from++;
    case 2: *to = *from++;
    case 1: *to = *from++;
   } while (--n > 0);
}

(Copyright 1984, 1988, Tom Duff)

dove count è il numero di byte da copiare, e i byte sono copiati dal vettore puntato da "from" al vettore puntato da "to" (che è un registro di output mappato in memoria, ragion per cui non viene incrementato). Risolve il problema di gestire i byte residui quando count non è un multiplo di 8 inframmezzando uno switch al ciclo che copia 8 byte alla volta. (Che ci crediate o no, *è* legale disporre le etichette di uno switch all'interno di una istruzione annidata nello switch, come il do-while in questo caso.) Quando ha annunciato la sua tecnica agli sviluppatori C, Duff ha notato che la sintassi dello switch, e in particolare il suo comportamento "fall through" (ciò che accade quando manca il break in uno dei rami), sono stati a lungo fonte di controversie, e che "questo codice può essere certamente usato come argomento in questo dibattito, anche se non so se è un argomento a favore o a sfavore".

20.36: Quando sarà tenuta la prossima Obfuscated C Code Contest (IOCCC) (Gara del Codice C Oscuro)? Come posso ottenere una copia dei programmi vincitori dell'edizione attuale e di quelle precedenti?

La programmazione della gara è legata alle date delle conferenze USENIX, alle quali vengono annunciati i vincitori. Per ottenere una copia delle regole e le guideline per le sottomissioni, inviate e-mail con Subject: "send rules" a:

{apple,pyramid,sun,uunet}!hoptoad!judges o
judges@toad.com

(Notate che questi non sono gli indirizzi a cui inviare le vostre sottomissioni).

I vincitori della gara vengono pubblicati in rete un po' di tempo dopo la conferenza USENIX in cui sono annunciati. I programmi vincenti degli anni passati (dal 1984 in poi) sono archiviati presso ftp.uu.net (vedi 18.16) nella directory pub/ioccc/; vedi anche http://reality.sgi.com/csp/ioccc/ .

Come soluzione estrema, i programmi vincenti passati possono essere ottenuti spedendo e-mail all'indirizzo di cui sopra, con Subject: "send ANNO winners", dove ANNO è un singolo anno a quattro cifre, un range di anni, o "all" (tutti).

20.37: A cosa serve la keyword "entry" citata in K&R1?

La keyword è stata riservata per lasciare la possibilità di funzioni con più punti di ingresso dotati di nome, alla FORTRAN. Non risulta che sia mai stata implementata (e nessuno ricorda comunque che razza di sintassi avrebbe dovuto essere usata). È stata dismessa, e non è più una keyword in ANSI C. (Vedi anche 1.12.)

Riferimenti: K&R2 Appendice C.

20.38: Da dove viene il nome "C"?

Il C deriva dal linguaggio sperimentale "B" di Ken Thompson, che a sua volta era ispirato dal BCPL (Basic Combined Programming Language) di Martin Richards, il quale era una versione semplificata di CPL (Cambridge Programming Language). Per un po', c'è stato chi ha pensato che il successore del C sarebbe stato chiamato P (la terza lettera di BCPL) invece di D, ma ovviamente il discendente più famoso del C, oggi, è il C++.

20.39: Come si pronuncia "char"?

Gli inglesi pronunciano "char" in almeno tre modi diversi: "ciar" (come la parola inglese "character" suggerirebbe), "cher", o "car". La scelta è arbitraria.

20.40: Dove posso ottenere altre copie di questa lista? Dove trovo le vecchie edizioni?

Una copia aggiornata è disponibile da ftp.eskimo.com nella directory u/s/scs/C-faq/. È anche normalmente pubblicata su comp.lang.c al primo di ogni mese, con una linea Expires: che dovrebbe farla rimanere viva per tutto il mese. Una versione parallela è disponibile (e pubblicata) ed esiste anche una lista di cambiamenti per ogni aggiornamento significativo.

(Nota: la versione che state leggendo, in italiano, non è al momento disponibile da nessuna parte; né esiste alcuna garanzia che verrà aggiornata. Essa corrisponde alla versione inglese aggiornata all'1 Gennaio 2001).

Varie versioni di questa lista sono anche pubblicate presso newsgroups comp.answers e news.answers. Molti siti archiviano le pubblicazioni di news.answers postings e altre liste di FAQ lists, inclusa questa; due di tali siti sono rtfm.mit.edu (directory pub/usenet/news.answers/C-faq/ e pub/usenet/comp.lang.c/) e ftp.uu.net (directory usenet/news.answers/C-faq/). Un server archie (vedi 18.16) dovrebbe consentirvi di trovare le altre; chiedete "find C-faq". Se non avete accesso a ftp, un mailserver presso rtfm.mit.edu può inviarvi queste FAQ per posta: spedite un messaggio contenente solo la parola "help" a mail-server@rtfm.mit.edu. Vedete la lista delle meta-FAQ in news.answers per ulteriori informazioni.

Una versione ipertestuale (HTML) di queste FAQ è disponibile presso http://www.eskimo.com/~scs/C-faq/top.html. Altre URL che consentono di accedere a tutte le FAQ (anche con ricerca per argomento) sono http://www.cis.ohio-state.edu/hypertext/faq/usenet/FAQ-List.html e http://www.luth.se/wais/ .

Una versione estesa di queste FAQ è stata pubblicata dalla Addison-Wesley col titolo _C Programming FAQs: Frequently Asked Questions_ (ISBN 0-201-84519-9). Una lista di errata corrige per tale libro si trova presso http://www.eskimo.com/~scs/C-faq/book/Errata.html e ftp.eskimo.com in u/s/scs/ftp/C-faq/book/Errata.

Questa lista è un documento in evoluzione, che contiene le domande più frequenti prima del Grande Cambiamento di Nome; non è solo una collezione delle domande di questo mese. Le copie più vecchie sono obsolete e non contengono molto che la lista attuale non contenga, a parte errori di battitura qua e là.


Bibliografia

  • Americal National Standards Institute, _American National Standard for Information Systems -- Programming Language -- C_, ANSI X3.159-1989 (vedi 11.2). [ANSI]
  • Americal National Standards Institute, _Rationale for American National Standard for Information Systems -- Programming Language -- C_ (vedi 11.2). [Rationale]
  • Jon Bentley, _Writing Efficient Programs_, Prentice-Hall, 1982, ISBN 0-13-970244-X.
  • G.E.P. Box and Mervin E. Muller, "A Note on the Generation of Random Normal Deviates," _Annals of Mathematical Statistics_, Vol. 29 #2, June, 1958, pp. 610-611.
  • David Burki, "Date Conversions," _The C Users Journal_, February 1993, pp. 29-34.
  • Ian F. Darwin, _Checking C Programs with lint_, O'Reilly, 1988, ISBN 0-937175-30-7.
  • David Goldberg, "What Every Computer Scientist Should Know about Floating-Point Arithmetic," _ACM Computing Surveys_, Vol. 23 #1, March, 1991, pp. 5-48.
  • Samuel P. Harbison and Guy L. Steele, Jr., _C: A Reference Manual_, Fourth Edition, Prentice-Hall, 1995, ISBN 0-13-326224-3. [H&S]
  • Mark R. Horton, _Portable C Software_, Prentice Hall, 1990, ISBN 0-13-868050-7. [PCS]
  • Institute of Electrical and Electronics Engineers, _Portable Operating System Interface (POSIX) -- Part 1: System Application Program Interface (API) [C Language]_, IEEE Std. 1003.1, ISO/IEC 9945-1.
  • International Organization for Standardization, ISO 9899:1990 (vedi 11.2). [ISO]
  • Brian W. Kernighan and P.J. Plauger, _The Elements of Programming Style_, Second Edition, McGraw-Hill, 1978, ISBN 0-07-034207-5.
  • Brian W. Kernighan and Dennis M. Ritchie, _The C Programming Language_, Prentice-Hall, 1978, ISBN 0-13-110163-3. [K&R1]
  • Brian W. Kernighan and Dennis M. Ritchie, _The C Programming Language_, Second Edition, Prentice Hall, 1988, ISBN 0-13-110362-8, 0-13-110370-9. (Vedi 18.10.) [K&R2]
  • Donald E. Knuth, _The Art of Computer Programming_. Volume 1: _Fundamental Algorithms_, Second Edition, Addison-Wesley, 1973, ISBN 0-201-03809-9. Volume 2: _Seminumerical Algorithms_, Second Edition, Addison-Wesley, 1981, ISBN 0-201-03822-6. Volume 3: _Sorting and Searching_, Addison-Wesley, 1973, ISBN 0-201-03803-X. [Knuth]
  • Andrew Koenig, _C Traps and Pitfalls_, Addison-Wesley, 1989, ISBN 0-201-17928-8. [CT&P]
  • Stephen K. Park and Keith W. Miller, "Random Number Generators: Good Ones are Hard to Find," _Communications of the ACM_, Vol. 31 #10, October, 1988, pp. 1192-1201.
  • P.J. Plauger, _The Standard C Library_, Prentice Hall, 1992, ISBN 0-13-131509-9.
  • Thomas Plum, _C Programming Guidelines_, Second Edition, Plum Hall, 1989, ISBN 0-911537-07-4.
  • William H. Press, Saul A. Teukolsky, William T. Vetterling, and Brian P. Flannery, _Numerical Recipes in C_, Second Edition, Cambridge University Press, 1992, ISBN 0-521-43108-5.
  • Dale Schumacher, Ed., _Software Solutions in C_, AP Professional, 1994, ISBN 0-12-632360-7.
  • Robert Sedgewick, _Algorithms in C_, Addison-Wesley, 1990, ISBN 0-201-51425-7.
  • Charles Simonyi and Martin Heller, "The Hungarian Revolution," _Byte_,August, 1991, pp.131-138.
  • David Straker, _C Style: Standards and Guidelines_, Prentice Hall, ISBN 0-13-116898-3.
  • Steve Summit, _C Programming FAQs: Frequently Asked Questions_, Addison-Wesley, 1995, ISBN 0-201-84519-9. [La versione libro di queste FAQ; vedi anche http://www.eskimo.com/~scs/C-faq/book/Errata.html.]
  • Sun Wu e Udi Manber, "AGREP -- A Fast Approximate Pattern-Matching Tool," USENIX Conference Proceedings, Winter, 1992, pp. 153-162.
  • C'è un'altra bibliografia nella Revised Indian Hill Style Guide (vedi 17.9). Vedi anche 18.10.

    Steve Summit scs@eskimo.com