[1] E dov'è la chiamata a main()? Non c'è proprio! E' il compilatore che provvede a generare il codice eseguibile necessario a chiamarla automaticamente alla partenza del programma. Ciò non vieta, tuttavia, di chiamare main(), se necessario, dall'interno di qualche altra funzione o persino dall'interno di se stessa.
[2] In realtà è possibile definire anche funzioni che non verranno utilizzate da quel programma. Il caso tipico è quello dei programmi TSR (Terminate and Stay Resident), il cui scopo è caricare in memoria e rendere residente un insieme di routine che verranno chiamate da altri programmi o da eventi di sistema.
[3] L'aggettivo attuali è di uso comune, ma deriva da una pessima traduzione dell'inglese actual, che significa vero, reale.
[4] Vi sono però anche ragioni tecniche, e non solo formali, che rendono opportuni tali controlli da parte del compilatore: esse sono legate soprattutto alla gestione dello stack, l'area di memoria attraverso la quale i parametri sono resi disponibili alla funzione.
[5] Molte funzioni C utilizzano questo sistema per gestire situazioni di errore in operazioni basate su chiamate al DOS.
[6] Esistono funzioni di libreria specializzate nella gestione di parametri in numero variabile, che possono essere richiamate dalle funzioni definite dal programmatore per conoscere quanti parametri attuali sono stati passati, e così via. Si tratta del gruppo di funzioni va_start(), va_arg(), va_end().
[7] La parola chiave è stata scelta per analogia con il linguaggio Pascal, in cui il passaggio dei parametri avviene sempre da sinistra a destra, cioè "in avanti".
[8] Per la chiamata di una funzione "normale", il compilatore genera delle istruzioni PUSH per i parametri da passare, una istruzione CALL e le istruzioni POP necessarie a rimuovere dallo stack i parametri passati. La funzione si chiude con una RET. Se la medesima funzione è dichiarata pascal, il compilatore genera ancora le PUSH e la CALL, ma non le POP, in quanto la funzione, essendo sempre fisso il numero di parametri, può provvedere da sé alla pulizia dello stack terminando con una RET n, dove n esprime il numero di byte da eliminare dallo stack.
[9] Ma allora... il nome di una funzione è puntatore alla funzione stessa! Ricordate il caso degli array? Chi ha poca memoria può fare un ripasso...
[10] E' il caso dei programmi che incorporano funzioni per la gestione degli interrupt di sistema. Essi devono memorizzare l'indirizzo degli interrupt ai quali sostituiscono le proprie routine, al fine di poterli utilizzare in caso di necessità e per riattivarli al termine della propria esecuzione. Non sembra però il caso di approfondire ulteriormente l'argomento.
[11] Lo standard input (stdin) è un "file" particolare, che il DOS identifica normalmente con la tastiera. Esso può però essere rediretto ad un file qualunque mediante il simbolo "<". Supponendo di chiamare il nostro programma NOCOMENT, è sufficiente lanciare il comando
nocoment < pippo.c
per visualizzarne il contenuto privo di commenti.
[12] La gets() sostituisce l'a capo ("\n")di fine riga letto dal file con un null terminator. Ciò rende necessario andare a capo esplicitamente.
[13] E avrebbe potuto esserlo ancora di più, utilizzando una if....else in luogo della switch. Quest'ultima, però, rende più agevoli eventuali modifiche future al codice, qualora, ed esempio, si dovessero implementare elaborazioni diverse in corrispondenza di altri caratteri.
[14] Un suggerimento circa le stringhe? Eccolo: il carattere virgolette ("), gestito con una terza colonna nella tabella, potrebbe determinare un terzo stato elaborativo (la terza riga della tabella), che determina la normale visualizzazione di tutto ciò che si incontra e l'avanzamento del puntatore di una posizione soltanto. Anche le barre vengono visualizzate come se nulla fosse. Quando si incontra nuovamente il carattere virgolette si ritorna nello stato "Normale".
[15] Cioè oltre il fatidico limite dei 64 Kb che iniziano all'indirizzo contenuto in CS. La logica è del tutto analoga a quella descritta circa i puntatori "a dati".
[16] E' ovvio che il codice eseguito è fisicamente il medesimo.
[17] L'allocazione di variabili locali e il passaggio dei parametri avvengono modificando i valori di alcuni regsitri della CPU dedicati proprio alla gestione dello stack. Se il programma utilizza più spazio di quanto il compilatore ha assegnato allo stack, è molto probabile che vengano sovrascritti i dati memorizzati ai suoi "confini".
[18] Il metodo per richiedere più stack del default varia da compilatore a compilatore. Alcune implementazioni del C definiscono una variabile globale intera senza segno, _stklen, il cui valore può essere impostato al numero di byte richiesti come area di stack per il programma. Il valore di default è spesso 4 kb. Si noti che la funzione fattoriale() non pone problemi di stack: ogni istanza richiede unicamente 4 byte (per il long passato come parametro) oltre ai 2 o 4 byte necessari per l'indirizzo di ritorno della chiamata near o, rispettivamente, far. E' anche possibile richiedere al compilatore di inserire automaticamente una routine di controllo, in entrata ad ogni funzione, il cui scopo è verificare se nello stack vi sia spazio sufficiente per tutte le variabili locali ed interrompere il programma in caso contrario. Tale opzione risulta utile in fase di sviluppo; è tuttavia opportuno non utilizzarla nel compilare la versione di programma per il rilascio finale, in quanto essa ne diminuisce leggermente l'efficienza.
[19] Lo sono sicuramente le funzioni definite nel medesimo sorgente, dal momento che ogni modulo .OBJ non può superare i 64Kb.
[20] Del resto se main() è eseguita per prima, da quale punto del programma la si potrebbe chiamare?
[21] Il modulo di startup altro non è che un file .OBJ, collegato dal linker in testa al (o ai) file .OBJ generati a partire dal sorgente (o sorgenti) del programma, avente lo scopo di svolgere le operazioni preliminari all'invocazione di main(), tra le quali vi è il controllo della versione di DOS, la chiamata a _setargv__() e _setenvp__(), etc.; lo startup module relativo al modello di memoria tiny include la direttiva assembler ORG 100, che permette di ottenere un file .COM dall'operazione di linking. Il sorgente dello startup module, scritto solitamente in assembler, è generalmente fornito insieme al compilatore, per consentirne personalizzazioni. Un esempio di startup module, adatto però ai device driver, è presentato nel capitolo ad essi dedicato.
[22] L'ordine in cui sono elencati è quello in cui main() li referenzia. E' ovvio che essi sono copiati sullo stack in ordine inverso, come normalmente avviene nelle chiamate a funzione. I nomi ad essi attribuiti in tabella sono quelli convenzionalmente utilizzati dai programmatori C. Nulla vieta, se non il buon senso, di utilizzare nomi differenti nei propri programmi.
[23] A proposito della riga di comando si dicono alcune cosette piuttosto utili in un paragrafo dedicato.
[24] La funzione atoi(), ad esempio, richiede quale parametro una stringa e restituisce come int il valore numerico che essa esprime.
[25] Attenzione: sostenere che dopo avere eseguito l'ultima istruzione di main() il programma termina è diverso dal dire che il programma termina dopo avere eseguito l'ultima istruzione di main(), perché un programma può terminare anche in altri modi, e al di fuori di main(). Molto utilizzata è, allo scopo, la funzione di libreria exit(), che richiede quale parametro un intero e lo utilizza per "restituirlo" come se venisse eseguita una normale return in main(), e ciò anche se questa è definita void.