Capitolo 3 - Elementi fondamentali di un programma in C

 

 

 

 

            3.1 - Prototipo di programma in C

 

 

                Il testo che segue é un esempio quasi minimale di programma in C, che dovrebbe potere essere compilato ed eseguito con successo con ogni versione del linguaggio:

 

  (3.1.1)       /*    S O M M A . C  -  esempio 1

                  scopo: mostrare la forma generale di un programma.

            */

 

            #include <stdio.h>

 

            main ( )

            {

                int a = 2, b = 1, c ;

 

                  c = a + b ;

                  printf ( “ la somma di %d e %d é %d”, a, b, c ) ;

                  a = c / ( a + b ) ;

                  printf ( “\n verifica del rapporto : %d “, a ) ;

            }

 

                Compilato e messo in esecuzione il programma, sullo schermo compaiono le righe:

 

  (3.1.2)       la somma di 2 e 1 é 3

            verifica del rapporto : 1

 

e pure non conoscendo le regole formali del linguaggio, é intuibile la relazione che collega quanto descritto nella sequenza inclusa tra le parentesi graffe in (3.1.1) ed il risultato ottenuto in (3.1.2).

 

                Meno evidente é il ruolo (o la necessità) di altri elementi che compaiono nel testo:

 

a)       La coppia di simboli (sequenze opposte di due caratteri scritti senza spaziatura) ‘/*’ e ‘*/’: si tratta di delimitatori di commenti, ossia di zone di testo che il compilatore C considera nulle, o più esattamente equivalenti ad un carattere di spaziatura. Il compilatore ignora il contenuto dei commenti, ma per chi deve leggere il testo del programma ed interpretarne scopo e logica essi possono essere di grande aiuto; si deve inoltre osservare che la scrittura di commenti ‘intelligenti’ costringe il programmatore alla continua verifica dell’aderenza tra l’analisi del problema per cui il programma é progettato e l’algoritmo, di cui lo stesso programma costituisce la realizzazione, o implementazione.

 

b)       La riga che inizia con il simbolo ‘#’ e che non é chiusa da ‘;’, come é invece richiesto per tutte le istruzioni in linguaggio C; in effetti essa non é una istruzione, ma una metaistruzione, o istruzione per il compilatore, al quale si richiede di compiere qualche azione particolare. Nel caso specifico, la sua presenza é resa necessaria dalle righe in cui compare la parola printf, utilizzata per descrivere la forma delle scritture da generare a video.  I termini di tale necessità sono approfonditi oltre.


 

c)       La parola main seguita dalla coppia di parentesi tonde ‘( )’, che in questo caso sono vuote, ma in altri potrebbero contenere ulteriori informazioni; in inglese essa significa principale; la sua presenza in un programma C é richiesta per individuare dove inizia la descrizione delle operazioni da eseguire, o flusso principale. Questa descrizione implica che esso potrebbe essere accompagnato da altre descrizioni di flussi secondari o ausiliari.

 

d)       La coppia di parentesi graffe aperta ‘{‘ e chiusa }; in C esse sono i delimitatoti di un contesto, che può essere detto blocco, di cui marcano esplicitamente inizio e fine. La terminologia adottata é analoga, ma non identica, a quella del Pascal o degli elementi strutturati del Fortran ( if .. end if, do .. end do); il blocco può anche essere definito come istruzione strutturata, composta da diversi dettagli che costituiscono un insieme omogeneo, che in (3.1.1) è la descrizione completa della operazioni di main ( ). In un programma in C compaiono spesso molti blocchi, ed è bene abituarsi subito ad una rappresentazione ordinata delle relative parentesi graffe, che conviene scrivere sempre una sulla verticale dell’altra, allineando su una colonna più interna il testo che esse racchiudono.

 

                In un programma C che sia scritto in un unico testo, come é la norma per la sperimentazione didattica (ma non della realtà applicativa), i soli elementi che costituiscono lo ‘scheletro obbligato’ sono:

 

  (3.1.3)                   main ( ) { }

 

