• Non ci sono risultati.

Manuale Mondadori Linguaggio C

N/A
N/A
Protected

Academic year: 2021

Condividi "Manuale Mondadori Linguaggio C"

Copied!
60
0
0

Testo completo

(1)

Il linguaggio C

Piero Gallo – Pasquale Sirsi

(2)

Il linguaggio C

Generalità sul linguaggio C

I linguaggi di programmazione possono essere classificati in base a diversi crite-ri. Le classificazioni non servono per stabilire se certi linguaggi sono migliori o preferibili rispetto ad altri, ma per distinguere che cosa possono fare e a quale scopo possono essere utilizzati. Una tra le più importanti classificazioni distingue i linguaggi di basso livelloda quelli di alto livello.

I linguaggi di basso livello permettono di programmare il computer inserendo le istruzioni che possono essere eseguite direttamente dal microprocessore. Sono linguaggi più “vicini” alla logica della macchina che a quella del program-matore.

L’Assemblyè il classico linguaggio di basso livello attraverso il quale si manipola direttamente la memoria, i registri del microprocessore e, tramite altre funzioni di basso livello fornite dal sistema operativo, si può accedere direttamente all’har-dware (per esempio a una porta, a una unità a disco e così via).

I linguaggi di alto livello sono più intuitivi e comprensibili per il programmato-re, ma le loro istruzioni non sono direttamente eseguibili dal microprocessoprogrammato-re, è necessario che siano trasformate in istruzioni di basso livello a opera di spe-cifici software (compilatori/interpreti).

I linguaggi di alto livello (come il Pascal, il Basice così via) aiutano molto il pro-grammatore nella fase di stesura del codice. Questi linguaggi trattano i dettagli ri-guardanti il computer in maniera astratta: non è necessario conoscere come il processore riesce a sommare due numeri, basta usare il simbolo +. Non è ne-cessario conoscere la locazione di memoria in cui registrare i dati da utilizzare, poiché è possibile assegnare ai dati un nome simbolico e mnemonico (una varia-bile). Allo stesso modo, non è necessario conoscere i dettagli del sistema operati-vo e dell’hardware per poter leggere o scrivere un file su disco.

Il C è un linguaggio di programmazione di alto livello. Progettato e realizzato da

Dennis Ritchienel 1972, fu sviluppato presso gli AT&T Bell Laboratories allo sco-po di ottenere un linguaggio di alto livello per l’implementazione dei sistemi ope-rativi. Il linguaggio C fu utilizzato per la prima volta in modo estensivo per la ri-scrittura del codice del sistema operativo UNIX dal linguaggio Assembly; inevitabilmente, molte delle sue caratteristiche derivano dalle specifiche esigen-ze legate alla realizzazione di un sistema operativo in generale, e di UNIX in par-ticolare. Benché sia strettamente legato a quest’ultimo sistema, il C può essere impiegato con i principali sistemi operativi oggi disponibili. Steve Johnson, nella metà degli anni Settanta, scrisse un compilatore C trasportabile su sistemi diver-si dal PDP-11: da allora il C cominciò a essere utilizzato come linguaggio in altri diver- si-stemi operativi, come MS-DOS e CPM/80.

Nel 1978 fu pubblicato il libro The C Programming Language di Brian Kernighan e Dennis Ritchie (K&R): la pubblicazione servì come definizione ufficiale del lin-guaggio, anche se già dalla distribuzione della versione V7 erano presenti alcune caratteristiche assenti in nel volume citato.

Nella metà degli anni Ottanta il comitato X3JI1 dell’ANSI (American National

Standards Institute) sviluppò uno standard per il linguaggio C che aggiungeva

im-portanti elementi e ufficializzava molte caratteristiche presenti nei diversi compi-latori realizzati successivamente alla pubblicazione del libro di Kernighan e Ritchie. Da allora l’ANSI C cominciò a essere considerato la versione “ufficiale” del linguaggio rimpiazzando quella di K&R.

(3)

Le principali caratteristiche del linguaggio C

Elenchiamo di seguito le caratteristiche principali del linguaggio C.

• È un linguaggio general purpose, ossia può essere impiegato per codificare progetti software di natura diversa, da quelli real time a quelli che operano su basi di dati, da quelli tecnico-scientifici fino alla realizzazione di moduli per il sistema operativo.

• È un linguaggio strutturatodi tipo imperativo (o procedurale).

• È un linguaggio case sensitive, ossia distingue tra lettere minuscole e lettere maiuscole. Ad esempio, la variabile PIPPO è diversa dalle variabili Pippo e pippo.

• È un linguaggio compilato. Il problema dei linguaggi imperativi compilati è che sono legati alla piattaforma su cui vengono compilati: se compiliamo un programma sotto UNIX, quel programma potrà essere eseguito esclusivamen-te su sisesclusivamen-temi operativi UNIX.

• Nella versione ANSI, è facilmente portabilee perciò multipiattaforma; infatti, può essere compilato su un’ampia gamma di computer.

• Pur essendo un linguaggio di alto livello (in quanto possiede strutture di alto li-vello), e nonostante possa definirsi potente ed efficiente, il C è un linguaggio relativamente di medio livello, in particolare per quanto riguarda la rappresen-tazione dei dati.

• Produce programmi efficienti. Essendo nato per implementare sistemi opera-tivi, il C evita, ove possibile, qualsiasi operazione che possa determinare un de-cadimento del livello di efficienza del programma eseguibile.

• Produce programmi di dimesioni ridotte. Il C ha, infatti, una sintassi che si presta alla concisione e alla scrittura di codice compatto.

Descrizione sintattica di linguaggi di programmazione

Per descrivere la sintassi di un linguaggio di programmazione ci serviamo di un formalismo molto diffuso: la BNF(Backus-Naur Form o Forma Normale di Backus). Al fine di rendere lo studio semplice ed efficace, nella nostra trattazione impie-gheremo soltanto alcuni simboli propri della BNF e poche regole. In particolare:

• le parole chiave del linguaggio saranno scritte in grassetto;

• le parole racchiuse tra parentesi angolari < > rappresentano le categorie sin-tattiche, ossia elementi generali del linguaggio che, nei vari programmi, sa-ranno sostituiti con opportune occorrenze; ad esempio:

Il linguaggio C

• le parentesi quadre [ ] racchiudono blocchi opzionali, ossia che possono anche non essere presenti. Ad esempio:

int <NomeVariabile> [= <ValoreIniziale>]

• le parentesi graffe { } racchiudono blocchi con possibilità di ripetizione. Ad esempio:

int <NomeVariabile> {, <NomeVariabile>}

indica che subito dopo il metodo è possibile inserire una lista opzionale di pa-rametri separati da una virgola;

• il simbolo | ha il significato di OR. Ciò significa che all’interno di una lista di parole chiave separate da questo simbolo occorre sceglierne soltanto una. Ad esempio, in base alla seguente sintassi:

int | float <NomeVariabile>

la variabile di nome <NomeVariabile> dovrà essere preceduta dalla parola chiave int oppure da float.

<NomeVariabile>=<Espressione>

A = B

A = C + 2 * D C = 0

(4)

. . .

La struttura di un programma in linguaggio C è la seguente:

Anatomia

di un programma C

Direttive al preprocessore

Come si evince dalla figura, un programma in linguaggio C è formato da un insieme di sottoprogrammi (che impareremo a chiamare con il loro vero nome, funzioni). Riportiamo di seguito una schematizzazione più generale della struttura di un programma in linguaggio C.

direttive per il preprocessore

dichiarazioni globali – variabili, costanti e prototipi

main()

{

 variabili locali alla funzione main

 istruzioni della funzione main

}

f1()

{

 variabili locali alla funzione f1

 istruzioni della funzione f1

}

f2()

{

 variabili locali alla funzione f2

 istruzioni della funzione f2

} . . . fn() {

 variabili locali alla funzione fn

 istruzioni della funzione fn

}

Sottoprogramma 2 Sottoprogramma 1 Programma principale (main) Dichiarazione di variabili globali

e/o dei prototipi delle funzioni

Sottoprogramma N Facoltativo Facoltativo Obbligatorio Facoltativo Facoltativo Facoltativo

(5)

mainrappresenta il programma principalee come tale deve essere sempre pre-sente, poiché il compilatore inizia l’esecuzione proprio da esso. Al main è asse-gnato il compito di gestire il controllo generale dell’attività del codice.

La sintassi della funzione main è la seguente:

[<TipoRestituito>] main([<ElencoParametri>]) {  <Istruzione1>;  <Istruzione2>;  ...  <IstruzioneN>; } dove:

• <TipoRestituito> indica il tipo di dato restituito dalla funzione, il quale, in mancanza di esplicita dichiarazione (cioè per default), è un valore intero (int);

