CAPITOLO 1 - Primi programmi in C++.

1.3 – Gli errori

Ogni istruzione del programma deve seguire rigorosamente le regole di una opportuna grammatica. Ad esempio nel caso di un programma in C++ ogni istruzione deve terminare sempre con il punto e virgola, oppure: il nome del programma principale di un codice sorgente deve essere sempre la parola chiave main. D’altronde è ciò che capita in qualsiasi lingua: non ogni sequenza di parole dell’italiano forma una frase significativa, occorre che la sequenza obbedisca ad opportune regole grammaticali. Tuttavia mentre l’essere umano è spesso in grado di comprendere una frase anche se questa contiene qualche errore grammaticale, se in un programma c’è anche un solo errore sintattico, esso non può essere compilato.

È per questo motivo che il compilatore, una volta ricevuto il programma sorgente in input, per prima cosa lo analizza tramite un programma detto parser sintattico, e solo se non riscontra errori sintattici passa alle fasi successive che sono le seguenti:

  1. Traduce il programma sorgente in linguaggio assembly
  2. Traduce il programma assembly in programma macchina
  3. Esegue il link per trasformarlo in programma oggetto eseguibile

Il programma oggetto generato dal compilatore verrà automaticamente salvato nella stessa cartella in cui è stato inserito il programma sorgente, con lo stesso nome del file sorgente (ma con estensione exe nel sistema operativo Windows ad indicare che si tratta di un programma eseguibile). Nel nostro esempio nella stessa cartella in cui abbiamo salvato il programma primo.c troveremo sia primo.o ( in programma macchina prima che passi al linker) che l’eseguibile primo o primo.exe (nel caso di Windows). Da ora in poi basterà cliccare sull’icona relativa per mandarlo direttamente in esecuzione.

Se invece sono presenti degli errori sintattici, il compilatore si limita a segnalare, nel riquadro a destra in basso dello schermo, gli errori riscontrati. A volte, solo il primo errore riscontrato è segnalato sempre in maniera corretta dal parser; pertanto è buona regola correggere il primo errore con l’editor e compilare nuovamente il programma e continuare così fino a quando il parser non segnala più errori.

Sintassi: regole che indicano il modo in cui parole e simboli del linguaggio possono combinarsi fra loro.

Errori di sintassi: parte del programma che viola le regole della sintassi del linguaggio.

Durante l’esecuzione del programma questo può interrompersi e inviare un opportuno messaggio di errore. Ad esempio ciò accade se al momento in cui occorre valutare l’espressione x/y la variabile y ha valore pari a zero.

Errori di run-time: errori che si verificano durante l’esecuzione del programma per cui risulta impossibile la sua prosecuzione. Infine può accadere che il programma sia privo di errori sintattici, giri regolarmente, ma dia risultati sbagliati. Si parla in tal caso di errori semantici o logici.

Se ci accorgiamo che il programma non è corretto, è completamente inutile tentare la prima modifica del programma che ci viene in mente, nella speranza che l’errore scompaia. Occorre ragionare attentamente sull’algoritmo proposto e capire esattamente dove l’algoritmo è sbagliato prima di modificare il sorgente. Trovare e correggere gli errori che si presentano nel programma sorgente è il significato del termine inglese debugging.

Il programma successivo è molto semplice; esso calcola il numero di articoli acquistati in un negozio conoscendo la spesa complessiva ed il costo dell'articolo.

//Calcola Numero Articoli #include <iostream> using namespace std; int main() { float SpesaTot,CostoArt,NumArt; cout<<"Scrivi Spesa Totale e Costo Articolo:"; cin>>SpesaTot>>CostoArt; NumArt=SpesaTot/CostoArt; cout<<"Numero Articoli="<<NumArt; }

sul calcolo spesa libri. Il programma successivo calcola la spesa relativa all'acquisto di alcuni libri di cui si conoscono prezzo e quantità.

1.4 - Struttura di un programma in C++

A titolo esemplificativo, analizziamo il programma primo; esso chiarisce qual è la struttura di un programma C++. La prima linea inizia con i simboli
// che rappresentano un commento: //calcola la circonferenza

I commenti sono un modo importante per documentare il proprio programma migliorandone la leggibilità. Possono essere inseriti in qualunque parte del programma e non producono alcun effetto nella fase esecutiva: il compilatore semplicemente li ignora. Il simbolo // rappresenta un commento a linea singola; in altre parole tutta la linea che segue i due caratteri // viene ignorata. Per i commenti su più linee, è necessario utilizzare il simbolo /* all'inizio del commento e */ alla fine:

/* questo è un commento
su più linee*/

Ritornando al nostro esempio vediamo che il programma vero e proprio comincia solo dopo una successione di opportune direttive al compilatore. Segue l'intestazione del programma data dalle parole chiave int main(),ed infine il blocco delle istruzioni vere e proprie racchiuso tra parentesi graffe.

direttiva: una istruzione rivolta al compilatore o ad una parte di esso.

Abbiamo già definito un blocco come un qualsiasi raggruppamento di istruzioni interne ad un Programma. Esso viene evidenziato in C++ come "una sequenza di istruzioni racchiusa tra parentesi graffe". Un blocco può contenere altri blocchi al suo interno.

Le direttive al prepocessore devono precedere l'intestazione del programma. Nel nostro esempio è presente la direttiva:

#include <iostream>

La direttiva di inclusione avverte il preprocessore di includere il file associato alla libreria che gestisce l'input e l'output standard (standard Input Output) dei tipi base verso file o verso opportuni dispositivi di ingresso e uscita come stampante, schermo, tastiera. Tali file, tradizionalmente noti come header (talvolta hanno il suffisso h), perché inseriti in testa al codice, contengono codice sorgente che provvede alle dichiarazioni per le costanti, funzioni ed altri tipi di entità di cui un programma o una libreria potrebbe aver bisogno. Essi sono conservati nella sottocartella include inserita nella cartella principale del compilatore. La sintassi per includere un header file che è parte della libreria del C++ è la seguente:

#include <nome file header>

Continuando nel nostro esempio vediamo che dopo l'intestazione del programma int main() seguono, racchiuse tra parentesi graffe, tutte le istruzioni necessarie alla soluzione del problema. Le parentesi graffe rappresentano dei simboli che delimitano un'istruzione composta ( o blocco ). La parola main sta per principale: essa è la function ( il processo ) principale del programma.

Un'ultima osservazione sull'aspetto generale del programma. Possiamo notare che il corpo del main ( per intenderci, la parte compresa tra le parentesi graffe ) rientra di alcuni caratteri verso destra. Questa peculiarità, che va sotto il nome di indentazione, è molto utile per avere un'idea, con un colpo d'occhio, di quali sono le istruzioni incluse nel blocco del main. È buona norma fissare il numero di caratteri per ogni livello di rientro: possiamo ritenere sufficienti i due spazi che abbiamo lasciato nel programma.

Passiamo ora ad esaminare sintatticamente le varie componenti presenti nel corpo del programma. Se in prima approssimazione diciamo che un processo è un gruppo di istruzioni in codice, ci rendiamo conto che un processo ha bisogno di oggetti ovvero di locazioni di memoria dove immagazzinare i valori. Non ci meraviglia dunque che un programma contenga nomi per riferirsi a processi e agli oggetti che questi manipolano.

Più in generale possiamo dire che nel nostro programma compaiono le seguenti componenti:

parola chiave: è una parola che ha un significato ben preciso e che non può essere usata per nessun altro motivo. Sono parole chiave nel nostro programma main, const e float.

identificatore: è adoperato come nome di un oggetto o processo. Da un punto di vista sintattico un identificatore inizia con una lettera dell'alfabeto oppure con il simbolo di underscore: _ , eventualmente seguito da una o più lettere dell'alfabeto e/o cifre e/o il simbolo di underscore. Alcuni identificatori sono standard o riservati, gli altri sono definiti da chi ha scritto il programma. Nel nostro esempio main è il nome standard obbligatorio del programma principale, cin e cout sono gli identificatori (standard) che rappresentano il file standard di ingresso, di solito la tastiera, e il file standard di uscita, di solito lo schermo del visore. Invece pi, raggio e circonferenza sono identificatori non standard di oggetti. Il primo è il nome di una costante, gli altri due sono nomi di variabili.

È bene sottolineare che gli identificatori in C++ sono case sensitive, cioé distinguono le lettere minuscole dalle maiuscole, per cui gli identificatori rappresentati da Raggio, raggio, RAGGIO sono 3 identificatori distinti.