ma poiché é difficile immaginare un programma che non effettui alcuna operazione sullo schermo o con la tastiera, anche la metaistruzione #include <stdio.h> é un obbligo di fatto.

 

 

 

 

 

 

            3.2 - Le istruzioni del programma

 

 

                Il programma, come viene comunemente inteso, è la parte contenuta nelle parentesi graffe che seguono main ( ): nell’esempio (3.1.1) esso è formato da cinque righe, tutte chiuse da un punto e virgola, che sono le istruzioni che compongono il programma.

 

  (3.2.1)                   Un’istruzione in linguaggio C é la descrizione di una richiesta operativa scritta

                               secondo le regole formali della sintassi del linguaggio; essa deve essere sempre

                               chiusa dal simbolo ‘;’, che ha funzione di terminatore di istruzione.

 

  (3.2.2)                   Un’istruzione può avere natura dichiarativa oppure operativa; nel primo caso essa

                               determina il significato di una entità logica che l’utente individua assegnandone in

                               modo arbitrario il nome, mentre nel secondo caso l’istruzione descrive una azione

                               da compiersi durante l’esecuzione del programma.

 

                Nell’esempio (3.1.1) esiste una istruzione dichiarativa, che determina la natura logica dei tre oggetti noti simbolicamente con i nomi a, b, c, o variabili, seguita da quattro istruzioni operative, che costituiscono una semplice sequenza lineare, con implicita richiesta di esecuzione ordinata dalla prima alla quarta.

 

                Nella (3.2.1) é importante il significato di terminatore, in cui é implicito anche il ruolo di separatore di istruzioni, ossia di elemento che permette di riconoscere la fine di una richiesta operativa e l’inizio della successiva.  Il C dispone di un altro simbolo che ha funzione di separatore tra istruzioni, la virgola ‘,’. La parte operativa in (3.1.1) può essere scritta anche come:

 

  (3.2.3)                   c = a + b,

            printf ( “ la somma di %d e &d é %d”, a, b, c ),

            a = c / ( a + b ),

            printf ( “\n verifica del rapporto : %d “, a ) ;


 

                Questa caratteristica non ha controparte negli altri linguaggi di larga diffusione ed é quindi una caratteristica unica del C, che la utilizza per evidenziare la natura sequenziale di un insieme di operazioni; si rende però necessaria una ulteriore precisazione:

 

  (3.2.4)                   Una istruzione si dice composta se é costituita da una sequenza di descrizioni

                               operative separate da virgole,  semplice nel caso opposto.

 

                La scrittura (3.2.3) é sintatticamente corretta quanto la (3.1.1), ma è poco diffusa, forse perché ritenuta logicamente poco chiara; si preferisce cioè in genere una notazione uniforme, quale quella dei terminatori, anche se esistono casi in cui le istruzioni composte non sono facilmente sostituibili.

 

                Nell’esempio (3.1.1) é presente un altro caso di impiego delle virgole in ruolo di separatori, in cui però è diverso il contesto; si tratta della istruzione dichiarativa:

 

  (3.2.5)                   int a = 2, b = 1, c ;

 

il cui scopo é di informare il compilatore sulla natura delle tre variabili utilizzate dalle operazioni del programma, in modo di fissarne le regole per la rappresentazione in memoria (in questo caso la normale notazione posizionale binaria dotata di segno su 16 bit) e quelle per operazioni sui dati stessi.

 

                La (3.2.5) è un caso particolare della forma generale di dichiarazione:

 

  (3.2.6)                   tipo lista ;

 

in cui tipo è il nome di una forma di dato accettabile per il C (nell’esempio, int per intero), mentre lista e una sequenza di nomi di singole variabili, separate da virgole. L’impiego delle virgole come separatori di lista é pressoché universale nei linguaggi di programmazione.

 

                Nella (3.2.5) è presente un’altra particolarità del linguaggio C: non si é solo dichiarato che gli oggetti logici a, b, c esistono con una certa natura, ma si è contestualmente definito il valore che debbono avere inizialmente i primi due.

 

                La distinzione tra i termini dichiarare e definire é importante: con il primo ci li limita ad asserire l’esistenza di un certo oggetto logico circoscrivendone la natura, cosa che permette al compilatore di stabilire, come già detto,  una posizione in memoria e le regole per la trattazione di dati; con il secondo, invece, si indica sempre un procedimento operativo che determina i dettagli.

 

                Nel caso attuale a e b sono definiti nel senso che non solo viene loro assegnata una posizione in memoria, ma in essa viene anche memorizzato un ben determinato valore durante la compilazione: all’avviamento dell’esecuzione tale valore esiste già, mentre é ancora indefinito quello di c, il quale risulta definito solo dopo completato quanto previsto dalla prima istruzione operativa, ossia:

 

  (3.2.7)                   c = a + b ;

 

                Si noti che il fatto di identificare inizialmente a con una costante non ne modifica la natura di variabile; nella terza riga operativa di ha infatti:

 

  (3.2.8)                   a = c / ( a + b ) ;

 