• <ElencoParametri> rappresenta l’insieme dei parametri che il main accetta in ingresso (i parametri saranno trattati approfonditamente in seguito). Nel caso in cui il main non abbia parametri in ingresso occorre comunque far se-guire la parola main da una coppia di parentesi tonde. Ciò perché, come vedre-mo meglio più avanti, il main può essere considerato una funzione che può ri-cevere degli argomenti (ossia i parametri): essi, se presenti, dovranno essere racchiusi all’interno di tali parentesi.

Hai notato che nella sintassi le varie istruzioni (<Istruzione1>, >Istruzione2> e così via) sono spostate un po’ a destra rispetto alle parentesi graffe? Le istru-zioni vengono rientrate per indicare la loro dipendenza, cioè per evidenziare che l’istruzione è contenuta in un’altra. È buona abitudine curare questi inco-lonnamenti poiché, così facendo, si garantisce una migliore leggibilità del codi-ce. La tecnica di questi incolonnamenti è chiamata indentazione.

Come si evince dalla sintassi, quando il compilatore richiama il main esegue il gruppo di istruzioni racchiuso all’interno della coppia di parentesi graffe { } che definiscono il corpodel programma principale. Dal punto di vista tecnico, le pa-rentesi graffe permettono di incapsulare istruzioni composte. Tali istruzioni pos-sono consistere nella definizione del corpo di un sottoprogramma (come accade nel nostro caso per il main), oppure nel riunire più linee di codice che dipendono dalla stessa istruzione di controllo, come nel caso in cui diverse istruzioni vengo-no eseguite a seconda del risultato vero/falso del test di un’istruzione.

Dalla sintassi si ricava, inoltre, che ogni istruzione C termina con un punto e vir-gola(;). Su una singola riga è anche possibile scrivere più istruzioni, ma è buona norma scriverne solo una al fine di migliorare la leggibilità del codice.

Notiamo l’utilizzo delle parentesi tonde e graffe. Le parentesi tonde () vengono utilizzate in unione con i nomi dei sottoprogrammi (le funzioni), mentre le paren-tesi graffe {} vengono utilizzate per delimitare le espressioni. Infine, il punto e virgola (;) indica la fine di un’istruzione.

Il linguaggio C ha un formato piuttosto flessibile: istruzioni lunghe possono esse-re continuate su righe successive senza problemi: il punto e virgola segnala al compilatore che è stata raggiunta la fine dell’istruzione. Inoltre, è possibile intro-durre spazi allo scopo di garantire una maggiore leggibilità del programma. Un errore molto comune è proprio quello di dimenticare il punto e virgola: in que-sto caso il compilatore concatenerà più linee di codice, generando un’espressio-ne priva di qualsiasi significato. Per questo motivo il messaggio d’errore che il compilatore restituisce non indica la mancanza del punto e virgola, quanto la pre-senza di qualcosa di incomprensibile. Attenzione dunque a non scordare i punti e virgola e a interpretare correttamente i messaggi del compilatore.

(6)

L’alfabeto del linguaggio C

L’alfabeto del linguaggio C è formato da:

1. le lettere minuscole e maiuscole dell’alfabeto inglese (dalla a fino alla z):

a b c d e f g h i j k l m n o p q r s t u v w y z A B C D E F G H I J K L M N O P Q R S T U V X Y Z

2. le dieci cifre decimali (dallo 0 al 9): 0 1 2 3 4 5 6 7 8 9

3. i seguenti caratteri speciali:

– i simboli di punteggiatura . , : ;

– i segni matematici + – * / = % < >

– i simboli spazio # $ & ! ^ | \ ‘ “ _ ? ~

– le parentesi [ ] ( ) { }

4. i simboli non grafici (o non stampabili), come quelli di:

nuova linea (new line);

nuova pagina (form feed);

spazio indietro (backspace);

– tabulazione orizzontale.

Le regole lessicali

Grazie all’alfabeto del oinguaggio, il programmatore piò definire le parole da utiliz-zare nel programma sorgente. Naturalmente tali parole devono essere valide, ossia essere riconosciute come formalmente valide dal compilatore del linguaggio. Ogni linguaggio prevede un certo numero di categorie lessicali; in C possiamo di-stinguere le seguenti categorie:

• parole riservate;

• identificatori (creati dal programmatore);

• costanti letterali;

• segni di punteggiatura e operatori;

• commenti.

Le parole chiave

Il linguaggio C (come qualsiasi altro linguaggio di programmazione) consente la realizzazione di programmi utilizzando un insieme di “parole chiave” (keyword) che non possono essere utilizzate dal programmatore come nomi di variabili. Lo standard ANSI ha definito il seguente insieme di parole chiave:

auto break case const

continue default do double

else enum extern float

for goto if int

long register return short

signed sizeof static struct

switch typedef unsigned void

volatile while

Alfabeto e regole

lessicali

(7)

Gli identificatori

Gli identificatori sono i nomi definiti dal programmatore per riferirsi a sei diverse categorie di oggetti:

• variabili;

• costanti simboliche;

• etichette;

• tipi definiti dal programmatore;

• funzioni;

• macro.

Le variabilisono contenitori di valori; ogni variabile può contenere un singolo va-lore, che può cambiare nel tempo. Il tipo di una variabile viene stabilito una volta per tutte e non può essere modificato.

Le costanti simbolicheservono a identificare valori che non cambiano nel tem-po. Per tale motivo non possono essere considerate veri contenitori, ma solo come nomi utilizzati per identificare un valore.

L’etichetta è un nome che identifica una istruzione del programma; è utilizzata dall’istruzione di salto incondizionato (goto).

Il tipoidentifica un insieme di valori e di operazioni definite su di essi; ogni lin-guaggio (o quasi) fornisce un certo numero di tipi primitivi (a cui è associato un identificatore predefinito) e dei meccanismi che permettono la costruzione di nuovi tipi (ai quali il programmatore deve poter associare un nome) a partire da questi.

Funzione(in C function) è il termine che viene utilizzato per indicare i sottopro-grammi.

La macroè in sostanza un alias utilizzato per contrassegnare una porzione di co-dice. Come vedremo più avanti, le macro non sono trattate dal compilatore, ma da un precompilatore che si occupa di eseguire alcune elaborazioni sul codice sor-gente prima che questo venga effettivamente compilato.

Gli identificatoriin C devono iniziare con una lettera o con un carattere di under-score _. Possono essere seguiti da un numero qualsiasi di lettere, cifre o undersco-re; viene fatta distinzione tra lettere maiuscole e minuscole (abbiamo già detto che il C è un linguaggio case sensitive). Tutti gli identificatori presenti in un pro-gramma devono essere diversi tra loro, indipendentemente dalla categoria a cui appartengono. Gli identificatori non possono iniziare con una cifra numerica e non possono contenere spazi. Ad esempio, non sono identificatori validi:

2biciclette primo numero

mentre sono validi i seguenti:

j MIN max eta_studente _secondo_nome

Sebbene il linguaggio non preveda un limite alla lunghezza massima di un identi-ficatore, è praticamente impossibile non prescrivere un limite al numero di carat-teri considerati significativi, per cui ogni compilatore distingue gli identificatori in base a un certo numero di caratteri iniziali tralasciando i restanti.

Il numero di caratteri considerati significativi varia comunque da sistema a siste-ma, anche se lo standard ANSI C ha stabilito che sono validi i primi 1024 caratte-ri di un identificatore.

(8)

In C un’espressione è una combinazione di operatori e operandi che definisce un’elaborazione e dà origine a un valore. A elaborazione terminata, l’espressione restituisce un risultato il cui tipo di dato determina il tipo dell’espressione stessa. Gli operandisono combinazioni di costanti, variabili semplici e strutturate, chia-mate di funzioni o, ancora, di altre espressioni, e rappresentano i valori che ven-gono manipolati nell’espressione. Nello studio degli operandi, particolare impor-tanza assumono le variabili, le costanti e il tipo che può essere loro assegnato. Gli operatori sono simboli che specificano come devono essere manipolati gli operandi dell’espressione. La valutazione di un’espressione viene effettuata ese-guendo le operazioni indicate dagli operatori sui loro operandi secondo le regole di precedenza degli operatori tipiche dell’aritmetica.

Variabili e costanti

L’identificatore serve per individuare univocamente un dato. Ricordiamo che è buona norma utilizzare nomi che consentano di richiamarne immediatamente il significato. In funzione della possibilità di cambiamento del loro valore durante l’elaborazione, i dati si classificano in variabili(il cui valore può variare nel corso dell’elaborazione) e costanti(il cui valore non cambia).

Le costanti o valori letterali rappresentano quantità esplicite e come tali non vanno dichiarati. Una generica costante può essere:

• un numero intero(positivo o negativo);

• un numero intero decimale in virgola mobile (in notazione scientifica o in quella standard);

• un carattere singolo;

• una stringa(sequenza di caratteri), che può terminare con:

– un valore nullo;

– un carattere speciale.

Le costanti numeriche intere

Nel linguaggio C una costante intera si può rappresentare in sistemi di numera-zione diversi. In particolare:

• una costante intera decimale è scritta utilizzando una sequenza di numeri decimali (da 0 a 9 senza virgola); ad esempio, 85 è una costante numerica de-cimale;