costante: una grandezza che non può cambiare durante l'esecuzione di un programma.
variabile: una grandezza che invece può cambiare sia da una esecuzione ad un'altra sia durante una stessa esecuzione. Per poter definire una variabile occorre innanzitutto definirne il tipo. Nel nostro esempio le variabili raggio e area sono di tipo float, cioé numeri relativi a singola precisione. Alle variabili come anche alle costanti corrisponde un indirizzo di un'opportuna area della memoria centrale, indirizzo destinato a contenere il valore della variabile. L'occorrenza di una variabile in una istruzione può dunque fare riferimento al suo indirizzo (left-value della variabile) oppure al contenuto di questo indirizzo (right-value). Così all'interno del blocco di istruzioni delimitato da parentesi graffe la prima occorrenza di raggio si riferisce al suo indirizzo, la seconda al suo valore.

tipo: l'insieme dei valori che una variabile può assumere e contemporaneamente l'insieme delle operazioni che si possono effettuare su variabili di quel tipo.

tipi standard o di base: i tipi predefiniti in un particolare linguaggio di programmazione. I tipi base del C++ sono: il tipo intero int, float e double per rappresentare i numeri relativi ed i numeri razionali a singola e doppia precisione rispettivamente, il tipo char per rappresentare i caratteri, il tipo boolean le cui variabili possono assumere due soli valori true e false (rispettivamente vero e falso).

letterale: sono le occorrenze di costanti in un programma. Nel nostro esempio compare la costante 3.1415 ma anche le stringhe costanti "Inserisci Raggio= ", "Area = ", la costante pi. Mentre è possibile riferirsi alla prima nel programma tramite l'identificatore pi ciò non è possibile per le altre che sono perciò costanti anonime. I letterali compaiono colorati nell'ambiente CodeBlocks.

espressione: rappresenta il valore che si ottiene applicando opportune operazioni ben definite ad uno o più operandi che possono essere costanti, letterali e variabili. In una espressione le operazioni vengono indicate con particolari simboli detti operatori. Sono unari gli operatori che agiscono su di un solo operando, binari quelli che agiscono su due operandi. Ci soffermeremo in particolare sulle espressioni numeriche e su quelle booleane. Nell'esempio sono espressioni (e quindi a run-time restituiscono un valore): raggio*raggio*pi, come pure tutto ciò che viene inviato a printf per essere stampato, come i letterali di cui sopra e il riferimento finale alla variabile area.

simbolo speciale o ausiliario: uno o anche due caratteri consecutivi che sono usati per scopi particolari come ad esempio il punto e virgola che serve ad indicare la fine di una istruzione, le parentesi graffe che indicano inizio e fine di una istruzione composta, la virgola, che è usata come separatore.

Definizione di costante. Ha la seguente sintassi:
const type identificatore=letterale dove const è una parola riservata, segue type che è il nome del tipo assegnato alla costante, il nome della costante, il simbolo = e il valore assegnatole. Ad esempio

const int N=100;

definisce un valore N intero costante pari a 100.

Definizione di variabile. Occorre far precedere al nome della variabile il nome del suo tipo. Si possono dichiarare contemporaneamente più variabili dello stesso tipo separandole con una virgola. Ad esempio:
float r1,r2,media;

definisce le variabili r1, r2, media come numeri reali (float), i cui valori iniziali sono sconosciuti e imprevedibili. È anche possibile assegnare un valore iniziale ad una variabile usando l'operatore di assegnazione = come in:
int i,j=1,somma;
dove i, j, somma sono variabili intere; ma, mentre i e somma assumono inizialmente un valore sconosciuto, j assume il valore 1.

In effetti una definizione di una variabile o costante può essere scritta in un qualsiasi punto del programma, purché preceda la prima occorrenza della variabile o costante in questione.

In questo testo daremo di volta in volta delle regole alle quali è opportuno adeguarsi se si desidera imparare a scrivere programmi leggibili. Iniziamo dunque con la prima di tali regole:

REGOLA 1. E' sempre utile definire tutte le costanti e le variabili nella parte iniziale del programma o della funzione che le utilizza. In questo modo, chi legge il programma sa dove reperire le informazioni sul tipo delle variabili e delle costanti.