con cui il valore di a viene ricalcolato, sostituendo in memoria quello precedente.

 


 

            3.3 - Le assegnazioni

 

 

                Le (3.2.7) e (3.2.8) sono esempi di istruzioni di assegnazione, che sono certamente il tipo di istruzione più frequente, avendo un po' il ruolo di ‘mattoni da costruzione’ per un programma, e costituiscono il modo preferenziale per definire il valore di una variabile ed eventualmente ridefinirlo nel corso del programma. La loro forma generale in C é descrivibile come:

 

  (3.3.1)                   nome = espressione ;

 

in cui nome individua una variabile dichiarata in precedenza, ed espressione è, almeno nel caso aritmetico, che comunque é il solo previsto dal linguaggio C, una qualunque combinazione di valori e di operatori ben formata secondo le consuete regole algebriche, incluso l’impiego di parentesi per superare le precedenze gerarchiche.

 

                L’impiego del simbolo ‘=‘ come relazione tra primo e secondo membro può essere fuorviante: non si tratta infatti di una eguaglianza aritmetica, ma solo del procedimento operativo:

 

  (3.3.2)                   assegna alla variabile a primo membro il valore che deriva dalla valutazione

                               dell’espressione a secondo membro,

               

con cui é perfettamente lecita, ad esempio:

 

  (3.3.3)                   k = k + 1 ;

 

che in termini di eguaglianza aritmetica é priva di significato, o meglio sempre falsa. Secondo la (3.3.3), invece, i due termini k a primo e secondo membro non sono identici in senso logico, poiché l’azione descritta é univoca e non ambigua, ed il secondo cessa di esistere appena il valore del primo viene memorizzato.

 

                Poiché quando viene valutata un’espressione il suo valore finale é presente in un dispositivo atto alla trasformazione dei dati (in genere un registro di calcolo nel microprocessore), gli estensori del C hanno approfittato della opportunità che ciò offre per estendere il modo ‘normale’ di utilizzare le assegnazioni; in questo linguaggio é ammessa una assegnazione multipla, nella forma:

 

  (3.3.4)                   nome_1 = nome_2 = espressione ;

 

da intendere come:

 

  (3.3.5)                   nome_1 = ( nome_2 = espressione ) ;

 

semplicemente convenendo:

 

  (3.3.6)                   una assegnazione é essa stessa una espressione, il cui valore coincide con quello

                               della espressione che figura all’ultimo posto.

 

                Per rendere operativa tale convenzione, basta memorizzare il valore della espressione prima per la variabile nome_2, poi per la variabile nome_1, senza cioè eseguire un trasferimento intermedio dalla memoria ai registri del microprocessore, come accade invece, ad esempio, nella sequenza equivalente di istruzioni Fortran (si noti l’assenza dei separatori):

 

  (3.3.7)                   nome_2 = espressione

                               nome_1 = nome_2

 

                Convenzioni come questa permettono di ottimizzare i tempi di esecuzione in macchina di certe catene di operazioni, ed in C ne esistono diversi altri esempi.


 

 

            3.4 - Le funzioni

 

 

                Le ultime osservazioni di rilievo sul primo esempio di programma (3.1.1) riguardano un contesto in cui é presente una coppia di parentesi tonde con significato assai diverso da quelle che si hanno in espressioni aritmetiche come la (3.2.8).

 

                Ciò accade in due casi, con le parole main e printf; il significato é simile a quello delle consuete funzioni aritmetiche, come si ha nelle notazioni matematiche:

 

  (3.4.1)                   k = f ( h )                               z = g ( x, y )

 