• una costante intera ottaleè scritta utilizzando una sequenza di numeri ottali (da 0 a 7) preceduta dal carattere 0; ad esempio, 042 rappresenta una costan-te incostan-tera ottale;

• una costante intera esadecimaleè scritta utilizzando una sequenza di simbo-li esadecimasimbo-li (0, 1, 2, …, 9, A, B, C, D, E, F) preceduta dalla coppia di caratte-ri 0x oppure 0X; ad esempio: 0XA, 0X18f sono costanti intere esadecimali. Come si evince dall’ultimo esempio (0X18f), le lettere dalla A alla F possono anche essere scritte in minuscolo.

Le costanti numeriche decimali o in virgola mobile

Una costante numerica decimale o in virgola mobile rappresenta un numero rea-le che può essere scritto in forma decimarea-le oppure mediante la notazione scien-tifica. Nella forma decimale le costanti si compongono di due parti, quella intera e quella decimale, separate dal punto (si utilizza infatti la notazione anglosassone, che prevede il punto al posto della virgola).

I compilatori riconoscono una costante come numerica decimale soltanto se vie-ne rappresentata con la seguente configuraziovie-ne, che riportiamo secondo le rego-le della BNF:

[<Segno>][<ParteIntera>][.<ParteDecimale>][E[<Segno>]<Esponente>] dove la lettera E indica la base 10.

Gli operandi: variabili

e costanti

(9)

Ad esempio, le seguenti costanti sono corrette:

0 // 0 in virgola mobile

480E+4 // 480 * 10000 (10 elevato a 4)

.14e–2 // 0.0014

–3.5e+3 // –3500.0

mentre non sono riconosciute dal compilatore le seguenti costanti:

.58E E25

I caratteri

Le costanti di tipo carattere sono singoli simboli racchiusi tra singoli apici, ad esempio: ‘b’, ‘!’, ‘3’. In C++, come in tutti i linguaggi di programmazione, il numero 8 è diverso dal simbolo ‘8’ che, in-vece, è un carattere:

I caratteri sono generalmente memorizzati come codici ASCII a 8 bit (dipende comunque dall’im-plementazione). È anche possibile definire caratteri estesi di tipo wchar_t (wide character type), memorizzati come codici Unicode a 16 bit.

I caratteri speciali

All’interno delle istruzioni C è possibile inserire caratteri speciali non stampabili. Ad esempio, per vi-sualizzare sul monitor una stringa con un carattere di “a capo” si utilizza una sequenza speciale di ca-ratteri che inizia con una barra rovesciata \, in inglese backslash, definita sequenza di escape: il pri-mo carattere permette di uscire dalla normale rappresentazione, il secondo specifica quale carattere non stampabile si desidera produrre (ovviamente utilizzando un carattere stampabile).

Un esempio di carattere speciale è l’apice (‘), che spesso sostituisce l’apostrofo: in quest’ultimo caso va indicato come carattere speciale facendolo precedere da una barra rovesciata. In generale, quindi, utilizzeremo le sequenze di escape per rappresentare caratteri non stampabili, numeri ot-tali, numeri esadecimali e caratteri che non possono essere scritti direttamente.

La tabella seguente riporta le sequenze di escape definite dal C.

Gli operandi: variabili e costanti

Sequenza di escape Descrizione

\n Nuova riga (new line) \t Tabulazione (tab) \a Segnale sonoro (alert) \f Nuova pagina (form feed)

\r Ritorno a capo della stessa riga (ritorno carrello o carriage return) \v Tabulazione verticale

\b Una battuta indietro (backspace) \\ Barra rovesciata (backslash) \’ Apice singolo

\” Doppi apici \? Punto di domanda \0 Fine stringa \<CifraOttale> Numero ottale \<CifraEsadecimale> Numero esadecimale

Le costanti stringa

Una costante stringa(o stringa letterale) è composta da una sequenza di caratteri terminata con la sequenza di escape \0 e racchiusa all’interno di una coppia di virgolette. Un carattere è un qual-siasi componente di un insieme predefinito di caratteri, o alfabeto. Molti computer impiegano l’in-sieme di caratteri ASCII o Unicode. Ad esempio: “Ciao mondo!”è una costante stringa formata da dodici caratteri (compreso il carattere speciale e lo spazio, che è anch’esso un carattere). Il caratte-re speciale \0 viene inserito automaticamente dal compilatocaratte-re (e come tale può non essecaratte-re speci-ficato esplicitamente dal programmatore) nel momento in cui questo riconosce una costante strin-ga all’interno del programma sorgente.

(10)

I tipi primitivi (built-in)

I tipi rappresentano le categorie di informazioni che il linguaggio consente di ma-nipolare. I tipi di dati elementari gestiti dal linguaggio C dipendono dall’architet-tura del computer, perciò è particolarmente difficile definire la dimensione delle variabili numeriche; noi ci limiteremo a fornire solo definizioni relative. Normalmente, il riferimento è dato dal tipo numerico intero (int) la cui dimensio-ne in bit corrisponde a quella della parola, ovvero dalla capacità dell’unità aritme-tico-logica del microprocessore. A seconda delle varie architetture, la dimensione di un intero normale è di 32 o 64 bit. I valori minimi e massimi consentiti e le-gati al compilatore sono definiti all’interno di 2 file: limits.he float.h.

I tipi di dati primitivi, che riportiamo nella seguente tabella, rappresentano un va-lore numericosingolo:

Tipi di dato primitivi

e dichiarazione

di variabili e costanti

I tipi primitivi possono essere estesi utilizzando appositi qualificatoriquali short,

long, signede unsigned. I primi tre fanno riferimento al numero di byte occupa-ti, mentre gli altri modificano il modo di valutare il contenuto di alcune variabili. Nella seguente tabella riassumiamo i tipi primitivi con le combinazioni ammissi-bili dei qualificatori.

Tipo Descrizione

char Carattere (generalmente di 8 bit; anche il tipo char è trattato come un numero) int Intero normale

float Virgola mobile a precisione singola double Virgola mobile a precisione doppia

Tipo Abbreviazione Descrizione

caratteri char Tipo char per il quale non conta sapere se il segno è considerato o meno

signed char Tipo char usato numericamente con segno

unsigned char Tipo char usato numericamente senza segno

interi short int short Intero più breve di int, con segno. Ad esempio, se il tipo int occupa 4 byte, signed short int signed sort il tipo short int ne occuperà 2

unsigned short int unsigned short Tipo short senza segno

int Intero normale, con segno

signed int

unsigned int unsigned Tipo int senza segno

long int long Intero più lungo di int, con segno. Ad esempio, se il tipo int occupa 4 byte, signed long int signed long il tipo long int ne occuperà 8

unsigned long int unsigned long Tipo long senza segno

reali float Tipo a virgola mobile a precisione singola

double Tipo a virgola mobile a precisione doppia

(11)

Fra i tipi primitivi non abbiamo menzionato il tipobooleano. Come sappiamo, l’insieme del tipo booleano è co-stituito da due valori di verità:true (vero) efalse(falso). Nel linguaggio C non esiste un tipo per rappresenta-re questi due valori booleani; si rimedia simulando variabili booleane con variabili interappresenta-re: in un’esprappresenta-ressione lo-gica, infatti, un valore uguale a 0 equivale al valore false mentre un valore diverso da 0 equivale a true. Verso la fine degli anni ’90 lo standard del linguaggio C venne sottoposto a una revisione che portò alla pubbli-cazione ISO 9899/1999 nel 1999. Questo standard, adottato dell’ANSI nel marzo 2000 e indicato con la sigla

C99, prevede tra l’altro le seguenti nuove caratteristiche:

• le dichiarazioni delle variabili possono essere collocate ovunque (come in C++); prima, erano consentite una di seguito all’altra e all’inizio dell’enunciato di composizione;

• nuovi tipi di dato inclusi bool e complex;

• lunghezza variabile degli array;

• introduzione del commento di una linea con // (preso dal C++);

• nuove librerie di funzioni come snprintf();

• nuovi header file come stdint.h.

Dichiarazione di variabili e costanti

Una variabile è un oggetto a cui è associato:

• un nome (identificatore);

• un tipo (insieme dei valori che può assumere);

• un’area di memoria (per contenere il valore assunto);

• un indirizzo (riferimento all’area di memoria);

• un valore (in istanti diversi la variabile può assumere valori differenti, purché siano tutti coerenti con il tipo). La dichiarazione di una variabile in C avviene in base alla seguente sintassi:

[<Qualificatore>] <Tipo> {, <Identificatore> [=<ValoreIniziale>]}; Ad esempio:

int b;

char carattere, lettera;

unsigned int tappo = 10;

char frase[20];

sono dichiarazioni di variabili valide in C. In dettaglio, con la prima viene dichiarata la variabile intera b, con la se-conda vengono dichiarate le variabili carattere e lettera, entrambe di tipo carattere, con la terza viene dichiarata la variabile tappo di tipo intero senza segno, alla quale è assegnato il valore iniziale 10, e infine con la quarta viene dichiarata una variabile stringa di nome frase che potrà contenere un testo lungo al massimo 20 caratteri.