In definitiva le variabili hanno un nome ( l'identificatore ), un tipo ( tipi base: interi, reali, caratteri ) che deve essere precisato nella definizione, una dimensione ( che dipende dal tipo ) che rappresenta la quantità di byte memorizzati, un valore che varia nel corso del programma ( è invece sempre lo stesso per le costanti ), un ambito di validità ( detto scope ) che rappresenta il frammento di programma in cui la variabile è riconosciuta come tale, ed infine un indirizzo della memoria del computer in cui si trova il suo valore. La variabile "raggio" del nostro programma primo ha il nome "raggio", è di tipo float con una dimensione di 4 byte, un valore che viene determinato dal programma ( ricordiamo che tale valore viene immesso da tastiera ) ed un indirizzo che può essere conosciuto inserendo l'operatore & (detto operatore di indirizzo) davanti alla variabile: &raggio.
Il tipo double ha invece una dimensione di 8 byte.
Il tipo int occupa 4 byte ed il tipo char occupa 1 byte. I caratteri sono rappresentati attribuendo loro un codice numerico ( codice ASCII), ad esempio il codice attribuito alla lettera A è 65. All'interno di un programma una costante letterale di tipo carattere deve essere sempre racchiusa tra due apici, ad esempio: 'r', '^' '@'
La parte successiva del nostro programma è formata dalle vere e proprie istruzioni ( statement ), azioni che la macchina esegue utilizzando le variabili e costanti dichiarati in precedenza. È bene che gli identificatori che rappresentano delle variabili siano dei nomi significativi. Alle variabili vanno assegnati dei nomi che ci ricordino il valore che rappresentano ( raggio e area come raggio e area del cerchio, NomeAlunno se rappresenta il nome di un alunno, etc). L'ultimo esempio ci consegna una semplice regola:

REGOLA 2. Se l'identificatore deve rappresentare un oggetto descrivibile con più parole, allora conviene utilizzare tutte le parole della descrizione con la lettera iniziale in maiuscolo (Esempio: "Prezzo della mela nella giornata" si può rappresentare con "PrezzoMelaGiornata" o "PrezzoMelaOggi").

namespace

Un namespace, o "spazio dei nomi", è una collezione di nomi di entità, definite dal programmatore, omogeneamente usate in uno o più file sorgente. Esso ha lo scopo di evitare confusione ed equivoci nel caso siano necessarie molte entità con nomi simili, fornendo il modo di raggruppare i nomi per categorie.
Se non utlizziamo l'istruzione "using namespace std;", siamo costretti ad usare, nelle istruzioni di Input/Output, il termine "std::", che sta ad indicare che quel simbolo (cin,cout, endl,...) fa parte dello spazio dei nomi standard (std). Il successivo programma esegue ancora il calcolo della circonferenza senza l'istruzione "using namespace std;".

// Calcola lunghezza circonferenza #include <iostream> int main () { float raggio,circonferenza; const float pi=3.1415; std::cout<<"Inserisci raggio :"; std::cin>>raggio; circonferenza=2*raggio*pi; std::cout<<"Circonferenza="<<circonferenza<<std::endl; return 0; }

define

Un'altra modalità per definire delle costanti è l'uso della parola chiave define, con la quale è possibile associare un nome simbolico ad una costante numerica o non.
Tale istruzione va inserita subito dopo tutti gli include, all'inizio del programma; esempi:
#define pigreco 3.1415
#define saluti "Salutiamo tutti i presenti al convegno"
#define max(x,y) ((x)>(y) ? (x):(y))

Ogni istruzione è come un'assegnazione multipla: afferma che all'interno del programma, ogni occorrenza del primo termine deve essere sostituito dalla seconda parte, cioé
il termine pigreco va sostituito con 3.1415
il termine saluti va sostituito con "Salutiamo tutti i presenti al convegno"
il termine max(a+2,b-1) va sostituito con ((a+2)>(b-1) ? (a+2):(b-1))

In particolare la terza istruzione è detta macro perchè rappresenta una funzione in quanto restituisce un valore.
Le funzioni sono discusse ampiamente nel capitolo 4, mentre l'operatore condizionale ? qui.

Definizione e dichiarazione.

Nel testo faremo spesso uso dei termini definizione e dichiarazione utilizzandoli, a volte, impropriamente come sinonimi. Per esempio, l'istruzione
int Numero, x; è una definizione perché riserva spazio nella memoria per le variabili, mentre
#define dimensione 100 è una dichiarazione perché non riserva spazio in memoria.

Dichiarazione: l'entità viene associata all' identificatore (nome della variabile o della funzione) e ne viene dichiarata la tipologia, ma non viene né riservata la memoria per le variabili, né specificato il codice per le funzioni.

Definizione: si associa l'identificatore alla entità e ne si definisce la tipologia; inoltre, viene riservata memoria per le variabili, o specificato il codice per le funzioni.