in cui le parentesi vengono utilizzate per la determinazione degli argomenti, o parametri, che costituiscono i dati per i quali il procedimento operativo indicato con il simbolo f o g deve essere eseguito, generando un valore che risulterà essere quello di k e z rispettivamente.

 

                L’analogia non é però una identità; per il C viene infatti impiegato il termine funzione per indicare un procedimento operativo comunque complesso con un nome simbolico che ne semplifica l’impiego, ma non é detto che una funzione debba generare uno valore; possono cioè esistere funzioni che, come suggerisce l’impiego di printf, si esauriscono con lo stesso procedimento operativo, senza determinare un valore che si possa identificare come risultato delle operazioni compiute.

 

                Si deve avvertire che l’esempio scelto non é del tutto corretto, poiché la funzione printf genera in realtà un valore, che nell’esempio (3.1.1) non é considerato; dichiarando una opportuna variabile intera, si sarebbe potuto scrivere:

 

  (3.4.2)                   int k ;

                               ........

                               k = printf ( ...... ) ;

 

ottenendo come valore di k il numero di dati effettivamente scritti; é però raro che printf venga impiegata in questa forma, salvo che per la ricerca ed il controllo degli errori, come può essere necessario a livelli sofisticati di programmazione.

 

                Un altro termine di uso corrente é sottoprogramma, che definisce il procedimento interessato come un normale programma, cioè un insieme di istruzioni scritte secondo la sintassi del linguaggio, progettate però non per un impiego ‘autonomo’, ma per essere ‘messe al servizio’ di un altro programma, che le utilizza come una unica unità operativa.

 

                La definizione di sottoprogrammi é una esigenza comune a tutti i linguaggi di programmazione; il C indica con il nome di funzione ogni tipo di sottoprogramma, indipendentemente dal fatto che le operazioni che esso svolge si concludano in un valore finale, oppure no; in altri linguaggi si preferiscono denominazioni distinte, ad esempio utilizzando le parole function e subroutine per il Fortran, o function e procedure per il Pascal. o function e sub per le versioni correnti del Basic.

 

  (3.4.3)                   L’elemento sintattico per l’individuazione di una funzione in un programma C è la

                               presenza di parentesi tonde in un contesto che non sia quello di una espressione

                               aritmetica, caso che comprende la chiusura di una intera espressione in una tale

                               coppia di parentesi.


 

            3.5 - Identificatori e parole chiave

 

                Per completare l’esame degli elementi presenti nell’esempio (3.1.1) ci occupiamo ora della prima riga presente dopo il commento iniziale, cioè della metaistruzione #include <stdio.h>; ciò richiede l’esatta definizione di alcuni termini.

               

  (3.5.1)                   La programmazione strutturata é un modo di organizzare l’analisi dei

                               problemi, e quindi di implementarne gli algoritmi, che incoraggia la

                               suddivisione del programma in insieme di sottoprogrammi, ognuno

                               progettato per risolvere un dettaglio specifico del problema.

               

                Nel corso dell’analisi strutturata l’utente crea almeno due tipi di risorse che richiedono nomi simbolici: le variabili e le funzioni. Dai principi enunciati nel seguito deriva una conseguenza che le accomuna nella richiesta di dichiarazioni esplicite:

 

  (3.5.2)                   Con il termine identificatore si intende una qualunque sequenza di caratteri

                               (intuitivamente una ‘parola’) di cui si possono fissare inizio e fine per la

                               presenza di elementi separatori, il più ovvio dei quali é la spaziatura ‘ ‘; sono

                               pure separatori tutti i simboli di operatori aritmetici, quello di assegnazione,

                               tutti i tipi di parentesi, i commenti, il carattere di tabulazione ed il passaggio

                               a nuova riga, che per il C é in generale equivalente ad una spaziatura.

 

                Ne deriva, in particolare, che un identificatore non può contenere una spaziatura, la cui presenza (o quella di un elemento equivalente, come tabulazione o cambio di riga)  é invece essenziale in altri contesti: ad esempio se nella prima riga dopo l’apertura della parentesi graffa in (3.1.1) si scrivesse:

 

  (3.5.3)                   inta = 2 ;

 