Non si deve mai dimenticare che il C è case sensitive, cioè distingue le lettere maiuscole da quelle minuscole. Notiamo, infine, il punto e virgola che segue sempre ogni dichiarazione.

Tipi di dato primitivi e dichiarazione

di variabili e costanti

TIPO BYTE OCCUPATI RANGE

char 1 da –128 a 127 unsigned char 1 da 0 a 255 short int 2 da –32768 a +32767 int 4 da –21474863648 a +21474863647 long int 8 da 10–309a 10+308 usigned short 2 da 0 a 65535 unsigned int 4 da 0 a 4294967295 unsigned long 8 notazione IEEE standard float 32 notazione IEEE standard double 64 notazione IEEE standard long double 128 notazione IEEE standard

(12)

Un operatoreè un simbolo che “opera” su una o più espressioni, producendo un valore che può essere assegnato a una variabile.

Gli operatori in programmazione permettono di ottenere un determinato valore da un’operazione che si compie su una o più variabili all’interno del programma; così come l’operatore + serve per sommare due numeri in matematica, analoga-mente serve per compiere la stessa operazione in un programma scritto in C. Ovviamente ci sono delle differenze: innanzitutto le operazioni del C sono quelle basilari (per funzioni più avanzate dobbiamo usare librerie apposite) e hanno un risultato finito, contrariamente a quelle matematiche che possono avere un risul-tato simbolico o infinito.

Tutti gli operatori producono sempre un risultato e alcuni possono anche modifi-care uno degli operandi. Alcuni operatori sono rappresentati simbolicamente con un singolo carattere, altri con due caratteri senza spazi separatori. Se non è espli-citamente evidenziato altrimenti, tutti gli operatori operano su tipi primitivi. Gli operatori che andremo ad analizzare si dividono in:

• operatori di assegnamentoche consentono di assegnare un valore alle varia-bili;

• operatori aritmeticiche comprendono somma, sottrazione, moltiplicazione, divisione intera e divisione con modulo;

• operatori di confronto che permettono di verificare determinate condizioni, come ad esempio l’uguaglianza o la disuguaglianza;

• operatori logicida utilizzare con le istruzioni condizionali e iterative;

• operatori sui bit;

• operatori speciali.

L’operatore di assegnamento

Sappiamo che il C è un linguaggio profondamente basato sul paradigma impera-tivo e questo significa che un programma C è fondamentalmente una sequenza di assegnamenti di valori a variabili.

L’operatore di assegnamento è denotato dal simbolo = (segno di uguale) e viene applicato con la sintassi:

<Lvalue> = <Rvalue>;

Il termine <Lvalue> indica una qualsiasi espressione che si riferisca a una regio-ne di memoria (in geregio-nerale un identificatore di variabile), mentre il termiregio-ne <Rvalue> indica una qualsiasi espressione la cui valutazione produca un valore. Il risultato dell’assegnamento è, pertanto, il valore prodotto dalla valutazione del-la parte destra (<Rvalue>) e ha come effetto coldel-laterale l’assegnazione di tale va-lore alla regione di memoria denotato dalla parte sinistra (<Lvalue>). Ecco alcu-ni esempi: a = 5; orazio = ‘a’; pluto = orazio; a = a + 7; b = 4 + 25;

Come si evince dal precedente esempio, l’uso del segno di uguale in questo caso non ha niente a che vedere con la matematica, poiché serve ad assegna-re un valoassegna-re e non a effettuaassegna-re confronti; per questi ultimi in C si usa l’opera-tore == (attenzione a non inserire spazi tra i due caratteri =).

L’operatore

(13)

Una variabile può apparire sia a destra che a sinistra di un assegnamento: se si trova a destra produce il valore contenuto nella variabile, se invece si trova a sini-stra denota la locazione di memoria a cui riferisce. Poiché un identificatore di va-riabile può trovarsi contemporaneamente su entrambi i lati di un assegnamento, è necessario disporre di una semantica non ambigua: come in qualsiasi linguag-gio imperativo (Pascal, Basic e così via), la semantica dell’assegnamento impone che prima si valuti la parte destra e poi si esegua l’assegnamento del valore pro-dotto all’operando di sinistra.

Ad esempio, supponiamo di avere la variabile A con valore 6; con l’istruzione:

x = 3 * a;

alla variabile x viene assegnato il risultato di 3*a, cioè 18, quindi il risultato del-l’operazione di assegnamento (x = 3*a) è proprio 18.

Esaminiamo ora un particolare. L’assegnazione stessa è un’espressione con un valore (il valore dell’espressione x = 3*a è 18), che può venire usato in un’altra assegnazione. Ad esempio, l’istruzione:

y = (x = 3*a);

contiene un’assegnazione multipla: dapprima viene assegnato il valore 3*a alla variabile x e poi si assegna il risultato ottenuto alla variabile y. Con l’istruzione:

y = x;

il valore di x viene “copiato” in y: le due variabili rimangono completamente in-dipendenti, cioè la modifica di una delle due non influenza l’altra. Ciò è vero solo se x e y sono variabili di tipo primitivo. Il comportamento di questo operatore nel caso di operandi non primitivi verrà esaminato più avanti.

Occorre prestare attenzione: un’assegnazione multipla non può essere utilizzata come una inizializzazione in una dichiarazione. Ad esempio, l’istruzione:

int k = h = 33;

è errata perché le inizializzazioni non sono assegnazioni; sono simili, ma il com-pilatore le tratta in maniera differente. La maniera corretta di procedere in questo caso è:

int k = 33, h = 33;

L’assegnazione di valori a una variabile segue specifiche regole in funzione del tipo di dato associato alla variabile in questione. In tal senso occorre tener presen-te che:

le variabili intere (int) si assegnano utilizzando un numero senza parte decimale;

le variabili reali (float, double) si assegnano utilizzando per la virgola la notazio-ne inglese, cioè quella in cuisi usa un punto (e non una virgola) per dividere la parte intera da quelle frazionaria;

le variabili carattere (char) si assegnanoracchiudendo il carattere tra due apici;

• le variabili stringa (una stringa è una concatenazione di caratteri) si assegnano

racchiudendo la stringa tra virgolette.

Riportiamo un esempio riassuntivo di ognuna delle precedenti assegnazioni:

int b =20;

char carattere = ‘b’, lettera = ‘m’;

float tappo = 10.2;

char frase[20] = “Ciao a tutti”;

(14)

Gli operatori aritmetici

Gli operatori aritmetici si dividono in:

1. operatori unari, che possono essere postfissi e prefissi:

<Operando> <Operatore> (postfissi)

<Operatore> <Operando> (prefissi)

2. operatori binari, che hanno la forma:

<Operando1> <Operatore> <Operando2>

Gli operatori unari aritmetici si applicano a un solo operando e ne modificano il valore. Si distinguono i seguenti:

• incremento: ++ Ad esempio: p++; /* equivale a p = p+1 */ • Decremento: –– Ad esempio: p–– /* equivale a p = p–1 */

Gli operatori unari aritmetici possono essere posti prima dell’operando (prefissi) o dopo l’operando (postfissi) e il loro valore varia secondo questa posizione: l’opera-tore prefisso modifica l’operando prima di utilizzarne il valore, mentre l’operal’opera-tore postfisso modifica l’operando dopo averne utilizzato il valore. Ad esempio, con:

x = 10;

y = x++; /* postfisso: si assegna prima il valore della variabile x

 alla variabile y e poi si incrementa la variabile x

 In altri termini equivale a:

 y = x;

 x++ */ si ottiene y = 10 e x = 11. Invece con:

x = 10;

y = ++x; /* prefisso: prima si incrementa la variabile X e poi

 si assegna il valore alla variabile Y. In altri termini

 equivale a:

 x++;

 y = x; */ si ottiene y = 11 e x = 11.

Gli operatori binari aritmeticisi applicano a due operandi e non ne cambiano il valore, ma memorizzano il risultato. In C si utilizzano gli operatori binari riporta-ti nella tabella all’inizio di pagina seguente.

Operatori aritmetici,

di confronto e logici

(15)

L’operatore di assegnamento può anche essere compattato, cioè abbinato a un operatore aritmetico. In genere le espressioni del tipo:

<NomeVariabile> = <NomeVariabile> <Operatore> <Espressione> possono essere scritte nella seguente forma abbreviata:

<NomeVariabile> <Operatore> = <Espressione>

Operatori aritmetici, di confronto e logici

X Y X && Y X || Y !X

false false false false true false true false true true true false false true false true true true true false Operatore Simbolo Risultato

Addizione + Addiziona due operandi o concatena due stringhe Sottrazione – Sottrae il secondo operando dal primo

Moltiplicazione * Moltiplica i due operandi

Divisione / Divide il primo operando per il secondo. L’operatore di divisione, applicato a variabili di tipo intero produce un risultato troncato della parte decimale