non si avrebbe più una dichiarazione di variabile ma una semplice assegnazione, che in quel contesto sarebbe scorretta, poiché utilizzerebbe una variabile non dichiarata.

 

  (3.5.4)                   La sintassi del linguaggio C definisce le parole chiave, ossia gli identificatori

                               che hanno significato predefinito e che non é lecito impiegare in altro modo che

                               quello fissato operativamente dalla sintassi stessa.

 

                Ad esempio, in (3.1.1) compaiono le parole chiave main e int, mentre le variabili a, b, c sono casi di identificatori creati dall’utente.

 

  (3.5.5)                   Ogni identificatore creato dall’utente é indefinito, ossia ad esse non é collegata

                               nessuna natura logica predefinita; ne deriva che la prima volta che esso compare

                               nel programma, ciò deve accadere nel contesto di una dichiarazione esplicita.

                               Una dichiarazione non é necessariamente all’inizio del programma, ma può

                               essere fatta limitatamente ad un blocco delimitato da una coppia di parentesi

                               graffe, dopo la quale la risorsa dichiarata torna ad essere indefinita  (si veda la

                               parte sulle classi di memoria più avanti).

 

                Questo principio é stato osservato in (3.1.1) scrivendo per prima la dichiarazione delle tre variabili, in parte modificata in definizione, come la sintassi permette. Segue la lista completa delle parole chiave del linguaggio C, come definite dallo standard ANSI.

 

(3.5.6)                    

argc

argv

auto

break

case

char

const

continue

default

do

double

else

enum

envp

extern

float

for

goto

if

int

long

main

register

return

short

signed

sizeof

static

struct

switch

typedef

union

unsigned

void

volatile

while


 

                Si noti che tra di esse non compare printf, che nel programma (3.1.1) é pure stata utilizzata, senza apparente dichiarazione esplicita della sua natura. Casi analoghi avrebbero potuto verificarsi se vi fosse stato bisogno di calcolare una radice quadrata, ad esempio come in:

 

  (3.5.7)                   double x, y ;

                               .........

                               y = sqrt ( x ) ;

 