Resto (modulo) % Fornisce il resto della divisione tra due operandi interi. Non è possibile utilizzare l’operatore di calcolo del modulo quando gli operandi sono in virgola mobile. Se si divide il modulo per zero, si avrà un’eccezione: se l’operazione eccede il limite inferiore (underflow) il risultato sarà zero, se eccede il limite superiore (overflow) si avrà un’approssimazione

Scrittura compatta Scrittura equivalente Esempio

x += y x = x + y totale += 52; /* equivale a totale = totale + 52 */ x –= y x = x – y totale –= 40; /* equivale a totale = totale – 40 */ x *= y x = x * y totale *= 10; /* equivale a totale = totale * 10 */ x /= y x = x / y totale /= 2; /* equivale a totale = totale / 2 */ x %= y x = x % y totale %= 2; /* equivale a totale = totale % 2 */

Gli operatori di confronto (relazionali)

Gli operatori di confronto richiedono operandi di tipo primitivo e producono sem-pre un risultato booleano. Sono i seguenti:

> maggiore >= maggiore o uguale == uguale < minore <= minore o uguale != diverso

Gli operatori di confronto restituiscono il valore logico true se la relazione è veri-ficata, altrimenti restituiscono il valore false.

Gli operatori logici

Gli operatori logici richiedono come operandi delle espressioni booleane e produ-cono un risultato booleano. Sono i seguenti:

|| OR && AND ! NOT

Gli operatori && e || non valutano l’operando destro se non è necessario al risul-tato, quindi il risultato dell’operatore è indipendente dal valore dell’operando de-stro. Ad esempio:

(3>1) || (x>7) non viene valutato

(16)

Le direttive

al preprocessore

Siamo pronti per realizzare il primo programma in C. Come è tradizione nei ma-nuali di linguaggi di programmazione, ne realizziamo uno che visualizza sul video la frase Ciao mondo!:

#include <stdio.h>

main( )

{

 /* Questo programma scrive la frase Ciao mondo! sul video */

 printf(“Ciao mondo!”);

 system(“PAUSE”);

}

Il programma, dopo essere stato digitato, compilato ed eseguito, visualizza sul vi-deo quanto segue:

Diciamo subito che l’istruzione:

system (“PAUSE”);

fa restare il computer “in attesa” (è proprio questa istruzione che fa apparire la frase Premere un tasto per continuare). In altri termini, permette di mantenere il programma aperto fino a quando l’utente non preme un tasto qualsiasi per chiu-derlo. Consente, pertanto, la visualizzazione del risultato evitando la chiusura im-mediata della finestra al termine dell’esecuzione.

L’istruzione system (“PAUSE”); funziona in ambiente Windows. Alcuni compila-tori C richiedono l’inclusione della libreria stdlib.h per poterla utilizzare. stdlib.h è il file header che dichiara funzioni e costanti di utilità generale: allo-cazione della memoria, controllo dei processi, conversione tra tipi e così via.

Il C è un linguaggio compilato. Come sappiamo, i compilatori sono programmi che accettano in ingresso un programma scritto in linguaggio di alto livello (detto

programma sorgente) e lo traducono interamente in un programma in linguag-gio macchina (detto programma oggetto): a traduzione ultimata, il programma potrà essere eseguito. Lo scopo di questo processo è quindi di tradurre il program-ma originale (codice sorgente) in uno seprogram-manticamente equivalente, program-ma eseguibi-le su una determinata macchina.

A differenza di altri linguaggi, la compilazione in C prevede una prima fase di preelaborazione, detta precompilazione, svolta da un apposito modulo denomi-nato preprocessore.

Il processo di creazione e compilazione di un progetto C dipende strettamente dal tipo di computer e di sistema operativo che si utilizza. Possiamo compila-re ed eseguicompila-re programmi con il C in qualsiasi piattaforma, ma per ognuna di essa (Linux, Windows e così via) occorrerà scegliere un opportuno ambiente di sviluppo. Per poter utilizzare sia il linguaggio C sia il linguaggio C++ si può, tra l’altro, usare l’ambiente di sviluppo integrato (IDE – Integrated Development

Environment) Visual C++ (che fa parte della suite Microsoft Visual Studio)

op-pure Dev-c++, liberamente scaricabile da Internet, o, ancora, utilizzare l’am-biente testuale DOS utilizzando appositi comandi per compilare, linkare ed eseguire i programmi digitandoli nella finestra del “Prompt dei comandi”.

(17)

Le direttive al preprocessore

Il preprocessoreè un programma richiamato automaticamente durante la pri-ma fase della compilazione, che esamina ed effettua un’elaborazione prelimi-nare del sorgente. Il preprocessore (o precompilatore), seguendo alcune diretti-ve scritte dal programmatore, modifica il file sorgente effettuando sostituzioni di alcune parti di codice (preprocessing). Il file sorgente così modificato viene successivamente elaborato dal compilatore vero e proprio.

Lo scopo del preprocessore è quello di rendere un programma sorgente più sem-plice da modificare e da adattare a diversi ambienti operativi. Normalmente il preprocessore è incluso nel compilatore del linguaggio, oppure è richiamato in modo assolutamente trasparente al programmatore. In fase di preelaborazione non viene eseguita alcuna azione di traduzione del codice, viene soltanto effettua-ta una preparazione del programma sorgente per la successiva compilazione. Le istruzioni di dichiarazione per il preprocessore vengono chiamate dichiarative di precompilazioneo direttive al preprocessoree iniziano con il carattere # se-guito dalla parola chiave (if, elif, else, endif, define, undef, ifdef, ifndef, include, line,

error, pragma) e dalla dichiarazione della direttiva. Le direttive al preprocessore non terminano con il punto e virgola, vengono solitamente inserite in testa al programma (ma possono trovarsi anche altrove, tra due funzioni, all’interno degli altri sottoprogrammi o del main) e vengono valutate in fase di compilazione, dove vengono sostituite con codice C appropriato.

Il simbolo # viene chiamato “pound” negli Stati Uniti, “hash” al di fuori degli Stati Uniti e “cancelletto” in Italia. Verosimilmente il termine hash deriva da “hatch” ossia “portellone”, “cancello”, porta composta da sbarre incrociate così raffigura il simbolo stesso.

Per ora concentriamo l’attenzione sulla direttiva #include; delle altre ci occupere-mo dettagliatamente più avanti.

La direttiva #includeconsente di inserire nel file sorgente i file header, ossia spe-cifici file di intestazione con estensione ‘.h’ (da header) che contengono infor-mazioni necessarie al programma. In particolare, essi contengono le definizio-ni delle variabili, delle espressiodefinizio-ni costanti e dei prototipi delle funziodefinizio-ni (ossia il nome, i parametri e il valore restituito dalla funzione). Questi file costituisco-no le librerie standarddel linguaggio.

Ad esempio, nel file header stdio.h(STandarD Input/Output) sono contenute le de-finizioni delle variabili, delle espressioni costanti e i prototipi delle funzioni di in-put e di outin-put della libreria standard del C. Se il file stdio.h non venisse incluso all’interno del programma, il compilatore non riconoscerebbe le funzioni stan-dard e genererebbe un errore di funzione non definita. Il preprocessore, leggen-do la direttiva #include, copia (o meglio “include”) il file dichiarato all’interno del sorgente del programma. Una volta elaborato dal preprocessore quindi, il file sor-gente conterrà al posto della riga #include il file stdio.h stesso (perciò aumenterà di dimensioni). Il file così modificato verrà poi elaborato dal compilatore. La diret-tiva va racchiusa all’interno di parentesi angolari o di doppi apici. Ad esempio,:

#include <stdio.h>

#include “stdio.h”

indicano al compilatore di includere e compilare insieme al programma il file

stdio.h che contiene i prototipi delle funzioni standard di input/output (I/O) del C,

richiamabili poi da un qualsiasi punto del programma. Se si racchiude la direttiva tra parentesi angolari (< >), il file verrà cercato nelle apposite cartelle definite dal compilatore, mentre se la si racchiude tra i doppi apici (“ “), il file verrà dap-prima ricercato nella cartella corrente. Quest’ultima soluzione viene adottata quando si scrivono funzioni appositamente sviluppate per realizzare un program-ma e le si vuole racchiudere tutte in un apposito file.

(18)

Dichiarazione delle costanti

In precedenza abbiamo esaminato la direttiva #include che ci ha permesso di “in-cludere” nel file sorgente i file header specificati.

Esistono altre direttive di precompilazione: una di esse ci sarà molto utile per di-chiarare le costanti. In precedenza abbiamo visto che esistono globalmente due tipi di costanti:

• esplicite: esprimono direttamente dei valori:

– 24 costante di tipo int

– 24L costante di tipo long

– 3.145 costante di tipo double

– ‘A’ costante di tipo char

• simboliche: sono rappresentate da nomi simbolici che il programmatore adot-ta per indicare valori prefissati. Queste cosadot-tanti hanno un tipo espresso impli-citamente nel valore e devono essere dichiarate in precedenza.

Dichiarare una costante significa associare un simbolo a un valore. A differenza di quanto avviene per le variabili, tale valore non cambierà mai durante l’esecuzio-ne del programma. In C esistono due modi per dichiarare una costante:

• attraverso la parola chiave const;

• attraverso la direttiva #define.

Con il qualificatore constè possibile definire costanti tipizzate, ossia che accetta-no un valore ammissibile per il tipo di dato al quale è associato. La sintassi è la se-guente:

const {, <Tipo> <NOMECOSTANTE> = <ValoreIniziale>};

Ad esempio:

const float PGRECO = 3.14;

const char C = ‘a’;

const int POSTI = 100;

const char[20] ERRORE = “Rilevato errore!”;

Le costanti simboliche di questo tipo sono sostanzialmente delle variabili per le quali il compilatore non concede la modifica. Per una questione di stile, utilizze-remo nomi interamente in maiuscolo per riferirci alle costanti.

Se si definisce una costante tramite const, il compilatore farà sì che in fase di ese-cuzione vi sia una locazione di memoria relativa alla costante, in cui sarà presen-te il valore associato alla spresen-tessa.

La definizione di costante tramite la direttiva #define ha la seguente forma sintat-tica:

#define <Identificatore> <TestoSostitutivo>

Dopo la direttiva #define si indicherà l’identificatore della costante, che prende il nome di costante palese, e il relativo valore. Come tutte le direttive al precompi-latore, anche #define non deve terminare con il punto e virgola. Riprendiamo le precedenti dichiarazioni fatte con il qualificatore const e riproponiamole con la di-rettiva #define:

#define PGRECO 3.14

#define C ‘a’

#define POSTI 100

#define ERRORE “Rilevato errore”

Costanti e tipi di dato

definiti dall’utente

(19)

Se si definisce una costante tramite la direttiva #define, il compilatore sostituirà a tutte le occorrenze dell’identificatore il valore associato; in questo modo non vi sarà alcuna locazione di memoria associata alla costante e il valore sarà inserito direttamente all’interno del codice.

La direttiva #define può essere utilizzata anche per scopi che esulano dalla dichia-razione di costanti il cui valore sarà oggetto di calcolo all’interno del programma. Prendiamo in esame le due seguenti dichiarazioni:

#define BEGIN {

#define END }

Grazie a queste direttive, all’interno del programma sarà possibile evitare di inse-rire le parentesi graffe e digitare, al loro posto, le parole dichiarate, quindi begin per la parentesi graffa aperta ed end per la parentesi graffa chiusa: sarà il prepro-cessore, poi, a sostituire i valori specificati. Nel seguito amplieremo la conoscen-za di questa direttiva introducendo le macro.

Tipi definiti dall’utente

Il linguaggio C offre la possibilità di assegnare un alias(soprannome) a un qual-siasi tipo primitivo oppure derivato (ossia quelli introdotti dall’utente e derivati dai tipi fondamentali, che esamineremo più avanti), il che contribuisce a rendere il codice più portabile, più leggibile e a evitare espressioni altrimenti complesse. Tale assegnazione si effettua utilizzando la clausola typedef. Con essa non si de-finisce un nuovo tipo, ma si introduce un nome che corrisponde a un tipo defini-to. La sintassi è la seguente:

typedef <NomeTipo> <NuovoNomeTipo>;

A titolo di esempio consideriamo la seguente dichiarazione:

typedef float tiporeale;

tiporeale variabile;

che rinomina il tipo primitivo float con l’alias tiporeale.

Questa clausola può essere molto utile in determinate situazioni. Prendiamo in esame l’assenza del tipo booleano. Servendoci di typedef possiamo ovviare all’in-conveniente. Ad esempio, con la dichiarazione:

typedef int boolean;

è possibile dichiarare variabili di tipo boolean che possono assumere tutti i valori interi, poiché sappiamo che la regola base è che i valori diversi da zero rappresen-tano il vero, mentre il valore zero rappresenta il falso:

int a, b;

boolean finito;

Considerato che in moltissimi programmi vi è l’esigenza di evidenziare che una variabile è utilizzata esclusivamente per memorizzare un valore booleano, si pos-sono premettere definizioni analoghe alle seguenti che ci permettono di definire proprio le costanti TRUE e FALSE:

#define TRUE 1

#define FALSE 0

typedef int BOOL;

(20)

Il programma principale

Dopo le direttive al precompilatore, il nostro programma che scrive sul video la frase Ciao mondo! presenta il suo programma principale caratterizzato dalla paro-la chiave main.

Il main deve essere sempre presente poiché costituisce il punto di ingresso di un programma C: l’esecuzione di un programma, infatti, inizia dalla prima istruzio-ne del main e termina con l’ultima. Eventuali altri sottoprogrammi (ossia altre funzioni) entreranno in gioco solo se e quando richiamate (direttamente o indiret-tamente) dal main. In assenza di una funzione main, il compilatore non può pro-durre un programma eseguibile.

La struttura generale del main è la seguente:

main ( )

{

}

che può anche essere scritta come segue:

main ( ) { } oppure: main( ) { } o, ancora: main( ) {  }

Questi vari modi di scrivere il main possono sembrare strani (e in effetti lo sono) ma sono corretti e comprensi dal compilatore. In sostanza non è importante la

po-sizione dei simboli, ma è importante che ogni parentesi aperta venga poi chiusa.

Questi due programmi non producono alcun risultato in quanto non contengono istruzioni: sono programmi vuoti; possono produrre un errore di tipo warningda parte del compilatore, ossia degli avvertimentiche non bloccano la creazione del-l’eseguibile, si limitano ad avvertire il programmatore della possibilità che venga prodotto un programma eseguibile errato e malfunzionante. Generalmente i mes-saggi di tipo warning possono anche essere ignorati.

Per inserire le parentesi graffe utilizzando una tastiera che non riporta esplici-tamente il tasto, si può usare una tra le seguenti combinazioni di tasti: { Alt+123 (tastierino numerico) Ctrl+Alt+Shift+[ Shift+AltGr+[ } Alt+125 (tastierino numerico) Ctrl+Alt+Shift+] Shift+AltGr+]

All’interno di un programma in linguaggio C compaiono statement (istruzioni), ossia righe che rappresentano dichiarazioni di oggetti e risorse che saranno usa-te nel programma, e righe che racchiudono le istruzioni del programma che go-vernano l’elaborazione.

(21)

Gli statement vengono costruiti utilizzando l’alfabeto, le parole chiave e le regole lessicali del linguaggio.

I commenti

Riprendiamo il nostro primo programma realizzato in precedenza. Subito dopo le direttive al precompilatore è presente la frase Questo programma scrive la frase

Ciao mondo sul video: è un commento.

In un programma C, come in qualsiasi altro linguaggio, i commenti hanno valore soltanto per il programmatore e vengono ignorati dal compilatore. Nel linguaggio C è possibile inserire i commenti in due modi diversi.

1. Secondo il puro stile C, ovvero racchiudendoli tra i simboli /* e */, ad esempio:

/* Questo è un commento in stile C */

Questo metodo, detto commento a blocchi, permette di commentare più li-nee di codice senza dover ripetere a ogni linea i simboli di commento.

2. Secondo lo stile proprio del C++ (se il compilatore è conforme a standard più recenti, come il C99), ovvero facendoli precedere dal simbolo //. In questo modo il commento si estende solo fino al termine della linea. Ad esempio:

// Questo è un commento in stile C++

Nel primo caso è considerato commento tutto ciò che è racchiuso tra /* e */; il commento, quindi, si può anche trovare in mezzo al codice. Analizziamo il se-guente esempio:

main( )

{

 int x = 10; /* Questo è un commento diviso

 su tre righe.

 Questa è l’ultima riga di commento */

 x = /* Questo commento è valido */ = 5;

}

Nel secondo caso, proprio del C++, è invece considerato commento tutto ciò che segue // sino alla fine della linea. Ne consegue che non è possibile inserire un commento di questo tipo in mezzo al codice, o dividerlo su più righe (a meno che anche l’altra riga non cominci con //). Vediamo un esempio:

main( )

{

 int x = 10; // Questo è un commento valido

 x = 100; // Attenzione! La riga successiva produce un errore

 Questa riga non è un commento: non è preceduta da //

}

Diversi ambienti di sviluppo evidenziano i commenti in uno specifico colore per permettere al programmatore di distinguere meglio tra il codice e i commenti stessi.

(22)

La gestione

dell’output

Invece della variabile a è anche possibile scrivere un’espressione, ad esempio:

int a = 20;

printf(“Il valore della variabile a e’: %d”, a+2);

che scriverà il valore della variabile a aumentato di due unità. Il valore della varia-bile resta però invariato, in quanto non è stato riassegnato.