in cui é evidente la presenza delle parentesi tonde che distinguono le funzioni, associate all’identificatore sqrt (da SQuare RooT, o radice quadrata in inglese).

 

                I procedimenti di calcolo o elaborazione che si possono intuire, ‘nascosti’ sotto le denominazioni sqrt o printf, non sono certamente riducibili ad una semplice operazione elementare, ma richiedono una apposita programmazione che può essere anche molto complessa. Non avrebbe evidentemente molto senso obbligare un utente ad apprendere tutti i dettagli richiesti a questo scopo, per di più al fine di scrivere funzioni che una volta messe a punto possono venire utilizzate in modo generale, indipendentemente dalla logica del programma che le richiede.

 

                Per tale motivo queste due funzioni, e molte altre, sia di tipo matematico, sia per elaborazioni generiche, sono state programmate una volta per tutte contestualmente al compilatore dal produttore dello stesso; sono quindi state compilate ed inserite in una forma ‘pronta per l’uso’ nella libreria standard, fornita insieme al compilatore stesso; tale libreria viene automaticamente consultata nella fase di link per inserire una copia delle funzioni necessarie ad un programma specifico nella sua versione eseguibile.

 

                Anche per le funzioni della libreria standard vale però il principio (3.5.5), vale a dire esse pure richiedono una serie di dichiarazioni e convenzioni per renderne coerente l’impiego, informazioni che l’utente dovrebbe quindi conoscere esattamente e riscrivere ogni volta in ogni nuovo programma.

 

                Una tale richiesta sarebbe evidentemente assurda; poiché la necessità non può però essere elusa, assieme alla libreria standard viene fornito un insieme di testi già pronti per questo scopo; essi vengono detti file di testata (in inglese header file) e sono generalmente distinti dal suffisso ‘.h’.

 

                Essi si trovano di norma in uno speciale direttorio di nome INCLUDE la cui collocazione sul disco é nota al sistema operativo, che viene automaticamente consultato dal compilatore quando nel testo del programma si incontra un’istruzione come ad esempio:

 

  (3.5.8)                   #include <stdio.h>

                               #include <math.h>

                               #include <string.h>

 

                Il verbo inglese include viene inteso in senso letterale: il compilatore consulta il direttorio di sistema alla ricerca del file menzionato tra i due simboli  ‘<‘ e ‘>‘; se lo trova, l’intero testo in esso presente viene logicamente incluso nel programma al posto della richiesta #include <...>.

 

                Per verificare quanto asserito, basta modificare il programma (3.1.1) eliminandone la metaistruzione di inclusione; non verranno segnalati errori, né nella compilazione, né nel link, ma durante l’esecuzione non si otterranno in genere scritture corrette, provando così la necessità della metaistruzione stessa.

 

                I due simboli ‘<‘, ‘>‘ hanno essi pure un significato speciale; poiché spesso l’utente ritiene conveniente (o necessario) costruire propri file di testata con definizioni utilizzate da più programmi, o anche con intere sezioni di programmi e sottoprogrammi, la metaistruzione di inclusione é disponibile anche per questo scopo, con una differenza di notazione, sostituendo i simboli suddetti con una coppia di doppi apici, ad esempio come in:

 

  (3.5.9)                   #include “mio.h”


 

                I doppi apici informano il compilatore che il file richiesto, per il quale l’estensione .h non é obbligatoria, non deve essere cercato nel direttorio di sistema, ma in questo caso nel medesimo direttorio in cui si trova il testo del programma C in corso di compilazione; sarebbe possibile anche indicare completamente un cammino più complesso, come ad esempio:

 

  (3.5.10) #include “\inc\mio.h”

 

 

                Stabilito così che l’impiego corretto delle funzioni di libreria richiede la conoscenza del file di testata cui esse fanno riferimento, si deve ancora dire come reperire le relative informazioni.

 

                Poiché esse dipendono dal costruttore della particolare versione di C che si sta utilizzando, la sola fonte che fa testo é data dalla documentazione del fornitore, certamente disponibile sotto forma di manuali scritti, ma anche di documentazione on-line, come quella disponibile con il programma qh (QuickHelp) della Microsoft.

 

                E’ consigliabile consultare subito tale documentazione, in primo luogo per sapere quali sono le funzioni disponibili, ma soprattutto per avere informazioni dettagliate sulle loro modalità di impiego; molte di esse presuppongono la conoscenza dettagliata della trattazione delle funzioni, che affronteremo più avanti; ci limitiamo a menzionare gli elementi essenziali:

 

                - quale o quali file di testata sono richiesti

                - quale tipo di valore genera la funzione

                - quanti sono i parametri debbono essere utilizzati e di che tipo di dato si tratta

 

 

 

 

 

            3.6 - Formazione dei nomi di identificatori

 

                Gli elementi che possono comparire in un programma in linguaggio C si dividono in due classi: simboli che descrivono una funzione, o ‘operativi’, ed identificatori, o ‘parole’; i secondi, a loro volta, sono parole chiave oppure ‘nomi’ creati dall’utente, specifici quindi di un programma particolare.

 

                Ci occupiamo brevemente delle regole di formazione dei nomi:

 

  (3.6.1)         (a) - Un identificatore é formato da una sequenza arbitraria di caratteri alfanumerici

                               (alfabetici e numerici), oltre al carattere speciale ‘_’ (underscore), con i caratteri

                               alfabetici maiuscoli considerati distinti dai rispettivi minuscoli; il carattere iniziale

                               del nome non può però essere numerico.

                      (b) - Nessun altro carattere é ammesso negli identificatori oltre a quelli citati al

                               punto precedente.

                      (c) - Ogni compilatore C distingue nomi per una lunghezza di almeno 31 caratteri.

                      (d) - Gli identificatori creati dall’utente non possono coincidere con alcuna parola

                               chiave, ma é ovviamente permesso che essi contengano come sottosequenza la

                               medesima sequenza di caratteri riconosciuta come parola chiave: ad esempio, in

                               ‘cifra’ compare come parte la parola chiave ‘if’.

                      (e) - E’ buona regola, anche se non un obbligo, evitare di utilizzare il carattere ‘_’

                                come iniziale di un nome, perché è convenzione comune utilizzarlo in questa

                               posizione per le esigenze della libreria standard, con le operazioni della quale un

                               tale nome potrebbe interferire.

 

                Le regole sono molto semplici, ed é intuibile il motivo delle limitazioni: se fosse permesso di fare uso di caratteri speciali, come ‘+’, ‘*’, ‘(‘, ecc. nella formazione di nomi, oppure di ridefinire le parole chiave, diverrebbe molto difficile interpretare il significato delle istruzioni.