Riprendiamo il programma di esempio con il quale abbiamo visualizzato sul vi-deo la frase Ciao mondo!. All’interno del main del nostro programma è presente la funzione printf, che ci permette di controllare ciò che viene stampato, nel sen-so che permette di decidere che cosa stampare e in quale forma. Il funzionamen-to più semplice di questa funzione è il seguente: la printf scorre la stringa di for-mato racchiusa tra virgolette da sinistra verso destra e scrive a video (il dispositivo di uscita standard, stdout) tutti i caratteri che incontra. È proprio il caso del no-stro programma in cui è presente l’istruzione

printf(“Ciao mondo!”);

che scrive sul video la frase racchiusa tra virgolette.

Tuttavia la funzione printf, oltre a stampare semplici stringhe di testo, può anche essere utilizzata per stampare il contenuto di variabili. In questo caso, la funzione

printf riceve sempre come primo argomento tra le parentesi tonde una stringa di

formato (cioè una sequenza di caratteri delimitata dalle virgolette), ma dopo di essa si dovrà inserire un numero qualsiasi di altri argomenti, in base alla necessi-tà. La forma sintattica più generale è, quindi, la seguente:

printf(<StringaDiFormato> , <Espressione>)

All’interno della <StringaDiFormato> dovranno essere presenti appositi segna-posto per ciascuna espressione successivamente elencata, in modo da specifica-re in quale punto della stringa deve essespecifica-re posizionato il valospecifica-re dell’espspecifica-ressione e di che tipo di valore si tratta.

Tali segnaposto prendono il nome di specificatori di formatoe sono introdotti dal simbolo %. Quando si è in presenza di specificatori di formato, il funzionamento è il seguente: la printf scorre la stringa di formato da sinistra verso destra e scrive a video tutti i caratteri che incontra; quando trova un carattere %, identifica il tipo di dato che deve essere stampato mediante il carattere (o i caratteri) che lo seguo-no. L’elemento % indica che in quel punto è necessario stampare il valore di un’espressione che è di un tipo ben preciso. La funzione printf utilizza tale infor-mazione per convertire e formattare il valore ottenuto dall’espressione che segue la stringa di formato. Una volta valutato, formattato e scritto a video il valore del-l’espressione, la funzione continua nell’analisi della stringa di formato, fino al prossimo % o alla fine della stringa. Facciamo alcuni esempi:

int a = 20; /* dichiarazione ed inizializzazione */

printf(“Il valore della variabile a e’: %d”, a);

%d specifica che si deve valutare l’espressione che segue la stringa come un

nu-mero intero decimale. Il risultato a video sarà la scritta:

Stringa di formato Specificatore di formato

Espressione che segue la stringa

(23)

Il seguente esempio evidenzia, invece, come inserire più specificatori di formato all’interno del-la stringa. L’importante è inserirli nel punto in cui occorra scrivere il valore di un’espressione:

int a = 10;

b = 20;

printf(“Il valore di a e’ %d e il valore di b e’ %d ”, a, b); Il risultato sarà:

La gestione dell’output

Le specifiche introdotte dal simbolo % non sono soltanto identificatori di formato, ma an-che specificatori di conversione: indicano, infatti, il tipo di valore risultante dall’espressio-ne e come tale tipo di dato deve essere convertito in caratteri da visualizzare sullo schermo. Riprendiamo gli esempi appena visti. Se per un qualsiasi motivo l’espressione che segue la stringa ha un valore reale (ad esempio, una variabile dichiarata di tipo float) e si utilizza lo specificatore %d, verrà comunque stampato qualcosa, che però non corrisponderà al valo-re esatto. La ragione è che un int utilizza la metà dello spazio occupato da un float. Per tale motivo, verrà visualizzato solamente il contenuto dei primi due byte, che verranno interpre-tati come la rappresentazione in complemento a due di un numero intero con segno. Tutto ciò è molto lontano dal corrispondere anche solo alla parte intera del numero reale, rappre-sentato in complemento a due ma notazione a virgola mobile.

I due aspetti importanti da ricordare sono quindi i seguenti:

1. l’identificatore che segue il % specifica il tipo di variabile che deve essere visualizzato e il formato dell’espressione che segue;

2. nel caso in cui vi sia una differenza tra l’identificatore indicato e il valore calcolato del-l’espressione, il dato visualizzato non è necessariamente corretto e può causare errori an-che sugli altri elementi della printf.

Alla luce di quanto detto, esaminiamo un programma completo in C:

#include <stdio.h>

int a = 20;

main()

{

 printf(“Il valore della variabile a è: %d”, a+2);

 system(“PAUSE”);

}

Compilando ed eseguendo il programma otterremo il risultato riportato nella figura, che è corretto ma presenta due inconvenienti:

1. la è accentata non viene visualizzata correttamente;

2. dopo aver visualizzato il valore della variabile viene visualizzato il messaggio Premere un

tasto per continuare… sulla stessa riga rendendo il tutto non proprio leggibile.

Per la gestione dell’output si utilizzano anche le funzioni putchar()e puts()che visualizzano a video, rispettiva-mente, un carattere e una stringa. Queste funzioni non utilizzano gli specificatori di formato. Un esempio del loro utilizzo è il seguente:

 char a = ‘y’;

 char nome[10] = “piero”;

 putchar(a);

 puts(nome);

Se si desidera stampare sulla carta anziché sul video, occorre utilizzare l’istruzione fprintf che funziona con le stesse modalità di printf. È necessario soltanto specificare il nome della stampante,stdprn(stampante stan-dard o predefinita), per indicare la ridirezione dal video alla stampante (cioè da stdout alla stampante). Ad esempio, la seguente istruzione invia l’output alla stampante:

(24)

Gli specificatori di formato

In precedenza abbiamo esaminato gli specificatori %d e %f per visualizzare ri-spettivamente il risultato di espressioni intere e reali. Gli specificatori di formato previsti dall’ANSI C sono i seguenti:

Gestione dell’output

e sequenze di escape

Tra il simbolo % e lo specificatore di formato è anche possibile inserire altre op-zioni di formato:

• un numero intero specifica l’ampiezza del campo, ossia imposta il numero di posizioni nelle quali il valore deve essere visualizzato;

• uno zero (0) indica che le posizioni a sinistra, occupate dal numero da visua-lizzare, devono essere riempite con zeri;

• il segno meno (–) è utilizzato per eseguire la giustificazione a sinistra del dato;

• due numeri separati dal punto servono per indicare rispettivamente l’ampiez-za del campo e il numero di cifre decimali.

Consideriamo le seguenti dichiarazioni di variabili e analizziamo le nuove opzioni:

int a = 2;

char x = ‘s’; /* l’assegnazione di un carattere a una variabile char avviene

 racchiudendo il carattere tra due singoli apici */

float y = 3.14; /* la notazione usata per rappresentare la virgola è quella

 inglese, cioè quella in cui si usa un punto (e non una

 virgola) per dividere la parte intera da quella frazionaria */

char nome[10] = “Piero”; /* per memorizzare le stringhe (che tratteremo

 detttagliatamente in seguito, si usa il tipo char

 indicando il nome della variabile seguito dal

 numero massimo di caratteri previsto per la

 stringa, racchiuso tra parentesi quadre e il

 deve essere racchiuso tra virgolette*/

Specificatore di formato Espressione A video

%c char carattere singolo

%d (%i) int intero decimale con segno %e (%E) float o double formato esponenziale %f float o double reale con segno

%g (%G) float o double utilizza %f o %e in base alle esigenze %o int valore in base 8 senza segno %p pointer valore di una variabile puntatore %s array of char stringa (sequenza) di caratteri %u int intero senza segno

%x (%X) int valore in base 16 senza segno

Istruzione Output printf(“%c”, x); s printf(“%d”, a); 2 printf(“%20d”, a); 2 printf((%020d”, a); 00000000000000000002 printf(“%f”, y); 3.140000 printf(“%20.2f”, y); 3.14 printf(“%20d %10.2f”, a, y); 2 3.14 printf(“%-20d %-10.2f”, a, y); 2 3.14

printf(“%s”, nome); Piero printf(“%10s”, nome); Piero printf(“%-10s”, nome); Piero

(25)

Poiché il simbolo % è utilizzato per introdurre uno specificatore di formato, per visualizzare questo simbolo (ad esempio, per visualizzare una percentuale) oc-corre riportarlo due volte. Ad esempio, per visualizzare a video la frase Il tasso

di sconto è del 10% scriveremo:

 printf(“Il tasso di sconto e\’ del 10%%”);

Le sequenze di escape

In precedenza abbiamo visto che alcuni caratteri non sono stati visualizzati cor-rettamente a video (ad esempio la è accentata) e che non abbiamo avuto modo di scrivere frasi su righe diverse. A risolvere tali problemi pensano alcuni codici di controllo detti tecnicamente sequenze di escape, che non stampano caratteri vi-sibili ma contribuiscono a formattare ciò che viene stampato. Le sequenze di escape iniziano con il carattere backslash (\) e sono interpretate come un singolo carattere:

Presentiamo subito degli esempi. Il seguente programma:

#include <stdio.h> main() {  printf(“prima riga ...\n”);  printf(“seconda riga ...\n”);  system(“PAUSE”); }

Visualizza quanto è riportato nella figura. Vediamo un altro esempio:

#include <stdio.h>

int a = 0;

main()

{

 printf(“%a”); /* emette un beep */

 printf(“Il valore di a e\’%d\n”, a);

 printf(“E l\’altro valore qual e\’\? %d\n”, a+10);

 system(“PAUSE”);

}

Gestione dell’output e sequenze di escape

Sequenza di escape Significato Descrizione

\b backspace Cancella (una battuta indietro) \f Form feed avanzamento carta

\n New line nuova linea

\r Carriage return a capo (senza una nuova linea) \t Tab tabulatore

\’ Single quote Apice \” Double quote Doppi apici \0 End of string Fine stringa \\ Backslash Barra contraria \? Question mark Punto di domanda \a Bell Segnalazione acustica

(26)

ESEMPI

Gestione dell’output

e sequenze di escape

Esempio 1

Realizzare un programma che visualizzi i limiti di tutti i tipi interi; questi limiti sono costanti memorizzate nel file header limits.h.

#include <stdio.h> #include <limits.h> #include <stdlib.h>    main() {

 printf(“minimum char = %d\n”, CHAR_MIN);

 printf(“maximum char = %d\n”, CHAR_MAX);

 printf(“minimum short = %d\n”, SHRT_MIN);

 printf(“maximum short = %d\n”, SHRT_MAX);

 printf(“minimum int = %d\n”, INT_MIN);

 printf(“maximum int = %d\n”, INT_MAX);

 printf(“minimum long = %d\n”, LONG_MIN);

 printf(“maximum long = %d\n”, LONG_MAX);

 printf(“minimum signed char = %d\n”, SCHAR_MIN);

 printf(“maximum signed char = %d\n”, SCHAR_MAX);

 printf(“maximum unsigned char = %d\n”, UCHAR_MAX);

 printf(“maximum unsigned = %u\n”, UINT_MAX);

 printf(“maximum unsigned short = %d\n”, USHRT_MAX);

 printf(“maximum unsigned long = %u\n”, ULONG_MAX);

 system(“PAUSE”);

}

Esempio 2

Realizzare un programma che visualizzi sul video la frase ciao mondo servendosi di sole variabili char.

#include <stdio.h> main() {  char a=99,b=105,c=97,d=111,e=32,f=109,g=111,h=110,i=100,j=111;  printf(“%c%c%c%c%c%c%c%c%c%c\n”, a,b,c,d,e,f,g,h,i,j);  system(“PAUSE”); }

Esempio 3

Realizzare un programma che visualizzi sul video la tavola pitagorica riportata nella figura servendosi delle tabulazioni.

x 2 3 4 5 6 7 8 9 2 4 3 6 9 4 8 12 16 5 10 15 50 25 6 12 18 24 30 36 7 14 21 28 35 42 49 8 16 24 32 40 48 56 72 9 18 27 36 45 54 63 72 81

(27)

ESEMPI

Gestione dell’output e sequenze di escape

#include <stdio.h> main() {  printf(“\n\nx \t 2 \t 3 \t 4 \t 5 \t 6 \t 7 \t 8 \t 9 \n”);  printf(“2 \t 4 \n”);  printf(“3 \t 6 \t 9 \n”);  printf(“4 \t 8 \t 12 \t 16 \n”);  printf(“5 \t 10 \t 15 \t 20 \t 25 \n”);  printf(“6 \t 12 \t 18 \t 24 \t 30 \t 36 \n”);  printf(“7 \t 14 \t 21 \t 28 \t 35 \t 42 \t 49 \n”);  printf(“8 \t 16 \t 24 \t 32 \t 40 \t 48 \t 56 \t 64 \n”);  printf(“9 \t 18 \t 27 \t 36 \t 45 \t 54 \t 63 \t 72 \t 81 \n \n \n”);  system(“PAUSE”); }

Esempio 4

I seguenti frammenti di codice chiariscono la funzione degli operatori unari ++ e ––.

int i;

i = 15;

printf(“%d, %d, %d”, ++i, i++, i); ...

Il risultato che si ottiene è:

 16, 16, 17

Infatti, l’espressione ++i incrementa i prima di stamparlo a video (quindi i è 16); la successiva i++ viene valutata al valore corrente di i (16) che verrà poi incre-mentato (a 17); infine, l’espressione i è il valore di i dopo il postincremento (17).

#include <stdio.h> #include <stdlib.h> main() {  int i, n;  i=4;  n=i;  printf(“Valore di n all’inizio: %d\n”,n);  n=i++;

 printf(“Valore di n dopo i++: %d\n”,n);

 n=++i;

 printf(“Valore di n dopo un ulteriore ++i: %d\n”,n);

 n+=i*(n–4);

 printf(“Valore finale di n: %d\n”,n);

 system(“PAUSE”);

}

La variabile n è inizialmente uguale a i, ovvero vale 4. Poi, n=i++; ma siccome l’operatore ++segue il nome della variabile, prima il valore di i viene

memorizza-to in n, poi il valore di i viene incrementamemorizza-to. Ecco così che n vale ancora 4, men-tre i vale 5. Successivamente n=++i; in questo caso l’operatore ++ precede il nome della variabile, quindi prima il valore di i viene incrementato (era 4, era sta-to incrementasta-to a 5 dall’istruzione i++, ora diventa 6) e poi il risultasta-to viene me-morizzato in n (che, quindi, vale 6). Infine, l’espressione i*(n–4) ovvero 6 * (6 – 4) = 12 viene sommata al precedente valore di n (per via dell’operatore +=), che era 6, quindi 12 + 6 = 18.

(28)

Le istruzioni di input

In C esistono vari modi per effettuare l’acquisizione di dati, ma tutti hanno in co-mune il fatto che prelevano i dati dal dispositivo standard di input: la tastiera. Prima di esaminarli, descriviamo brevemente i dispositivi standard.

In C esistono tre dispositivi standard predefiniti (denominati anche stream):

stdin(standard input), stdout(standard output) e stderr(standard error).

Il file stdin è associato alla tastiera e ogni lettura da tastiera viene vista come una lettura dal file stdin.

Il file stdout (che abbiamo analizzato trattando la funzione printf) è associato al di-spositivo di visualizzazione (il monitor) e ogni scrittura su di esso viene vista come una scrittura sul monitor.

Il file stderr è associato comunemente con il monitor ed è utilizzato per visualiz-zare messaggi di errore: ogni scrittura sul file stderr viene vista come una scrittu-ra sul monitor.

La funzione getc

La funzione getc(non annoverata tra le funzioni ANSI C) legge un carattere dal di-spositivo standard indicato e lo restituisce convertito in un intero. In altri termini, questa funzione attende che venga premuto un tasto sulla tastiera, dopodiché ne restituisce immediatamente il valore. Il carattere digitato non viene visualizzato sullo schermo (la funzione analoga che però visualizza il carattere digitato sullo schermo è getche()). #include <stdio.h> int c; main() {  c = getc(stdin);  printf(“\n%d”, c);  system(“PAUSE”); }

Nell’esempio viene letto un carattere dallo standard input (stdin) e memorizzato nella variabile c come un intero. Ad esempio, se digitiamo la lettera A, nella varia-bile c viene memorizzato il numero 65, ossia il suo codice ASCII. Questa funzio-ne è molto utile per leggere singoli caratteri.

La funzione getchar

La funzione getchar()equivale a getc(stdin) e, di conseguenza, legge un carattere da stdin. Nonostante getchar() sia una funzione prevista dallo standard ANSI, il suo comportamento può differire a seconda dell’implementazione del compilato-re. In alcune versioni, infatti, restituisce immediatamente il valore del carattere immesso da tastiera, in altre, invece, attende fino a quando non viene premuto il tasto Invio. #include <stdio.h> int c; main() {  c = getchar();  printf(“\n%d”, c);  system(“PAUSE”); }

Riferimenti

Documenti correlati

terminazione normale del programma, e restituisce al sistema operativo il valore di status.. Codici

Perfezionato l’inserimento dei rispettivi allegati, relativi alla documentazione probante prevista per ogni singolo intervento, si procede all’inoltro della domanda,

Perfezionato l’inserimento dei rispettivi allegati, relativi alla documentazione probante prevista per ogni singolo intervento, si procede all’inoltro della domanda,

■ Bibliografia 103.. 123, ha riordinato il quadro normativo di tutela della salute e della sicurezza nei luoghi di lavoro. In particolare per i temi della sorveglianza

e restituisce il valore booleano true se e solo se esistono due colonne. distinte contenenti il

[r]

Con riferimento alla serie storica dei massimi annui dei colmi di piena osservati alla stazione San Martino del fiume Chisone sottoporre le distribuzioni LOGNormale e di Gumbel

Numero di parametri sulla linea di comando Incrementato di uno, in quanto il nome del programma viene considerato come un parametro Se non vi sono argomenti effettivi, vale 1 Se