Graziano Donati Corso di Informatica Per il IIIo anno degli Istituti Tecnici Commerciali Centro Studi Bellini Graziano Donati Corso di Informatica Per il IIIo anno degli Istituti Tecnici Commerciali Centro Studi Bellini Programma di Informatica - IIIo - Istituto Tecnico Commerciale GLI ALGORITMI. Il concetto di algoritmo. Proprietà fondamentali di un algoritmo. Rappresentazione degli algoritmi. Linguaggio di progetto. Diagrammi di flusso. Elaborazione. Decisione. Entrata e uscita dati. Cicli iterativi. INTRODUZIONE ALLA PROGRAMMAZIONE. Il linguaggio C++. Il concetto di assegnamento. Struttura generale di un programma C++. Direttive al preprocessore. Funzione void. Le dichiarazioni. Il corpo del programma. Le librerie principali. ISTRUZIONI DI INGRESSO E USCITA. Le istruzioni cin e cout. Inserimento di dati. Visualizzazione dei risultati. Esempi. ISTRUZIONE CONDIZIONALE If-else. La decisione e l'istruzione condizionale. Condizioni annidate. Scelta multipla. Esempi. ISTRUZIONI DI CICLO. Le istruzioni for, while, do. Rappresentazione delle istruzioni cicliche in un algoritmo. Esempi di utlizzo. LE FUNZIONI. Utilizzo delle funzioni nella programmazione strutturata. Variabili locali e globali. Passaggio dei parametri. Chiamata di una funzione. VETTORI ED ARRAY. Dichiarazione di vettori e di array. Utilizzo dei vettori e degli array nella programmazione. Esempi. Prefazione Scopo di questa dispensa è quello di esaminare le nozioni fondamentali riguardanti la scrittura di programmi senza esaminare in dettaglio alcun linguaggio di programmazione. Una scelta precisa viene invece effettuata a livello di modello di programmazione, a vantaggio del paradigma imperativo, senza che ciò impedisca un breve esame anche degli altri paradigmi. Il linguaggio cui si fa riferimento, pur senza dettagliarne i costrutti e le istruzioni, è il c++; per questo anche il linguaggio di progetto per la descrizione degli algoritmi, proposto nel prossimo capitolo, si ispira alla sintassi del linguaggio c++. La dispensa inizia con la teoria degli algoritmi, allargando il discorso all’attività di analisi da svolgere come primo passo del ciclo di vita dei programmi; si passa poi alle generalità sui paradigmi di programmazione, alla suddivisione in livelli dei linguaggi, alle modalità per la loro traduzione; successivamente si illustrano le più importanti strutture dati astratte e si conclude con un breve esame sulla documentazione e produzione del software. Graziano Donati Centro Studi Bellini Cap 1 GLI ALGORITMI Gli algoritmi Il termine algoritmo è ispirato dal nome del matematico e astronomo arabo del VII° secolo Abu Ja Mohammed Ibn Musa Al-Khowarizmi che, oltre ad essere l’inventore dello zero e dell’algebra, ha la paternità di quello che è considerato uno dei primi algoritmi della storia: il metodo per sommare due numeri incolonnandoli e procedendo cifra per cifra da destra verso sinistra tenendo conto dei riporti. Per ‘algoritmo’ si può intendere un «insieme di istruzioni che definiscono una sequenza di operazioni mediante le quali si risolvono tutti i problemi di una certa classe»; in particolare è importante l’ultima parte della definizione dove si specifica che un algoritmo deve avere tra le sue caratteristiche quella della ‘generalità’. Un’altra fra le numerose definizioni possibili può essere la seguente: «metodo di elaborazione da applicare a certi dati iniziali per ottenere dei dati finali o risultati». In ogni caso è importante che, in partenza, il problema da risolvere sia «ben posto» e cioè che: • sia chiaro l’obiettivo da raggiungere; • i dati di partenza sia noti e sufficienti; • il problema sia risolubile da parte di chi lo affronta. Se manca una di queste condizioni l’algoritmo non può essere individuato. Si deve anche notare come sia importante, quando si affronta un problema, non confondere il risultato (ciò che si vuole ottenere) con la soluzione (il metodo che conduce al risultato elaborando i dati di partenza). L'algoritmo quindi rappresenta proprio la soluzione o procedura risolutiva di un problema, o meglio di una determinata classe di problemi. Classe di Problemi Algoritmo (Procedure Risolutiva) Risultati L’individuazione dell’algoritmo risolutivo di un problema è solo il primo passo di un procedimento in più fasi che conduce alla soluzione del problema stesso in modo automatico con l’utilizzo del sistema di elaborazione. Questo procedimento verrà dettagliato in seguito, ma possiamo già accennare che il passo successivo consiste nella trasformazione dell’algoritmo in un programma scritto usando un linguaggio di programmazione; si può affermare che l’algoritmo e il relativo programma sono due descrizioni, fatte con mezzi diversi, dello stesso metodo di soluzione. Formalizzazione di un problema Generalmente, la descrizione di un qualsiasi problema (o classe di problemi) consiste in un predicato (affermazione espressa in lingua italiana, inglese, ecc) nel quale si espone il contesto, le condizioni e la struttura della problematica che si deve affrontare e nel quale si pongono in evidenza le variabili che costituiscono i dati disponibili o dati di ingresso e nel quale si esplicitano le variabili che costituiscono i dati che si vogliono ottenere o dati di uscita. Quindi, una classe di problemi è costituita dai seguenti elementi: • Condizioni, contesto e struttura della problematica da affrontare • Elenco dei dati che abbiamo a disposizione (dati di ingresso) • Elenco dei dati che si vogliono ottenere (dati di uscita) Ad esempio, consideriamo il problema costituito dalla soluzione di un'equazione algebrica di secondo grado. La formalizzazione di tale problema, secondo lo schema indicato, potrebbe portare alle seguenti conclusioni: 1) Condizioni, contesto e struttura della problematica L'equazione di secondo grado è costituita da un'uguaglianza algebrica del tipo: ax 2 + bx + c = 0 Nella quale i tre parametri a,b,c sono tre numeri reali noti, mentre il numero reale x è incognito. Risolvere tale equazione algebrica in campo reale, significa determinare quei valori (uno o più di uno) dell'incognita x che sostituiti nell'uguaglianza la soddisfano effettivamente (cioè rendono il primo membro uguale al secondo e riducono l'uguaglianza ad una identità). 2) Elenco dei dati che abbiamo a disposizione Ogni qual volta viene assegnato il problema che consiste nel risolvere un'equazione di secondo grado, ci devno essere forniti i tre numeri reali a,b,c. Quindi tali numeri sono i dati di partenza su cui dobbiamo lavorare per risolvere il problema. Quindi: Dati di ingresso: a,b,c (numeri reali) 3) Elenco dei dati che si vogliono ottenere La soluzione dell'equazione di secondo grado consiste nel determinare i valori reali di x che rendono vera l'uguaglianza. Dalla teoria matematica delle equazioni, sappiamo che in generale, per un'equazione di secondo grado esistono due valori di x che rendono vera l'uguaglianza. Quindi, almeno in generale: Dati di uscita: x1,x2 (numeri reali) Problema: ax2+bx+c = 0 a Dati noti x1 ? b Dati da ricavare c x2 Quindi, il problema dell'equazione di secondo grado, è un problema con tre ingressi (i tre numeri reali a,b,c) e due uscite (i due numeri reali x1 e x2). Il punto interrogativo all'interno del rettangolo è il nostro ALGORITMO (per ora non noto), cioè la procedura risolutiva che ci consentirà, a partire dai tre dati di ingresso a,b,c (numeri reali) di ottenere i due dati di uscita x1,x2 (numeri reali). Il nostro scopo è quello di determinare e descrivere tale algoritmo, cioè quella procedura o sequenza di elaborazioni e operazione che applicate ai dati di ingresso (numeri a,b,c) ci consente di ottenere i dati di uscita (numeri x1,x2). Dovremo quindi sostituire tale punto interrogativo con una descrizione dei passi e delle elaborazioni che formano l'algoritmo (cioè la procedura risolutiva). In generale quindi, è sempre possibile rappresentare un problema in forma grafica mediante la seguente simbologia: I1 I2 U1 Algoritmo U2 ? In Um Descrizione delle caratteristiche del problema Sul lato sinistro del rettangolo indichiamo n frecce di ingresso I1,I2, ... , In che rappresentano gli n dati di ingresso del problema. Sul lato destro del rettangolo indichiamo m frecce di uscita U1, U2, ... , Un che rappresentano gli m dati di uscita del problema. Gli n dati di ingresso del problema I1,I2, ... , In vengono supposti noti, gli m dati di uscita U1, U2, ... , Un sono considerati da ricavare. Nella descrizione del problema, esporremo il contesto, le condizioni e la struttura della problematica. Daremo una spiegazione degli n dati di ingresso, degli m dati di uscita e di come essi si correlano al problema. A questo punto, il prossimo passo consiste nel determinare il nostro punto interrogativo, cioè l'ALGORITMO, ovvero la procedura risolutiva che ci consente di passare dagli n dati di ingresso agli m dati di uscita. Tale algoritmo dovrà essere ricavato mediante uno studio della specifica classe di problemi che si deve affrontare in questo caso, dopo di chè dovrà essere descritto per mezzo di qualche linguaggio. L'Algoritmo (procedura risolutiva) Il punto interrogativo contenuto nel rettangolo rappresenta ciò che ci consente di passare dai dati di ingresso noti ai dati di uscita che dobbiamo determinare. Quindi il punto interrogativo rappresenta la procedura che consente di risolvere il problema, ovvero il nostro ALGORITMO. L'algoritmo è quindi un insieme di meccanismi, operazioni, elaborazioni, scelte che applicate, in un dato ordine, ai dati di ingresso noti, ci consentono di ottenere i dati di uscita richiesti. Quindi, un algoritmo può essere pensato come una successione di passi di elaborazione (operazioni matematiche e non, operazioni logiche, scelte, ecc) che applicati in un ben determinato ordine, costruiscono la nostra procedura risolutiva, in grado di farci passare dai dati di ingresso noti, ai dati di uscita da ottenere. Proprietà degli algoritmi Distinguiamo da ora in poi l’aspetto della risoluzione del problema da quello dell’esecuzione del relativo algoritmo; distinguiamo cioè tra ‘risolutore’ (l’uomo, che individua il metodo di soluzione del problema) ed ‘esecutore’ (la macchina che deve eseguire tale metodo, descritto tramite un algoritmo). L’algoritmo è solo la ‘descrizione delle operazioni’ da svolgere; è quindi un’entità statica costituita da istruzioni, anch’esse statiche. Quando l’algoritmo viene eseguito si ottiene un ‘flusso d’esecuzione’ che invece è ovviamente dinamico ed è costituito da ‘passi’ (ogni passo è l’esecuzione di un’istruzione). A tale proposito può essere utile pensare alla differnza esistente tra una ricetta di una torta (descrizione delle operazioni da svolgere) e la sua «esecuzione» per la realizzazione della torta. [ ] Nel contesto informatico l’esecutore, cioè il sistema di elaborazione, è una macchina non intelligente, capace solo di eseguire istruzioni molto elementari (anche se in modo rapidissimo), ma senza alcuna capacità critica o di ragionamento autonomo. Un algoritmo deve allora avere le seguenti proprietà: • ‘generalità’, della quale abbiamo accennato in precedenza: non è un algoritmo il metodo che mi permette di stabilire se 19 è un numero primo, lo è invece il metodo che permette di decidere se un qualsiasi numero naturale è primo; • ‘comprensibilità’: l’algoritmo deve essere espresso in una forma comprensibile all’e- secutore; questo non vuol dire che gli algoritmi debbano essere espressi nel linguaggio proprio della macchina, cioè quello binario, e neanche che si debbano subito utilizzare i lin- guaggi di programmazione, ma significa che si deve rispettare fin da subito un formalismo abbastanza rigido nella loro stesura; • ‘eseguibilità’: l’algoritmo deve anche essere eseguibile dalla macchina e quindi non deve prevedere operazioni ad essa sconosciute (l’insieme delle istruzioni eseguibili da un computer ha ‘cardinalità finita’) o che non possano essere eseguite in un tempo finito (le istruzioni devono avere ‘complessità finita’); • ‘finitezza’: l’algoritmo deve avere un numero finito di istruzioni; • ‘riproducibilità’: esecuzioni successive dell’algoritmo sugli stessi dati di input devono dare lo stesso risultato; • ‘non ambiguità’: non possono esserci istruzioni vaghe, che prevedano un comportamen- to probabilistico o la scelta casuale del prossimo passo da eseguire; la macchina non è infatti in grado di prendere decisioni complesse con ragionamento autonomo e quindi il metodo di soluzione deve essere ‘completo’ (devono essere previste tutte le possibilità che possano verificarsi durante l’esecuzione) e ‘deterministico’ (in caso contrario verrebbe meno anche la caratteristica della riproducibilità); • ‘discretezza’: l’esecuzione deve avvenire per passi discreti; i dati coinvolti nei calcoli assumono valori che cambiano tra un passo e il successivo e non assumono altri valori «intermedi»; • ‘non limitatezza dell’input’: non deve esserci un limite (almeno in linea teorica) alla lunghezza dei dati di input (e quindi anche alla capacità di memoria dell’esecutore); • ‘non limitatezza dei passi d’esecuzione’: devono essere possibili (almeno in linea teorica) esecuzioni costituite da un numero infinito di passi. Le ultime due proprietà possono sembrare irrealistiche nelle applicazioni pratiche, ma sono necessarie per i seguenti motivi: • se ci fosse un limite alla lunghezza dei dati di input verrebbe meno la proprietà della gene- ralità: ad esempio non potremmo più concepire un algoritmo per decidere se un qualsiasi numero naturale è primo oppure no; • esistono funzioni che si dimostra essere computabili a patto di ammettere un numero infinito di passi nell’algoritmo di calcolo. Gli algoritmi 4 Si deve comunque notare che queste grandi potenzialità teoriche degli algoritmi (input di lunghezza infinita e esecuzioni con infiniti passi) non sono sufficienti ad assicurare che tutte le funzioni siano computabili o, detto in altri termini, che tutti i problemi prevedano un algoritmo risolutivo. Esiste ad esempio il famoso problema della «terminazione degli algoritmi»: si può dimostrare che «non esiste un algoritmo che permetta di stabilire se un qualsiasi altro algoritmo abbia termine». Questi temi prettamente teorici inerenti la «teoria della computabilità» e che comprendono tra l’altro lo studio degli automi, della macchina di Turing, della «Tesi di Church-Turing«, pur molto interessanti, esulano dagli scopi di queste dispense e non vengono quindi ulteriormente trattati. Rappresentazione degli algoritmi Per quanto detto in precedenza appare del tutto impraticabile l’idea di affidare la descrizione di un algoritmo all’uso del linguaggio naturale (nel nostro caso la lingua italiana) con il rischio di introdurre ambiguità dovute alla presenza di sinonimi, omonimie, modi diversi di intendere la stessa frase. Si pensi ad esempio alle possibili diverse interpretazioni che si possono dare alle seguenti frasi: «succede al Sabato», «lavoro solo da due anni», «mi piace molto la pesca». Le interpretazioni e le sfumature delle frasi espresse in linguaggio naturale, spesso dipendenti dal contesto, possono essere colte solo dalla mente umana che è un «esecutore» molto più sofisticato, versatile ed elastico di qualsiasi macchina. Per descrivere gli algoritmi si deve quindi ricorrere a ‘linguaggi formali’ creati appositamente (e quindi artificiali); in queste dispense prenderemo in considerazione due esempi: • un linguaggio ‘lineare’ basato sul testo, spesso denominato ‘linguaggio di progetto’; • un linguaggio ‘grafico’ basato su simboli chiamato ‘diagramma di flusso’ (flow chart) o ‘diagramma a blocchi’. Notiamo che, qualsiasi sia il metodo utilizzato per la rappresentazione dell’algoritmo, vale sem- pre la regola che le operazioni da esso descritte vengono eseguite una alla volta nell’ordine in cui sono scritte (a meno che non intervengano costrutti di programmazione particolari che alterano il normale flusso di esecuzione). Prima di illustrare i due metodi di rappresentazione degli algoritmi è importante chiarire natura e ruolo delle variabili. Concetto di variabile Per ‘variabile’ si intende un oggetto che ha un nome e che permette di immagazzinare e conservare un determinato valore; l’insieme delle variabili utilizzate in un certo algoritmo prende il nome di ‘area di lavoro’ relativa ad esso. In generale una variabile è caratterizzata da tre elementi: [ ] • ‘tipo’: indica se la variabile è un valore intero, reale, un carattere e così via; • ‘nome’: deve essere univoco e «significativo» (cioè inerente al ruolo che la variabile ricopre nell’elaborazione); • ‘contenuto’: il valore che in un certo passo dell’elaborazione è assegnato alla variabile. In un algoritmo possono essere poi presenti altri oggetti, detti ‘costanti’, che sono invece delle entità non modificabili. Consideriamo ad esempio l’algoritmo (per il momento descritto con normali frasi della lingua italiana) per calcolare il perimetro di un quadrato di lato 5: • esegui l’operazione 5*4; • poni il risultato nella variabile P; • visualizza (o stampa) il valore del risultato P. In questo esempio vengono usate la variabile P e le costanti 4 e 5. Si noti in particolare la natura della seconda istruzione («poni ...») che è chiamata ‘istruzione di assegnazione’ ed è «distruttiva» in quanto, assegnando un nuovo valore a P cancella il contenuto precedente di tale variabile. L’algoritmo appena descritto non è degno neppure di essere considerato tale perché manca del tutto di generalità; una versione migliore è la seguente, ottenuta sostituendo una variabile ad una costante in modo da ottenere il metodo di calcolo del perimetro di un quadrato di lato L qualsiasi: • acquisisci in input (leggi) il valore del lato ponendolo in L; • esegui l’operazione L*4; • poni il risultato nella variabile P; • stampa il valore di P. In questo caso abbiamo la presenza delle variabili P ed L e della costante 4. Sostituendo anche a quest’ultima con una una variabile si ha una versione ancora più generale dell’algoritmo, che permette di calcolare il perimetro di un poligono regolare di N lati lunghi L: • leggi il numero dei lati e la misura del lato ponendoli in N e L; • esegui l’operazione L*N; • poni il risultato nella variabile P; • stampa il valore di P. Gli algoritmi 6 Linguaggio di progetto Il linguaggio di progetto che utilizziamo non fa parte di quelli ufficialmente riconosciuti come lo «Pseudo Pascal» o i linguaggi «L2P» e «L3P» creati dal professor Callegarin, ma è definito a scopo dimostrativo solo per queste dispense. Si tratta di linguaggio basato su alcune parole della lingua italiana, sul simbolo di assegnazione «=», sui normali operatori aritmetici e di confronto. Per quanto riguarda la sintassi delle istruzioni si fa riferimento a quella del linguaggio di programmazione c prevedendo quindi: • l’uso del «;» per terminare ogni istruzione; • i simboli «{» e «}» per iniziare e terminare i blocchi di istruzioni sequenziali; • l’uso degli operatori «==», «!=» rispettivamente per l’uguaglianza e la disuguaglianza; • l’uso degli operatori logici «!», «&&», «||» corrispondenti al NOT, AND, OR, rispettivamente; • l’uso delle parentesi tonde per racchiudere i controlli logici. Con questo strumento dobbiamo descrivere tutte le normali fasi di un algoritmo che sono: • ‘inizio’ dell’algoritmo: con la parentesi graffa aperta; • ‘dichiarazione delle variabili’: effettuata usando le parole intero, reale, carattere, stringa seguite dal nome della variabile o delle variabili separati da virgole; • ‘acquisizione dei dati di input’: con l’istruzione leggi e le variabili di input indicate tra parentesi; • ‘elaborazioni varie’: con gli opertaori aritmetici, logici e il simbolo di assegna- zione «=»; • ‘emissione dei risultati’: istruzione stampa e le variabili di input indica- te tra parentesi; • ‘fine’ dell’algoritmo: con parentesi graffa chiusa. Come esempio riscriviamo l’algoritmo per il perimetro del poligono regolare: { intero L,N,P; leggi (N,L); P = N*L; stampa(P); } Torniamo ancora brevemente sull’istruzione di assegnazione P = N*L; la logica di un’istruzione di questo tipo è la seguente: • a sinistra del simbolo di assegnazione ci può essere solo una variabile (non una costante o un’espressione di qualche genere); Gli algoritmi 7 • a destra del simbolo di assegnazione può trovarsi una variabile, una costante o un’espressione di qualsiasi genere; • viene valutato l’oggetto presente a sinistra e il valore risultante viene assegnato alla variabile a destra, sovrascrivendo il valore precedentemente contenuto in essa. Diagrammi di flusso I ‘diagrammi di flusso’ permettono di descrivere gli algoritmi basandosi su simboli grafici contenenti le operazioni da eseguire. I simboli si chiamano appunto blocchi e si differenziano per la loro forma in base alla funzione che svolgono nella descrizione dell’algoritmo; sono uniti da archi orientati (frecce) che permettono di esprimere la direzione del flusso di elaborazione. Nella figura 2.2 è riportata la lista dei blocchi fondamentali con indicazione del ruolo svolto all’interno del diagramma. Figura 2.2 Dal blocco di inizio avremo sempre una freccia uscente, nel blocco di fine solo una freccia entrante; negli altri blocchi una freccia entrante ed una uscente ad eccezione del blocco di decisione che in uscita prevede due flussi. Un caso a parte è il blocco connettore che non ha un ruolo effettivo nella descrizione dell’algoritmo ma serve solo ad unire parti diverse di un diagramma suddiviso per motivi di spazio. Come esempio di primo banale diagramma di flusso, vediamo ancora una volta l’algoritmo per il calcolo del perimetro del poligono regolare, mostrato nella figura 2.3. Gli algoritmi 8 Figura 2.3 Strutture di controllo fondamentali degli algoritmi Le istruzioni che compongono un algoritmo sono organizzate in ‘strutture’ che permettono di avere un ‘controllo’ sul flusso di elaborazione. Le ‘strutture di controlo fondamentali’, cioè quelle grazie alle quali si può descrivere qualsiasi algoritmo, sono: • ‘sequenza’; • ‘selezione’; • ‘iterazione’ o ‘ciclo’. Sequenza La sequenza è una struttura molto banale e presente in qualsiasi algoritmo in quanto contiene semplicemente un successione di istruzioni che devono essere eseguite nell’ordine in cui sono elencate e questo corrisponde appunto alla natura di un qualsiasi algoritmo. Negli algoritmi meno banali avremo però la presenza di strutture di sequenza all’interno di altri tipi di strutture. L’inizio e la fine di una sequenza si indicano nel linguaggio di progetto con la parentesi graffa aperta e chiusa mentre nei diagrammi di flusso non sono previsti simboli particolari. L’algoritmo del perimetro del poligono regolare è un esempio in cui è presente solo la struttura di sequenza; come ulteriore esempio vediamo, con il linguaggio di progetto, l’algoritmo per lo scambio del valore di due variabili: { intero A,B,C; leggi (A, B); C = A; A = B; B = C; stampa(A, B); } Gli algoritmi 9 In questo algoritmo la variabile C non è né di input né di output ma è comunque indispensabile per l’elaborazione che deve essere svolta; si dice allora che è una ‘variabile di appoggio’. Selezione La struttura, o ‘costrutto’ di selezione permette di: • eseguire una o più istruzioni al verificarsi di una certa condizione; in tal caso si parla di ‘selezione a una via’; • eseguire una o più istruzioni al verificarsi di una certa condizione ed altre istruzioni se non si verifica; in questo caso si parla di ‘selezione a due vie’. Nel linguaggio di progetto il costrutto di selezione a una via si indica con: se (condizione) { sequenza di istruzioni } Mentre quello di selezione a due vie con: se (condizione) { sequenza1 di istruzioni } altrimenti { sequenza2 di istruzioni } Osservando questi primi costrutti si può già notare come le strutture di controllo possano essere ‘annidate’ le une nelle altre: infatti all’interno dei rami delle istruzioni di selezione si trovano delle «sequenze» di istruzioni (che possono comunque essere costituite anche da una sola istruzione). Come esempio consideriamo il problema dell’individuazione della soluzione di un’equazione di primo grado, a*x = b, scrivendo il relativo algortimo in linguaggio di progetto: { intero a,b,x; leggi (a,b); se ( (a==0) && (b==0) ) { stampa ("Equazione indeterminata"); } se ( (a==0) && (b!=0) ) { stampa ("Equazione impossibile"); } se (a!=0) { Gli algoritmi Figura x 2.9 = 10 b/a; stampa (x); } } In questo esempio si è fatto uso di un’importante convenzione per la scrittura dell’algoritmo che è l’‘indentazione’; si tratta di scrivere leggermente spostate verso destra (di qualche spazio o di una tabulazione) le sequenze di istruzioni annidate dentro altre strutture allo scopo di migliorare la leggibilità dell’algoritmo. Si tratta di una tecnica molto utile, anche e soprattutto quando gli algoritmi diventano programmi e si devono scrivere sorgenti, spesso molto lunghi e complessi, usando un qualche linguaggio di programmazione. Nell’esempio è stata indentata anche la sequenza principale dell’algoritmo, quella contenuta tra le parentesi graffe di inizio e fine. Naturalmente l’indentazione non è obbligatoria né nel linguaggio di progetto né usando i linguaggi di programmazione veri e propri, ma è fortemente consigliata. I suoi effetti benefici si apprezzano meglio riscrivendo l’algoritmo precedente usando strutture di selezione a due vie annidate invece che una sequenza di tre strutture di selezione a una via: { intero a,b,x; leggi (a,b); se ( (a==0) && (b==0) ) { stampa ("Equazione indeterminata"); } altrimenti { se ( (a==0) && (b!=0) ) { stampa ("Equazione impossibile"); } altrimenti { x = b/a; stampa (x); } } } Il fatto che si siano scritti due algoritmi per la soluzione di uno stesso problema è da considerare del tutto normale (e anzi, ce ne sono anche diversi altri); infatti i metodi di soluzione possono essere vari e differenti l’uno dall’altro, sarà compito del bravo programmatore individuare la soluzione migliore tra quelle possibili. Per quanto riguarda i diagrammi di flusso, non esiste un blocco particolare per designare il costrutto di selezione; si fa uso, in modo opportuno, del blocco di decisione, come mostrato nella figura 2.9 (a sinistra abbiamo la selezione a una via, a destra quella a due vie). Gli algoritmi 11 Figura 2.9 Nella successiva figura 2.10 vediamo l’algoritmo per la soluzione dell’equazione di primo grado nella «seconda versione» (quella con le selezioni annidate). Figura 2.10 Concludiamo questa breve introduzione sul costrutto di selezione osservando come il suo utilizzo permetta di scrivere algoritmi «completi» secondo la definizione data nel paragrafo 2.1. In realtà sarebbe meglio dire che tale costrutto permette di «tentare» di scrivere algoritmi che tengano conto di tutte le eventualità possibili al momento dell’esecuzione, ad esempio al variare dei dati in input; questa infatti non è un’operazione semplice, specie in presenza di problemi di una certa consistenza, ed è uno dei principali motivi di malfunzionamento di molti programmi. Gli algoritmi 12 Iterazione Figura 2.13 La struttura, o costrutto, di iterazione (o ciclo) permette di ripetere una o più istruzioni in sequenza per un numero potenzialmente infinito di volte. Grazie alla sua presenza è possibile che gli algoritmi pur avendo la proprieta della «finitezza» possano (in linea teorica) prevedere esecuzioni con un numero infinito di passi (come detto nel paragrafo 2.1). La ripetizione del gruppo di istruzioni soggette alla iterazione è soggetta al verificarsi di una certa condizione ed il controllo su di essa può essere in «testa» alla struttura oppure in «coda» alla stessa. Ci sono quindi due tipi di iterazione: • ‘iterazione con controllo in testa’; • ‘iterazione con controllo in coda’. Nel linguaggio di progetto i due tipi vengono realizzati nel modo seguente: mentre (condizione) { sequenza di istruzioni } esegui { sequenza di istruzioni } mentre (condizione); In entrambi i casi siamo in presenza di un ‘corpo del ciclo’, che corrisponde alla sequenza di istruzioni (anche una sola) da ripetere, e di un ‘controllo del ciclo’ rappresentato dalla condizione posta in testa o in coda al costrutto. Il ‘criterio di arresto’ del ciclo (o l’‘uscita’ dal ciclo) si ha quando la condizione cessa di essere vera; si dice quindi che entrambi questi tipi di iterazione sono ‘per vero’. La fondamentale differenza tra i due costrutti di ciclo è nel fatto che il corpo del ciclo con controllo in coda viene sempre eseguito almeno una volta. Come per la selezione, anche per l’iterazione non esistono blocchi particolari da usare nei diagrammi di flusso; si deve ancora una volta usare in modo appropriato il blocco di decisione. Nella figura 2.13 vediamo a sinistra il ciclo con decisione in testa e a destra quello con decisione in coda; vengono indicati i componenti dei costrutti, controllo e corpo, e i punti di ingresso e uscita dai costrutti stessi. Gli algoritmi 13 Figura 2.13 Prima di vedere esempi riguardanti i cicli introduciamo l’uso di alcuni tipi di variabili che hanno un ruolo importante in molti algoritmi iterativi: • si dice ‘contatore’ una variabile che si usa per contare il numero di volte che si è eseguita una certa iterazione; si inizializza di solito a 0 oppure a 1, secondo come si imposta il ciclo; • si dice ‘accumulatore’ una variabile che si usa per contenere il risultato di operazioni (somme o prodotti) successive; si imposta a zero se si devono accumulare somme, a 1 se si accumulano prodotti. Come primo esempio vediamo l’algoritmo per il calcolo del prodotto tra due interi positivi a e b come successione di somme: 1 { 2 intero a,b,p,cont; 3 leggi (a,b); 4 p = 0; 5 cont = 0; 6 mentre (cont < b) { p = p + a; 7 cont = cont + 1; 8 } 9 stampa(p); 10 11 } In questo listato sono stati aggiunti i numeri di riga in modo da facilitare la stesura della ‘tavola di traccia’ che è uno strumento grazie al quale si può esaminare il flusso di elaborazione dell’algoritmo e verificarne la correttezza. Si tratta di una tabella in cui si indicano nelle colonne i nomi delle variabili (almeno le più importanti e significative per l’elaborazione) e nelle righe i passi dell’algoritmo che via via si svolgono partendo da valori di input scelti a piacere; può essere utile inserire anche una colonna in cui si indica il valore di verità della condizione da cui dipende l’esecuzione dell’iterazione. Quando si esegue la tavola di traccia si deve avere l’accortezza di esaminare il comportamento dell’algoritmo anche quando i valori di ingresso sono ‘critici’ o ‘ai limiti’; nel caso del Gli algoritmi 14 prodotto avremo tale situazione per a uguale a zero, b uguale a zero o entrambi uguali a zero. Vediamo una prima tavola di traccia del nostro algoritmo scegliendo due valori «normali» delle variabili, ad esempio a=5 , b=3 : Passo Istr. cont p 1 2 3 4 5 6 7 8 9 10 11 12 13 14 3 4 5 6 7 8 6 7 8 6 7 8 6 10 0 0 0 1 1 1 2 2 2 3 3 3 0 0 0 5 5 5 10 10 10 15 15 15 15 contr. ciclo V V V V V V V V V F F Il valore di output è, correttamente, 15. Adesso compiliamo la tavola di traccia di uno dei casi critici, ad esempio con a=7 e b=0 : Passo Istr. cont p 1 2 3 4 5 3 4 5 6 10 0 0 0 0 0 0 0 contr. ciclo F F Il valore di output è, correttamente, 0. Infine vediamo un altro caso abbastanza particolare con a=8 e b=1: Passo 1 2 3 4 5 6 7 8 Istr. 3 4 5 6 7 8 6 10 cont 0 0 0 1 1 1 p 0 0 0 8 8 8 8 contr. ciclo V V V F F Il valore di output è, correttamente, 8. Come ulteriore esempio vediamo un algoritmo simile per il calcolo dell’elevamento a potenza come successione di prodotti. In questo caso introduciamo una prima forma rudimentale di ‘controllo dell’input’; vogliamo cioè verificare che i valori di partenza siano: la base a positiva e l’esponente b non negativo (il fatto che siano entrambi interi viene invece ancora dato per scontato e non viene controllato). Gli algoritmi 1 15 { 2 intero a,b,p,cont; 3 esegui { leggi (a,b); 4 5 } 6 mentre ( (a <= 0) || (b < 0) ); 7 p = 1; 8 cont = 0; 9 mentre (cont < b) { 10 p = p * a; cont = cont + 1; 11 } 12 stampa(p); 13 14 } La verifica viene fatta con l’uso di un ciclo con controllo in coda che fa ripetere l’input finché i valori non sono corretti; si noti come sia indispensabile l’uso di questo tipo di ciclo in quanto almeno una volta le istruzioni del corpo devono essere eseguite. Altra osservazione importante riguarda il valore di partenza dell’accumulatore p che è 1 in quanto l’accumulo avviene tramite dei prodotti. Anche in questo caso eseguiamo la tavola di traccia con i valori a=5 , b=3 prendendo in esame solo i passi successivi al controllo dell’input che non influiscono sul calcolo della potenza: Passo Istr. cont p 1 2 3 4 5 6 7 8 9 10 11 12 13 7 8 9 10 11 9 10 11 9 10 11 9 13 0 0 0 1 1 1 2 2 2 3 3 3 1 1 1 5 5 5 25 25 25 125 125 125 125 contr. ciclo V V V V V V V F F Il valore di output è, correttamente, 125. Adesso compiliamo la tavola di traccia del caso critico più significativo, quello con b=0 (e a qualsiasi, ad esempio, 12): Passo Istr. cont p 1 2 3 4 7 8 9 13 0 0 0 1 1 1 1 Il valore di output è, correttamente, 1. contr. ciclo F F Gli algoritmi 16 Il successivo esempio riguarda l’individuazione del massimo tra un certo numero di valori immessi in successione grazie un apposito ciclo. { intero val, carattere max; risposta; leggi (val); max = val; esegui { leggi (val); se (val > max) { max = val; } stampa ("Vuoi continuare (s/n)"); leggi (risposta); } mentre ( (risposta != ’n’) && (risposta != ’N’) ); stampa(max); } In questo esempio si può evidenziare l’uso della tecnica di ‘elaborazione fuori ciclo’ che permette di inserire un primo valore che viene impostato come massimo; successivamente, tramite un ciclo con controllo in coda, si inseriscono gli altri valori uno alla volta facendo per ognuno il confronto con il valore massimo e adeguando quest’ultimo se necessario. Come ultimo esempio vediamo l’algoritmo per il calcolo della media di n valori. 1 { 2 intero n, val, cont, somma; 3 reale media; 4 esegui { stampa ("Numero di valori desiderati "); 5 leggi (n); 6 7 } 8 mentre (n <= 0); 9 somma = 0; 10 cont = 0; 11 mentre (cont < n) { 12 stampa("Inserire un valore "); 13 leggi (val); 14 somma = somma + val; cont = cont + 1; 15 16 } 17 media = somma/n; stampa (media); 18 19 } In questo algoritmo si fa uso di un ciclo con controllo in coda per inserire, in modo controllato, la quantità di valori su cui impostare il ciclo principale (righe da 4 a 8); il ciclo non si conclude se il valore inserito non è maggiore di zero e questo permette, appunto, di avere un piccolo controllo sul valore n . Successivamente si esegue il ciclo, ad ogni passo del quale il valore immesso viene accumulato nella variabile somma (righe da 11 a 16). Gli algoritmi 17 Infine, esauritosi il ciclo, si calcola e si stampa a video la media (righe 17 e 18). In questo caso vediamo anche la tavola di traccia considerando solo la parte più significativa dell’algoritmo, cioè quella che comprende il ciclo principale di elaborazione; i valori della variabile val sono indicati in tabella al momento dell’esecuzione della relativa istruzione di input, mentre per n supponiamo venga immesso il valore 3 : Passo Istr. somma media val cont 1 2 3 4 5 6 7 8 9 10 11 12 13 14 16 17 9 10 11 12-13 14 15 11 12-13 14 15 11 12-13 14 15 11 17 0 0 0 0 7 7 7 7 10 10 10 10 18 18 18 18 6 7 7 7 7 3 3 3 3 8 8 8 8 8 0 0 0 0 1 1 1 1 2 2 2 2 3 3 3 contr. ciclo V V V V V V V V V V V V F F Concludiamo il paragrafo con i diagrammi di flusso relativi all’algoritmo della potenza calcolata come successione di prodotti e al calcolo della media di n valori, illustrati rispettivanmente nelle figure 2.24 e 2.25. Figura 2.24: potenza come succ. di prodotti Figura 2.25: media di n valori Strutture di controllo derivate degli algoritmi Le ‘strutture di controllo derivate’ hanno questo nome in quanto si possono ottenere dal quelle fondamentali, e ne estendono le potenzialità, ma non sono essenziali per la stesura degli algoritmi. Esistono due strutture derivate: • ‘selezione multipla’; • ‘iterazione calcolata’. Selezione multipla Con la selezione multipla si possono eseguire sequenze diverse di istruzioni in base al valore di una certa variabile intera. Nel linguaggio di progetto il costrutto da utilizzare è: valuta (var) { ca- so valore1: sequenza di istruzioni 1 Gli algoritmi 19 caso valore2: sequenza di istruzioni 2 . . . caso valoreN: sequenza di istruzioni N de- fault: sequenza di istruzioni N+1 } Nei diagrammi di flusso si può utilizzare il blocco mostrato nella figura 2.27. Figura 2.27 Il costrutto di selezione multipla non è essenziale perché al suo posto si possono usare opportune combinazioni di selezioni «normali». Come esempio di uso di questa struttura derivata vediamo la realizzazione di un semplice ‘menu di scelta’ che esegue istruzioni diverse in base al valore scelto dall’utente. { intero scelta; ese- gui { stampa ("Menu delle scelte"); stampa ("1 - Funzione 1"); stampa ("2 - Funzione 2"); stampa ("3 - Funzione 3"); stampa ("9 - Fine"); leggi (scelta); valuta (scelta) { caso 1: Gli algoritmi 20 stampa ("Hai scelto la funzione 1"); salta; caso 2: stampa ("Hai scelto la funzione 2"); salta; caso 3: stampa ("Hai scelto la funzione 3"); salta; caso 9: stampa ("Arrivederci e grazie!"); salta; default: stampa ("Scelta errata"); } mentre (scelta != 9); } L’istruzione ‘salta’ porta il flusso di esecuzione al termine del costrutto ‘valuta’ e ha lo scopo di non far eseguire le istruzioni dei casi successivi. Il caso ‘default’ viene inserito per eseguire un blocco di istruzioni nel caso il valore della variabile valutata non sia nessuno di quelli previsti. Ovviamente l’esempio non ha alcuna utilità pratica e serve solo a mostrare le modalità di impostazione e gestione del menu. Iterazione calcolata L’iterazione calcolata è solo un modo più compatto di scrivere il ciclo con controllo in testa; per tale motivo nei diagrammi di flusso non esiste alcun costrutto particolare che la rappresenti. Nel linguaggio di progetto abbiamo invece: per (cont=valore1;cont<valore2;cont=cont+1) { sequenza di istruzioni } Il significato dell’istruzione è abbastanza intuibile: • cont viene inizializzato al valore1; • il ciclo viene eseguito finché rimane vera la condizione che cont sia minore di valore2 ; • ad ogni passo del ciclo cont viene incrementato di una unità. Rispetto all’iterazione con controllo in testa vista precedentemente si notano: • lo spostamento dell’istruzione di inizializzazione del contatore, che prima avveniva fuori dal costrutto del ciclo e adesso è inglobata nell’intestazione; • lo spostamento dell’istruzione di incremento del contatore, che prima avveniva nel cor- po del ciclo e adesso è inglobata nell’intestazione. Gli algoritmi 21 Quello mostrato può essere considerato solo un esempio di ciclo calcolato: nulla vieta infatti di avere valori iniziali, condizioni e incrementi del tutto diversi. Come esempio vediamo l’algoritmo per la stampa della tabellina pitagoriga per i valori da 1 a 10 dove usiamo due iterazioni calcolate una annidata nell’altra. { intero i, j; per (i=1;i<=10;i=i+1) { per (j=1;j<=10;j=j+1) { stampa (i*j); } } } Nella figura 2.31 vediamo anche il relativo diagramma di flusso. Figura 2.31 La programmazione strutturata Con il termine ‘programmazione strutturata’ si intende una tecnica di realizzazione de- gli algoritmi e dei relativi programmi che prevede il solo utilizzo delle strutture di controllo fondamentali. In realtà si può parlare di algoritmi o programmi ‘strutturati’ anche in presenza di strutture di controllo derivate, in quanto esse, come abbiamo visto in precedenza, sono ottenibili da quelle fondamentali. Gli algoritmi 22 Ogni struttura di controllo può essere immaginata come un blocco, con un solo flusso in entrata ed uno solo in uscita, che al suo interno può contenere una singola istruzione o una ulteriore struttura di controllo (annidamento). Come esempio consideriamo il problema del calcolo del fattoriale di un numero n maggiore di 1 e risolviamolo con i tre diversi algoritmi descritti dai diagrammi a blocchi delle figure 2.32, 2.33 e 2.34; nelle tre soluzioni viene tralasciato il controllo sulla validità dell’input. Figura 2.32 Figura 2.33 Gli algoritmi 23 Figura 2.34 Questi tre algoritmi sono ‘funzionalmente equivalenti’ cioè danno gli stessi risultati partendo dagli stessi dati di input. Il primo algoritmo contiene un ciclo con controllo in testa, il secondo un ciclo con controllo in coda, il terzo è da scartare perché contiene un ciclo non valido che ha il controllo né in testa né in coda. Il teorema di Jacopini-Bohm Ci si potrebbe chiedere se le tre strutture fondamentali formano un insieme di strutture ‘completo’ (cioè sufficiente a descrivere tutti gli algoritmi). A tale proposito esiste il teorema di ‘Jacopini - Bohm ’ del 1966 (che non dimostriamo) che afferma: «ogni algoritmo può essere espresso con le sole tre strutture di controllo fondamentali». Quindi è lecito pretendere da parte dei programmatori l’uso delle tecniche di programmazione strutturata, anche perché con esse si hanno i seguenti vantaggi: • maggiore facilità di uso della metodologia top-down (illustrata nel prossimo paragrafo); • maggiore leggibilità degli algoritmi; • maggiore possibilità di individuazione di eventuali errori nella logica risolutiva. Nella figura 2.35 vediamo: a sinistra una porzione di algoritmo non strutturato (il ciclo ha il controllo né in testa, né in coda), a destra la sua strutturazione grazie all’uso di una variabile di tipo ‘switch’ (segnalatore), chiamata sw: Gli algoritmi 24 Figura 2.35 L’uso di variabili di segnalazione avviene solitamente nel seguente modo: • la variabile viene «spenta» (assume il valore 0) all’inizio dell’algoritmo; • viene poi «accesa» (assume il valore 1) al verificarsi di una certa condizione; • infine viene «testata» in modo da decidere le azioni da intraprendere in base al suo valore. Le tecniche Top-Down Un algoritmo si ottiene come risultato di un procedimento di ‘analisi’ che può essere suddiviso in tre fasi: • ‘ definizione del problema’ con i suoi dati in ingresso ed in uscita; • individuazione del ‘metodo’ di soluzione; • ‘descrizione’ dell’algoritmo, con il linguaggio di progetto o con il diagramma a blocchi. Una metodologia di analisi molto usata e anche molto conveniente è quella che fa uso delle tecniche top-down o delle ‘scomposizioni successive’. Esse si basano appunto su una scomposizione del problema in sottoproblemi che a loro volta possono essere ulteriormente scomposti fino ad arrivare a sottoproblemi molto semplici costituiti solo da operazioni molto elementari, direttamente «eseguibili» da parte dell’esecutore. Naturalmente tale metodologia è utile soprattutto in presenza di problemi abbastanza complessi; in caso di problemi semplici, come quelli degli esempi finora esaminati, la sequenza di operazioni proposta nei vari algoritmi risolutivi, può essere considerata già sufficientemente semplice. Ad ogni sottoproblema viene associato un «sottoalgoritmo» o sottoprogramma, che nei diagrammi di flusso è individuato dal blocco rettangolare con le bande laterali mostrato nella figura 2.2 (tralasciamo invece l’uso dei sottoalgoritmi nel linguaggio di progetto). Gli algoritmi 25 Il dettaglio del sottoalgoritmo viene descritto con i consueti blocchi scrivendo però nel blocco iniziale il suo nome e l’eventuale lista dei valori (parametri) su cui deve operare e nel blocco di fine la parola «ritorno» seguita dall’eventuale valore di ritorno. Come esempio consideriamo il problema del calcolo del coefficente binomiale «c su k» dove c e k sono due interi positivi con c > k. La soluzione proposta si basa sulla formula di calcolo del coefficente e cioè: c! / (k!*(c-k)!) dove il simbolo «!» indica il fattoriale; nell’algoritmo si fa uso di un opportuno sottoalgoritmo per il calcolo del fattoriale di un valore n . Il tutto è mostrato nella figura 2.36. Figura 2.36 In generale le tecniche di analisi e programmazione top-down sono da consigliare per almeno tre motivi: • ‘semplificazione’ dell’algoritmo o del programma che viene scomposto in parti pre- sumibilmente più semplici; occorre però prestare attenzione all’incremento di complessità dovuto all’esigenza di integrare i vari sottoprogrammi; • ‘risparmio di istruzioni’ nel programma; nell’esempio precedente appare abba- stanza ovvio come, con l’uso del sottoprogramma fat , si evita di scrivere tre volte il pro- cedimento per il calcolo del fattoriale, come sarebbe richiesto dalla formula del coefficente binomiale; • ‘riuso del codice’; si parla in questo caso di ‘programmazione modulare’ cioè del fatto che si possono scrivere sottoprogrammi (o ‘moduli’) in modo che possano essere riutilizzati per programmi diversi e da persone diverse. Gli algoritmi 26 Questi temi, pur molto interessanti, esulano dagli scopi di questa dispensa e quindi non vengono ulteriormente approfonditi al pari delle nozioni sui parametri e sui valori di ritorno dei sottoprogrammi; per approfondimenti su questi aspetti si rimanda allo studio di specifici linguaggi di programmazione. Concludiamo con un ultimo esempio, mostrato nella figura 2.37, in cui vediamo il diagramma di flusso relativo alla gestione di un menu che permette di scegliere tra tre opzioni, ognuna corrispondente ad un certo sottoprogramma (non ulteriormante dettagliato). i Corso sul Linguaggio C++ Edizione 2010 1 Capitolo 1. Fondamenti del linguaggio C Caratteristiche del linguaggio, creazione dei primi programmi . 1.1 Caratteristiche e storia del linguaggio C. Nel 1972, presso i Bell Laboratories, Dennis Ritchie progettava e realizzava la prima versione del linguaggio C. Ritchie aveva ripreso e sviluppato molti dei principi e dei costrutti sintattici del linguaggio BCPL, sviluppato da Martin Richards, e del linguaggio B, sviluppato da Ken Thompson, l'autore del sistema operativo Unix. Successivamente gli stessi Ritchie e Thompson riscrissero in C il codice di Unix. Il C si distingueva dai suoi predecessori per il fatto di implementare una vasta gamma di tipi di dati (carattere, interi, numeri in virgola mobile, strutture) non originariamente previsti dagli altri due linguaggi. Da allora ad oggi il C ha subito trasformazioni: la sua sintassi è stata affinata, soprattutto in conseguenza della estensione object-oriented (C++). Il C++, come messo in evidenza dallo stesso nome, rappresenta una evoluzione del linguaggio C: il suo progettista (Bjarne Stroustrup) quando si pose il problema di trovare uno strumento che implementasse le classi e la programmazione ad oggetti, invece di costruire un nuovo linguaggio di programmazione, pensò bene di estendere un linguaggio già esistente, il C appunto, aggiungendo nuove funzionalità. In questo modo, contenendo il C++ il linguaggio C come sottoinsieme, si poteva riutilizzare tutto il patrimonio di conoscenze acquisito dai programmatori in C (linguaggio estremamente diffuso in ambito di ricerca) e si poteva fare in modo che tali programmatori avessero la possibilità di acquisire le nuove tecniche di programmazione senza essere costretti ad imparare un nuovo linguaggio e quindi senza essere costretti a disperdere il patrimonio di conoscenze già in loro possesso. Così le estensioni ad oggetti hanno fornito ulteriore linfa vitale al linguaggio C. Le principali caratteristiche del linguaggio C e C++ sono: - Si tratta di un linguaggio general purpose, ovvero può essere utilizzato per realizzare programmi di natura diversa (dai sistemi operativi, ai programmi gestionali e ai videogiochi) - Produce dei programmi efficienti. E’ stato progettato per scrivere più facilmente i programmi che costituiscono un sistema operativo i quali devono essere molto efficienti. Normalmente, prima della creazione del linguaggio C, i sistemi operativi venivano scritti in Assembler. Il linguaggio C è un linguaggio di alto livello (più vicino al linguaggio dell’uomo) che però mantiene anche delle caratteristiche tipiche dei linguaggi di basso livello (più vicini al linguaggio della macchina e quindi più scomodi da utilizzare per il programmatore). - Supporta sia il paradigma procedurale che il paradigma della programmazione orientata agli oggetti. Con il paradigma procedurale il programma consiste in una serie di istruzioni che dicono al computer come acquisire i dati in ingresso e produrre i risultati finali, mentre con il paradigma orientato agli oggetti il programma è costituito da una serie di oggetti software ognuno di quali ha determinate caratteristiche, il programmatore mette insieme e/o costruisce questi oggetti. 1.2 Differenze principali tra C e C++ Come detto il linguaggio C++ è un’estensione del linguaggio C e quindi tutti i programmi C possono essere compilati da un compilatore C++ Corso sul Linguaggio C++ Edizione 2010 Il principale miglioramento del linguaggio C++ rispetto al linguaggio C è stata l’introduzione dei costrutti necessari per realizzare programmi orientati agli oggetti. Nel seguito noi non ci occuperemo della programmazione ad oggetti ma solo di quella procedurale per la quale le differenze tra C e C++ sono limitate, eccone alcune: - I commenti. Con il linguaggio C i commenti vanno racchiusi tra i simboli /* ……*/, nel linguaggio C++ è stato introdotto anche il simbolo // che indica al compilatore che tutto quello che segue nella riga è un commento; - Dichiarazioni all’interno dei blocchi. Con il C++ sono consentite, con il C no. - Nuovi nomi di tipi. Sono stati introdotti nuovi nomi di tipi che semplificano la notazione. - E’ stato introdotto lo specificatore const. - Più funzioni possono utilizzare lo stesso nome, per distinguerle basta che abbiano un numero o tipo di parametri diverso (overloading di funzioni). - Si possono richiamare le funzioni con un numero di parametri inferiore a quello dichiarato, i parametri non passati vengono sostituiti con valori standard - Sono stati introdotti gli operatori new e delete per il controllo dell’allocazione dinamica della memoria. 1.3 Installazione del Dev-C++ Il linguaggio C è un linguaggio compilato, questo significa che dopo aver scritto il programma sorgente, per poterlo eseguire bisogna prima compilarlo, ovvero tradurlo in linguaggio macchina. Il programma risultante dopo questa traduzione è detto programma oggetto. La traduzione viene fatta da un altro programma che si chiama compilatore. Per poter scrivere ed eseguire i programmi in linguaggio C bisogna procurarsi un compilatore C. Sul server del laboratorio è presente l’ambiente di sviluppo Dev C++ versione 4.01 che comprende un compilatore C++ che è open source e distribuito con licenza GNU ed è quindi liberamente scaricabile e utilizzabile nel rispetto della licenza. Per gli aggiornamenti si può visitare il sito del produttore: http://www.bloodshed.net/c/index.html. Attualmente è disponibile anche una versione più recente, la versione 4.9.9.2, (presente anch’essa sul server nella cartella “Aggiornamenti”) ma, sebbene sia più comoda da utilizzare, è ancora una versione beta ed è diversa da quella ufficialmente utilizzata per le olimpiadi dell’informatica. Per installare il compilatore e il debugger (programma per facilitare la correzione degli errori) presenti sul server (insieme a molto altrro materiale) all’indirizzo \\192.168.2.198\CorsoCPP\, bisogna seguire le seguenti istruzioni. 1) Aprire la cartella DevCPP e Copiare la cartella C++ che si trova all’interno sul proprio computer; 2) Decomprimere il file “devcpp4.zip” ed eseguire il file “setup.exe”; ciò installerà la versione 4.0 del Dev-C++. 3) Sostituire il file “DevCpp.exe” contenuto nella directory di installazione del Dev-C++ (ad esempio “C:\Dev-C++”) con il file “DevCpp.exe” contenuto dentro l’archivio devcpp401.zip. 4) Decomprimere il file insight5_win32.zip (che contiene il debugger per l’ambiente) e che si trova nella cartella Debugger; nell’archivio sono contenute due cartelle: “bin” e “share”. Copiare la directory “share” nella directory di installazione del Dev-C++ (ad esempio “C:\Dev-C++”), e copiare i file contenuti nella cartella “bin” nella cartella “bin” della directory di installazione del Dev-C++ (ad esempio C:\Dev-C++\bin) 5) A questo punto avete installato l’ambiente di sviluppo. Conviene crearsi una cartella personale sul disco rigido del computer dove mettere tutti i programmi che scriverete e impostarla come cartella di default per l’ambiente di sviluppo. Per farlo prima create la cartella (per esempio la cartella c:\<vostronome>\cpp); poi lanciate l’ambiente di sviluppo da start programmiDevC++Dev-C++; rispondete si alla richiesta se abbinare le estensioni .cpp, .dev ecc. al Dev-C++; dopo aver avviato l’ambiente di sviluppo fate click su OptionsEnvironment Options sulla barra dei menù e nella scheda Preferences, impostate la Default directory con il percorso della directory che avete creato per i programmi C++ (vedi figura che segue). 2 Corso sul Linguaggio C++ Edizione 2010 1.4 Gli elementi di un programma C++ Un qualsiasi programma C++ è costituito dai seguenti elementi: Direttive al preprocessore Dichiarazioni di variabili globali, funzioni e prototipi di funzioni Programma principale (funzione main) Funzione 1 Funzione N Tutti gli elementi sono facoltativi esclusa la funzione main (il programma principale) che deve sempre esserci. Le regole con cui deve essere scritto un programma o un’istruzione di un programma sono dette regole sintattiche in analogia per le regole del nostro linguaggio naturale. Per descrivere queste regole si utilizzano spesso, nei manuali dei linguaggi di programmazione, i grafi sintattici che rappresentano tutti i modi in cui si possono costruire le frasi valide del linguaggio. Per esempio, utilizzando un grafo sintattico per descrivere quanto detto prima, un programma C++ deve essere costruito nel seguente modo: 3 Corso sul Linguaggio C++ Edizione 2010 Programma C++ Funzione main() Direttive al preprocessore Variabili globali Funzioni e Prototipi di funzioni Dichiarazioni altre funzioni Il programma Principale (così come qualsiasi funzione) deve avere la seguente struttura: [<Tipo restituito>] main ([elenco parametri]) { [istruzione1;] [istruzione2;] …….. [istruzione n;] } <Tipo restituito> indica il tipo di dati restituito dalla funzione, in mancanza di indicazione si sottintende int ovvero intero. Una delle istruzione della funzione è normalmente: return <valore> che fa terminare la funzione restituendo il valore indicato (se non viene inserita il compilatore inserisce automaticamente return 0). Il più semplice programma C è quindi il seguente: int main() { return 0 } Se una funzione non restituisce nessun valore si può anche indicare, come tipo, la parola chiave void che rappresenta un tipo di dato particolare. Questo è il modo in cui si dichiarano nel linguaggio C le procedure. Per esempio se il programma principale non deve restituire nessun valore si può scrivere: void main() { } Nel seguito, utilizzeremo principalmente questa seconda sintassi per la specificazione della funzione che costituisce il programma fondamentale main. 1.5 Dal codice sorgente al codice eseguibile Vediamo ora come si realizza praticamente un programma C++. I passi da seguire per creare un programma eseguibile sono i seguenti: 1. Scrittura del programma sorgente; Apriamo l’ambiente di sviluppo Dev-C++ e scegliamo New Source File dal menù File. Verrà visualizzato il file default.txt che contiene la base per un qualsiasi programma C. A questo punto scriviamo il programma, per esempio quello della figura che segue. Alla fine salviamo il file usando la voce Save Unit o Save Unita s .. dal menù File. Il programma verrà salvato con il nome che scegliamo e normalmente con l’estensione .cpp (verificare che la cartella sia quella giusta); 2. Compilazione e linking. Un programma scritto in linguaggio C non può essere eseguito così com’è perché il computer capisce solo il linguaggio macchina. Per eseguire un programma bisogna compilarlo (tradurlo in linguaggio macchina) e unirci i sottoprogrammi standard del linguaggio (operazione di link), dopo queste due operazioni viene creato un programma in linguaggio macchina eseguibile con estensione 4 Corso sul Linguaggio C++ Edizione 2010 .exe, questo secondo programma può essere mandato in esecuzione. Per eseguire la compilazione e il linking bisogna scegliere Compile dal menù Execute. Se non vengono trovati errori verrà creato il programma eseguibile con lo stesso nome del programma sorgente ma con estensione “.exe”. Nella scheda Compiler che si trova nel riquadro posto nella parte inferiore della finestra del Dev-C++, vengono visualizzati i messaggi del compilatore. Se la compilazione è andata a buon fine verrà viaulizzato il messaggio <nome programma> compiled successfully, altrimenti vengono visualizzati gli errori trovati con l’indicazione della riga dove sono stati trovati; 3. Esecuzione. Scegliere Run dal menù Execute. Viene aperta la finestra di esecuzione dove va l’output del programma e il programma viene eseguito. Al termine la finestra di esecuzione viene chiusa automaticamente. Per esempio provate a scrivere il seguente programma: Vediamo che cosa significa quello che abbiamo scritto. Dalla parola main, seguita da parentesi tonda aperta e chiusa, inizia l'esecuzione del programma. Il corpo del programma, che comincia dalla parentesi graffa aperta e finisce alla parentesi graffa chiusa, è composto da una serie di istruzioni. Le istruzioni cout << …. sono operazioni che riguardano l’invio di stringhe (sequenze di caratteri racchiuse fra doppi apici) verso una unità di output: nel nostro caso il monitor. Nell’esempio proposto cout sta ad indicare il canale di output e il simbolo << è l’operatore di output. L’istruzione cout << “abc”; si potrebbe così tradurre: inserisci nel canale di output la stringa specificata. Per quanto riguarda l’instradamento verso il canale, per il momento, si può pensare come ad una conduttura che porta al monitor e nella quale si inseriscono una di seguito all’altra le stringhe specificate. L’istruzione system("PAUSE") impedisce alla schermata di esecuzione di scomparire senza darci il tempo di vedere il risultato del nostro programma, questa istruzione manda in esecuzione il comando PAUSE del sistema operativo. Questo comando invia nel canale di output la stringa “Premere un tasto per continuare …” e aspetta fino a quando non viene premuto un tasto. Se aprite una finestra di comandi (StartProgrammiAccessoriPrompt dei comandi) potete provare a scrivere il comando PAUSE e vederne l’effetto. Con la funzione system si può mandare in esecuzione qualsiasi comando del sistema operativo o programma. Per esempio: • system (“CLS”) pulisce la finestra di output (CLS=Clear Screen). 5 Corso sul Linguaggio C++ Edizione 2010 • system(“Color 1F”) imposta il colore blu come sfondo e il colore bianco come primo piano (il comando generale è color xy dove x e y sono due cifre esadecimali con x=colore di sfondo e y=colore di primo piano) Per utilizzare la funzione system nei nostri programmi dobbiamo inserire all’inizio #include <stdlib.h> Ogni istruzione deve terminare con un carattere di punto e virgola. Se paragoniamo un programma ad un testo, le istruzioni sono le frasi e ogni istruzione è costituita da una serie di parole dette token separate le une dalle altre da spazi o simboli (per esempio la punteggiatura). Così come il significato di una frase non cambia in base al numero di spazi tra le parole, anche il numero di spazi inserito tra le parole di una istruzione C++ è irrilevante, mentre cambia il significato della frase se si inserisce uno spazio tra i caratteri di una parola spezzandola. Per poter utilizzare cout, come le altre funzioni di entrata/uscita, si deve inserire all'inizio del testo la direttiva al preprocessore #include <iostream.h> che avverte il compilatore di includere i riferimenti alla libreria dei canali standard di input/output (iostream sta per canali di input/output). Il C++ è un linguaggio case-sensitive ovvero distingue tra lettere maiuscole e minuscole; dunque occorre fare attenzione, se si scrive MAIN() o Main() non si fa riferimento a main(). Proviamo a compilare ed eseguire il programma che abbiamo scritto, se non ci sono errori nella finestra di esecuzione compare la scritta : abcdefghilmnopqrstuvzPremere un tasto per continuare … Se si desidera che ogni stringa venga prodotta su una linea separata, si deve inserire \n nel canale subito dopo la stringa e prima della chiusura dei doppi apici (\n è un carattere speciale che fa andare a capo il cursore sul monitor, si tratta del carattere new line), come nel seguente listato. #include <iostream.h> #include <stdlib.h> main(){ cout << "abc" << “\n”; cout << "def" << “\n”; cout << "ghi" << “\n”; cout << "lmn" << “\n”; cout << "opqrs" << “\n”; cout << "tuvz" << “\n”; system(“PAUSE”); } Eseguendo il programma si otterrà la visualizzazione delle seguenti stringhe di caratteri abc def ghi lmn opqrs tuvz Premere un tasto per continuare …. In questo caso la prima stringa (abc) viene stampata su video a partire dalla posizione attuale del cursore. Se si vuole cominciare la stampa delle stringhe da una nuova riga basta inserire \n anche all’inizio o in qualsiasi punto anche all’interno della stringa da stampare come nei seguenti esempi: cout << “\n” << “abc” << “\n”; cout << “\nabc\ndef”; 6 Corso sul Linguaggio C++ Edizione 2010 Qui prima si passa ad una nuova riga, poi si stampa la stringa specificata e quindi si posiziona il cursore in una nuova riga. In generale è bene tenere presente che l’effetto di ogni cout è quello di stampare a partire dalla posizione in cui si trovava il cursore (in generale a destra dell’ultima stampa). Per poter modificare tale comportamento è necessario inserire gli opportuni caratteri di controllo. In effetti la sequenza \n corrisponde ad un solo carattere, quello di nuova linea (newline). Esistono altri caratteri di controllo, tutti vengono indicati con una sequenza che inizia con la barra rovesciata(\), di seguito vengono riportati quelli che hanno utilizzo più frequente: Tabella 1. Sequenze di escape Sequenza Effetto porta il cursore all’inizio della riga successiva (ASCII 10 = new line) porta il cursore al prossimo fermo di tabulazione (ogni fermo di tabulazione è fissato ad 8 caratteri) (ASCII 9) \a (alert) fa un beep \f (form feed) nuova pagina (ASCII 12) \r (cariage return) ritorno del carrello nella stampa (ASCII 13) \’ stampa un apice \” stampa le virgolette \\ barra rovesciata \? Punto di domanda \b (Bakspace) Indietro di un carattere. (ASCII 8) \<cifreOttali> Il carattere corrispondente nel codice ASCII al numero ottale \x<cifreEsadec> Il carattere corrispondente nel codice ASCII al numero esadecimale \n \t La sequenza costituita dalla barra rovesciata seguita da uno o più caratteri viene detta sequenza di escape e corrisponde ad un solo carattere. Utilizzando una sequenza di escape si può inserire anche il carattere corrispondente ad un qualsiasi codice ASCII in due modi: utilizzando la barra rovesciata (\) seguita dal codice ASCII scritto in ottale oppure la barra rovesciata e la x (\x) seguita dalle cifre esadecimali del codice ASCII (per esempio \x41 corrisponde al carattere ‘A’). Quando si fanno delle modifiche al programma sorgente per provarlo bisogna compilarlo ed eseguirlo ma attenzione a chiudere l’eventuale finestra di un esecuzione precedente perché, se è ancora aperta, il programma eseguibile è bloccato e il compilatore non può sostituirlo con quello nuovo. Purtroppo, il dev-c++ non segnala nessun errore in questo caso, semplicemente rimanda in esecuzione il vecchio programma. 1.6 le direttive al preprocessore A differenza di altri linguaggio, la compilazione di un programma in linguaggio C prevede una prima fase detta precompilazione, svolta da un apposito modulo detto preprocessore (che non è un dispositivo hardware ma solo una particolare funzione del programma compilatore). Il Preprocessore è normalmente incluso nel compilatore. Nella fase di precompilazione il programma viene preparato per la successiva compilazione. E’ possibile inserire in un programma C delle istruzioni che verranno eseguite dal preprocessore, queste istruzioni sono dette direttive al preprocessore o dichiarative di precompilazione, tutte le direttive al preprocessore hanno la seguente struttura: #<nome direttiva> <dichiarazione della direttiva> con i grafi sintattici: direttive al preprocessore # nome direttiva dichiarazione direttiva Una delle direttive più utilizzate è la direttiva include per esempio: #include <iostream.h> 7 Corso sul Linguaggio C++ Edizione 2010 oppure #include “iostream.h” Questa direttiva dice al preprocessore di inserire nel file sorgente il file “iostream.h” che è un file che contiene i riferimenti alle funzioni standard di Input e Output. Se si usano le parentesi angolari (<…>) il preprocessore ricercherà il file nelle cartelle di sistema (che si possono modificare intervenendo nella scheda Directories visualizzata con la voce di menù OptionsCompiler Options), se si usano i doppi apici cercherà prima nella cartella corrente. Se invece di inserire la direttiva si copiasse il contenuto del file “iostream.h” nel programma sorgente il risultato della compilazione sarebbe lo stesso. I file con estensione .h sono file di intestazione e contengono le dichiarazioni necessarie per richiamare, all’interno del codice sorgente, i sottoprogrammi inclusi in librerie esterne. Molte librerie di sottoprogrammi sono incluse nell’ambiente di sviluppo e, inoltre, qualsiasi programmatore può creare ulteriori librerie personali. Il programmatore che ha creato la libreria deve anche creare uno o più file di intestazione che dovranno essere inclusi nei programmi che utilizzeranno la libreria. Con l’introduzione nel linguaggio C++ di nuove funzionalità, come i template e i namespace, che non esistevano nel linguaggio C, le librerie standard sono state riscritte. Nei programmi C++, comunque, si possono usare sia le vecchie versioni delle librerie che le nuove. In genere i file da includere per le nuove versioni hanno lo stesso nome dei vecchi con una ‘c’ davanti e senza l’estensione .h. Per esempio <stdio.h> diventa <cstdio> e <stdlib.h> diventa <cstdlib>. Se si decide di utilizzare le nuove versione delle librerie bisogna anche specificare che nel programma si userà il namespace std che è quello in cui sono inclusi tutti i nomi di funzione e gli identificatori delle librerie standard. Per esempio si può scrivere: #include <cstdlib> #include <iostream> using namespace std; invece di #include <stdlib.h> #include <iostream.h>. Attenzione, il nuovo file <iostream> richiama al suo interno tutte le librerie standard (quelle che iniziano con “st”) per cui nella maggior parte dei programmi si può anche includere solo la libreria <iostream>: #include <iostream> using namespace std; Nel seguito ho utilizzato la vecchia sintassi che è compatibile con tutte le versioni del linguaggio. 1.7 Le funzioni di I/O Fra le istruzioni più importanti di un programma ci sono quelle di input/output cioè quelle che servono per far leggere i dati da elaborare (input) e visualizzare i risultati dell’elaborazione (output). Per ora utilizzeremo due sole istruzioni con sintassi semplificata, l’istruzione cout, che abbiamo già visto, per l’output e l’istruzione cin per l’input. La sintassi è la seguente: per l’output cout << espressione per l’input cin >> variabile cin sta per console input e dice al computer mandare i dati provenienti dall’unità di input standard (normalmente la tastiera) dentro una o più variabili (se ci sono più variabili, durante l’esecuzione bisogna inserire più dati con 8 Corso sul Linguaggio C++ Edizione 2010 un ritorno a capo oppure uno spazio tra uno e l’altro). Il simbolo >> può essere tradotto con “memorizza in” e dice al computer di prelevare un dato dalla console di input e memorizzarlo nelle variabile. 1.8 Il debug dei programmi con Dev C e Dev Pas Dopo aver installato il compilatore C o pascal e il debugger seguendo le note di installazione riportate sopra, normalmente il compilatore viene impostato automaticamente per generare un programma eseguibile il più possibile efficiente. Purtroppo per poter eseguire il debug è necessario inserire nel programma eseguibile riferimenti al programma sorgente che aumentano le dimensioni del file eseguibile e lo rendono meno efficiente. Quando il compilatore viene installato viene automaticamente impostato per non inserire queste informazioni nel file eseguibile e quindi il debug non funziona! Per farlo funzionare bisogna seguire la seguente procedura: 1. Scegliere Options - Compiler options dal menu 2. Spuntare la casella "Generate debugging information" dalla scheda "Linker" della finestra delle opzioni del compilatore e fare click su OK 3. Compilare il programma (ricompilare se era stato già compilato) ed eseguire il programma facendo click sull'apposita icona della barra degli strumenti oppure sulla voce del menù Execute oppure con F8. 9 Corso sul Linguaggio C++ Edizione 2010 4. Comparirà la finestra del debug con una copia del programma sorgente. Facendo click su Run (o sull'icona corrispondente della barra degli strumenti ) il programma si avvierà, comparirà la finestra del programma (la console per i programmi non windows) e la prima istruzione eseguibile verrà evidenziata nella finestra del debug. 5. Dalla finestra del sorgente è possibile: inserire e togliere punti di interruzione facendo clik con il mouse a sinistra dell'istruzione corrispondente (un punto di interruzione viene visualizzato con un quadratino nero); eseguire un'istruzione alla volta facendo click su l’icona Step ( ) della barra degli strumenti oppu- re su Next ( ), la differenza tra le due si nota solo se l’istruzione è una chiamata ad un sottoprogramma (con Step si passa alla prima istruzione del sottoprogramma, con Next verrà eseguito l’intero sottoprogramma e si passa all’istruzione successiva); far proseguire il programma fino al prossimo punto di interruzione ( cali ( ), visualizzare le variabili lo- ), ecc. Note per l’esecuzione del debugger Se si vuole eseguire un programma utilizzando il debug bisogna avere le seguenti accortezze: 1. la cartella che contiene il file sorgente deve trovarsi sullo stesso volume dove è installato il dev-pas o il dev-c; 2. prima di eseguire il debug accertarsi di aver compilato il programma con l’opzione “Generate debug- 10 Corso sul Linguaggio C++ Edizione 2010 3. ging information” attivabile dal menù OptionsCompiler Options; se si utilizzano dei file in input questi devono trovarsi nella cartella BIN contenuta nella cartella di installazione del del C (per es: C:\Dev-C++\Bin). Note per la versione 4.9.9.2 L’ultima versione del DEV C, presenta dei bug per quanto riguarda i nomi dei file. Se si salva il programma con nomi di file lunghi che contengono caratteri speciali come spazi e parentesi tonde, possono verificarsi problemi. Per esempio se si salva un programma con il nome “Prova programma” e poi si salva nella stessa cartella un altro programma con nome “Prova programma (1)” quando si manda in esecuzione il secondo parte l’eseguibile del primo invece del secondo. Si raccomando di usare nomi corti e non contenenti caratteri speciali. 11 Corso sul Linguaggio C++ Edizione 2010 12 2 Capitolo Corso sul Linguaggio C++ Edizione 2010 2. Tipi di dati, variabili e costanti, le espressioni 2.1 Calcolo dell’area di un rettangolo Un qualsiasi programma, in genere, legge dei dati e produce dei risultati. Tutti i dati utilizzati, sia quelli iniziali che quelli intermedi e finali vengono conservati in memoria. I dati di un programma si distinguono in due categorie: variabili e costanti. I dati variabili sono quelli che possono cambiare da un’esecuzione all’altra dello stesso programma o durante una stessa esecuzione, i dati costanti, invece, assumono sempre lo stesso valore ogni volta che si esegue il programma. Supponiamo di voler calcolare l'area di un rettangolo i cui lati hanno valori interi. Entrano in gioco due variabili, la base e l'altezza, il cui prodotto è ancora un valore intero, l'area appunto. Il programma potrebbe essere il seguente: #include <iostream.h> #include <stdlib.h> // Calcolo area di un rettangolo int main() { int base, altezza; int area; cout << "Calcolo AREA RETTANGOLO \n \n"; cout << "Valore base: "; cin >> base; cout << "\nValore altezza: "; cin >> altezza; area = base*altezza; cout << "\nBase: " << base << " Altezza: " << altezza; cout << "\nArea: " << area << "\n\n"; system("PAUSE"); return 0; } Per rendere evidente la funzione espletata dal programma si è inserito un commento: // Calcolo area rettangolo Il doppio simbolo // indica inizio di commento. Tutto ciò che segue fino alla fine della riga non viene preso in considerazione dal compilatore: serve solo a scopo documentativo. I commenti possono estendersi su più linee e apparire in qualsiasi parte del programma. Naturalmente, per quanto notato prima, il doppio // deve precedere ogni commento in una nuova riga. Se il commento si estende su più righe, può essere più comodo adottare un altro sistema: fare precedere il commento da /* e inserire */ alla fine del commento. Tutto ciò che appare nelle zone così racchiuse non viene preso in considerazione dal compilatore e non ha alcuna influenza sul funzionamento del programma, è però importantissimo per chi legge il programma: è infatti nelle righe di commento che 13 Corso sul Linguaggio C++ Edizione 2010 viene specificato il senso delle istruzioni che seguiranno. Cosa, questa, non immediatamente comprensibile se si leggono semplicemente le istruzioni del linguaggio. Subito dopo main() sono presenti le dichiarazioni delle variabili intere necessarie: int base, altezza; int area; La parola chiave int specifica che l'identificatore che lo segue si riferisce ad una variabile di tipo intero; dunque base, altezza e area sono variabili di questo tipo. Anche le dichiarazioni così come le altre istruzioni devono terminare con un punto e virgola. Nel nostro esempio alla dichiarazione del tipo della variabile corrisponde anche la sua definizione che fa sì che le venga riservato uno spazio in memoria centrale. Il nome di una variabile la identifica, il suo tipo ne definisce la dimensione e l'insieme delle operazioni che vi si possono effettuare. Le dichiarazioni delle variabili dello stesso tipo possono essere scritte in sequenza separate da una virgola: int base, altezza, area; Dopo la dichiarazione di tipo sono specificati gli identificatori di variabile, che possono essere in numero qualsiasi, separati da virgola e chiusi da un punto e virgola. L’istruzione cout << "Calcolo AREA RETTANGOLO \n \n"; visualizza su video la stringa di caratteri "Calcolo AREA RETTANGOLO” seguita da due righe vuote. Le due istruzioni cout << "Valore base: "; cin >> base; servono per leggere il valore della base. Prima viene visualizzata la frase “Valore base: “, poi il sistema aspetta che l’operatore batta un numero sulla tastiera e lo trasferisce nella variabile base. La forma grafica data al programma è del tutto opzionale; una volta rispettata la sequenzialità e la sintassi, la scrittura del codice è libera. In particolare più istruzioni possono essere scritte sulla stessa linea, si possono inserire spazi e righe vuote dove si desidera. Non bisogna dimenticare, però, di inserire il carattere ; (punto e virgola) dopo ogni istruzione. Lo stile grafico facilita enormemente il riconoscimento dei vari pezzi di programma e consente una diminuzione di tempo nelle modifiche, negli ampliamenti e nella correzione degli errori. Analogamente la riga successiva serve per far leggere il valore dell’altezza e inserirlo nella variabile altezza. L’istruzione area = base*altezza; è un’istruzione di assegnazione. Essa dice al computer di calcolare il valore dell’espressione a destra dell’uguale e assegnare il risultato alla variabile a sinistra del simbolo =; cioè inserisce nello spazio di memoria riservato a tale variabile il valore indicato. L'operatore asterisco effettua l'operazione di prodotto tra la variabile che lo precede e quella che lo segue, è dunque un operatore binario. Nel linguaggio C++ è possibile assegnare lo stesso valore a più variabili contemporaneamente. Per esempio base = altezza = 5; 14 Corso sul Linguaggio C++ Edizione 2010 In questo caso prima verrebbe assegnato il valore 5 alla variabile altezza e quindi, il risultato dell’assegnazione (cioè 5), viene assegnato alla variabile base. Le ultime istruzioni visualizzano su video i valori delle variabili in input e il risultato. 2.2 Definizione di variabili Tutte le variabili utilizzate in un programma devono essere preventivamente definite. La definizione di una variabile è un’istruzione che serve per creare la variabile in memoria. In generale la definizione di variabili ha la seguente forma: , Modificatore della modalità di memorizzazione Modificatore di tipo Tipo di dato Identificatore = Valore iniziale Opzionalmente si può premettere ad una definizione di variabile la modalità di memorizzazione che indica al computer come e dove memorizzare la variabile. Le modalità di memorizzazione sono le seguenti: Modalità di memorizzazione Dichiara al compilatore che l’elemento va memorizzato ….. auto Nelle celle della memoria centrale dell’elaboratore (è quella di default) register Negli elementi hardware dell’elaboratore che costituiscono la memoria più veloce. In genere i compilatori cercano di memorizzare queste variabili nei registri interni della CPU o nella memoria cache. extern È definito in un altro file sorgente static Nella memoria centrale ma il valore non viene mai cancellato durante l’esecuzione del programma volatile Nella memoria centrale, ma il valore può essere modificato, oltre che dal programma anche direttamente dall’hardware. Questa modalità viene utilizzata in casi particolari come quando si devono gestire i dati provenienti o da inviare ad un MODEM o ad altre periferiche. Per esempio: static int a dice al computer di creare una variabile di tipo intero in uno spazio che non deve essere cancellato quando il sottoprogramma che contiene la definizione termina (normalmente tutte le variabili di un sottoprogramma vengono cancellate quando il sottoprogramma termina l’esecuzione). Oltre alla modalità di memorizzazione si può inserire in una definizione di variabile un modificatore di tipo, i principali modificatori di tipo sono i seguenti: modificatore di tipo const Descrizione Indica al compilatore che il dato definito è un dato costante che quindi non potrà subire variazione durante l’esecuzione del pro15 Corso sul Linguaggio C++ Edizione 2010 signed unsigned Short Long gramma Il valore è un numero con il segno Il valore è un numero senza segno Riduce il numero di byte riservati a contenere il dato Aumenta il numero di byte riservato a contenere il dato Per esempio la definizione short int num; dichiara una variabile che occupa solo due byte (numeri da -32768 a + 32767); short unsigned int num; dichiara una variabile sempre di due byte ma senza segno e quindi i valori possono andare da 0 a 65536 Il tipo di dato indica al sistema la dimensione della variabile e l'insieme delle operazioni che si possono effettuare su di essa. La dimensione può variare rispetto all'implementazione; molte versioni del C++, come quelle sotto il sistema operativo MS DOS per esempio, riservano per gli int uno spazio di due byte, il che permette di lavorare su interi che vanno da -32768 a +32767, altre implementazioni, come per esempio quelle per ambiente Windows, riservano uno spazio di quattro byte permettendo valori compresi fra -2.147.483.648 e +2.147.483.647. Tra le operazioni permesse fra int vi sono: la somma (+), la sottrazione (-), il prodotto (*) e la divisione (/). Nel seguito esamineremo i principali tipi di dato. Ogni variabile deve avere un nome che identifica la variabile all’interno del programma e per questo viene detto identificatore. Esistono delle regole da rispettare nella costruzione degli identificatori: devono iniziare con una lettera o con un carattere di sottolineatura _ e possono contenere solo lettere, cifre e _ (non possono contenere simboli di punteggiature, segni di operazione e altri caratteri speciali). Per quanto riguarda la lunghezza occorre tenere presente che soltanto i primi trentadue caratteri sono significativi, anche se nelle versioni del C meno recenti questo limite scende a otto caratteri. Sarebbe comunque opportuno non iniziare il nome della variabile con il carattere di sottolineatura ed è bene tenere presente che le lettere accentate, permesse dalla lingua italiana, non sono considerate lettere ma segni grafici e le lettere maiuscole sono considerate diverse dalle rispettive minuscole. Oltre a rispettare le regole precedentemente enunciate, un identificatore non può essere una parola chiave del linguaggio, né può essere uguale ad un nome di funzione in libreria o scritta dal programmatore. In generale è inoltre bene dare alle variabili dei nomi significativi, in modo che, quando si debba intervenire a distanza di tempo sullo stesso programma, si possa facilmente ricostruire l'uso che si è fatto di una certa variabile. A differenza di altri linguaggi di programmazione, nel linguaggio C, l’istruzione di definizione di una variabile può essere utilizzata anche per inizializzarla ovvero per assegnargli il valore iniziale. Per esempio: int somma = 0, prod=1; crea la variabile somma e ci mette dentro il valore 0, poi crea la variabile prod e ci mette 1. 16 Corso sul Linguaggio C++ Edizione 2010 2.3 Costanti Nel paragrafo 2.1 abbiamo esaminato il programma per calcolare l’area di un rettangolo. Supponiamo ora di voler scrivere un programma per calcolare l’area del cerchio che sappiamo si calcola con la formula area = πr2, dove π è un numero fisso che corrisponde approssimativamente a 3,14. Quindi per calcolare l’area del cerchio bisogna indicare al computer anche il valore di π che è un dato costante del problema. Il programma potrebbe essere il seguente: #include <iostream.h> #include <stdlib.h> #define PIGRECO 3.14 // Calcolo area di un cerchio int main() { float raggio, area; cout << "Calcolo AREA Cerchio \n \n"; cout << "Raggio: "; cin >> raggio; area = PIGRECO*raggio*raggio; cout << "\nArea: " << area << "\n\n"; system("PAUSE"); return 0; } In questo caso le variabili raggio e area sono state dichiarate di tipo float perché questo tipo può contenere anche numeri con la virgola, inoltre è stata inserita la direttiva al preprocessore #define PIGRECO 3.14 Questo dice al preprocessore che ogni volta che trova la serie di caratteri PIGRECO deve sostituirli con il numero 3.14. In effetti se nel programma sorgente togliamo la direttiva define e sostituiamo l’istruzione area=PIGRECO*raggio*raggio con area = 3.14 * raggio * raggio il programma eseguibile sarebbe esattamente lo stesso. In pratica una costante è una valore fisso nel programma e noi possiamo indicarlo nelle istruzioni o scrivendo il valore o definendo un nome simbolico che rappresenta il valore. I valori costanti che si possono inserire in un programma C++ possono essere di vario tipo: • Numeri decimale interi (per esempio 10, 30, -56, 4545445 …) • Numeri ottali interi (per esempio 064, 010, … ) iniziano sempre con uno zero e contengono le cifre da 0 a 7; • Numeri esadecimali interi (0x23F, 0x30, 0xBB …) iniziano sempre con 0x e contengono le cifre da 0 fino a F; • Numeri decimali con la virgola o in virgola mobile (per esempio 3.14, -2.0, -0.1, 6.567E+7, 0.450123e-3 …). Per rappresentare la virgola si usa il punto; • Un carattere singolo (per esempio ‘a’, ‘B’, ‘7’…), deve essere racchiuso tra due apici; • Una stringa di caratteri (“Mario”, “Buongiorno mondo”, ….), la stringa è un gruppo di caratteri racchiuso tra virgolette (“); tutte le stringhe terminano con il carattere speciale \0 (codice ascii 0) che viene inserito automaticamente dal compilatore; 17 Corso sul Linguaggio C++ Edizione 2010 Quando un certo valore viene utilizzato in modo ricorrente è opportuno rimpiazzarlo con un nome simbolico; inoltre spesso conviene definire le costanti per aumentare la leggibilità di un programma perché in alcuni casi il nome di un dato indica più chiaramente, a chi legge il programma, il significato dell’istruzione. Per esempio se abbiamo codificato le mansioni svolte dagli impiegati di una azienda con dei numeri e 1 significa operaio, 2=impiegato, 9=dirigente l’istruzione mansione=DIRIGENTE è più chiara di mansione =9 per sostituire la costante 9 con la parola DIRIGENTE dobbiamo inserire la direttiva #define DIRIGENTE 9 nel programma prima dell’uso della costante. Il nome di una costante può essere qualsiasi identificatore valido in C++, comunque è uso comune utilizzare esclusivamente caratteri maiuscoli per le costanti e caratteri minuscoli per le variabili per distinguere chiaramente le une dalle altre. In sintesi, l'uso delle costanti migliora due parametri classici di valutazione dei programmi: flessibilità e possibilità di manutenzione. Con il linguaggio C++, oltre alla direttiva define (presente nel linguaggio C) per definire le costanti si può utilizzare il modificatore di tipo const e quindi si può anche scrivere: const float PIGRECO=3.14; 2.4 I tipi di dati e le operazioni permesse Una delle caratteristiche che differenziano il linguaggio C rispetto agli altri linguaggi è la maggiore disponibilità di tipi di dati e di operatori. Per quanto riguarda i tipi di dati abbiamo già visto la possibilità di utilizzare i modificatori di tipo e di modalità di memorizzazione che di fatto amplia la gamma dei tipi disponibili. Negli esempi che abbiamo visto finora abbiamo utilizzato due tipi di dato, numeri interi (tipo int) e numeri con la virgola (tipo float). Ogni linguaggio di programmazione mette a disposizione dei programmatori un insieme di tipi di dati che il programmatore può utilizzare come contenitori per i dati dei suoi programmi. Nel linguaggio C++ i tipi di dato fondamentali sono: 18 Corso sul Linguaggio C++ Edizione 2010 void bool elementari char int float Tipi di dato double enum array strutturati strutture union puntatori Il tipo int Il tipo int viene utilizzato per memorizzare numeri interi relativi. Il numero viene scritto nella memoria del computer in binario, i numeri negativi (non ammessi se si utilizza il modificatore di tipo unsigned) vengono rappresentati con il complemento a 2. A seconda del modificatore di tipo utilizzato abbiamo: short int signed short int unsigned short int long int signed long int Unsigned long int int numeri da - 32768 a +32 767, occupa 2 byte, Numeri da 0 a 65535 occupa 2 byte numeri da -2147483648 a +2147483647 occupa 4 byte Numero da 0 a 4294967295, 4 byte La dimensione può coincedere con short int o con long int a seconda dell’implementazione. In genere quelle sotto il sistema MS-DOS corrispondevano a short int, quelle sotto Windows come quella che utilizziamo noi (Dev-C++) corrispondono a long int Le operazioni permesse sono: la somma (+), la sottrazione (-), il prodotto (*), e la divisione (/), il modulo o resto della divisione (%), l’assegnazione (=), l’incremento (++) e il decremento (--), oltre alle combinazioni delle quattro operazioni con l’assegnazione (+=, -=, *=, /=). 19 Corso sul Linguaggio C++ Edizione 2010 Il tipo char Una variabile di tipo char può memorizzare un solo carattere. In realtà nella memoria viene memorizzato il codice ASCII del carattere e quindi un numero intero da 0 a 255. Per questo motivo una variabile char occupa un solo byte ed è compatibile con il tipo intero. Per questo tipo di variabili sono definite tutte le operazioni definite per i numeri interi, in più è possibile assegnare alla variabile anche un singolo carattere, per esempio, visto che il codice ASCII della lettera ‘B’ è 66, le due istruzioni seguenti sono equivalenti:: e char lett=’B’; char lett = 66; Una variabile di tipo char può essere utilizzata anche per contenere numeri interi senza segno da 0 a 255 oppure numeri interi con segno da -128 a + 127, a seconda se si definisce come unsigned char o signed char ed è compatibile con i tipi numerici, ma se viene inviata in output con cout verrà visualizzato sempre il carattere corrispondente al codice ASCII memorizzato e non il numero. Come detto prima sulle variabili di tipo char sono definite tutte le operazioni definite con il tipo intero, quindi le quattro operazioni principali (+, -, *, /) ma anche incremento e decremento (++, --), il modulo (%) e le combinazioni delle quattro operazioni con l’assegnazione (+=, -=. *=, /=). Quindi è corretta sintatticamente un’istruzione del tipo: lett=lett +2; Dopo questa operazione, supponendo che la variabile lett sia stata creata con l’istruzione precedente di questo paragrafo, il codice ASCII presente nella variabile viene aumentato di 2 e quindi la lettera contenuta non sarà più ‘B’ ma ‘D’. Da notare che le costanti di tipo char, ovvero costituite da un solo carattere, si racchiudono tra due apostrofi (‘), mentre le costanti stringa sono sempre racchiuse utilizzando il doppio apice (“), per esempio ‘A’, ‘k’ sono costanti di tipo char mentre “Mario” è una costante di tipo stringa. Oltre al tipo char, in molte versioni del linguaggio, è stato inserito anche il tipo wchar_t che occupa 2 bytes (16 bit) invece di uno e viene utilizzato per memorizzare i caratteri con il codice Unicode invece del codice ASCII. Il codice Unicode permette di rappresentare un numero di caratteri molto maggiore di 255, praticamente tutti i caratteri esistenti nelle lingue del mondo. I tipi float e double I due tipi float e double servono per memorizzare i numeri reali. La rappresentazione interna è quella binaria virgola mobile. Ogni variabile di tipo float occupa 4 bytes e può contenere numeri fino a 10±38 ma di tutte le cifre del numero solo le sette più significative sono memorizzate esattamente. Il tipo double occupa 8 bytes, la rappresentazione interna è sempre binario virgola mobile, può memorizzare numeri fino a 10±308 con esatte le 15 cifre più significative. Per questi tipo non si può utilizzare i modificatori short,long, signed e unsigned, in pratica i numeri vengono memorizzati sempre con il segno e il tipo float corrisponde a un ipotetico short double, solo per il tipo double si può anche utilizzare il modificatore long con il DEV-C++, le variabili di tipo long double occupano 12 bytes. Il fatto che i numeri sono memorizzati in binario virgola mobile permette di memorizzare numeri molto grandi e anche molto piccoli in modo abbastanza preciso e in poco spazio ma crea anche un problema, infatti, i numeri frazionari che in decimale hanno un numero finito di cifre spesso in binario diventano periodici e quindi con infinite cifre, per questo motivo anche un numero come 0.1 non può essere memorizzato esattamente in questo tipo di variabili. 20 Corso sul Linguaggio C++ Edizione 2010 Il tipo bool Le variabili di tipo bool occupano un solo byte e possono contenere solo i due valori: booleani true e false. Per questo tipo sono definite solo le operazioni logiche ovvero && (AND), || (OR), ! (NOT). È importante notare che queste operazioni sono diverse dalle corrispondenti operazioni che operano sui bit e che si applicano a tutte i tipi di variabili ovvero & (AND), | (OR), e ~(inversione dei bit). Un esempio di dichiarazione di variabile bool è il seguente: bool finito = false; Nel linguaggio C il tipo bool non esiste, in sua vece si può utilizzare il tipo int infatti, come qualsiasi altro dato, anche il risultato di un espressione logica è un numero binario e precisamente il numero 0 corrisponde al valore falso mentre il numero 1 o un numero diverso da zero corrisponde al valore vero. Questo significa che, sia nel linguaggio C che nel linguaggio C++, si possono utilizzare come variabili logiche anche le variabili intere. L’operatore speciale sizeof Per ottenere esattamente il numero di byte occupati da una variabile o da un tipo di dati si può ricorrere all’operatore speciale sizeof ovvero “misura di”. La sintassi è la seguente: sizeof(<TipoDato>) oppure sizeof(<NomeVariabile>) L’esempio che segue è un programma che mostra la quantità di memoria occupata da alcune variabili elementari.: #include <iostream.h> #include <stdlib.h> int main() { /* visualizza il numero di bytes assegnati in memoria ad alcune variabili */ int i; short int si; long int li; char c; wchar_t wc; float f; bool b; double d; long double ld; cout << "bytes occupati da char: " << sizeof(c)<< "\n"; cout << "bytes occupati da float: " << sizeof(f)<< "\n"; cout << "bytes occupati da double: " << sizeof(d)<< "\t long double: " << sizeof(ld) <<"\n"; cout << "bytes occupati da bool: " << sizeof(b)<< "\n"; cout << "bytes occupati da int: " << sizeof(i)<< "\t long int: " << sizeof(li) << "\t short int: " << sizeof(si)<<"\n"; cout << "bytes occupati da wchar_t: " << sizeof(wc) << "\n" ; system("PAUSE"); return 0; } Mandandolo in esecuzione con il DEV-C++ si otterrà questo risultato: 21 Corso sul Linguaggio C++ Edizione 2010 2.5 Le espressioni Un espressione è una sequenza di variabili, costanti e operatori, eventualmente racchiusi a gruppi tra parentesi tonde () per modificare l’ordine dei calcoli. Per esempio: x = a * 3 / (somma - 5.3) Nell’esempio x, a e somma sono variabili, 3 e 5.3 sono costanti, gli altri segni, escluse le parentesi che servono solo per modificare l’ordine dei calcoli, sono operatori. Un’espressione può anche essere definita come una sequenza di operazioni elementari dove ogni operazione è definita da un segno detto operatore e da uno o più valori che possono essere variabili o costanti detti operandi. Nel linguaggio C sono definiti molti più operatori rispetto agli altri linguaggi. Le espressioni hanno molte più possibilità e questa è una delle caratteristiche che lo rende preferibile per alcune applicazioni. Per esempio sono stati introdotti operatori come quello dell’incremento o decremento di una variabile che, in genere, sono presenti in tutti i linguaggi macchina ma non nei linguaggi ad alto livello. In questi linguaggi operazioni come a++ o a-- del linguaggio C, vengono sostituite con istruzioni del tipo a=a+1 o a = a-1, le quali però, tradotte in linguaggio macchina, in genere, danno luogo a programmi meno efficienti. Gli operatori del linguaggio C++ si possono raggruppare nelle seguenti categorie: • Operatori di assegnamento • Operatori aritmetici • Operatori relazionali • Operatori logici • Operatori sui bit • Operatori speciali Gli operatori di assegnamento L’operatore principale di assegnamento è il carattere =. Per esempio: base = 3; Con questa istruzione viene inserito 3 all’interno della variabile con nome ‘base’, questo significa che si inserisce il numero 3 nello spazio di memoria riservato alla variabile base. x = y; In questo caso il valore della variabile y vene copiato nella variabile x, le due variabili continueranno ad occupare spazi di memoria separati e indipendenti. Nella maggior parte dei casi l’istruzione di assegnamento ha la seguente sintassi: <identificatore> = <espressione> e serve per inserire il risultato dell’espressione a destra del simbolo = nella variabile con nome <identificatore>. Per il linguaggio C++ questa è un’operazione che produce un valore come risultato. Il valore risultante corrisponde al valore assegnato alla variabile. Per questo motivo, in questo linguaggio, sono possibili le assegnazioni multiple del tipo: prezzo = importo = q * pu; in questo caso viene prima calcolato il prodotto delle variabili q e pu, il risultato viene assegnato alla variabile importo, lo stesso risultato viene assegnato anche alla variabile prezzo. Esistono anche altri operatori di assegnazione, precisamente gli operatori +=, -=, *=, /=, %=, <<=, >>=, &=, !=, ^=. 22 Corso sul Linguaggio C++ Edizione 2010 Si tratta della combinazione di operazioni aritmetiche o logiche con l’assegnazione. Sono state introdotte in C perché possono essere tradotte in modo efficiente in linguaggio macchina. Per esempio le istruzioni: km += 37; k1 += k2; a += (b/2); equivalgono rispettivamente a: km = km+37; k1 = k1+k2; a = a+(b/2); Chi è pratico di programmazione avrà notato che questa è l’istruzione tipica degli accumulatori. Ma l’operatore = si può anche utilizzare insieme ad altri operatori aritmetici, per esempio: km -= 6; // toglie 6 ai km percorsi ovvero km = km - 6 lato *= 2; // moltiplica il lato per 2 ovvero lato = lato * 2 volume /= 3; // divide il volume per 3 ovvero volume = volume / 3 quindi, più in generale vale la seguente sintassi: <identificatore> <operatore>= <espressione> corrisponde a <identificatore> = <identificatore> <operatore> <espressione> La sintassi completa per l’assegnamento è <lvalue> <operatore di assegnamento> <rvalue> dove <lvalue> sta per left value,ovvero valore di sinistra e può essere un identificatore ma anche un elemento di un vettore o un’espresssione che da come risultato un puntatore dereferenziato; <operatore di assegnamento> è uno degli operatori che abbiamo visto (=, +=, -=, *=, /=, %=, <<=, >>=, &=, !=, ^=); <rvalue> (right value) è una qualsiasi espressione che restituisce un valore compatibile con il tipo di <lvalue>. Operatori aritmetici Naturalmente sono presenti nel linguaggio C++ gli operatori aritmetici disponibili negli altri linguaggi per le operazioni fondamentali: addizione (+), sottrazione(-), moltiplicazione (*), divisione (/), resto della divisione (%). Questi operatori vengono detti binari perché l’operazione viene compiuta su due operandi, per esempio: b+c a * 10 5/3 7-c in generale per far eseguire l’operazione al computer si scrivono i due operandi con il segno di operazione (l’operatore) in mezzo. Gli operatori binari non cambiano il valore dei due operandi ma memorizzano il risultato. L’operazione % corrisponde al resto della divisione e si può eseguire solo con numeri interi, per esempio: 10 % 3 da come risultato 1 e 20 % 5 da come risultato 0. Oltre agli operatori visti sopra sono disponibili nel C++ anche l’operatore ++ e -- detti rispettivamente di incremento e decremento. Questi due operatori si applicano ad un solo operando modificandone il valore per cui vengono detti unari. Il loro effetto è rispettivamente quello di incrementare o decrementare di 1 il valore dell’operando. 23 Corso sul Linguaggio C++ Edizione 2010 Per esempio dopo l’esecuzione del seguente pezzo di programma: int a=10, b=-20; a++; b--; nella variabile a ci sarà il valore 11 e nella variabile b il valore -21. Per aggiungere uno alla variabile z si può scrivere in due modi: ++z; z++; cioè mettere l'operatore ++ prima o dopo del nome della variabile. Le due notazioni vengono dette rispettivamente prefissa e postfissa In generale, le due forme sono equivalenti. La differenza importa solo quando si scrive una espressione che contiene z++ o ++z. Scrivendo z++, il valore di z viene prima usato poi incrementato: int x,z; z = 4; x = z++; // due variabili intere // z vale 4 // anche x vale 4 ma z vale 5 Difatti, prima il valore di z (4) è stato assegnato ad x, poi il valore di z è stato incrementato a 5. Scrivendo ++z, il valore di z viene prima incrementato e poi usato: int x,z; z = 4; x = ++z; // due variabili intere // z vale 4 // ora x vale 5 come z Difatti, prima il valore di z (4) è stato incrementato a 5, poi il nuovo valore di z (5) è stato assegnato ad x. Bisogna stare attenti e cercare di evitare che in una stessa espressione compaia più volte la stessa variabile con applicate operazioni di incremento o decremento postfisse perché i risultati dipendono da quando il compilatore fa eseguire l’incremento o decremento, per esempio dopo l’esecuzione delle seguenti istruzioni: int b=5, a ; a=b++ + b++ ; in b ci sarà il valore 7 e in a ci sarà il valore 10 perché i due incrementi di b verranno eseguiti dopo l’addizione, mentre dopo l’esecuzione delle seguenti istruzioni: int b=5; bool a; a= b++ < b; in a ci sarà il valore vero perchè il compilatore Dev-C++ fa eseguire l’incremento di b prima di eseguire il confronto <. Operatori relazionali Gli operatori relazionali sono operatori binari, servono per confrontare due valori e producono un risultato booleano ovvero il valore true o false a seconda se il risultato del confronto è vero o falso. Sono i seguenti: > maggiore >= maggiore o uguale < minore <= minore o uguale == uguale != diverso Per esempio: 5 >= 3 da risultato true; 2 == (10 / 5) da risultato true; 7 != 7 da risultato false; 24 Corso sul Linguaggio C++ Edizione 2010 Operatori logici Gli operatori logici richiedono come operandi valori o espressioni booleane e il risultato è ancora un valore logico di tipo vero o falso. Gli operatori logici sono: || OR Risultato falso solo se entrambi gli operandi sono falsi && AND Risultato vero solo se entrambi gli operandi sono veri ! NOT inverte il valore di verità dell’unico operando Gli operatori || e && non valutano l’operando di destra se non è necessario, ovvero se il primo operando è vero il risultato di || sarà vero indipendentemente dal valore del secondo operando e quindi non serve valutarlo, nel caso del && se il primo operando è falso il risultato sarà falso senza bisogno di valutare il secondo operando. Per esempio: (5 > 1) || (3>6) è vero mentre (5>1) && (3 > 6) è falso Operatori sui bit A tutte le variabili viene assegnato uno spazio di memoria dove viene memorizzato il valore contenuto nella variabile. Il valore è sempre rappresentato tramite una serie di bit. Ogni bit è una cifra binaria e quindi può assumere solo i due valori 0 e 1 che nell’algebra booleana rappresentano rispettivamente i valori falso e vero, per cui il valore di una qualsiasi variabile può essere visto come una sequenza di valori vero e falso. Le operazioni sui bit sono operazioni effettuate sui singoli bit che costituiscono il valore degli operandi considerati indipendentemente uno dall’altro. Tutte queste operazioni, come pure quella di incremento e decremento, sono in genere disponibili in tutti i linguaggi macchina e quindi possono essere tradotte in modo molto efficiente in questo linguaggio avvalorando la tesi che vuole il linguaggio C più vicina al linguaggio macchina rispetto agli altri linguaggi di alto livello. Gli operatori sui bit sono: Operatore Descrizione & AND bit a bit, il risultato è 1 solo se entrambi i bit sono 1 | OR bit a bit, il risultato è 1 solo se almeno uno dei due bit è 1 ^ XOR (OR esclusivo) bit a bit, il risultato è 1 solo se uno solo dei due bit è=1 ~ Complemento a 1 ovvero inversione di tutti i bit >> Shift a destra. Spostamento a destra di tutti i bit con propagazione del bit del segno (il primo bit più a sinistra), in pratica corrisponde a dividere il numero binario per 2x dove x è il numero dei bit di scorrimento. Se non si vuole la propagazione del segno bisogna utilizzare le variabili unsigned. << Shift a sinistra ovvero spostamento a sinistra di tutti i bit e riempimento dei bit che si liberano a destra con zeri. In pratica corrisponde a moltiplicare il numero per 2x dove x è il numero dei bit di scorrimento. Per esempio supponiamo di aver definito due variabili con l’istruzione short int a=25,b=20, considerato che il tipo short int ha una rappresentazione interna in binario a 16 bit con complemento a 2 per i numeri negativi, il contenuto delle due variabili corrisponderà ai due numeri 25 e 20 scritti in binario e sarà il seguente: 25 Corso sul Linguaggio C++ Edizione 2010 a = 0000000000011001 b = 0000000000010100 per cui si avrà che: a & b 0000000000010000 = 16 a | b 0000000000011101 = 29 a ^ b 0000000000001101 = 13 ~a 1111111111100110 = -26 ~b 11111111111111101011 = -21 a >> 2 0000 0000 0000 0110 = 6 (risultato dello spostamento a destra di due bit) a << 2 0000 0000 0110 0100 = 100 (risultato dello spostamento a sinistra di due bit) Per verificarlo basta scrivere il seguente programma: #include <iostream.h> #include <stdlib.h> int main() { short int a=25, b=20 ; cout << " a = " << a << "\t b = "<< b << endl; cout << " a & b = " << (a & b) << "\t a | b = " << (a | b) << "\t a ^ b = " << (a ^ b) << endl; cout << "~a = " << (~a) << "\t ~b = " << (~b) << endl; cout << " a << 2 = "<<(a << 2)<<"\t a >> 2 = "<<( a >> 2)<< endl; system("PAUSE"); return 0; } Si noti che invece della sequenza di escape \n si può utilizzare la costante predefinita endl per inviare in output il carattere new line, mandandolo in esecuzione si otterrà il seguente output: Regole di valutazione di un’espressione Le espressioni possono essere combinazione di operatori e operandi anche complesse ma comunque portano sempre ad un risultato univoco. Per capire come il computer calcola una espressione bisogna sapere che i calcoli vengono effettuati dall’unità aritmetico-logica (ALU) che è una specie di calcolatrice contenuta nell’unità centrale di elaborazione (CPU) di ogni computer. L’unità aritmetico-logica può eseguire una sola operazione alla volta e quindi per calcolare un’espressione bisogna eseguire in sequenza le varie operazioni memorizzando i risultati parziali così come faremmo noi con una calcolatrice normale. Se per esempio consideriamo l’espressione 5 + 3 * 6, il risultato sarà diverso se eseguiamo prima l’addizione e poi la moltiplicazione o viceversa prima la moltiplicazione e poi l’addizione. Nel primo caso esce 48 nel secondo esce 23. Il programmatore deve conoscere l’ordine con cui il computer eseguirà i calcoli, per questo in ogni linguaggio di programmazione sono state fissate delle regole che indicano come verrà calcolato il valore di una qualsia26 Corso sul Linguaggio C++ Edizione 2010 si espressione, stabilendo esattamente l’ordine con cui devono essere applicati gli operatori agli operandi. Le regole per il linguaggio C++ sono le seguenti: 1. L’impiego di parentesi. Se si utilizzano le parentesi tonde, le espressioni racchiuse all’interno delle parentesi vengono calcolate prima di eseguire i calcoli posti fuori dalle parentesi. Il compilatore traduce l’espressione in modo da far calcolare prima le espressioni racchiuse nelle coppie di parentesi più interne, poi sostituisce tutto il blocco che inizia con “(“ fino a “)” con il risultato dell’espressione, e procede man mano fino a quelle più esterne; 2. Livello di precedenza degli operatori. Gli operatori sono divisi in gruppi con livelli di precedenza diverso, in mancanza di parentesi vengono prima eseguite prima le operazioni con livello più alto, poi le altre in ordine fino a quelle di livello più basso. Per conoscere il livello di precedenza delle operazioni si veda la Tabella 2 che segue, dove sono riportati tutti gli operatori divisi in gruppi in base al livello di priorità. Per esempio le moltiplicazioni e le divisioni vengono eseguite prima delle addizioni e sottrazioni, quindi l’espressione precedente 5 + 3 * 6 produce il risultato 23; 3. Le regole di associatività. Quando c’è una serie di operandi e operatori con lo stesso livello di precedenza, si seguono le regole di associatività. Per la maggior parte degli operatori vale l’associatività a destra, cioè le operazioni vengono eseguite partendo da quella più a sinistra fino a quella più a destra, fanno eccezione gli operatori di assegnamento per i quali vale l’associatività a sinistra. Quindi, quando viene calcolata un espressione scritta in C++ vengono prima calcolate le espressioni racchiuse tra parentesi, poi vengono eseguiti in calcoli in ordine di precedenza degli operatori, se ci si trova con una sequenza di operandi e operatori con lo stesso livello di precedenza si segue la regola dell’associatività, questo significa che se si trova un espressione del tipo: operando1 op1 operando2 op2 operando3 dove <op1> e <op2> sono operatori con lo stesso livello di priorità allora il computer segue la regola dell’associatività, se l’associatività è a destra, come avviene normalmente per la maggior parte delle operazioni, si procede da sinistra verso destra e precisamente si calcolerà: (operando1 op1 operando2) op2 operando3 se invece l’associatività è a sinistra (come avviene per l’operatore =) si procederà da destra verso sinistra e quindi si calcolerà: operando1 op1 (operando2 op2 operando3) Operatori speciali Vedremo solo due operatori speciali: ? utilizzato con tre operandi (ternario), una espressione logica e due espressioni qualsiasi dello stesso tipo, produce il risultato di una delle due espressioni scegliendolo in base al valore dell’espressione logica; , utilizzato con due operandi (binario), ha il più basso livello di priorità, inferiore anche agli operatori di assegnamento, è associativo a destra, fa calcolare dal computer in sequenza le due espressioni, rendendo possibile l’utilizzo del risultato della prima nella seconda; La sintassi dell’operazione ? è la seguente: <espressione logica>?<espressione1>:<espressione2> 27 Corso sul Linguaggio C++ Edizione 2010 si utilizza quando vogliamo sceglier tra due risultati diversi in base ad una condizione. Per esempio se lo sconto è del 10% quando si acquista una quantità di prodotto che va da 1 a 5 mentre è del 20% se si acquistano più di 5 unità del prodotto, per assegnare il valore alla variabile sconto possiamo utilizzare la seguente espressione: sconto = quant > 5 ? 20 : 10; Si noti che nell’espressione precedente non servono le parentesi perché in base alla precedenza degli operatori (vedi Tabella 2 sotto) viene eseguita prima l’operazione >, poi l’operazione ? ed infine l’operazione =. Se i possibili sconti fossero stati tre, per esempio 5% fino a due pezzi, 10% da tre pezzi a cinque e 20% oltre i 5 pezzi, avremmo potuto calcolare lo sconto sempre con una sola istruzione nel modo seguente: sconto = quant > 5 ? 20 : (quant >2 ? 10 :5); La sintassi dell’operazione , (virgola) è la seguente: <espressione1>,<espressione2> potendosi ripetere l’operazione quante volte si vuole si possono scrivere anche espressioni del tipo: <espressione1>,<espressione2>,<espressione3>, …….,<espressioneN> L’effetto di queste operazioni è semplicemente quello di far calcolare al computer le espressioni nell’ordine partendo dalla prima più a sinistra fino all’ultima a destra. Se qualcuna di queste espressioni contiene operazioni di assegnamento, nelle seguenti verranno eventualmente utilizzati i valori delle variabili così come modificate delle espressioni precedenti. Per esempio, con l’istruzione: k = (i=10, j=20, i+j); alla variabile k verrà assegnato il valore 30. Si noti che in questo caso l’utilizzo delle parentesi è indispensabile per far eseguire l’operazione , (virgola) prima dell’assegnamento (operazione =), senza parentesi a k sarebbe stato assegnato il valore 10. Riassunto degli operatori del linguaggio C++ Tabella 2. Gli operatori disponibili nel linguaggio C++ raggruppati in ordine di precedenza da quelli con precedenza maggiore fino a quelli con precedenza minore (le linee più marcate segnano la divisione tra un gruppo e l’altro). Simbolo :: . -> [] () () sizeof sizeof ++ ++ --~ Rappresenta l’operazione …. visibilità Selezione elemento Selezione elemento Indicizzazione Chiamata di funzione Conversione di tipo Dimensione di un oggetto Dimensione di un tipo di dati Incremento prefisso Incremento postfisso Decremento prefisso Decremento postfisso Inversione dei bit Sintassi <nome_calsse>::<elemento> <oggetto>.<elemento> <puntatore>-><elemento> <identificatore>[<espressione>] <nome_funz.>(<lista parametri>) <tipo dati>(<espressione>) sizeof(<espressione>) sizeof(<tipo dati>) ++<variabile> <variabile>++ --<variabile> <variabile>-~<espressione> 28 Corso sul Linguaggio C++ Edizione 2010 Simbolo ! + & * new delete delete[] () Rappresenta l’operazione …. Sintassi NOT !<espressione logica> Cambiamento di segno -<espressione numerica> Più unario +<espressione numerica> Indirizzo di &<identificatore> Dereferenziazione *<espressione> Allocazione dinamica new <tipo dato> Deallocazione dinamica delete <puntatore> Deallocazione di un array delete[] <puntatore> Conversione di tipo nella notazione (<tipo dato>)<espressione> cast Selezione indirizzo elemento <oggetto>.*<puntatore_elemento> Selezione indirizzo elemento <puntatore>->*<puntatore_elemen.> Moltiplicazione <espressione>*<espressione> Divisione <espressione>/<espressione> Resto della divisione <espressione>%<espressione> Addizione <espressione>+<espressione> Sottrazione <espressione>-<espressione> Shift a sinistra <espressione> << <numero bit> Shift a destra <espressione> >> <numero bit> Minore di <espressione> < <espressione> Minore o uguale a <espressione> <= <espressione> Maggiore di <espressione> > <espressione> Maggiore o uguale a <espressione> >= <espressione> Uguale a <espressione> == <espressione> Diverso da <espressione> != <espressione> And bit a bit <espressione> & <espressione> XOR (OR esclusivo) bit a bit <espressione> ^ <espressione> OR bit a bit <espressione> | <espressione> AND logico <espr.logica> && <espr.logica> OR logico <espr.logica> || <espr.logica> Espressione condizionale <espr.logica>?<espr1>:<espr2> Assegnazione <identificatore>=<espressione> Assegnazione composta <identificatore><operatore>=<espressione> .* ->* * / % + << >> < <= > >= == != & ^ | && || ? = *= /= %= += = <<= >>= &= |= ^= , Virgola (esegue le due espressioni in sequenza) 2.6 <espressione1>,<espressione2> La conversione di tipo Abbiamo visto che una espressione può consistere in più calcoli e che il computer esegue questi calcoli uno alla volta memorizzando i risultati intermedi in variabili temporanee che poi u29 Corso sul Linguaggio C++ Edizione 2010 tilizza nei calcoli successivi. Il tipo di queste variabili temporanee dipende dall’operazione e in genere è sempre dello stesso tipo degli operandi utilizzati. Per esempio consideriamo il seguente programma: int main() { int uno=1, due=2; float tre; tre = uno/due; cout << tre <<endl; system("PAUSE"); return 0; } Ci aspetteremmo di vedere in output il risultato 0.5, visto che la variabile tre è di tipo float, invece verrà visualizzato 0. Questo dipende dal fatto che con il linguaggio C++, il calcolo uno/due, produce un risultato di tipo int essendo i due operandi uno e due entrambi di tipo int. Il risultato sarà quindi 0 e non 0.5. Il problema si verifica quando in un’espressione gli operandi sono di tipo diverso, infatti ci sono operazioni che permettono, entro certi limiti, l’utilizzo di operandi di tipo diverso, per esempio possiamo eseguire operazioni aritmetiche con un operando di tipo double e un altro di tipo int oppure, come nel programma precedente, possiamo assegnare ad un variabile di tipo float un valore intero. Quando gli operandi di un’operazione sono di tipo diverso, il linguaggio C++ sceglie per il risultato temporaneo dell’operazione, il tipo più capiente tra quello degli operandi in modo da evitare perdita di informazioni, e, prima di fare il calcolo, converte l’altro operando nel tipo scelto. Si dice in questo caso che viene effettuata una conversione implicita di tipo detta cast implicito. Il tipo double è più capiente del tipo float ed entrambi sono più capienti del tipo int perché qualsiasi numero intero può essere memorizzato in una variabile di tipo float o double (che accettano rispettivamente numeri fino a 1038 e 10308) mentre se spostiamo una variabile di tipo float in una intera perderemo la parte dopo la virgola. Ogni volta che si assegna un valore di un tipo più capiente ad una variabile meno capiente si può avere una perdita di informazioni, per esempio se assegniamo un valore double ad una variabile float è molto probabile che ci sarà una diminuzione del numero di cifre significative corrette. Se mettiamo in ordine i tipi numerici dal meno capiente al più capiente abbiamo la seguente sequenza: charshort intintlong intfloatdoublelong double per esempio dopo l’esecuzione delle seguenti istruzioni: int uno=1, float due=2.0, tre; tre = uno/due; alla variabile tre verrà assegnato correttamente il valore 0.5 perché il computer quando esegue l’operazione uno/due convertirà automaticamente il valore di uno in float, eseguirà una divisione tra numeri con la virgola e memorizzerà il risultato temporaneo in una variabile di tipo float. 30 Corso sul Linguaggio C++ Edizione 2010 È anche possibile indicare al computer il tipo di dati in base al quale deve essere svolta l’operazione usando il cosiddetto cast esplicito la cui sintassi è: (<tipo dati>)<espressione> Per esempio per far funzionare correttamente il programma precedente, senza modificare il tipo delle variabili, si può modificare l’istruzione per la divisione nel seguente modo: int main() { int uno=1, due=2; float tre; tre = (float) uno/due; cout << tre <<endl; system("PAUSE"); return 0; } premettendo la parola (float) tra parentesi all’espressione, si forza il computer ad eseguire i calcoli con il tipo float. È possibile convertire i dati da un formato ad un altro anche utilizzando le funzioni di conversione la cui sintassi è: <tipo dati>(<espressione>) per esempio: int(10.5*3) → 31 invece di 31.5, char(6*10+5) produce il numero 65 in uno spazio di un solo byte. Ma in questo caso l’espressione viene calcolata normalmente e, solo dopo, il risultato viene convertito nel formato specificato. Quindi se nel programma precedente avessimo scritto float(uno/due) invece di (float)uno/due avremmo ottenuto come risultato 0 invece di 0.5, perché l’espressione uno/due tra due numeri interi da 0 e questo risultato, trasformato in float, è sempre 0. 2.7 Le enumerazioni Fra i tipi elementari introdotti nel paragrafo 2.4 c’è anche il tipo enum. In realtà non si tratta di un vero tipo di dati ma di un insieme di possibili tipi che il programmatore può definire. In pratica il programmatore può definire un nuovo tipo attraverso un elenco di identificatori che rappresentano i possibili valori che questo tipo può assumere. Per esempio: enum colori {giallo, arancione, rosso, verde, blu}; // (1) In questo modo viene creato un nuovo tipo di dati che si chiama colori e che può assumere solo cinque valori diversi a cui vengono assegnati i nomi giallo, arancione, rosso, verde, blu. Dopo aver definito il tipo colori, come per qualsiasi altro tipo, per utilizzarlo dobbiamo definire le variabili, per esempio: colori a,b; a e b diventano variabili di tipo colori. A queste variabili, nel programma, si potrà assegnare solo valori di tipo colori. a = rosso; b = verde; // (2) 31 Corso sul Linguaggio C++ Edizione 2010 In realtà il compilatore associa ogni identificatore dell’elenco fornito dal programmatore con un numero intero e nel programma considera questi identificatori come costanti dichiarate dal programmatore. Nell’esempio dell’istruzione (1), le parole giallo, arancione, rosso, verde, blu, diventano costanti esattamente come se fossero state definite con l’istruzione: const int giallo=0, arancione=1, rosso=2, verde=3, blu=4; Quindi le variabili di tipo colori sono in realtà delle variabili di tipo intero a cui, però, si possono assegnare solo i numeri da 0 a 4. Con l’istruzione (2) viene assegnato il numero 2 alla variabile a e il numero 3 alla variabile b. Se si visualizza in output un variabile di tipo colori verrà visualizzato il numero contenuto al suo interno e non il nome del colore associato a quel numero! Le operazioni ammesse con le variabili di tipo dichiarato attraverso l’istruzione enum sono le stesse ammesse con il tipo intero, quindi ++, --, +, -, *, /, %. Inoltre queste variabili sono compatibili con il tipo intero. Abbiamo visto che il computer assegna automaticamente i numeri da zero in poi agli identificatori definiti nelle enumerazioni ma il programmatore può anche decidere lui quali numeri assegnare, per esempio: enum mesi {gennaio=1, febbraio, luglio=7, agosto}; mesi mese1, mese2; si avrà gennaio = 1, febbraio =2, luglio = 7, agosto =8 inoltre, si possono anche dichiarare le variabili con la stessa istruzione enum: enum mesi {gennaio=1, febbraio, luglio=7, agosto} mese1,mese2; 2.8 Esercizi 1) Calcolare le seguenti espressioni scritte nel linguaggio C++ a) 3*(15-2*5)/(4*10/8) b) 2+5*(40 / (2*4) /2)+(10/3) [Risultato:3; 15;] 2) Scrivere le espressioni logiche per: • Vedere se un numero x è positivo • Vedere se un numero x è pari; • Vedere se un numero x è compreso nell'intervallo [A, B] chiuso; • Vedere se un numero x è esterno all'intervallo [A, B] chiuso; • Vedere se un numero intero x è multiplo di 5 e, contemporaneamente, è dispari; 3) Calcolare ili risultato delle seguenti espressioni logiche (1 per "vero", 0 per "falso") 5>2; 10<3; 5>7 || 3>4; 10<2 || 5>4; 7<1 || 4<5; 3>0 && 5>2; 8>10 && 5>2; 7>8 != 7>10; 3>2 != 2<3; 5<5 == 4>3; 4) Sia dato il seguente pezzo di programma: long int d = 1267894; float media; int k = 10; media = d/k; cout << (float) media; qual è il valore che viene stampato? 32 Corso sul Linguaggio C++ Edizione 2010 5) Indicare il valore assunto dalla variabile f nelle seguenti espressioni: int a=1, b=2, c=3, d=4, e=5, f; a) f = 5 + --a - e / b++; b) f = ++a - c-- + e/--b; c) f = d + c * b-- + e / a++; 6) Indicare il valore assunto dalla variabile c nelle seguenti espressioni: int a=5, b=6, c; a) c = (a == b) ? ++a : ++b; b) c = (a++ == b) ? b++ : b--; c) c = (++a == b) ? ++b : --b; 7) Determinare quale è la relazione che assume valore vero quando x è esterno all'intervallo [A,B] e y è interno allo stesso intervallo? a) (x<A) && (x>B) && (y>=A) && (y<B); b) ((x<A)||(x>B)) && ((y>=A)&&(y<=B)); c) ((x<A)||(x>B)) && ((y>=A)||(y<=B)); d) ((x<A)||(x>B)) || ((y>=A)||(y<=B)); e) ((x<A)&&(x>B)) && ((y>=A)||(y<=B)); f) ((x<A)||(x>B)) || ((y>=A)&&(y<B)); 8) Sia data una variabile dichiarata come char c;. Ipotizzando che essa contenga un carattere compreso tra '0' e '9', come si trasferisce in una variabile intera v il valore decimale della cifra rappresentata da c? a) v = atoi(c); (atoi- ascii to int- trasforma una stringa contenente cifre in un numero int - come val() del Basic- ma il parametro deve essere un puntatore) b) v = (int) c ; c) v = c - '0'; d) v = c ; e) nessuna delle precedenti 9) Quale dei seguenti valori di a e b produce il valore vero per la condizione: (a > 0) && ((b < 0) || (b > 1)) Risposte: a)a = 5; b = 0 b)a = 5; b = 2 c)a = -1; b = 5 d)a = 1; b = 1 10) Data una misura di tempo espressa in ore minuti e secondi descrivere un algoritmo che la trasformi in secondi. 11) Descrivere un algoritmo per trasformare una misura espressa in secondi in un'altra espressa in ore minuti e secondi. 12) Dato un numero n compreso tra 1 e 365, considerandolo come giorno n-esimo dell'anno, descrivere un algoritmo per determinare a quale giorno della settimana corrisponde sapendo che il primo giorno dell'anno era domenica. (output un numero da 1 a 7, 1=lunedì 7 = domenica.) 33 Corso sul Linguaggio C++ Edizione 2010 13) Assegnata la paga oraria di un operaio calcolare la paga settimanale sapendo che si lavora 7 ore al giorno, il sabato si lavora solo 4 ore pagate però il doppio e la domenica non si lavora. 14) Trovare l’algoritmo per calcolare il numero medio di medici per paziente in un giorno qualsiasi dell’anno in un ospedale di cui si conosce il numero dei medici, il numero totale dei pazienti che circolano in un anno e il numero medio di giorni di degenza di ogni paziente. 15) Convertire lire in Euro (1 euro = 1936,27 lire) 16) Data una misura espressa in metri trovare l’equivalente in piedi e yarde sapendo che: 3 piedi = 1 yarda = 0,91439 metri 17) Sapendo che si può passare dai gradi Celsius ai gradi Fahrenheit con la seguente formula: gradi Fahrenheit = gradi Celsius * 9/5 + 32 Scrivere un programma per trasformare una misura della temperatura da gradi Celsius a Fahrenheit e uno per fare il contrario. Risposte esercizi Esercizio 4) = 126789 Esercizio 5) a) = 3; b) = 4; c) = 15 Esercizio 6): a) = 7; b) = 6; c) = 7 Esercizio 7) = b) Esercizio 8) = c) c-‘0’ Esercizio 9) = b) 34 3 Capitolo Corso sul Linguaggio C++ Edizione 2010 3. Le istruzioni e le strutture di controllo Un programma C++ è un insieme di istruzioni. Le istruzioni possono essere di due tipi: • Istruzioni eseguibili, corrispondono ad un azione eseguita dal computer; • Istruzioni di dichiarazione, non corrispondono a nessuna operazione ma servono per definire il modello dei dati o gli oggetti utilizzati dal programma oppure danno indicazioni al compilatore sulle funzioni utilizzate nel programma. Nel programma sorgente è possibile alternare a piacere istruzioni dichiarative ed eseguibili purché quando si utilizza un oggetto quest’ultimo sia stato già definito in precedenza. Le istruzioni possono essere anche classificate nel seguente modo: Di assegnazione Espressioni semplici chiamata funzione di Di dichiarazione e definizione return Istruzioni del C++ break Di salto continue goto blocco strutturate selezione if ... else switch while cicli do…while for …. 35 Corso sul Linguaggio C++ Edizione 2010 3.1 La programmazione strutturata Abbiamo visto che il linguaggio C++ permette di implementare sia i programmi realizzati con il modello procedurale che quelli realizzati secondo il modello della programmazione object oriented, entrambi, però, prevedono la traduzione di algoritmi nel linguaggio di programmazione, anche se nella programmazione procedurale, questa è l’operazione base che realizza l’attività del “programmare”. Sappiamo che un algoritmo è la descrizione di un procedimento ovvero di una serie finita di azioni che opera su un’insieme di dati in ingresso per produrre un insieme di risultati. Questa descrizione deve essere non ambigua ovvero un qualsiasi esecutore che esegue l’algoritmo, se parte dagli stessi dati, deve eseguire esattamente le stesse azioni e deve arrivare esattamente agli stessi risultati. Un algoritmo, inoltre, rappresenta sempre un procedimento generale che si può eseguire più volte cambiando i dati in input. Le azioni che verranno svolte, possono essere diverse a seconda dei dati iniziali. Quindi, nell’algoritmo, oltre alla descrizione chiara e univocamente interpretabile delle azioni da eseguire, viene anche indicato l’ordine con cui queste azioni devono essere eseguite, inoltre ci può essere l’indicazione che una o più azioni devono essere ripetute e/o eseguite solo in alcuni casi. Queste indicazioni non sono descrizioni di azioni che l’esecutore deve compiere ma piuttosto servono a indicare esattamente la sequenza con cui le azioni devono essere eseguite e quando devono essere eseguite. Se l’algoritmo viene descritto con un diagramma di flusso (detto anche diagramma a blocchi) le azioni sono descritte in rettangoli mentre la sequenza di esecuzione (flusso) e le condizioni che determinano l’esecuzione o meno delle azioni sono rappresentate dalle frecce e dai rombi, dando luogo a schemi grafici diversi quando si cambiano le indicazioni relative a come si susseguono le azioni. I vari modi con cui mettere insieme le azioni per formare un algoritmo vengono definiti da alcuni autori 1 modelli di composizione o schemi di composizione ma sono noti in generale come strutture di controllo della programmazione. I principali modelli di composizione sono: La sequenza, la selezione e la ripetizione: La sequenza consiste nella descrizione di una serie di azioni che devono essere eseguite una dopo l’altra nell’ordine indicato, per esempio: Inizio Azione 1 … Azione 2 Azione N Fine La selezione consiste nella descrizione di una o più azioni che devono essere eseguite solo se si verifica una determinata condizione, per esempio: no si no condizione azione A) Selezione ad una via 1 condizione Azione1 B) Selezione a due vie A. Andronico e altri, Manuale di Informatica, Zanichelli 1979 36 si Azione2 Corso sul Linguaggio C++ Edizione 2010 La ripetizione o ciclo consiste nella descrizione di una o più azioni che devono essere ripetute un numero di volte variabile in base ad una condizione fissata. Per esempio: si Azione condizione no si azione condizione no A) Ripetizione precondizionale B) Ripetizione postcondizionale Se consideriamo tutti i diagrammi di flusso che si possono realizzare utilizzando solo gli schemi di composizione di sequenza, selezione e ripetizione sopra indicati e supponiamo che un azione rappresentata nello schema possa essere o un’azione elementare che l’esecutore capisce o un azione complessa corrispondente ad una serie di azioni elementari rappresentabile sempre con uno dei tre schemi base su indicati, otteniamo un sottoinsieme di tutte le descrizioni di algoritmi possibili. Questo sottoinsieme è quello degli schemi di flusso strutturati. In base al famoso teorema enunciato dai due matematici italiani Böhm e Jacopini 2, un qualsiasi algoritmo può sempre essere descritto utilizzando uno schema di flusso strutturato e quindi usando solo le strutture di controllo della sequenza, selezione e ripetizione. Anzi il teorema afferma che basta anche solo la struttura della sequenza, quella della selezione ad una via e un solo tipo di ciclo (per esempio quello postcondizionale). Per far eseguire un algoritmo da un computer bisogna descriverlo utilizzando un linguaggio che possa essere, in qualche modo, compreso dal computer. Hanno tale caratteristica i linguaggi di programmazione. Ogni azione elementare dell’algoritmo deve corrispondere ad un’istruzione elementare del linguaggio di programmazione utilizzato. Per comunicare al computer gli schemi di composizione delle azioni si utilizzano le istruzioni di controllo. Il teorema di Böhm-Jacopini ci assicura che in un linguaggio, per poter tradurre un qualsiasi algoritmo, bastano le istruzioni di controllo per la sequenza, selezione e ripetizione purché all’interno di queste istruzioni si possa inserire una qualsiasi altra istruzione (elementare o di controllo). I linguaggi di programmazione che possiedono le istruzioni di controllo con le caratteristiche su menzionate sono detti linguaggi strutturati. Si è visto che l’utilizzo di un linguaggio strutturato rende più semplice individuare gli errori nei programmi perché permette di evitare l’utilizzo dell’istruzione di salto (l’istruzione goto). Quest’ultima istruzione era molto utilizzata nei primi linguaggi di programmazione e permette la traduzione di un qualsiasi schema, ma rende molto più complesso, per il programmatore, tenere sotto controllo il flusso del programma ovvero la sequenza delle azioni che verranno eseguite in tutti i casi possibili. Il primo linguaggio strutturato fu il Pascal, oggi tutti i linguaggi moderni di programmazione sono linguaggi strutturati. 2 vedi http://it.wikipedia.org/wiki/Teorema_di_Jacopini-Bohm. 37 Corso sul Linguaggio C++ Edizione 2010 I programmi che utilizzano solo le istruzioni di controllo per i tre schemi base visti sopra sono detti programmi strutturati 3. Il linguaggio C (e di conseguenza anche il linguaggio C++) è un linguaggio strutturato. Nei prossimi paragrafi vedremo le istruzioni utilizzate per tradurre le tre strutture di controllo fondamentali della programmazione in questo linguaggio. 3.2 Le istruzioni semplici La principale istruzione semplice è quella di assegnazione che abbiamo già visto. Abbiamo già visto anche le istruzioni per la dichiarazione di variabili, le istruzioni per dichiarare e definire oggetti di altro tipo le vedremo man mano che studieremo gli oggetti in questione. Ogni istruzione semplice finisce sempre con il carattere ; (punto e virgola), esiste anche l’istruzione nulla costituita dal solo carattere ; ; Se viene inserita in un programma serve a dire al computer che il quel punto non deve fare niente. In questo paragrafo vedremo le istruzioni di salto: break continue return goto Normalmente le istruzioni di un programma vengono eseguite una di seguito all’altra, nell’ordine in cui sono scritte, inoltre è possibile utilizzare le istruzioni di controllo come quelle di selezione per far eseguire alcune istruzioni si e altre no, oppure quelle di ripetizione per far ripetere più volte una serie di istruzioni. In ogni istante è in esecuzione una sola istruzione del programma ed esiste nella CPU un puntatore che indica la posizione della prossima istruzione che sarà eseguita, questo puntatore si chiama program counter. Con le istruzioni di salto è possibile modificare la sequenza con cui dovrebbero essere svolte le istruzioni del programma e, quindi, tutte le istruzioni di salto producono la modifica del program counter. Le istruzioni break e continue servono per modificare la normale sequenza con cui vengono eseguite le istruzioni in un blocco interno ad una selezione o ad un ciclo e le vedremo nei prossimo paragrafi. L’istruzione return determina la fine dell’esecuzione di un sottoprogramma e fa tornare il punto di esecuzione al programma chiamante. Come abbiamo già visto return serve anche per terminare il programma principale main. L’istruzione goto (go to = vai a) è detta istruzione di salto incondizionato e produce come effetto il trasferimento del punto di esecuzione ad una posizione definita dal programmatore. La sintassi è: goto <etichetta> Questo comando ordina al computer di portare il punto di esecuzione nella posizione in cui il programmatore ha posizionato l’etichetta. Una etichetta è un qualsiasi identificatore valido seguito dal carattere : (duepunti). Per esempio: inizio: contatore++; ………. goto inizio; L’uso di questa istruzione, come detto nel paragrafo precedente, può e dovrebbe essere evitato perché rende difficile il controllo del programma durante l’esecuzione. Al suo posto si possono utilizzare le istruzioni strutturate elencate nei prossimi paragrafi. 3 Vedi http://it.wikipedia.org/wiki/Programmazione_strutturata. 38 Corso sul Linguaggio C++ Edizione 2010 3.3 La sequenza Per dire al computer di eseguire una serie di istruzioni in un determinato ordine nel linguaggio C si utilizza il blocco: { <istruzione1>;<istruzione2>; ………[<istruzioneN]; } In pratica si scrivono le istruzioni nell’ordine in cui devono essere eseguite (se si trattta di istruzioni semplici devono finire con il carattere ; -punto e virgola-) e si racchiude l’elenco tra parentesi graffe, un tale insieme di istruzioni si dice blocco. Le istruzioni interne ad un blocco possono essere istruzioni elementari o istruzioni di controllo. Se si utilizza il linguaggio C++ si possono inserire in un blocco anche istruzioni di dichiarazione. Per esempio tutte le istruzioni della funzione main nei programmi che abbiamo visto sono un blocco. Un blocco può contenere al suo interno una qualsiasi istruzione di controllo e quindi anche altri blocchi. Per esempio: int main() { int i=1; { int j=20; k=30; …… } i++; …….. return 0; } } blocco interno il blocco contenente tutte le istruzioni della funzione main contiene al suo interno un altro blocco che inizia con le dichiarazioni int j=20; k=30. Il raggruppamento delle istruzioni in blocchi è importante per l’area di validità delle variabili, infatti ogni variabile dichiarata in un blocco può essere utilizzata solo nelle istruzioni del blocco stesso e in quelle di tutti i blocchi contenuti in esso che seguono la dichiarazione, ma non può essere utilizzata al di fuori del blocco. Quindi nell’esempio precedente la variabile i può essere utilizzata sia nel blocco che inizia dopo di int main() che in quello interno, mentre le variabili j e k possono essere utilizzate solo nelle istruzioni racchiuse tra le due parentesi graffe del blocco interno. Area di validità delle variabili Le variabili dichiarate in un blocco si dicono variabili locali appunto perché possono essere utilizzate solo all’interno del blocco. È possibile però definire anche variabili globali che valgono in tutti i blocchi, per farlo bisogna dichiarare le variabili fuori da qualsiasi blocco, subito dopo le direttive al preprocessore, per esempio: #include <iostream.h> // variabili globali int totale=0; int main() { float prezzo; int q; } la variabile totale è una variabile globale mentre le variabili prezzo e q sono variabili locali della funzione main. 39 Corso sul Linguaggio C++ Edizione 2010 Ciclo di vita delle variabili Tutte le variabili vengono create quando viene eseguita la dichiarazione ma le variabili globali vengono cancellate solo alla fine del programma mentre, normalmente, le variabili locali vengono cancellate quando termina il blocco in cui sono state dichiarate. È possibile evitare la cancellazione delle variabili locali alla fine del blocco utilizzando il modificatore della modalità di memorizzazione static che abbiamo visto nel paragrafo 2.2. Per esempio consideriamo il seguente programma: int main() { int i; for (i=1; i<10; i++) /*ripete 9 volte il blocco sottostante */ { static int contatore=1; cout << contatore << ";" ; contatore++; } cout << endl; system("PAUSE"); return 0; } Sapendo che l’istruzione for è una delle istruzioni di controllo per i cicli che vedremo fra poco e che in questo caso ha semplicemente l’effetto di far ripetere 9 volte le istruzioni del blocco interno, il risultato in output sarà: 1;2;3;4;5;6;7;8;9; Premere un tasto per continuare ... Mentre se si toglie la parola static nella dichiarazione della variabile contatore il risultato sarà: 1;1;1;1;1;1;1;1;1; Premere un tasto per continuare ... perché alla fine del blocco la variabile verrà cancellata e quindi, quando il blocco viene ripetuto, la variabile contatore viene ricreata con il valore 1. Anche le variabili globali possono essere definite static, in questo caso non viene modificato il ciclo di vita, perché tutte le variabili globali vengono cancellate alla fine del programma, ma viene modificata l’area di validità ed ha senso quando il programma è costituito da più moduli ovvero più file sorgenti, in quanto la variabile sarà visibile solo nel modulo dove è stata definita mentre normalmente le variabili globali sono visibili in tutti i moduli dal momento in cui sono state definite in poi. 3.4 La Selezione Il linguaggio C++ prevede due istruzioni per la selezione: l’istruzione if per la selezione semplice a una o due vie e l’’istruzione switch per la selezione a più vie. La sintassi dell’istruzione if è la seguente: if ( espressione logica ) Istruzione1 else Istruzione2 durante l’esecuzione il computer valuta l’espressione logica, se il risultato è vero (diverso da zero) verrà eseguita l’istruzione1 altrimenti, se è presente la clausola else, verrà eseguita 40 Corso sul Linguaggio C++ Edizione 2010 l’istruzione2. Se non c’è la clausola else la selezione è ad una sola via, altrimenti è a due vie. L’istruzione o le istruzioni presenti dentro l’if possono essere sia istruzioni semplici che strutturate, quindi possono essere anche un blocco ({ ……}) oppure un’altra istruzione if, in questo caso si dice che la seconda istruzione if è annidata nella prima. Il blocco deve obbligatoriamente essere utilizzato se le istruzioni da eseguire nel caso vero o nel caso falso sono più di una. Per esempio: If (a > b) max = a ; oppure if (a > b) {tmp=a ; a=b ; b=tmp ;} sono entrambe selezioni ad un via, l’istruzione max=a nel primo caso e il blocco costituito da tre istruzioni nel secondo caso, verranno eseguiti solo se a>b. Le seguenti istruzioni if sono esempi di selezione a due vie. If (b == 0) cout << "Indetermintata !"; else cout << "Impossibile!"; If (a >10) {e++ ; a =\ 10;} else {e-- ; a=*10 ;} Problema: equazione di primo grado Scrivere un programma per far risolvere al computer una qualsiasi equazione di primo grado in una variabile. Analisi del problema Si definisce equazione di primo grado in una variabile una qualsiasi equazione matematica riconducibile alla forma: ax+b=0 dove a e b sono due numeri qualsiasi. Risolvere un'equazione significa trovare tutti i numeri che sostituiti alla x verificano l'uguaglianza. Si può dimostrare che Se a<> 0 allora esiste una sola soluzione : x = -b/a Se a= 0 e b<>0 allora l'equazione non ammette soluzioni Se a=0 e b=0 allora le soluzioni sono infinite Noi ci limiteremo alle equazioni nel campo dei numeri reali. Per risolvere un equazione di primo grado è necessario conoscere il valore di a e di b. Per questo la prima cosa che il computer deve fare e procurarsi i valori dei coefficienti a e b. Dopo, se a è diverso da zero, deve calcolare e visualizzare la soluzione, altrimenti visualizza la frase "Equazione impossibile!" se b è diverso da zero oppure "infinite soluzioni" se b è uguale a zero. Il semplice programma che fa fare queste cose al computer è il seguente: #include <iostream.h> #include <stdlib.h> // Programma Eqaz1 : Soluzione delle equazioni di primo grado int main() { float a,b,x; cout << "Inserisci i due coefficienti: "; cin >> a; cin >> b; if (a==0) if (b==0) cout << "Equanzione indetrminata!\n"; 41 Corso sul Linguaggio C++ Edizione 2010 else cout << "Equazione impossibile!\n"; else {x= -b/a; cout << "\nLa soluzione è: " << x << endl;} system("PAUSE"); return 0; } Si noti che quando due istruzioni if sono annidate, come nell’esempio precedente, il primo else che si trova si riferisce sempre all’ultimo if, perché prima di proseguire l’istruzione if esterna bisogna completare quella interna. Quello che segue è il diagramma a blocchi corrispondente al programma. Inizio Scrivi “Inserisci i coefficienti:” Leggi a,b a == 0 Si x = -b/a Scrivi x b == 0 Scrivi “Equazione impossibile!” Si Scrivi “Equazione indeterminata!" Fine 3.5 Selezione multipla La selezione si utilizza quando in un algoritmo c’è un gruppo di azioni che deve essere eseguito solo in un determinato caso. Se i casi possibili, e i relativi gruppi di azioni, sono due utilizza la selezione a due vie. Se i casi sono più di due si possono utilizzare le istruzioni if annidate: If (caso == 1) { \\ primo gruppo di istruzioni ………… } else if (caso == 2) { \\ secondo gruppo di istruzioni ………… } else if (caso == 3) 42 Corso sul Linguaggio C++ Edizione 2010 { \\ terzo gruppo di istruzioni ………… } ma per questo tipo di algoritmi la maggior parte dei linguaggi di programmazione mette a disposizione anche un’istruzione apposita, quella per la selezione multipla, che per il linguaggio C++ è l’istruzione switch. La sintassi è la seguente: istruzione switch switch ( espressione numerica ) { blocco switch } blocco switch case costante : istruzione default : istruzione L’espressione numerica in base alla quale viene effettuata la scelta deve essere di tipo intero (quindi può essere anche di tipo char). Il computer valuterà l’espressione e poi farà proseguire il programma dal punto in cui c’è scritto case x: con x = risultato dell’espressione. Se non c’è nessuna costante nel blocco switch che corrisponde al risultato dell’espressione verranno eseguite le istruzioni poste dopo la parola chiave default, se non c’è nemmeno la clausola default il controllo passa all’istruzione successiva al blocco switch. La traduzione di questo costrutto in linguaggio macchina è più efficiente rispetto alla traduzione degli if annidati. Se fra i vari gruppi di istruzioni possibili ne deve essere eseguito solo uno, bisogna utilizzare anche l’istruzione break che fa saltare il punto di esecuzione all’istruzione successiva al blocco switch saltando quindi tutte le istruzioni rimanenti inserite nel blocco. Per esempio se vogliamo tradurre con l’istruzione switch le istruzioni if annidate viste all’inizio di questo paragrafo dobbiamo scrivere: switch (caso) { case 1: // primo gruppo di istruzioni ………… break case 2: // secondo gruppo di istruzioni ………… break case 3: // terzo gruppo di istruzioni ………… } Problema: una semplice calcolatrice Vogliamo fare un programma per realizzare una semplice macchina calcolatrice che fa le 4 operazioni fondamentali. Il computer deve leggere due numeri, chiedere quale operazione effettuare (+, -, *, /) e visualizzare il risultato. Il programma potrebbe esser il seguente: 43 Corso sul Linguaggio C++ Edizione 2010 #include <iostream.h> #include <stdlib.h> /* programma SCalc: Una semplice calcolatrice */ int main() {double a,b; char op; cout << "Inserisci il primo operando: "; cin >> a; cout << "Inserisci il secondo operando: "; cin >> b; cout << "Quale operazione (+ - * /)? "; cin >> op; switch (op) { case '+': cout << "Risultato = " << a+b << endl; break; case '-': cout << "Risultato = " << a-b << endl; break; case '*': cout << "Risultato = " << a*b << endl; break; case '/': cout << "Risultato = " << a/b << endl; break; default : cout << "Errore! bisogna inserire uno dei segni + - * /\n"; } system("PAUSE"); return 0; } Mandandolo in esecuzione e provando, per esempio, la moltiplicazione tra i due numeri 5 e 3 si ottiene: Inserisci il primo operando: 5 Inserisci il secondo operando: 3 Quale operazione (+ - * /)? * Risultato = 15 Premi un tasto per continuare ... 3.6 I cicli Il principale vantaggio che ha un computer rispetto ad un essere umano nell’eseguire un compito è la velocità di esecuzione. Un personal computer, per esempio, è in grado di eseguire qualche centinaio di milioni di operazioni aritmetiche al secondo, cosa che nessun essere umano è in grado di fare. Però per fargli eseguire una operazione bisogna dargli il comando opportuno e per fargli eseguire più operazioni bisogna scrivere un programma dove sono elencati i comandi per ciascuna operazione. Per fortuna anche se il computer esegue milioni di operazioni al secondo non è necessario scrivere programmi con milioni di istruzioni per ogni secondo di elaborazione, infatti la maggior parte dei programmi contiene relativamente poche istruzioni che però vengono fatte eseguire più volte, in alcuni casi anche milioni o miliardi di volte. Per far risolvere un problema complesso al computer bisogna spesso progettare un procedimento che preveda la ripetizione meccanica di una serie di azioni semplici e questo è un modo di affrontare i problemi a cui noi umani non siamo abituati. Per questo motivo le istruzioni per i cicli, cioè quelle che servono per far ripetere una serie di azioni al computer, sono molto importanti in ogni linguaggio di programmazione. Il linguaggio C ha tre istruzioni per i cicli: l’istruzione do (ripetizione postcondizionale), l’istruzione while (ripetizione precondizionale) e l’istruzione for (ripetizione con contatore). Istruzioni do e while Entrambe le istruzioni do e while servono per far ripetere una serie di azioni fin quando la condizione prefissata è vera, appena la condizione diventa falsa il ciclo termina. La differenza sta solo nel momento in cui la condizione viene valutata, nel caso del do la condizione viene controlla solo dopo l’esecuzione dell’istruzione del ciclo mentre nel caso del while prima. Per questo motivo il ciclo do viene detto postcondizionale mentre il ciclo while viene detto precondizionale. La condizione che 44 Corso sul Linguaggio C++ Edizione 2010 viene valutata ogni volta nel ciclo può essere una qualsiasi espressione logica e quindi anche un espressione con risultato intero (0 = falso, un numero diverso da zero = vero). Le azioni eseguite dal computer nei due casi sono quelle esplicitate nel seguente diagramma a blocchi: si istruzione condizione si istruzione no condizione no A) ciclo while (precondizionale) B) Ciclo do (postcondizionale) La sintassi delle due istruzioni è la seguente: Istruzione do do istruzione while ( condizione ) ; Istruzione while while ( condizione ) istruzione Per esempio con il seguente programma int main() { int i=5; do cout << i-- << "\t"; while (i>0); system("PAUSE"); return 0; } l’istruzione cout << i-- < “\t” viene eseguita mentre la condizione i>0 è vera e quindi viene ripetuta fin quando il valore di i arriva a zero. Il risultato in output sarà: 5 4 3 2 1 Premere un tasto per continuare In questo caso se avessimo usato l’istruzione while invece di do il risultato non sarebbe cambiato. Cambia qualcosa tra do e while nei casi in cui la condizione è vera già la prima volta che viene eseguita l’istruzione perché in questo caso con while il ciclo non viene eseguito mentre con do viene eseguito sempre almeno una volta. Istruzione for Il più comune tipo di ciclo è quello in cui si fa ripetere dal computer una serie di azioni un numero prefissato di volte. Questo tipo di ciclo si può realizzare utilizzando un contatore che parte da zero e le istruzioni do e while con la condizione per far continuare il ciclo impostata nel seguente modo: contatore < numero volte che il ciclo deve essere ripetuto Questo tipo di ciclo viene detto ciclo con contatore o ciclo enumerativo, considerato che è un tipo di ciclo molto utilizzato, la maggior parte dei linguaggi di programmazione ha un’istruzione apposita per esso: l’istruzione for. 45 Corso sul Linguaggio C++ Edizione 2010 La sintassi dell’istruzione for per il linguaggio C++ è la seguente: Istruzione for for inizializzazione ( ; Condizione ; incremento ) istruzione che corrisponde al seguente diagramma a blocchi: inizializzazione vera condizione falsa incremento istruzione prosegue il programma questo significa che, prima di tutto viene eseguita l’istruzione di inizializzazione, poi inizia il ciclo e viene valutata la condizione (quindi si tratta di un ciclo precondizionale), poi, se la condizione è vera, viene eseguita l’istruzione e l’incremento che è l’ultima operazione del ciclo. Per esempio se vogliamo far scrivere dieci volte su video la parola “ciao” basta inserire la seguente istruzione for: for (int i=0; i<10; i++) cout << "ciao\n" ; il computer prima crea la variabile i con zero dentro, poi inizia il ciclo in cui controlla se i<10 e, fin quando è vero, scrive ciao su video e incrementa i. Da notare che quando l’istruzione di inizializzazione comprende anche la dichiarazione della variabile come nell’esempio precedente, la variabile sarà creata appositamente per il ciclo e poi cancellata alla fine del ciclo, quindi si tratterà di una variabile locale con area di validità e ciclo di vita ristretta alle sole istruzioni del ciclo. Se le istruzioni del ciclo sono più di una bisogna inserire un blocco al posto dell’istruzione del ciclo. Per esempio supponiamo di voler scrivere il programma per far calcolare la media di n numeri letti da tastiera, il programma può chiedere “Quanti numeri?” e poi eseguire un ciclo per leggere i numeri e sommarli. Nel ciclo devono essere inserite più istruzioni, quelle per leggere un numero e quella per sommare il numero letto all’accumulatore: #include <iostream.h> #include <stdlib.h> // Programma media. Calcola la media di una serie di numeri in input int main() { float somma=0, num, media; int i,n; cout << "Quanti numero? "; cin >> n; for (i=0; i<n; ) {cout << "inserisci il numero " << ++i << ": "; cin >> num; somma += num;} media = somma / n; 46 Corso sul Linguaggio C++ Edizione 2010 cout << "La media e' = " << media << endl; system("PAUSE"); return 0;} In questo caso l’istruzione di incremento è stata omessa perché conveniva includerla tra le istruzioni del ciclo: cout << "inserisci il numero " << ++i << ": « ; si noti l’uso della notazione prefissa (++i) che produce in output il risultato dell’incremento. Mandando in esecuzione il programma con tre numeri in input (3, 5, 6) si ottiene in output : Quanti numeri ? 3 Inserisci il numero 1: 3 Inserisci il numero 2: 5 Inserisci il numero 3: 6 La media e’ = 4.66667 Premere un tasto per continuare ... Con tutti i tipi di cicli si possono usare anche le istruzioni di salto break e continue. L’istruzione break interrompe l’esecuzione del ciclo e fa proseguire il programma con l’istruzione successiva al ciclo. L’istruzione continue interrompe l’iterazione corrente senza completare le istruzioni presenti nel corpo del ciclo e, a differenza di break, fa proseguire il ciclo con l’iterazione successiva, nel caso del ciclo for il punto di esecuzione passa all’istruzione di incremento che è l’ultima istruzione del ciclo, nel caso dei cicli do e while passa al controllo della condizione. Problema numeri primi Visualizzare i numeri primi compresi tra due limiti a e b dati in input. I numeri primi sono quelli divisibili solo per 1 e per se stesso. Si può dimostrare che per vedere se un numero è primo basta controllare che non è divisibile per nessun numero compreso tra 2 e la radice del numero. Per fare questo possiamo utilizzare un ciclo ma se ad un certo punto troviamo un divisore è inutile continuare il ciclo fino alla fine perché già sappiamo che il numero non è primo. Il programma è il seguente: #include <iostream.h> #include <stdlib.h> #include <math.h> int main(){ /* programma numprimi. Visualizza tutti i numeri primi compresi tra due numeri a e b dati in input*/ int a,b,p,i; cout << "Inserisci il limite inferiore: "; cin >> a; cout << "Inserisci il limite superiore: "; cin >> b; for (p=a; p<=b; p++) {for (i=2;i<= sqrt(p);i++) //se c'è un divisore <= sqr(p) ferma il ciclo if (!(p % i)) break; // se il ciclo non è stato fermato il numero p è primo if (i > sqrt(p)) cout << p << " - "; } system("PAUSE"); return 0; } Per utilizzare la funzione sqrt (square root= radice quadrata) bisogna includere il file di header math.h. 47 Corso sul Linguaggio C++ Edizione 2010 3.7 Documentazione Il linguaggio C++ include poche operazioni e istruzioni base, molte altre sono disponibili in librerie esterne in genere fornite insieme al compilatore del linguaggio, per utilizzarle bisogna collegare le librerie che le contengono. Per esempio l’operazione radice quadrata ( x =sqrt(x)) o la potenza (xy = pow(x,y)) sono incluse in una libreria a parte e devono essere dichiarate prima di essere utilizzate. Per inserire la dichiarazione di tutte le funzioni matematiche basta includere all’inizio del programma il file di intestazioni math.h. Per farlo si può utilizzare la direttiva: #include <math.h> La quantità di funzioni esterne incluse nelle librerie fornite insieme al linguaggio è molto elevata, un elenco di quelle standard del linguaggio C, fornite insieme al compilatore g++ incluso nel pacchetto dev-C++, si può trovare nel file libc.htm presente sul server del laboratorio, scaricato dal sito ufficiale dell’organizzazione gnu: http://www.gnu.org/software/libc/manual e su internet all’indirizzo: http://www.silicontao.com/ProgrammingGuide/GNU_function_list/index.html oppure, più comodamente in una pagina con frame all’indirizzo: http://www.silicontao.com/ProgrammingGuide/GNU_function_list/index.html Per avere informazioni sulle peculiarità del linguaggio C++ implementato nel compilatore g++ della GCC (Gnu Compiler Collection) incluso nel DEV C++ si deve visitare il sito: http://www.gnu.org/software/gcc/onlinedocs/ purtroppo solo in lingua inglese. Una copia del manuale in formato pdf (gcc.pdf) è presente sul server nel laboratorio. Se avete installato il visual basic potete ottenere molte informazioni sul C++ e le sue librerie consultando la Microsoft Developer Network (ovvero la guida in linea di visual basic), nel caso della versione 6.0, per avere un elenco alfabetico di tutte le funzioni disponibili nel C++ si può andare al capitolo Visual C++ documentation Using visual C++ Visual C++ programmer guide Run-Time Library reference Alphabetic functions references. Anche se questa guida descrive il linguaggio Microsoft Visual C++ la maggior parte delle informazioni in essa contenute sono valide anche per il linguaggio Dev-C++ visto che entrambi aderiscono allo standard ANSI. Anche su Wikipedia trovate molte informazioni sul linguaggio C++ e sui compilatori disponibili. Trovate informazioni e spiegazioni anche per le librerie standard del C++ all’indirizzo: http://en.wikipedia.org/wiki/C%2B%2B_Standard_Library Per approfondire la conoscenza del linguaggio C++ un ottimo manuale è quello di Bruce Eckel, Thinking in C++, in due volumi, disponibile gratuitamente sul sito: http://www.mindview.net/, il primo volume disponibile anche in italiano, tradotto da Umberto Sorbo e altri, è presente sul server del laboratorio e all’indirizzo http://www.umbertosorbo.it. 3.8 Esercizi 18) Individuare gli errori di sintassi nelle seguenti istruzioni: a) cin >> a+2: b) cout >> “Prova”; c) FOR(a=1, a>10, a++); 48 Corso sul Linguaggio C++ Edizione 2010 d) a=b=c 19) Siano dati i seguenti cicli: a) for (i = 0; i<n; i=i+2) ; b) for (i = 0; i<n; i++) {i++; i++;} c) i=n/2; while (i < n) i *=2; d) i=n; do i-=n/2; while ( i >=0); Completare la seguente tabella indicando quante volte verrà eseguito il ciclo ed il valore della variabile i alla fine dello stesso supponendo che la variabile n = 10 Ciclo a) b) c) d) N° di volte Valore di i 20) Completare la tabella indicando cosa conterrà la variabile C dopo l'esecuzione delle seguenti istruzioni if annidate tenendo conto dei valori iniziali di A e B indicati nelle prime due colonne: if (A + B >10) if (A > 10 && B < 10) C=A - B; ELSE C=A - B - 100; else if (A < 0 || B < 0) C= A + B; ELSE C= A - B + 100; A 10 -10 5 60 B 20 -20 4 -10 C 21) Cosa fa il seguente frammento di programma? int i=0; while(i<50) {if (i%2) cout << i << “\t”; i++;} 22) Considerate il seguente frammento di programma: t=-1; for (i=1; i<=n; i++) if (f(i)) t=i; if (t>=0) cout << t ; Quale delle seguenti affermazioni è corretta? a) Il programma cerca e stampa il più piccolo intero x fra 1 e n tale che f(x)!=0; se tale intero non esiste, il programma entra in un ciclo infinito. b) Il programma cerca e stampa il più grande intero x fra 1 e n tale che f(x)!=0; se tale intero non esiste, il programma entra in un ciclo infinito. c) Il programma cerca e stampa il più piccolo intero x fra 1 e n tale che f(x)!=0; se tale intero non esiste, il programma non stampa nulla. d) Il programma cerca e stampa il più grande x fra 1 e n tale che f(x)!=0; se tale intero non esiste, il programma non stampa nulla. 49 Corso sul Linguaggio C++ Edizione 2010 23) Cosa scrivono in output i seguenti cicli? a) for (int i=1; i<7;i++) cout << i %5<<’ ‘; …………………………………………. b) for (int i=1; i<7;i++) cout << i*2-1 <<’ ‘; …………………………………………. c) int i=10; do {cout << i<<’ ‘; i -=5;} while (i>0); ……………………………………… d) int j=5,i=5; while(i>0) {cout << j <<’ ‘; if (i%2) j +=i else j-=i; i--;} ….…………….. 24) Considerate il seguente frammento di codice: float i,f; ... f=1/50.0; for (i=0.0; i!=1.0; i+=f) printf("A"); Quale dei seguenti effetti ha il ciclo for indicato? a)Stampa 50 volte il carattere A; b)Stampa 51 volte il carattere A; c)Stampa 49 volte il carattere A; d) Il ciclo potrebbe non terminare, stampando infinite volte il carattere A; e)Il compilatore segnala un errore; Solo selezione 25) Data la misura dei tre lati di un triangolo dire se è isoscele scaleno o equilatero. 26) Un lanificio acquista lana dai pastori della zona. La lana acquistata è di tre tipi diversi (che si acquistano a prezzi diversi), il prezzo inoltre cambia in base all'umidità, se questa supera il 30% il prezzo di acquisto si riduce del 15%. Fare un programma che aiuti l'impiegato a calcolare la somma da versare ai pastori che portano la lana. 27) Una cooperativa agricola vende ai propri soci il vino che produce al prezzo di € 1 al litro se bianco, 1.5 se rosso e 2 se D.O.C. E' possibile prendere il vino sfuso o imbottigliato, in questo secondo caso il prezzo aumenta ulteriormente di €. 0.20 a litro. Descrivere un programma per calcolare il prezzo del vino ogni volta che viene un cliente. 28) Un commerciante, in un mese, ha acquistato merce per X lire e ne ha venduto per Y lire. Sapendo che l'aliquota IVA sulla merce trattata è il 20% calcolare l'IVA da versare o l'IVA a credito a fine mese. 29) Fare un programma per calcolare l'IRPEF su un reddito assegnato. L’IRPEF, in base alla finanziaria 2007, si calcola così: a. Se il reddito è inferiore o uguale a 15.000€ si calcola il 23% del reddito b. se supera i 15.000 ma é inferiore a 28.001 si calcola il 23% sui primi 15.000 (=€.3450) + il 27% sulla parte eccedente i 15.000€. c. Se supera i 28.000 ma è inferiore ai 55001€ l’imposta dovuta è data da 6960€ (pari al massimo dello scaglione precedente) + il 38% sulla parte di reddito eccedente 28.000€ d. d. da 55.000€ fino a 75.000€ l’imposta è pari a € 17.220 (15000*0,23+13000*0,27+27000*0,38) + il 41% della parte di reddito eccedente 55.000€ e. e. oltre € 75.000 l’imposta è pari a 25420€ + il 43% della parte di reddito che supera 75000 euro. graficamente gli scaglioni possono essere rappresentati così: 27% 23% 15000 38% 43% 41% 28000 55000 50 75000 43% Corso sul Linguaggio C++ Edizione 2010 30) Far calcolare il prezzo di una quantità assegna di vino che costa € 2.1 al litro supponendo che se si acquista una quantità >= 20 litri si ha diritto al 10% di sconto. 31) Data in input una cifra qualsiasi, dopo averla arrotondata alle decine (per eccesso o per difetto a seconda che il resto superi 5 o no), indicare quanti biglietti da 10€, quanti da 50 € e quanti da 100€ sono necessari per pagare la somma inserita in input utilizzando il minor numero possibile di biglietti. Cicli 32) Dato in input un numero intero N calcolare N fattoriale. 33) Dato in input un numero qualsiasi x e un numero intero n positivo, calcolare il risultato di xn. 34) Assegnati n e d calcolare la somma dei primi n termini della progressione geometrica di ragione d: 1 + d + d2 + d3 + …. + dn 35) Assegnati n e d calcolare la somma dei primi n termini della progressione aritmetica di ragione d: 1+ 2d+ 3d+ 4d+ …+ nd 36) Calcolare il prodotto di due numeri A e B interi effettuando solo addizioni. 37) Calcolare il quoziente e il resto della divisione tra due numeri interi A e B effettuando solo sottrazioni. 38) Dato in input un numero intero qualsiasi controllare se è primo oppure no e visualizzare la risposta. 39) Dati due numeri interi A e B trovare il minimo comune multiplo. 40) Dati due numeri interi A e B trovare il massimo comun divisore con l’algoritmo di Euclide. Euclide ha dimostrato che, detti A e B due numeri interi con A>=B, se A è divisibile per B allora MCD(A,B) = B altrimenti MCD(A,B) = MCD(B, A%B). In pratica si può cercare il MCD tra due numeri più piccoli. Ripetendo questa operazione fino a quando i due numeri diventano divisibili si trova il Massimo Comun Divisore. 41) Dato in input un numero intero N visualizzare la somma dei suoi divisori. 42) Calcolare la somma dei numeri interi da 1 a n (n in input): 1+ 2 + 3 + 4 + ….. + n (verificare che corrisponde a n*(n+1)/2) 43) Far calcolare la somma dei primi n termini (N in input) della seguente successione: 1+ ½+ 1/3+ ¼ + 1/5 + 1/6 + 1/7+ ……+ 1/n 44) Far calcolare la somma dei primi n numeri dispari e verificare che corrisponde sempre a n*n 45) Scrivere le istruzioni per far visualizzare i primi n termini (n in input) delle seguenti successioni di numeri: 51 Corso sul Linguaggio C++ Edizione 2010 a) b) c) d) 2 1 1 2 4 6 8 10 12 14 ... -3 5 -7 9 -11 13 ... 2 9 4 25 6 49 8… 3 4 5 8 9 16 17 32 33… 46) Far visualizzare su video un menù con una serie di voci tipo quelle dell’esempio sottostante, chiedere la scelta e controllare che venga inserito un valore valido, visualizzare l’ozione corrispondente alla scelta, far terminare il programma quando viene scelta la funzione corrispondente a Fine. Possibili scelte 1) Funzione 1 2) Funzione 2 3) Funzione 3 4) Funzione 4 5) Fine Quale scegli? 47) Far calcolare la somma dei quadrati dei primi n numeri interi e verificare che corrisponde a n*(n+1)*(2*n+1)/6 48) Far calcolare la somma della seguente successione di numeri interi: 1*N + 2*(N-1)+ 3*(N-2)+ …… +(N-1)*2 + N*1 si tratta di sommare una serie di prodotti di due numeri dove il primo fattore varia da 1 a N mentre il secondo diminuisce da N a 1. (Questa somma, qualunque sia N è sempre = N*(N+1)(N+2)/6 ) 49) Far Calcolare la somma della seguente successione di numeri interi: 1*2 + 2*3 + 3*4 + 4*5 + …….. + (n-1)*n il primo fattore va da 1 a n-1 mentre il secondo da 2 a N. (Questa somma è sempre uguale a n*(n+1)(2*n-2)/6) 50) Calcolare la somma della seguente successione e verificare che corrisponde a n*(n+1)*(2*n+1)/3 n*(n+1) + (n-1)*(n+2)+ (n-2)*(n+3)+ ……….+ 2*(n+n-1) + 1*(n+n) 51) Far visualizzare la tabella dei codici ASCII. 52) Calcolare la somma della successione: Nel ciclo fermarsi quando l’ultimo termine sommato è inferiore ad un numero E dato in input. in( x) = y − y2 2 + y3 3 − 52 y4 4 + y5 5 + .... Corso sul Linguaggio C++ Edizione 2010 supponendo di avere in input x, con 0 <x < 2 e che y = x-1. Fermare il ciclo quando il valore assoluto dell’ultimo termine sommato è minore dell’errore E dato anch’esso in input. Il risultato è il logaritmo naturale di x approssimato con un errore massimo = E. 53) Dato in input un numero intero n trasformarlo in binario. 54) Far visualizzare i primi n termini della successione di Fibonacci: 1; 1; 2; 3; 5; 8; 13; 21; ….. (ogni numero è la somma dei due precedenti e i primi due sono entrambi = 1). 55) Dato in input un numero intero n e trasformarlo nel sistema di numerazione a base b (b in input) 56) Dato un numero x scritto in un sistema a base b (x e b in input) trasformarlo in decimale. 57) Scrivere il programma che, dato in input x, calcoli la somma della seguente successione: sen( x) = x − − + + + .... x9 x5 x7 x3 7! 9! 5! 3! come si vede alcuni i termini vengono sommati o sottratti alternativamente. Il risultato, se si continua all’infinito, è la funzione trigonometrica sen(x). Considerato che non si può continuare all’infinito, fermarsi quando il valore assoluto dell’ultimo termine sommato è minore dell’errore E dato in input (si può dimostrare che la somma di tutti i restanti termini è sempre minore del valore assoluto dell’ultimo termine sommato). 58) Come nell’esercizio precedente scrivere il programma che, dato in input un numero reale x, calcoli la somma della seguente successione: cos(x) = 1 − + − + + .... x2 x4 x6 x8 2! 4! 6! 8! Sommando gli infiniti termini di questa successione si calcola la funzione coseno(x). Nel ciclo fermarsi quando il valore assoluto dell’ultimo termine è minore dell’errore E dato in input. 59) Come per gli esercizi precedenti far calcolare la somma della seguente successione (x in input): ex = 1 + x + + + + + .... x2 x3 x4 x5 2! 3! 4! 5! x in questo caso viene calcolato e , fermarsi quando si somma il termine n, tn = xn/n! con n>x e abs(tn)*(n/(n-x))< E, dove E è l’errore massimo dato in input. 60) Scrivere un programma che dato in input un numero qualsiasi lo visualizzi invertendo l’ordine delle cifre. Doppio ciclo 61) Far visualizzare la tavola pitagorica fino ad un numero N assegnato. 53 Corso sul Linguaggio C++ Edizione 2010 62) Far visualizzare i primi n numeri primi (può essere utilizzato il programma per controllare se un numero è primo oppure no). 63) Far calcolare X elevato a N utilizzando solo addizioni (si possono utilizzare i programmi per fare la potenza e la moltiplicazione). 64) Far visualizzare tutti gli elementi della serie di Fibonacci (vedi esercizio 54) che siano anche numeri primi (composto dai due programmi Fibonacci + controllo numeri primi). 65) Scomporre un numero qualsiasi in fattori primi. 66) Assegnata una frazione A/B qualsiasi ridurla ai minimi termini 67) Cercare e far visualizzare tutti i numeri perfetti minori o uguali ad un numero N dato in input. I numeri perfetti sono quelli per i quali la somma dei divisori (escluso il numero considerato) è uguale al numero stesso, per esempio 6 (6=1+2+3) oppure 28 (28=1+2+4+7+14). 68) Scrivere un programma che faccia funzionare un computer come un registratore di cassa. Deve permettere di inserire i prezzi dei prodotti acquistati da ciascun cliente e stampare lo scontrino. A fine giornata deve visualizzare il totale incassato, il numero degli scontrini emessi, l'importo delle vendite diviso per quattro categorie merceologiche: Alimentari, Detersivi, Salumeria, Altro. 69) Assegnato N far visualizzare tutti i numeri primi minori di N che fanno parte della serie di Fibonacci. 70) Far leggere una serie di numeri in input e fare la media solo dei numeri primi. 71) Far generare tutte le possibili combinazioni di risultati di 4 partite di calcio. (es; 1111, 111X, 1112, 11X1, 11XX, 11X2, 1121 ecc. Si tratta delle permutazioni con ripetizione di 3 elementi a 4 a 4. Si può, per esempio, usare il sistema di num. ternario incrementando una variabile e trasformandola in ternario). Risposte esercizi Esercizio 18): a) dopo >> ci deve essere una variabile e non un’espressione b) dopo cout è permesso solo l’operatore << c) Il linguaggio C++ è case sensitive, FOR non è la stessa cosa di for, inoltre per separare l’istruzione di inizializzazione dalla condizione e dall’istruzione di incremento bisogna usare il punto e virgola (;) e non la virgola. d) manca il punto e virgola finale Esercizio 19) Ciclo a) b) c) d) N° di volte 5 4 1 3 Esercizio20) 54 Valore di i 10 12 10 -5 Corso sul Linguaggio C++ Edizione 2010 A B C 10 20 -110 -10 -20 -30 5 4 101 60 -10 70 Esercizio 21): scrive su video tutti i numeri dispari minori di 50. Esercizio 22) esatta d) Esercizio 23) a) b) c) d) for (int i=1; i<7;i++) cout << i %5<<’ ‘; …1 2 3 4 0 1 ……….……. for (int i=1; i<7;i++) cout << i*2-1 <<’ ‘; ………………1 3 5 7 9 11 ………………………….. int i=10; do {cout << i<<’ ‘; i -=5;} while (i>0); …………10 5…………………………………… int j=5,i=5; while(i>0) {cout << j <<’ ‘; if (i%2) j +=i else j-=i; i--;} …5 10 6 9 7……………….. . Esercizio 24) esatta d): il tipo double ha una rappresentazione interna in binario virgola mobile e un numero con la virgola tipo 1/50 potrebbe diventare periodico quando viene trasformato in binario - anzi 1/50 è periodico in binario perché non danno luogo a numeri binari periodici solo le frazioni che si possono rappresentare con un denominatore che è una potenza del 2 - e quindi ha infinite cifre per cui non può essere rappresentato esattamente, questo significa che sommando 50 volte 1/50 scritto in binario in maniera non esatta, probabilmente non otterremo 1 e quindi il ciclo potrebbe non finire mai perché non si verifica mai la condizione i=1.0 che fa terminare il ciclo. Esercizio 40) Il cuore del programma per trovare il MCD potrebbe essere il seguente: if (A < B) {r=A; A=B; B=r;} // ordina i due numeri r=A%B; while (r > 0) {A=B; B=r; r=A%B;} MCD = B; Esercizio 46) Il programma potrebbe essere il seguente: int main(){ short int scelta=0; /* ripete la visualizzazione del menù fino a quando non si sceglie la funzione FINE */ do { // visualizza il menù system("CLS"); // cancella la finestra di output cout << endl << "\t\tPossibili scelte" << endl; cout << "\t\t1) Funzione 1"<< endl; cout << "\t\t2) Funzione 2"<< endl; cout << "\t\t3) Funzione 3"<< endl; cout << "\t\t4) Funzione 4"<< endl; cout << "\t\t5) Fine"<< endl; cout << "\n\n\t\t\tQuale Scegli (1-5)?" ; cin >> scelta; // chiede la scelta // visualizza messaggio di errore se la scelta non è valida if (scelta <1 || scelta > 5) { cout << "\n\t\tErrore! Inserire un numero da 1 a 5!\n"; system("PAUSE"); } else { // esecuzione della funzione corrispondente switch (scelta) { case 1: cout <<"E' stata scelta la funzione 1\n"; break; case 2: cout <<"E' stata scelta la funzione 2\n"; break; case 3: cout <<"E' stata scelta la funzione 3\n"; break; case 4: cout <<"E' stata scelta la funzione 4\n"; break; } if (scelta < 5) system("PAUSE"); } 55 Corso sul Linguaggio C++ Edizione 2010 } while (scelta != 5); return 0;} Esercizio 51) Si può sfruttare la compatibilità tra il tipo di dati char e il tipo int più il fatto che l’istruzione cout visualizza una variabile char come carattere e una int come numero. Il programma potrebbe essere il seguente: int i; char x; for (i=1; i<=255; i++) {x = i; cout << i << '=' << x << '\t';} cout << endl; 56 4 Capitolo Corso sul Linguaggio C++ Edizione 2010 4. I sottoprogrammi I circuiti del computer sanno fare solo poche operazioni elementari le quali possono essere utilizzate nei programmi per costruire operazioni più complesse. Una volta scritto un programma, il computer diventa capace di compiere un'operazione in più rispetto a quelle che sapeva fare prima, quella realizzata attraverso il programma che, quindi, corrisponde a più istruzioni elementari. Per esempio i circuiti del computer non sono in grado di eseguire la radice quadrata di un numero ma, come abbiamo visto, esiste il programma sqrt, incluso nelle librerie di programmi fornite con il compilatore C++, che permette di svolgere questa operazione. Quando il computer calcola una espressione che comprende, per esempio, ‘sqrt(x)’, verrà prima eseguito il programma per calcolare la radice quadrata di x, poi verrà calcolato il resto dell’espressione sostituendo alle parole ‘sqrt(x)’ il risultato di questo programma. Non ha nessuna importanza, per i programmatori, il fatto che il calcolo della radice sia stata fatta con l’uso di un programma invece che direttamente dai circuiti dell’unità aritmetico-logica inclusa nel processore, il programma resta lo stesso. Se un programma viene inserito in un altro programma che lo manda in esecuzione e dopo prosegue con le proprie istruzioni, allora il programma viene chiamato sottoprogramma mentre l’altro programma viene chiamato programma principale. Un sottoprogramma può essere a sua volta programma principale se richiama altri sottoprogrammi. Qualsiasi programma può essere utilizzato come sottoprogramma da un altro programma. Tutti i programmi scritti per un computer fanno aumentare le cose che il computer può fare e i programmatori possono, nei programmi successivi, utilizzare tutte le operazioni complesse fatte dai programmi precedenti come se fossero operazioni elementari. Le istruzioni sono i mattoni che ci permettono di costruire i programmi, usare i sottoprogrammi significa usare delle parti prefabbricate e quindi ridurre la complessità dei programmi. Conviene sempre usare i sottoprogrammi ma in particolar modo conviene usarli quando lo stesso gruppo d'istruzioni deve essere inserito in più punti del programma. In questo caso, invece di scrivere ogni volta queste istruzioni, conviene costruire un sottoprogramma e inserire nel programma principale, ogni volta che serve, una chiamata al sottoprogramma. Nella maggior parte dei linguaggi di programmazione esistono due tipi di sottoprogrammi: le funzioni e le procedure. La differenza tra i due sta nel fatto che le prime restituiscono al programma principale un risultato che viene sostituito alla chiamata della funzione (come abbiamo visto per la funzione sqrt()) mentre le procedure non restituiscono nessun valore, semplicemente sono un gruppo di istruzioni che possono essere mandate in esecuzione in qualsiasi punto del programma principale. 57 Corso sul Linguaggio C++ Edizione 2010 Con il linguaggio C++ esiste un solo tipo di sottoprogramma, le funzioni, ma una funzione può anche non restituire alcun valore e quindi corrispondere alle procedure degli altri linguaggi. 4.1 Le funzioni Per inserire una funzione in un programma bisogna rispettare la seguente sintassi Definizione di una funzione tipo di dato nome funzione void elenco parametri formali ( Intestazione ) blocco istruzioni Corpo L’ultimo pezzo, il blocco istruzioni, corrisponde ad un elenco di istruzioni e dichiarazioni racchiuse tra un coppia di parentesi graffe, quelle che verranno eseguite ogni volta che dal programma principale si richiama la funzione, e viene anche detto corpo della funzione. Tutta la prima parte della definizione, ovvero quella che si ottiene togliendo il blocco istruzioni, viene anche detta intestazione della funzione. Il tipo di dato che si premette al nome della funzione indica il tipo del risultato calcolato dalla funzione e può essere uno qualsiasi dei tipi definiti compreso il tipo void che significa nessun tipo o assenza di tipo. Le funzioni definite con il tipo void non forniscono risultati e quindi corrispondono alle procedure degli altri linguaggi. Se si omette il tipo di dato verrà assunto il tipo int. Nel corpo della funzione ci deve essere sempre l’istruzione return eventualmente seguita da un’espressione, a meno che la funzione non sia stata definita come void nel qual caso l’istruzione return si può omettere perché il compilatore la inserisce automaticamente dopo l’ultima istruzione del blocco. L’istruzione return serve sia per far terminare l’esecuzione della funzione che per definire il risultato della stessa. Infatti, dopo una qualsiasi esecuzione della funzione, il risultato restituito sarà proprio quello fornito dall’espressione presente dopo la parola return. Questo risultato deve essere conforme al tipo di dati specificato prima del nome della funzione nell’intestazione. Il nome della funzione è un normale identificatore e può essere un nome qualsiasi inventato dal programmatore, bisogna però rispettare le regole già viste per i nomi delle variabili nel paragrafo 2.2, quindi deve iniziare con una lettera, può contenere solo lettere e cifre, non può contenere spazi, simboli di punteggiatura e altri simboli speciali con l’eccezione del simbolo “_”, non può essere una parola chiave del linguaggio, non può contenere lettere accentate e, in genere, solo i primi 32 caratteri verranno considerati (in alcune versione del linguaggio C solo i primi 8 caratteri). Conviene scegliere nomi brevi ma significativi in modo da ricordarci, quando rileggiamo il programma, a cosa serve la funzione. Per esempio le seguenti righe sono tutte intestazioni valide di funzioni: double media(double num1, double num2, double num3); int calcolo(int x=0, int y=0); 58 Corso sul Linguaggio C++ Edizione 2010 void ordina(float x, float y); Una funzione, normalmente, esegue un calcolo utilizzando i dati forniti dal programma principale, per esempio se nel programma principale scriviamo sqrt(4) il risultato sarà 2.0, se invece scriviamo sqrt(25) il risultato sarà 5.0. I dati forniti dal programma principale si chiamano parametri della funzione. La funzione sqrt richiede un solo parametro ovvero il valore di cui vogliamo calcolare la radice quadrata. Quando si definisce una funzione bisogna anche indicare il tipo e il numero dei parametri che il programma principale gli fornirà ogni volta che la manda in esecuzione, per questo motivo nella dichiarazione delle funzioni si inserisce anche un elenco parametri formali racchiuso tra parentesi tonde. L’elenco ha la seguente sintassi: elenco parametri formali tipo di dato nome parametro formale = valore di default , È possibile anche definire funzioni che non hanno bisogno di parametri in questo caso si può omettere l’elenco parametri formali o utilizzare in sua vece la parola chiave void. Per esempio le due intestazioni seguenti sono equivalenti: int prova(); int prova(void); Quando si manda in esecuzione una funzione per la quale sono stati definiti dei parametri formali il programma principale deve fornire i valori che devono essere utilizzati dalla funzione durante l’esecuzione e questi valori, detti parametri attuali, devono corrispondere per numero e tipo ai parametri formali definiti e devono essere forniti nello stesso ordine con cui sono elencati i parametri formali. Il richiamo di una funzione, ovvero il comando per mandarla in esecuzione, può essere inserito in qualsiasi punto nel corpo del programma principale. La sintassi è: Chiamata di funzione nome funzione ( Parametro attuale ) , Se per un parametro si specifica il valore di default vuol dire che il parametro è facoltativo e quindi può essere omesso quando si richiama la funzione. Se un parametro facoltativo viene omesso, al suo posto viene utilizzato il valore di default specificato nella definizione. Se si omette un parametro facoltativo ma non i successivi, bisogna in ogni caso inserire le virgole nell’elenco dei parametri attuali anche per i parametri omessi. Problema scrivere una funzione per calcolare la potenza intera di un numero qualsiasi L’operazione di elevamento a potenza, come abbiamo già detto, non fa parte delle operazioni di base presenti nel linguaggio C++. Viene fornita, però, insieme al Dev-C++, inclusa nella libreria di funzioni matematiche, anche la funzione pow la cui definizione è 59 Corso sul Linguaggio C++ Edizione 2010 double pow(double x, double y) e che quindi calcola la potenza xy con y esponente reale qualsiasi. Supponiamo però di volerci creare una nostra funzione potenza, semplificata rispetto a quella fornita con il Dev-C++, che esegua l’elevazione a potenza di un numero reale qualsiasi con un esponente intero relativo. Oltre a definire la funzione, che potremmo chiamare pow come l’analoga fornita con il linguaggio, bisogna scrivere anche un programma principale main() per provarla. Il programma completo potrebbe essere il seguente: #include <iostream.h> #include <stdlib.h> double pow( double x, int e=1) {/*programma potenza. Calcola x elevato a e, con e intero relativo qualsiasi */ double ris=1.0; for (; e>0; e--) ris *=x; // se e è positivo while (e<0) {ris /=x; e++;} // se e è negativo return ris; } int main() { // chiede base ed esponente e calcola la potenza double base; int esp; cout.precision(14); //visualizza i risultati con 14 cifre diprecisione cout << "Inserisci la base: "; cin >> base; cout << "Inserisci l'esponente: "; cin >> esp; cout << "Potenza = " << pow(base, esp) << "\tbase = " << pow(base) << endl; system("PAUSE"); return 0; } La funzione è stata definita con due parametri formali (base ed e). Arbitrariamente, per mostrare come si utilizzano i parametri facoltativi, è stato impostato come facoltativo il secondo parametro, l’esponente. Nel programma principale la funzione viene richiamata due volte nell’istruzione: cout << "Potenza = " << pow(base, esp) << "\tbase = " << pow(base) << endl; la prima volta con entrambi i parametri e la seconda con un solo parametro. Si noti l’istruzione cout.precision(14) che porta a 14 le cifre complessive visualizzate per ogni numero in output dall’oggetto cout, normalmente sono solo 7. 4.2 Il passaggio dei parametri Le istruzioni che costituiscono una funzione vengono eseguite quando la funzione viene richiamata e, solo in quel momento, vengono create tutte le variabili locali definite nel corpo della funzione. Anche i parametri sono variabili locali e quindi possono essere utilizzate solo all’interno del blocco che costituisce il corpo della funzione dove sono state dichiarate, ma non è detto che vengano create nel momento in cui la funzione viene richiamata, infatti, il passaggio dei parametri dal programma principale ad un sottoprogramma può avvenire in due modi: per valore o per riferimento. Se il passaggio di un parametro avviene per valore, quando il sottoprogramma parte viene creata una nuova variabile locale che viene inizializzata con il valore del parametro attuale fornito dal programma principale. Quindi il sottoprogramma utilizzerà una sua variabile locale che occupa in memoria uno spazio diverso rispetto a quello della variabile eventualmente fornita come parametro attuale dal programma principale e che, solo inizialmente, conterrà lo 60 Corso sul Linguaggio C++ Edizione 2010 stesso valore. Tutte le modifiche che il sottoprogramma effettuerà sul valore della variabile non modificheranno il valore della corrispondente variabile del programma principale. Quando termina l’esecuzione del sottoprogramma questa variabile locale verrà cancellata come tutte le altre variabili locali e il suo valore verrà perso, mentre la variabile del programma principale fornita come parametro attuale non sarà stata toccata. Quando, invece, il passaggio avviene per riferimento il programma principale non passa al sottoprogramma il valore della variabile ma la posizione in memoria della stessa. In questo caso il sottoprogramma, quando parte, non crea una nuova variabile locale ma usa quella del programma principale semplicemente chiamandola con un altro nome. Quindi se il sottoprogramma modifica il valore del parametro questa modifica si ripercuote sul corrispondente valore della variabile corrispondente del programma principale. In pratica, durante l’esecuzione del sottoprogramma, la variabile corrispondente al parametro formale e quella corrispondente al parametro attuale condividono lo stesso spazio di memoria e quindi, in realtà, corrispondono ad un'unica variabile che però ha due nomi diversi. Normalmente il passaggio dei parametri avviene sempre per valore salvo quando si premette al nome di un parametro formale l’operatore unario &. Questo operatore, posto davanti al nome di una variabile, produce come risultato la posizione della variabile in memoria. Per esempio se una variabile intera di nome somma contiene il valore 10 vuol dire che in memoria, nei quattro bytes assegnati alla variabile, è memorizzato il numero 10 scritto in binario. La memoria di ogni computer è divisa in celle numerate progressivamente e il numero assegnato ad ogni cella è detto indirizzo. Supponendo che ogni cella sia costituita da otto bit (un byte) e che alla variabile somma siano assegnate le celle con indirizzo 10320, 10321, 10322 e 10323 il valore dell’espressione &somma sarà 10320 e non dipenderà dal valore della variabile anzi, qualunque sia il valore della variabile, il risultato dell’operazione &<nome variabile> corrisponderà sempre alla posizione in memoria che è stata scelta per la variabile al momento della sua creazione. Quindi il compilatore assume che un parametro sarà passato per riferimento quando il programmatore ha posto, nella lista dei parametri formali, davanti al nome del parametro l’operatore &, mentre, in mancanza di questo operatore, assume il passaggio per valore. Per esempio: int calcolo(int x, int y); // entrambi i parametri saranno passati per valore void somma(int &ris, int n); // ris per riferimento n per valore void scambia(double &a, double &b); // entrambi i parametri per riferimento Problema: scrivere un sottoprogramma per scambiare il contenuto di due variabili Il sottoprogramma, che chiameremo scambia, riceve come parametri le due variabili da scambiare e ne scambia il contenuto. #include <iostream.h> #include <stdlib.h> void scambia(int &a, int &b); int main() { // Programma scambia. Prova del sottoprogramma scambia int prima=10, seconda=20; cout << prima << ", " << seconda << endl; scambia(prima, seconda); cout << prima << ", " << seconda << endl; system("PAUSE"); return 0; 61 Corso sul Linguaggio C++ Edizione 2010 } void scambia(int &a, int &b) {// sottoprogramma che scambia il contenuto di a con b int tmp; tmp = a; a=b; b=tmp; } Mandandolo in esecuzione verranno visualizzati su video prima i due numeri “10, 20” e poi, dopo l’esecuzione della funzione scambia, gli stessi numeri in ordine invertito “20, 10”. In questo caso, affinché il sottoprogramma svolga la funzione per la quale è stato progettato, ovvero quella di scambiare il contenuto di due variabili, il passaggio dei parametri deve avvenire per riferimento perché solo in questo modo nel sottoprogramma verranno utilizzate proprio le due variabili del programma principale e non due copie delle stesse. Se, invece, i parametri venissero passati per valore, l’esecuzione del sottoprogramma non comporterebbe nessuna modifica del contenuto delle variabili del programma principale e quindi la sue esecuzione non avrebbe nessun effetto. 4.3 I prototipi delle funzioni Si noti che nel programma precedente l’intestazione della funzione scambia è stata messa due volte, una volta all’inizio e la seconda dopo la funzione main, quando è stata inserita la definizione completa della funzione stessa. La riga che si trova all’inizio e che contiene solo l’intestazione della funzione seguita dal punto e virgola si chiama prototipo della funzione. Questo dipende dal fatto che ogni oggetto che si utilizza in un programma C++ deve essere dichiarato prima del suo utilizzo, perché il compilatore ogni volta che incontra un identificatore deve sapere di cosa si tratta, questo vale sia per le funzioni che per le variabili. Nel caso che stiamo esaminando, la funzione scambia veniva utilizzata all’interno della funzione main che precede la definizione della funzione scambia. Se non avessimo inserito il prototipo della funzione scambia prima della definizione della funzione main il compilatore avrebbe segnalato l’errore di mancata dichiarazione dell’oggetto in corrispondenza della riga: scambia(prima, seconda); dove viene richiamata la funzione, per evitare questo errore è stato necessario inserire il prototipo: void scambia(int &a, int &b); prima della definizione del programma principale (funzione main), questo dà al compilatore tutte le informazioni che gli sono necessarie sull’oggetto scambia dicendogli anche che sarà definito dopo. Avremmo potuto evitare di inserire il prototipo scrivendo la definizione della funzione scambia prima del programma principale main così come è stato fatto nel programma per il calcolo della potenza visto al paragrafo 4.1. Quando il programma contiene più funzioni può capitare che una funzione A richiami al suo interno una funzione B, in questo caso la definizione della funzione B o il suo prototipo deve precedere la funzione A, ma se capita che anche la funzione B richiama la funzione A allora si deve ricorrere obbligatoriamente al prototipo di almeno una delle due funzioni inserendolo prima della definizione di entrambe le funzioni. I programmatori C, in genere, premettono all’inizio del programma i prototipi di tutte le funzioni definite all’interno dello stesso, prima di tutto perché in questo modo all’inizio del programma c’è un elenco di tutte le funzioni che vale come documentazione e poi perché così non è più necessario stare attenti all’ordine con cui devono essere inserite le definizione delle funzioni nel programma. 62 Corso sul Linguaggio C++ Edizione 2010 4.4 Le principali funzioni presenti nelle librerie standard del linguaggio C Come già detto nel paragrafo 3.7 le funzioni predefinite incluse nelle librerie fornite con il linguaggio C e con il linguaggio C++ sono moltissime, un elenco dettagliato di quelle standard del linguaggio C si può trovare nel file libc.htm presente sul server e scaricabile dall’indirizzo: http://www.gnu.org/software/libc/manual oltre a quelle elencate in questo file sono disponibili per il linguaggio C++ altre funzioni e oggetti, come quelli inclusi nel file di header <iostream.h>, che sono presenti nelle versioni standard del C++ ma non nel linguaggio C. Nella tabella che segue si fornisce un piccolo elenco con le funzioni utilizzate in questo libro, con indicato il file di header che è necessario includere per utilizzarle e la pagina dove è possibile trovare esempi o una descrizione. Tabella 3 Elenco di alcune funzioni e costanti definite nelle librerie standard del C e utilizzate in questo libro Nome header sqrt pow math.h math.h log exp ceil math.h math.h math.h floor math.h trunc math.h rint M_PI RAND_MAX rand math.h math.h stdlib.h stdlib.h srand stdlib.h system stdlib.h strcpy string.h strncpy string.h strlen string.h strcat string.h strncat string.h Descrizione radice quadrata di un numero x Elevazione a potenza. Calcola x elevato a y prototipo pag. double sqrt (double x) 47,48 double pow (double base, double 48 esponente) Calcola il logaritmo naturale di un numero x double log (double x) Calcola e elevato a x (funzione inversa di log()) double exp (double x) Arrotonda per eccesso un numero al primo intedouble ceil (double x) ro più grande (es 1.5 2) Arrotondo per difetto un numero al primo intero double floor (double x) più piccolo (es 1.5 1 e -1.5 -2). Elimina la parte dopo la virgola da un numero double trunc (double x) (es 1.5 1.0 e -1.5 -1.0) arrotonda al più vicino numero intero double rint (double x) Costante= Π (pi greco) Costante=numero massimo generato con rand 76 genera un numero pseudocasuale nell’intervallo int rand (void) 76 0, RAND_MAX imposta il numero che verrà utilizzato per gene- void srand (unsigned int num) 77 rare il prossimo numero casuale Manda in esecuzione un qualsiasi comando del int system (const char *comando) 5 sistema operativo o un qualsiasi programma eseguibile copia stringa2 in stringa1 char * strcpy (char *stringa1, 84 const char *stringa2) copia esattamente n caratteri da stringa2 in char * strncpy (char *stringa1, 84 stringa1. Se stringa2 è più corta aggiunge ‘\0’. const char *stringa2, unsigned int n) calcola l’effettiva lunghezza di una stringa, in unsigned int strlen (const char 84 pratica la posizione al suo interno del carattere *s) ‘\0’ Concatena due stringhe, stringa2 viene aggiunta char * strcat (char *stringa1, 85 a stringa1 const char *stringa2) Aggiunge esattamente n caratteri a stringa1 char * strncat (char *stringa1, 85 const char *stringa2, unsigned int 63 Corso sul Linguaggio C++ Edizione 2010 Nome header strcmp string.h strstr string.h printf stdio.h fprintf stdio.h fopen stdio.h feof stdio.h scanf stdio.h fscanf stdio.h puts stdio.h fputs stdio.h gets stdio.h fgets stdio.h getchar stdio.h fgetc stdio.h time time.h Descrizione prototipo n) confronto tra due stringhe il risultato è zero se int strcmp (const char *s1, const sono uguali, altrimenti è la distanza tra I primi char *s2) due caratteri diversi. cerca stringa2 in stringa1 e se la trova restituichar * strstr (const char sce un puntatore alla posizione altrimenti resti- *stringa1, const char *stringa2) tuisce il puntatore NULL scrive in output il risultato di una serie di e- int printf (const char *schema, spressioni, formattandolo in base ad uno sche...) ma. Restituisce il numero di caratteri stampati come printf ma può essere utilizzata per scrive- int fprintf (FILE * stream, const re in un file su disco char *schema, ...) apre il flusso ovvero crea un collegamento tra il FILE * fopen (const char programma e il file su disco indicato nel primo *filename, const char *opentype) parametro; restituisce un puntatore all’oggetto FILE corrispondente Restituisce un valore diverso da zero se e solo int feof (FILE *stream) se è stata raggiunta la fine del file; alcune funzioni, come fgetc, restituiscono EOF se si raggiunge la fine del file ma anche se si verifica un errore! Legge una serie di dati da un fil di input e li tra- int scanf (const char *schema, ...) sferisce in una serie di variabili. Il risultato è il numero delle assegnazioni effettuate Come scanf ma può essere utilizzata per leggere int scanf (FILE *stream, const da un file su disco char *schema, ...) Scrive una stringa nel file stdout (normalmente int puts (const char *s) il video) Come puts() ma invia i dati nel file specificato int fputs (const char *s, FILE senza il carattere ‘\n’ *stream) legge una stringa dal file stdin (normalmente la char * gets (char *s) tastiera), legge tutti I caratteri fino al carattere ‘\n’ che non considera legge fino alla fine della riga ma al massimo char * fgets (char *s, int count, count-1 caratteri da un file e li strasferisce nella FILE *stream) stringa s che deve essere di dimensione count perché alla fine viene aggiunto ‘\0’. Legge anche gli eventuali spazi e il carattere di fine riga legge un carattere dal file stdin, se c’è un errore int getchar (void) viene restituita la costante EOF (normalmente = -1) come getchar() ma viene utilizzata per leggere int fgetc (FILE *stream) un carattere da un file su disco restituisce il numero di secondi trascorsi dalla time_t time (time_t *result) mezzanotte del 1/1/1970 considerati in base al tempo UTC(Universal Time Coordinated che corrisponde all’ora di Greenwich) , restituisce un tipo di dati time_t equivalente a int. Se viene fornito il parametro facoltativo il risultato viene memorizzato anche nella variabile puntata dal parametro 64 pag. 85 85 88, 106, 122 151 120 89 124, 151 90 121, 121 90, 122 121, 124 90 121 77 Corso sul Linguaggio C++ Edizione 2010 4.5 Meccanismo di esecuzione di un sottoprogramma e funzioni inline Quando un programma è in esecuzione sia le istruzioni che lo costituiscono, sia le variabili che utilizza, occupano spazio di memoria. Normalmente il programma e tutte le sue funzioni occupano una parte di memoria detta segmento codice, le variabili globali e quelle statiche (visibili solo localmente ma che non vengono cancellate quando il blocco dove sono state definite termina) occupano un'altra area di memoria detta segmento dati, le variabili locali vengono inserite nel segmento stack. Quest’ultima area viene gestita dinamicamente come una pila in cui, durante l’esecuzione, quando parte un programma, vengono inseriti sia i parametri attuali che tutte le variabili locali e, quando il sottoprogramma finisce, vengono eliminati. Quindi lo spazio utilizzato dello stack si allunga e si accorcia continuamente durante l’esecuzione. Osserviamo questo interessante programma tratto dal libro “Thinking in C++” di Bruce Eckel (vedi 9.1): #include <stdlib.h> #include <iostream.h> int cane, gatto, uccello, pesce; void f(int animale) { cout << "numero id animale: " << animale<< endl; } int main() { int i, j, k; cout << "f(): " << (long)&f << endl; cout << "cane: " << (long)&cane<< endl; cout << "gatto: " << (long)&gatto<< endl; cout << "uccello: " << (long)&uccello<< endl; cout << "pesce: " << (long)&pesce<< endl; cout << "i: " << (long)&i << endl; cout << "j: " << (long)&j << endl; cout << "k: " << (long)&k << endl; system("PAUSE"); return 0; } Il programma utilizza l’operatore & per mostrare la posizione in memoria di tutte le variabili e quella della funzione f(). Il cast (long) serve per visualizzare l’indirizzo delle variabili in decimale invece che in esadecimale. Mandandolo in esecuzione il risultato sarà diverso a seconda dello stato del computer, del sistemo operativo e del compilatore utilizzato, nel mio caso ho ottenuto: f(): 4198948 cane: 4272128 gatto: 4272132 uccello: 4272136 pesce: 4272140 i: 37879644 j: 37879640 k: 37879636 Come si vede la posizione della funzione f() è diversa da quella delle variabili cane, gatto, uccello e pesce che occupano posizioni consecutive a partire da 4272128 (ogni variabile int 65 Corso sul Linguaggio C++ Edizione 2010 occupa 4 bytes) e quella delle variabili locali i, j, k, anch’esse consecutive con ordine invertito, a sua volta è diversa sia da quella della funzione che da quella delle variabili globali. Quando un programma richiama una funzione prima di tutto inserisce nello stack il valore dei parametri attuali o il loro indirizzo, in base a quanto stabilito dal programmatore, poi trasferisce nello stack il program counter ovvero l’indirizzo della prossima istruzione del programma, quella che verrà subito dopo la chiamata del sottoprogramma e, infine, trasferisce il controllo alla prima istruzione del sottoprogramma. Quando il sottoprogramma finisce ovvero quando viene eseguita l’istruzione return, vengono eliminate tutte le variabili locali dallo stack (il codice macchina relativo a questa operazione è compreso nel codice del sottoprogramma), viene eseguita l’istruzione macchina RET che preleva dallo stack il program counter e lo ripristina, vengono eliminati i parametri dallo stack e si prosegue con il codice macchina che segue la chiamata del sottoprogramma. L’eventuale risultato della chiamata di una funzione viene normalmente inserito in un registro interno della CPU (l’accumulatore). Memoria centrale Segmento codice Posizione:4198948 Funzione f() Funzione main() Segmento dati Variabli globali: cane gatto uccello pesce Segmento stack Variabli locali: k j i Posizione:4272128 Posizione: 37879644 Questo sistema permette la ricorsione ovvero la possibilità per un sottoprogramma di richiamare se stesso. Quando un sottoprogramma richiama se stesso vengono inserite nello stack le copie dei valori o degli indirizzi dei nuovi parametri attuali e vengono ricreate le variabili locali e, mentre l’esecuzione precedente rimane sospesa, parte una seconda volta l’esecuzione dello stesso sottoprogramma. Quindi, in questo caso, nello stack saranno inserite due volte, probabilmente con valori diversi, tutte le variabili locali e i parametri attuali. Se anche durante la seconda esecuzione il sottoprogramma richiama se stesso, parte una terza esecuzione e così via, ci possono essere anche centinaia di esecuzioni dello stesso sottoprogramma sospese in attesa che venga completata l’ultima. Quando le stesse istruzioni devono essere eseguite più volte in punti diversi del programma, l’utilizzo delle funzioni permette di scrivere una sola volta le istruzioni da ripetere. Anche in memoria, nel segmento codice, queste istruzioni saranno scritte una sola volta, ma il meccanismo di passaggio dei parametri attraverso lo stack porta ad un rallentamento dell’esecuzione 66 Corso sul Linguaggio C++ Edizione 2010 dovuto al tempo necessario per l’inserimento dei valori nello stack e, successivamente, per la loro eliminazione. Anche quando una funzione non ha parametri verrà sempre inserito nello stack l’indirizzo di ritorno. Se vogliamo velocizzare il programma possiamo eliminare l’utilizzo dello stack riscrivendo le istruzioni del sottoprogramma ogni volta che servono. Il linguaggio C++ mette a disposizione del programmatore due sistemi per inserire più facilmente le stesse istruzioni in più parti del programma senza doverle ogni volta riscrivere: le macro e le funzioni inline. Con le macro è il preprocessore che si occupa di inserire le istruzioni dove il programmatore inserisce il nome della macro. Per scrivere una macro si utilizza la direttiva #define, per maggiori dettagli si consiglia di leggere gli approfondimenti consigliati. Segue un esempio di macro: #define prod(a,b) a*b …… int a=10,b=5; cout << prod(a+b,a-b); //definizione della macro prod(a,b) Quando il compilatore trova prod(x,y) lo sostituisce con x*y , qualunque siano le espressioni x e y quindi nell’esempio prod(a+b,a-b) viene sostituito con a+b*a-b e questo significa che nell’esempio, verrà visualizzato su video il risultato 55. Con le funzioni inline è invece il compilatore che fa il lavoro. In pratica, ogni volta che il compilatore trova la chiamata di una funzione invece di tradurla in linguaggio macchina con la chiamata del corrispondente sottoprogramma, inserisce in quel punto tutte le istruzioni della funzione. Per creare una funzione inline basta premettere nella definizione della funzione la parola chiave inline, per esempio: inline double area(double r) { return r * r * 3.14 ;} In genere le funzioni inline e le macro sono sottoprogrammi costituiti da poche istruzioni perché hanno lo svantaggio di far aumentare le dimensioni del codice. Naturalmente una funzione inline e una macro non possono essere ricorsive. 4.6 Le variabili static Sia le variabili che le funzioni possono essere dichiarate utilizzando il modificatore di modalità di memorizzazione static. Abbiamo visto già visto che se si utilizza static per una variabile locale quest’ultima sarà memorizzata nel segmento dati invece che nello stack e quindi non verrà cancellata alla fine del blocco in cui è stata definita. Se invece si utilizza static nella definizione di una variabile globale o di una funzione si cambia l’area di validità della variabile o della funzione, cioè le aree del programma in cui è possibile utilizzare ovvero è visibile la variabile o la funzione. Normalmente le variabili globali e le funzioni sono visibili in tutti i moduli del programma mentre quando vengono dichiarate come static diventano visibili solo nel modulo in cui sono state dichiarate. Per vedere l’effetto di static sulle variabili locali proviate il seguente programma: #include <iostream.h> #include <stdlib.h> void f() { 67 Corso sul Linguaggio C++ Edizione 2010 int contatore=1; cout << contatore << ";" ; contatore++; } int main(){ int i; for (i=1; i<10; i++) f(); cout << endl; system("PAUSE"); return 0; } Il programma richiama la procedura f() nove volte e quest’ultima, ogni volta, scrive su video il valore della sua variabile locale contatore, mandandolo in esecuzione si otterrà: 1;1;1;1;1;1;1;1;1; perché ogni volta che la procedura f() viene richiamata, la variabile contatore viene creata con il valore 1, stampata, incrementata e poi cancellata, quindi quando si visualizzerà la variabile il valore in essa contenuto sarà sempre 1. Se, invece, nella dichiarazione della variabile contatore aggiungiamo la parola static static int contatore=1; il risultato sarà: 1;2;3;4;5;6,7;8;9; perché, in questo caso, solo la prima volta che la procedura f() viene richiamata verrà creata la variabile contatore, tutte le volte successive l’istruzione di dichiarazione non verrà eseguita perché la variabile risulterà già esistente e quindi, in ognuna delle esecuzioni successive, verrà utilizzato il valore della variabile che è stato modificato nelle esecuzioni precedenti della stessa funzione. 4.7 Funzioni ricorsive Abbiamo già parlato della ricorsione e abbiamo detto che una funzione è ricorsiva quando nel blocco delle sue istruzione ce n’è almeno una che richiama direttamente o indirettamente se stessa. Quando una funzione richiama se stessa il computer esegue di nuovo la funzione lasciando sospesa l’esecuzione precedente. In pratica questa tecnica è un modo per far eseguire più volte una serie di istruzioni e quindi si tratta di un altro modo per implementare algoritmi con cicli. Infatti una funzione ricorsiva può essere sempre riscritta senza ricorsione, utilizzando i cicli ed è vero anche il viceversa, tutti i cicli possono essere riscritti utilizzando funzioni ricorsive ma, gli algoritmi risultanti con o senza la ricorsione non sono in genere equivalenti. Per esempio un ciclo postcondizionale tipo do {…blocco delle istruz.del ciclo…} while (condizione) può essere riscritto dichiarando una funzione ricorsiva tipo: void ciclo(eventuali variabili da utilizzare nel blocco o per la cond.){ …blocco delle istruz.del ciclo; IF (condizione) ciclo(…) else return; 68 Corso sul Linguaggio C++ Edizione 2010 e inserendo al posto dell’istruzione do vista sopra la chiamata alla funzione: ciclo(…); Analogamente anche un’istruzione for tipo: for (i=inizio; i<= fine; i++) { …blocco delle istruzioni del ciclo… } può essere tradotta dichiarando la seguente funzione ricorsiva ciclofor() int inizio, fine ; void ciclofor(int i){ if (i>= inizio) {ciclofor(i-1); {…blocco delle istruz.del ciclo…} } return ; } e inserendo al posto della riga con l’istruzione for la seguente chiamata alla funzione ricorsiva: ciclofor(fine); Anche se dal punto di vista dell’utente non c’è differenza tra i due modi di implementare i cicli, in realtà, i due programmi non sono equivalenti perché il computer eseguirà operazioni molto diverse. Nei due casi visti sopra, il programma scritto utilizzando le istruzioni do e for e sicuramente più efficiente di quello che utilizza le funzioni ricorsive ma non sempre è così, a volte, soprattutto in presenza di algoritmi complessi, l’utilizzo delle funzioni ricorsive semplifica notevolmente l’algoritmo e rende più chiaro il programma. Problema: scrivere una funzione per il calcolo del fattoriale di un numero. Dato un qualsiasi numero intero n, il fattoriale di n, che si indica con n!, è dato dal prodotto dei numeri da 1 a n: n! = 1 * 2 * 3 * 4 * …… * (n-1) * n per esempio 4! = 1 * 2 * 3 * 4 = 24; 5! = 1 * 2 * 3 * 4 * 5 = 120 Per convenzione si pone 0! = 1. La dichiarazione della funzione in C++ potrebbe essere la seguente: int fatt(int n) { int ris = 1; for (; n>1; n--) ris *= n; return ris; } Nella funzione fatt(n) si utilizza un ciclo for per moltiplicare i numeri da n fino a 2 e quindi si calcola: n * (n-1) * (n-2) * …….. * 3 * 2 che è la stessa cosa di 2 * 3 * …. * (n-2) * (n-1) * n perché la moltiplicazione è commutativa. Viene evitata la moltiplicazione per 1 che non cambia il risultato. 69 Corso sul Linguaggio C++ Edizione 2010 Ora si può osservare che se abbiamo calcolato 4! moltiplicando 2 * 3 * 4 = 24, per calcolare 5! non è necessario fare tutte le moltiplicazioni 2* 3* 4* 5 basta fare 24 * 5 e in generale se abbiamo calcolato (n-1)! Si ha che: n! = (n-1)! * n sfruttando questa proprietà si può riscrivere la funzione fatt() in modo ricorsivo così: int fatt(int n) { if (n > 1) return fatt(n-1)* n; else return 1; } In molti problemi si può individuare un procedimento di calcolo ricorsivo a volte pià semplice da implementare rispetto a quello iterativo. I procedimenti ricorsivi, in genere, si basano sull’individuazione di una condizione per l’uscita non ricorsiva e delle istruzioni ricorsive. In genere la struttura di una funzione ricorsiva è la seguente: If (condizione per l’uscita) calcolo base ; else calcolo ricorsivo ; 4.8 Esercizi 72) Dato il seguente frammento di programma: void somma(int &a , int b) { a+=b; b = a ; } void main() { int a = 3, b= 4; somma(a,b); } cout << "a= " << a << "; b= " << b; Cosa viene visualizzato? 73) Si consideri la seguente funzione. int funzione ( ){ int contatore = 0; int sum = 0; while (contatore <= 4){ contatore = contatore + 1; sum = sum + contatore; } return sum; } Quale valore restituisce la funzione? a)10 b)15 c)16 d) Nessuna delle risposte precedenti 74) Dato il seguente frammento di programma: void funzione(int x) { int k; for(k = 0; k < 10; k++) x += k; } void main() { int a = 3; funzione(a); cout << a; } Indicare quale valore viene stampato. 75) Considerando il seguente frammento di programma Int a, b; 70 Corso sul Linguaggio C++ Edizione 2010 void void void void f1(int f2(int f3(int f4(int &p, &p, &p, &p, int int int int &c) &c) &c) &c) {p=c;} {p=b;} {p=b;} {c=b;} dite quali delle seguenti chiamate non ha lo stesso effetto delle altre a) f1(a, b) b) f2(a, b) c) f3(b, a) d) f4(a, a) 76) Provate a scrivere la funzioni per calcolare il logaritmo naturale di x utilizzando l’algoritmo dell’esercizio 51) con due parametri double in(double x, double E=0.1e-6); x =numero di cui si vuole calcolare il logaritmo e E = errore massimo ammesso 77) Utilizzando gli algoritmi descritti negli esercizi 57) e 59) scrivere le funzioni per calcolare sen(x) e ex. Ricorsione 78) Sia definita la seguente funzione: int cond(){ return cond(); } Cosa succede all'istruzione if ((a>0) && cond()) cout << "ciao"; al variare di a? Cosa succede all'istruzione if (cond() && (a>0)) cout << "ciao"; al variare di a? 79) Euclide, nel 300 a.C., dimostrò le seguenti proprietà del massimo comun divisore: supponiamo che A e B siano due numeri interi qualsiasi tali che A>B, indicando con MCD(A,B) il massimo comun divisore di A e B, si ha che se A è divisibile per B allora MCD(A,B) = B altrimenti, posto R= A % B (resto della divisione tra A e B) si ha che MCD(A,B) = MCD(B,R) Utilizzando queste proprietà si può costruire una funzione ricorsiva che calcola il massimo comun divisore tra due numeri interi qualsiasi A e B. 80) Dato che xn = xn-1 * x se n>0, mentre è uguale a 1 se n è 0, scrivere una funzione ricorsiva per il calcolo di xn. 81) Scrivere una funzione ricorsiva per visualizzare l'ennesimo termine della serie di Fibonacci (i primi due sono uguali a 1, ognuno dei successivi è uguale alla somma dei due precedenti). 82) Considerate la seguente funzione: int f(int x) { if (!x) return 0; return (x%2)+f(x/2); } Tale funzione viene eseguita su tutti i valori di x compresi fra 100 e 500. Chiamate a il minimo valore ottenuto in queste esecuzioni, e b il massimo valore; quanto valgono a e b? 83) Cosa fa la seguente funzione ricorsiva? void removeHat(char cat) { for(char c = 'A'; c < cat; c++) cout << " "; if(cat <= 'Z') { cout << "cat " << cat << endl; removeHat(cat + 1); // chiamata ricorsiva 71 Corso sul Linguaggio C++ Edizione 2010 } else cout << "VOOM!!!" << endl; } Risposte esercizi Esercizio 72) Risposta a= 7; b=4 Esercizio 73) Risposta = b) Esercizio 74) Risposta = 3 Esercizio 75) Risposta = c) Esercizio 78) Risposta: la funzione cond() è una funzione ricorsiva che non finisce mai perché richiama indefinitivamente se stessa quando viene mandata in esecuzione quindi ad un certo punto esaurisce lo stack e produce un errore. Nel caso a) la condizione prevede prima la valutazione di a>0, se questa condizione è falsa allora la seconda non verrà controllata e quindi il programma prosegue senza errori mentre se a è maggiore di 0 allora viene valutata anche cond() e il programma si blocca con l’errore di esaurimento dello stack. Nel caso b) la condizione contiene per prima la valutazione di cond() e quindi, qualunque sia il valore di a, la funzione viene mandata sempre in esecuzione e si verificherà sempre l’errore.) Esercizio 82) Risposta a=1 b=8 la funzione f restituisce il numero di 1 presenti nella rappresentazione binaria del numero x se x != 0 Esercizio 83) Risposta: visualizza su video tutti i caratteri compresi tra il carattere fornito come parametro e il carattere ‘Z’, uno per ogni riga, visualizzando sulla riga, prima del carattere, un numero di spazi corrispondente alla posizione del carattere nell’alfabeto. 72 5 Capitolo Corso sul Linguaggio C++ Edizione 2010 5. Vettori, stringhe e puntatori 5.1 Cosa sono i vettori Abbiamo visto nel paragrafo 2.4che il C++ permette di utilizzare vari tipi di dati elementari: bool, char, int, float, double, ecc. Questi sono i tipi che possiamo utilizzare per creare le variabili nei nostri programmi. Una variabile è un contenitore e si chiama così perché il suo contenuto, detto valore della variabile, può cambiare. Ogni variabile può contenere solo un determinato tipo di dati. Possiamo immaginare una variabile come una scatola e, così come non è possibile inserire una videocassetta nel contenitore di una musicassetta o di un CD, allo stesso modo non si può inserire un numero con la virgola in una variabile di tipo int o char. Se la variabile SOMMA contiene il valore 25, potremmo rappresentarla graficamente nel seguente modo: Somma 25 Tutte le variabili che abbiamo visto finora hanno però una caratteristica in comune, sono variabili elementari ovvero possono contenere un solo dato! Tutte le variabili di tipo numerico possono contenere un solo numero, quelle di tipo carattere (char) possono contenere un solo carattere, ecc.. Quando in una variabile di qualsiasi tipo inseriamo un nuovo valore verrà automaticamente cancellato il valore precedente. Quasi tutti i linguaggi di programmazione permettono di mettere insieme più spazi di memoria del tipo di quelli utilizzati per le variabili per costruire quelle che vengono chiamate strutture di dati, che sono gruppi di variabili e possono, quindi, contenere più dati contemporaneamente. I vettori o array sono strutture di dati consistenti in un insieme di variabili tutte dello stesso tipo disposte in memoria consecutivamente. Se le variabili che costituiscono il vettore sono elementari, possiamo rappresentare graficamente un vettore disegnando una serie di caselle consecutive, per esempio un vettore di nome A, di 6 elementi, contenente i numeri 10, 15, 20, 35, 12, 48, potrebbe essere così disegnato: 10 15 20 35 12 48 A[0] A[1] A[2] A[3] A[4] A[5] Si definisce dimensione di un vettore il numero di elementi che lo costituiscono, per esempio il vettore A[] definito sopra ha dimensione 6. Un vettore è in pratica costituito da tanti spazi di memoria consecutivi ognuno dei quali può contenere un dato. Ognuna di queste caselle viene detta elemento del vettore. Gli elementi del vettore sono numerati in base alla loro posizione partendo da 0 (0 per la prima posizione, quindi 1 per la seconda, 2 per la terza ecc.), il numero 73 Corso sul Linguaggio C++ Edizione 2010 associato ad ogni elemento si dice indice, l’ultimo elemento di un vettore ha sempre indice = dimensione -1. Gli elementi dei vettori sono a tutti gli effetti delle variabili. In tutte le istruzioni in cui si possono utilizzare le variabili di un determinato tipo si possono utilizzare anche, al loro posto, elementi di un vettore delle stesso tipo. Nelle istruzioni del linguaggio C++, per indicare un elemento di un vettore, si scrive il nome del vettore seguito da un espressione numerica racchiusa tra parentesi quadre che rappresenta l’indice ovvero la posizione dell'elemento all'interno del vettore. Per esempio, se la variabile I contiene 5 e la variabile J contiene 3, le scritte A[I], A[5], A[10/2], A[J+2] indicano tutte lo stesso elemento del vettore A, quello con indice 5 che è il sesto elemento del vettore. Seguono alcuni esempi di istruzioni in cui vengono utilizzati dei vettori: cin >> importo[2]; conto[5]++; cout << somma[8]; // legge un numero e lo conserva nella terza casella del vettore A // incrementa di 1 il sesto elemento del vettore conto // visualizza il nono elemento del vettore somma; Se si modifica il valore di un elemento di un vettore gli altri elementi non vengono toccati così come quando si modifica il contenuto di una variabile le altre non subiscono variazioni. In pratica un vettore non è altro che un gruppo di variabili, tutte con lo stesso nome e dello stesso tipo, a cui si accede attraverso un indice. Così come si fa con le variabili anche prima di utilizzare un vettore bisogna dichiararlo. L’istruzione per dichiarare un vettore è simile a quella utilizzata per le variabili con l’aggiunta del fatto che bisogna specificare il numero di elementi del vettore tra parentesi quadre: dichiarazione vettore Tipo dato Valori iniziali { Nome vettore dimensione [ ] = Valori iniziali Valori iniziali Costanti } , Il nome di un vettore è un qualsiasi identificatore scelto dal programmatore, la dimensione deve essere una costante intera. Per esempio: int v[100]; //dichiara un vettore di 100 elementi di tipo int char frase[50]; // dichara un vettore di 50 caratteri double importo[3] = {500.0, 1464.4, 3220.2} ; L’ultima dichiarazione comprende anche l’inizializzazione del vettore e si poteva anche scrivere: double importo[] = {500.0, 1464.4, 3220.2} ; senza indicare la dimensione perché in questo caso il compilatore la ricava direttamente dal numero di valori racchiusi tra le parentesi graffe. 74 Corso sul Linguaggio C++ Edizione 2010 Il numero di valori iniziali può anche essere minore della dimensione, in questo caso tutti gli elementi per cui non è stato indicato nessun valore vengono inizializzati con un valore standard che per i tipi numerici è 0 (per i caratteri è il carattere ‘\0’). Per esempio int a[4] = {1, 2}; // inserisce nel vettore {1, 2, 0, 0} La dimensione del vettore deve essere una costante non può essere una variabile, per esempio int posti[n]; // errore! La dimensione non può essere una variabile da luogo ad un errore durante la compilazione se l’identificatore n è il nome di una variabile. Nel linguaggio C++ il nome del vettore, non seguito da parentesi quadre, rappresenta la posizione di memoria in cui è memorizzato il primo elemento del vettore, quindi se si cerca di visualizzare il contenuto del vettore a dichiarato prima, con una istruzione del tipo: cout << a ; non verrà visualizzato il contenuto del vettore a ma un numero esadecimale che indica la posizione in memoria del primo elemento del vettore. Come vedremo il nome di un vettore è a tutti gli effetti un puntatore costante (vedi paragrafo 5.9). Le versioni del linguaggio C che seguono le specifiche ISO segnalano come errore l’assegnazione: int a[100], b[100] …………… a=b; // errore di assegnazione anche se in alcune versione del linguaggio questa assegnazione è permessa (solo se i due vettori hanno la stessa dimensione e sono dello stesso tipo) e viene eseguita copiando tutti gli elementi del vettore b nel vettore a. 5.2 A cosa servono i vettori Un personal computer attuale (anno 2006), tipicamente è equipaggiato con una memoria centrale di 512MB. In una tale memoria si possono memorizzare oltre 128 milioni di numeri. Anche considerando che nella memoria centrale, oltre ai dati, sono memorizzate le istruzioni dei programmi, resta il fatto che un programma può gestire anche milioni di variabili. Se tutte le variabili fossero elementari un programma che gestisce molte variabili sarebbe inevitabilmente molto lungo perché ogni variabile dovrebbe essere dichiarata e avere un nome distinto dalle altre, inoltre per ogni variabile ci dovrebbe essere almeno un’istruzione che la utilizza e quindi la dimensione del programma crescerebbe inevitabilmente. Per contro la possibilità di gestire molti dati e la velocità di elaborazione sono i due principali punti di forza dei computer. Per esempio se vogliamo far incrementare di uno il valore delle quattro variabili semplici a, b, c, d, dobbiamo scrivere 4 istruzioni diverse: a++ ; b++ ; c++ ; d++ ; Se le variabili, invece di 4, fossero 1000 dovremmo scrivere 1000 istruzioni diverse e inventarci 1000 nomi diversi per le variabili. Se, invece, per memorizzare i 1000 valori utilizzassimo un vettore di 1000 elementi creato, per esempio, con l'istruzione int v[1000], per incrementare di uno tutti gli elementi del vettore basterebbe il seguente ciclo for: For (int i=0, i<1000, i++) v[i]++ ; Ogni volta che si deve eseguire la stessa operazione su molte variabili semplici dello stesso tipo bisogna scrivere più volte l'istruzione o le istruzioni che fanno eseguire l'operazione, quasi sempre in questo caso conviene utilizzare uno o più vettori per conservare i valori delle variabili e inserire l'istruzione o le istruzioni in un ciclo nel quale viene modificato ogni volta anche l'indice del vettore in modo che ad ogni esecuzione si operi su un elemento diverso. 75 Corso sul Linguaggio C++ Edizione 2010 Per esempio se vogliamo leggere da tastiera e conservare in memoria n numeri, con n <=100 possiamo scrivere: float num[100] ; int n ; cout << "Quanti numeri ? " ; cin >> n ; for (int i=0 ;i<n ; i++) { cout << "Inserisci l\’elemento " << i << " : " ; cin >> num[i] ; } Per visualizzare questi n numeri su video possiamo scrivere: for (int i=0; i<n; i++) cout << num[i] << "; " ; L'uso dei vettori costituisce uno strumento molto potente in mano ai programmatori e spesso permette di risolvere facilmente e brevemente problemi che altrimenti sarebbero complessi e/o lunghi e noiosi. Per esempio consideriamo il seguente problema: Problema: Far leggere una serie di numeri e visualizzare solo quelli superiori alla media Prima di tutto non sappiamo quanti numeri verranno inseriti ma, dovendo essere inseriti da tastiera, probabilmente non potranno essere molti e quindi possiamo limitare la soluzione del problema per esempio a massimo 100 numeri. Il problema non presenta difficoltà, basta far leggere tutti i numeri memorizzandoli nel computer, calcolare la media e poi, riesaminandoli ad uno ad uno, visualizzare solo quelli maggiori della media. E’ però necessario memorizzare i numeri. Se non utilizziamo i vettori dovremmo creare 100 variabili diverse, dovremmo far leggere i numeri che potrebbero essere anche di meno di 100, scrivere le istruzioni per sommare tutti i numeri letti, ecc. Come si può immaginare il programma sarebbe molto lungo. Con i vettori, invece, tutto il programma si riduce al seguente: // programma media. Legge una serie di numeri e visualizza quelli > media int main() { double num[100] ; double media=0; int n ; cout << "Quanti numeri (max 100)? " ; cin >> n ; for (int i=0 ;i<n ; i++) { // legge e somma i numeri cout << "Inserisci l\'elemento " << i << " : " ; cin >> num[i] ; media +=num[i]; //legge e somma } media = media /n; //Calcola la media for (int i=0 ;i<n ; i++) if (num[i] > media) cout << num[i] << "; " ; system("PAUSE"); return 0; } 5.3 Caricamento di un vettore con numeri a caso In alcune applicazioni, per esempio quelle di simulazione oppure nei videogiochi spesso è necessario utilizzare numeri scelti a caso. Il linguaggio C dispone di varie funzione per generare dei numeri pseudocasuali. Vengono detti pseudocasuali perchè in realtà questi numeri vengono calcolati attraverso funzioni matematiche e quindi non sono casuali come quelli che un bambino bendato potrebbe estrarre da un’urna del lotto, ma sembrano casuali, perché appaiono susseguirsi senza nessuna logica. Per esempio si può utilizzare la funzione rand() (random = casuale in inglese) che restituisce un numero intero casuale tra 0 e la costante RAND_MAX e si richiama senza parametri (attenzione, nel linguaggio C quando si richiama una funzione senza parametri bisogna sempre farla seguire dalle parentesi tonde aperte e chiuse, per esempio rand(), il nome da solo ha un altro significato: è un puntatore). 76 Corso sul Linguaggio C++ Edizione 2010 Per utilizzarla bisogna includere il file di intestazioni <stdlib.h>, lo stesso che serve per utilizzare la funzione system(). int main(){ // programma rnd. Generazione di numeri casuali int v[100], n; cout << "Quanti numeri? "; cin >> n; cout << " i numeri inseriti nel vettore sono:\n"; for (int i=0; i<n; i++) { v[i]= rand(); cout << v[i]<< '\t'; } system("PAUSE"); return 0; } mandandolo in esecuzione e chiedendo 10 numeri si ottiene in output qualcosa del tipo i numeri inseriti nel vettore sono: 41 18467 6334 26500 19169 15724 11478 29358 26962 24464 Purtroppo se si manda in esecuzione di nuovo lo stesso programma chiedendo di nuovo 10 numeri si ottengono gli stessi di prima perché il computer, per generare ogni nuovo numero casuale, applica una funzione matematica all’ultimo numero generato e la prima volta che si genera un numero casuale nel programma si parte da un numero fisso, il numero 1. Per evitare questo si può utilizzare la funzione srand( <numero iniziale>) che cambia il numero che verrà utilizzato per la prossima applicazione della funzione rand, insieme alla funzione time che restituisce il numero di secondi trascorsi dalla mezzanotte del 1/1/1970 ad oggi e quindi restituisce un valore diverso ad ogni esecuzione del programma. Per usare la funzione time bisogna includere il file di intestazioni <time.h>. In pratica bisogna inserire all’inizio del programma main la seguente istruzione: srand((int)time(0)); Anche se rand genera solo numeri interi tra 0 e RAND_MAX, questa funzione si può utilizzare per generare numeri con la virgola in un intervallo a piacere. Per esempio per generare numeri con la virgola tra 10 e 20 il programma precedente si può modificare nel seguente modo: int main(){ // programma rnd. Generazione di numeri casuali double v[100], n; srand(time(0)); cout << "Quanti numeri? "; cin >> n; cout << " i numeri inseriti nel vettore sono:\n"; for (int i=0; i<n; i++) { v[i]= 10 + (double)rand()/RAND_MAX*10; cout << v[i]<< '\t'; } system("PAUSE"); return 0; } 5.4 Ordinamento di un vettore Gli algoritmi di ordinamento sono algoritmi fondamentali, utilizzati in molti programmi, e quindi non possono mancare fra le conoscenze di un programmatore. L’ordinamento è un processo che consiste nel mettere un insieme di elementi in un ordine specifico. 77 Corso sul Linguaggio C++ Edizione 2010 L’ordinamento serve soprattutto a facilitare le successive ricerche. Per rendercene conto immaginiamo la difficoltà che avremmo nel cercare un nome in un elenco telefonico se questo non fosse ordinato in ordine alfabetico! Oltre agli elenchi telefonici si riordinano oggetti come l’archivio dei prodotti di un magazzino, i cataloghi dei libri di una biblioteca, l’archivio delle fatture, ecc. In tutti i casi l’ordinamento è indispensabile per poter effettuare successivamente le ricerche in un tempo ragionevole. Per ordinare un insieme di elementi, per esempio quelli appartenenti ad un vettore in memoria, si possono utilizzare vari procedimenti. Come sempre accade in informatica, la stessa operazione si può fare in tanti modi diversi, ognuna con dei vantaggi e svantaggi. Ordinare un vettore significa scambiare di posto gli elementi in modo tale che alla fine risultino nell’ordine richiesto. Per esempio se abbiamo in memoria il seguente vettore: 103 15 202 35 121 148 5 e vogliamo ordinarlo in ordine crescente dobbiamo effettuare una serie di scambi in modo che alla fine il vettore contenga gli stessi elementi ma disposti nell’ordine voluto ovvero: 5 15 35 103 121 148 202 Cominciamo con i metodi di ordinamento piu semplici: selezione, inserzione e bubble sort. Ordinamento per selezione Nel seguito supporremo sempre di voler ordinare il vettore in modo crescente, nel caso lo volessimo ordinare in modo decrescente basterebbero piccole modifiche ai programmi. Questo metotodo si chiama così perché ad ogni passaggio si esamina la parte di vettore ancora non ordinata, si cerca l’elemento più piccolo e lo si mette al primo posto (nel caso si volesse ordinare in modo decrescente il vettore si sceglie l’elemento più grande). In pratica si procede così: si inizia confrontando il primo elemento con tutti gli altri e ogni volta che se ne trova uno più piccolo si scambia con il primo; dopo questo passaggio l’elemento più piccolo di tutti si troverà al primo posto che è il suo posto nel vettore ordinato; poi si ripete la stessa cosa con gli elementi dal secondo in poi, cioè si selezione il più piccolo e si mette al secondo posto; poi si fa lo stesso con gli elementi dal terzo posto in poi e così di seguito fino a che non si arriva ad ordinare tutto il vettore. Supponendo che v[] sia il vettore e che n sia il numero di elementi dello stesso, il programma è il seguente: // ordinamento crescente per selezione for (i= 0; i<n-1; i++) // nel ciclo seguente cerca l'elemento più piccolo e lo mette al posto i for(j=i+1; j<n; j++) if (v[i] > v[j]) { x = v[i]; v[i]=v[j]; v[j]= x; // scambia di posto gli elementi } Se applichiamo questo algoritmo al vettore numerico dell’esempio precedente per il quale n = 7, otteniamo nei vari passaggi i seguenti cambiamenti: valori iniziali i=0 i=1 i=2 i=3 i=4 i=5 103 5 5 5 5 5 5 15 103 15 15 15 15 15 202 202 202 35 35 35 35 35 35 103 202 103 103 103 78 121 121 121 121 202 121 121 148 148 148 148 148 202 148 5 15 35 103 121 148 202 Corso sul Linguaggio C++ Edizione 2010 ad ogni passaggio si esaminano solo gli elementi da i in poi (quelli precedenti sono già ordinati). Le operazioni eseguite da un programma di ordinamento sono essenzialmente di due tipi: confronto tra due elementi del vettore e scambio. L’operazione principale e quella di confronto per cui un parametro significativo per misurare l’efficienza di un metodo di ordinamento è la stima del numero di confronti effettuato. Nel caso dell’ordinamento per inserzione al primo passaggio, quando i = 0, si confronta il primo elemento con tutti gli altri, in totale n-1 confronti, al secondo, quando i = 1 si confronta l’elemento v[1] cont tutti i seguenti, in totale n-2 confronti, e così via fino all’ultimo passaggio in cui si confrontano solo il penultimo e l’ultimo elemento. Quindi il numero totale dei confronti è: (n-1) + (n-2) + (n-3) + …… + 2 + 1 = n*(n-1)/2 Se si raddoppia il numero degli elementi del vettore il numero totale di scambi all’incirca si quadruplica, se il numero degli elementi aumenta di 10 volte il numero degli scambi aumenta di circa 100 volte perché è quasi proporzionale a n2! Ordinamento per inserzione Quando io voglio ordinare un pacco di compiti dei miei alunni in ordine alfabetico, in base al cognome dell’alunno, procedo nel seguente modo: poggio i compiti sul tavolo poi li prendo ad uno ad uno e ogni volta che prendo un nuovo compito lo inserisco in mezzo a quelli che ho già in mano in modo da mantenerli ordinati. Per esempio supponiamo che i compiti si trovino nel pacco in questo ordine: Lucisano, Scaringella, Calabrese, Melato, Borrelli, Testa, Presutto, Gravina, Bartoli, Senzani Prendo il primo, poi prendo il secondo mettendolo dopo il primo e quindi in mano ho: Lucisano, Scaringella poi prendo il terzo, Calabrese, e lo metto prima degli altri due perché i compiti che ho in mano devono essere ordinati, quindi in mano avrò: Calabrese, Lucisano, Scaringella prendo il quarto, Melato, e lo inserisco in mezzo agli altri sempre facendo in modo che siano ordinati: Calabrese, Lucisano, Melato, Scaringella procedo così fino a prendere l’ultimo compito, ogni volta inserisco il compito in mezzo agli altri che ho in mano in modo da rispettare l’ordine alfabetico: Borrelli, Calabrese, Lucisano, Melato, Scaringella Borrelli, Calabrese, Lucisano, Melato, Scaringella, Testa Borrelli, Calabrese, Lucisano, Melato, Presutto, Scaringella, Testa Borrelli, Calabrese, Gravina, Lucisano, Melato, Presutto, Scaringella, Testa Bartoli, Borrelli, Calabrese, Gravina, Lucisano, Melato, Presutto, Scaringella, Testa Bartoli, Borrelli, Calabrese, Gravina, Lucisano, Melato, Presutto, Scaringella, Senzani, Testa Questo modo di procedere si chiama ordinamento per inserzione, lo possiamo applicare anche ad un vettore: for (i=1, i<n , i++) { x= v[i]; //prendiamo l’elemento i ….. // lo inseriamo tra gli elementi che si trovano prima di i in modo che siano ordinati } Il sottoprogramma completo potrebbe essere il seguente: for (i=1; i<n; i++) { 79 Corso sul Linguaggio C++ Edizione 2010 x=v[i]; j = i-1; while (j>=0 && x < v[j]) {v[j+1]=v[j]; j=j-1;} v[j+1]=x; } per inserire l’elemento x al posto giusto lo confrontiamo con tutti gli elementi precedenti (che sono già ordinati) a partire da j=i-1, decrementando j fino a j=0. Ci fermiamo quando troviamo un elemento v[j] <= x, il posto di x sarà dopo questo elemento. Nel ciclo spostiamo anche tutti gli elementi che sono maggiori di x di una posizione più avanti in modo da liberare un posto per mettere x. Come si vede ci sono due condizioni che possono fermare il ciclo interno che serve a trovare il posto giusto per l’elemento x: se viene trovato un elmento v[j]<=x oppure se si supera il primo elemento del vettore (j<0). Si può evitare di controllare la seconda condizione, risparmiando un confronto per ogni ripetizione del ciclo, se utilizziamo l’elemento del vettore con indice 0 per mettere un elemento sentinella che blocca il ciclo. In questo caso il programma diventa: for (i=1; i<n; i++) { x=v[i]; v[0]= x j = i-1; while (x < v[j]) {v[j+1]=v[j]; j=j-1;} v[j+1]=x; } non è più necessario controllare che j sia >=0 perché appena j diventa = 0 il ciclo si fermerà sicuramente visto che in v[0] ci abbiamo messo x che non è < x. Naturalmente questo richiede che i dati debbano essere inseriti nel vettore a partire dall’elemento 1. Bubble sort Questo sistema consiste nel confrontare ogni elemento con il successivo e, se non sono in ordine, scambiarli di posto. Dopo questo primo passaggio l’elemento più grande finirà all’ultimo posto, che è il suo posto nel vettore ordinato, e quindi potrà essere escluso dai successivi passaggi. Si ripete lo stesso procedimento alla parte non ordinata fino a quando tutto il vettore sarà ordinato. j=n-1; while (j>0) { /* confronta ogni elem. da 0 a j con il successivo e scambia di posto le coppie che non sono in ordine */ for (i=0; i<j; i++) { if (v[i] > v[i+1]) {tmp=v[i]; v[i]= v[i+1]; v[i+1]=tmp;} } j = j-1; } se applichiamo questo procedimento al vettore di 7 elementi numerici rappresentato all’inizio del paragrafo otterremo le seguenti modifiche ad ogni passaggio: valori iniziali j=6 j=5 j=4 j=3 j=2 j=1 103 15 15 15 15 15 5 15 103 35 35 35 5 15 202 35 103 103 5 35 35 35 121 121 5 103 103 103 80 121 148 5 121 121 121 121 148 5 148 148 148 148 148 5 202 202 202 202 202 202 Corso sul Linguaggio C++ Edizione 2010 Come si vede gli elementi più piccoli si spostano, una posizione alla volta, verso la cima del vettore mentre quelli più grandi si spostano verso il fondo così come in una miscela di acqua ed altri elementi quelli più leggeri (per esempio le bollicine d’aria) si spostano verso l’alto e quelli più pesanti si spostano verso il basso. Da questa similitudine viene il nome bubble sort. Questo algoritmo può essere migliorato considerando che se da un certo punto in poi, diciamo da k in poi, non vengono effettuati più scambi significa che da quel punto il vettore è ordinato e quindi, nel passaggio successivo, ci si può fermare a k. Con questa modifica l’algoritmo diventa: // ordinamento bubble sort int j=n-1, i, k, tmp; while (j>0) { // k = posizione dove viene effettato l’ultimo scambio k=0; for (i=0; i<j; i++) { if (v[i] > v[i+1]) {tmp=v[i]; v[i]= v[i+1]; v[i+1]=tmp; k=i;} } j = k; } Quick sort I tre semplici metodi che abbiamo visto sopra richiedono tutti un numero di scambi dell’ordine di n2 e questo significa che se un vettore ha un numero di elementi dieci volte maggiore di un altro, richiede, per essere ordinato, all’incirca 100 volte il tempo dell’altro. Quindi i metodi diretti che abbiamo visto vanno bene per vettori con pochi elementi ma diventano lenti quando il numero degli elementi è elevato. Gli studiosi hanno elaborato molti altri algoritmi di ordinamento che chiamerei avanzati e che tentano di ridurre il numero di scambi e quindi il tempo necessario ad ordinare un vettore di grandi dimensioni. Nel seguito vedremo uno dei più veloci tanto che il suo inventore C. A. R. Hoare lo chiamò Quicksort. L’idea base di questo metodo è quello di dividere il vettore in due parti partendo da un elemento x e spostando tutti quelli maggiori di x a destra e tutti quelli minori a sinistra. Dopo questa operazione l’elemento utilizzato per effettuare la partizione si troverà al posto giusto nel vettore mentre la parte sinistra e la parte destra (che sono vettori più piccoli) possono essere ordinate separatamente e più velocemente rispetto all’intero vettore. Ognuna delle due partizioni viene ordinata ripetendo ricorsivamente il procedimento visto per l’intero vettore fino a che le partizioni non si riducono ad un solo elemento. Il sottoprogramma potrebbe essere il seguente (come vedremo al paragrafo 5.10 un vettore può essere passato come parametro ad un sottoprogramma); void quick(int *v, int s, int d) { /* ordinamento QuickSort v = vettore da ordinare s = posizione primo elemento della partizione da ordinare d = posizione ultimo elemento della partizione */ int i=s,j=d, x, tmp; //prende l'elemento centrale della partizione ma può essere uno qualsiasi x= v[(s+d)/2]; do { while (v[i] < x) i++; // trova il 1° elemento a sinistra not < x while (v[j] > x) j--; // trova il primo elemento a destra not > x // scambia i due elementi trovati 81 Corso sul Linguaggio C++ Edizione 2010 if (i<=j) {tmp=v[i]; v[i]=v[j]; v[j]= tmp; i++; j--; } } while (i <= j); // ripete fino a quando i e j si incontrano /* ripete l'operazione per le due partizioni che si sono create, che sono quella che va da s a j e quella da i a d, solo se hanno più di un elemento e partendo dalla più piccola */ if (s < j) if (j-s < d-i) {quick(v, s, j); quick(v,i,d);} else { if (i < d) quick(v,i,d); quick(v,s,j); } else if (i < d) quick(v,i,d); } per richiamarlo bisogna inserire nel programma principale la seguente riga (supponendo che il vettore da ordinare sia il vettore v[] e che n sia il numero di elementi): quick(v,0,n-1); 5.5 La ricerca in un vettore La ricerca consiste nell’esaminare il contenuto di un vettore allo scopo di verificare se un determinato elemento è presente o meno al suo interno e, se presente, individuarne la posizione. Se il vettore non è ordinato l’unico modo per raggiungere questo scopo è quello di esaminare tutti gli elementi fino a che non si trova quello cercato o si raggiunge la fine del vettore. Supponiamo che il vettore sia v[], che il numero di elementi sia n e che il valore da cercare sia memorizzato nella variabile elem, allora il pezzo programma per effettuare la ricerca potrebbe essere il seguente: for (i = 0; i<n; i++) if (v[i] == elem) break; dopo questo ciclo se i < n vuol dire che l’elemento è stato trovato nella posizione i altrimenti vuol dire che non è stato trovato. if (i<n) cout << "L'elemento e' stato trovato nella posizione " << i << endl; else cout << "L'elemento non c'e' nel vettore\n"; Questo tipo di ricerca si chiama ricerca sequenziale. Se il vettore è ordinato allora si può applicare una tipo di ricerca molto più veloce. L’algoritmo più utilizzato in questo caso è quello della ricerca binaria. Quando cerchiamo un nome nell’elenco telefonico di una città prendiamo una pagina centrale a caso e leggiamo il primo nome presente, se il nome che cerchiamo viene dopo quello che abbiamo letto, sappiamo che possiamo escludere dalla ricerca tutte le pagine precedenti, se, invece, viene prima, possiamo escludere quelle seguenti e quindi coun un solo confronto riduciamo a metà le pagine in cui cercare. Ripetiamo questa operazione nel gruppo di pagine che restano fino a quando individuiamo la pagina dove c’è il nome che cerchiamo. Per esempio supponiamo di dover cercare il nome Pietro in un vettore di 16 elementi contenente i seguenti nomi: 0. Anna 1. Amalia 2. Carlo 3. Dario 4. Elena 5. Federica 82 Corso sul Linguaggio C++ Edizione 2010 6. Giacomo 7. Irene 8. Loredana 9. Lorenzo 10. Maria 11. Mario 12. Michele 13. Pietro 14. Renata 15. Roberta se utilizziamo la ricerca binaria procederemo in questo modo: - confrontiamo Pietro con il nome che si trova nella posizione 7 (una delle due centrali) ovvero con Irene. Visto che non è quello che cerchiamo e che Pietro viene dopo Irene sappiamo che possiamo escludere dalla ricerca tutti gli elementi del vettore che si trovano nelle posizioni da 0 a 7; - prendiamo l’elemento centrale tra 8 e 15 che è l’ 11 dove c’è Mario che viene prima di Pietro quindi posso ridurre il campo di ricerca alle posizioni dalla 12 alla 15; - prendo l’elemento centrale di questo campo di ricerca che è il 13 dove troviamo quello che cercavamo. Abbiamo raggiunto lo scopo con solo 3 confronti mentre con la ricerca sequenziale sarebbero stati necessari 14 confronti. Si può dimostrare che mentre con la ricerca sequenziale sono necessari in media n/2 confronti con la ricerca binaria ne servono al massimo log2 n, questo significa che con un vettore di 1000 elementi, possiamo trovare quello che cerchiamo con solo 10 confronti e se il numero di elementi è 1000000 ne bastano 20! Il programma è il seguente: // ricerca binaria trovato = 0; i = 0; f = n-1; while (!trovato && i <= f) { p = (i+f)/2; if (vi[p] == elem) {trovato = 1; break;} else if (vi[p] > elem) f=p-1; else i =p+1; } La variabile trovato ci dice se l’elemento è stato trovato o meno mentre la variabile p indica la posizione in cui è stato trovato. 5.6 Le stringhe di caratteri Un nome, una frase e una qualsiasi sequenza di caratteri è quello che si intende per stringa di caratteri. Si tratta di un tipo di informazione molto comune, molti linguaggi dispongono di un tipo di dati apposito per le variabili stringhe, il linguaggio C++ purtroppo no, per memorizzare le stringhe bisogna utilizzare i vettori di char. Per esempio, per definire una variabile in cui memorizzare un nome, supponendo che i nomi non superino mai i 29 caratteri di lunghezza, si può scrivere: char nome[30] ; Una stringa, per il linguaggio C++, è sempre una serie di caratteri seguita dal carattere speciale ‘\0’ (codice ASCII 0), anche le stringhe costanti, che sono quelle racchiuse tra una coppia di virgolette (“). Quindi le due istruzioni seguenti con cui si dichiara e inizializza una stringa sono equivalenti: char descr[20]= "pasta" ; 83 Corso sul Linguaggio C++ Edizione 2010 char descr[20] = {‘p’, ‘a’, ‘s’. ‘t’, ‘a’, ‘\0’} ; Le istruzioni che il C++ mette a disposizioni sono quelle già viste per i vettori e, quindi, operazioni comuni negli altri linguaggi, come l’assegnazione di un valore costante ad una stringa, il trasferimento del contenuto di una stringa in un'altra, la concatenazione di due stringhe, danno luogo ad errori durante la compilazione: char nome1[30]="Mario", nome2[50]="Annamaria", frase[30] ; nome2 = "Lucia"; //Errore ! assegnazione incompattibile nome1 = nome2; //Errore! assegnazione non ammessa frase = "Ciao " + nome1; //Errore ! assegnazione e operazione incomp. Nel linguaggio C++ si dovrebbero utilizzare i cicli per realizzare le operazioni precedenti ma, per fortuna, ci sono molte funzioni di libreria, fornite con il compilatore, che svolgono egregiamente queste e tante altre operazioni comuni. Inoltre quando si crea un vettore di caratteri la sua dimensione è stabilita e non può cambiare durante l’esecuzione, per cui se si trasferisce in un stringa definita di 10 caratteri una frase con più di 9 caratteri c’è il rischio di andare a modificare le altre variabili collocate dopo quel vettore, con risultati imprevedibili. Per allargare o restringere lo spazio allocato per una stringa è necessaria una gestione dinamica della memoria abbastanza complessa. Per risolvere tutti questi problemi si potrebbe utilizzare la classe string inclusa nelle nuove librerie standard del C++, ma lo studio di questa classe esula dagli obiettivi di questo libro, chi volesse approfondire può leggere il secondo volume del libro “Thinking in C++” di Bruce Eckel dove è presente un intero capitolo dedicato all’argomento. In questo libro esamineremo le tradizionali funzioni del linguaggio C per le stringhe. Per utilizzarle nei nostri programmi bisogna includere l’header: #include <string.h> Le principali sono : strcpy(<stringa1>,<stringa2>) - Copia la stringa2 nella stringa1. Corrisponde ad un’istruzione di assegnazione, assegna il contenuto di stringa2 a stringa1. La seconda stringa può essere anche costante. Il computer non controllerà la lunghezza delle due stringhe, semplicemente trasferisce nella prima stringa ogni carattere della seconda stringa, fino al carattere ‘\0’ di terminazione incluso. É compito del programmatore assicurarsi che la stringa1 (quella di destinazione) sia abbastanza capiente. In caso di traboccamento l’errore non viene segnalato e i risultati sono imprevedibili. Il valore restituito dalla funzione strcpy è il puntatore all’inizio della stringa di destinazione ovvero la posizione di memoria di stringa1. Esempi: strcpy(nome,"Mario"); //assegna "Mario" al vettore di char nome strcpy(nome1,nome2); //copia il contenuto di nome2 in nome1 strncpy(<stringa1>,<stringa2>,<num>) - Funziona come strcpy ma trasferisce esattamente <num> caratteri, se stringa2 è più lunga di <num> caratteri trasferisce solo i primi <num> senza il carattere di terminazione, se è più corta trasferisce sempre esattamente <num> caratteri aggiungendo più volte il carattere di terminazione Char s1[30]=”Ciro è Bravo”, s2[30] = “Gino Bellitti”; strncpy(nome1,nome2,4); // a questo punto in s1 ci sarà “Gino è Bravo” strlen(<stringa>) - restituisce il numero di caratteri effettivamente contenuti nella stringa ovvero conta i caratteri fino a quello di terminazione (‘\0’). La lunghezza di una stringa è sempre un numero diverso rispetto alla dimensione del vettore di char che si può ottenere con sizeof, Una stringa può avere anche lunghezza 0 se il primo carattere in essa contenuto è quello di terminazione, in questo caso si dice che la stringa è vuota. 84 Corso sul Linguaggio C++ Edizione 2010 int a; char frase[20]= "Ciao"; a=strlen(frase); // assegna ad a=4 strcat(<stringa1>, <stringa2>) - Concatenamento di stringhe. Aggiunge a stringa1 la stringa2. Funziona come strcpy con la differenza che stringa2 viene copiata alla fine di stringa1 invece che all’inizio e, quindi, i caratteri di stringa2 vengono aggiunti a quelli di stringa1 invece di sovrascriverli. Char nome1[30]=”Anna”, nome2[30] = “Maria”; strcat(nome1,nome2); // a questo punto in nome1 ci sarà “AnnaMaria” strncat(<stringa1>,<stringa2>,<num>) - come strcat ma vengono aggiunti non più di <num> caratteri a stringa1. A differenza di strncpy viene sempre aggiunto alla fine il carattere di terminazione per cui il numero di caratteri aggiunti è, in genere, <num> +1. Anche qui è compito del programmatore assicurarsi che la prima stringa sia abbastanza grande da contenere tutti i caratteri aggiunti altrimenti si avranno risultati imprevedibili e nessun messaggio di errore. Char s1[30]=”Anna”, s2[30] = “Mariagiovanna”; strncat(s1,s2,5); // a questo punto in s1 ci sarà “AnnaMaria” In tutte le funzioni descritte in questo paragrafo il parametro <stringa1> è il nome di un vettore di char e, quindi, rappresenta la posizione in memoria del primo carattere della stringa, questo significa che è, come vedremo nei paragrafi 5.8 e 5.10, un puntatore. Se frase1 è un vettore, frase1 +3 rappresenta la posizione in memoria del terzo elemento del vettore, discorso analogo vale per frase1 +4 e in generale per frase1+x. Nel richiamare una delle funzioni di questo paragrafo posso anche mettere al posto di <stringa1> una espressione che punta ad una posizione interna alla stringa in quel caso l’operazione verrà eseguita a partire da quella posizione. Per esempio supponiamo di voler sostituire tre caratteri interni ad una stringa con altri tre, le istruzioni sono: Char s1[30]=”Gino è tuo amico”, s2[30]=”Bob è il suo cane”; strncpy(s1+7,”mio”,3); // a questo punto in s1 ci sarà “Gino è mio amico” strncpy(s1+7,s2+9,3); // a questo punto in s1 ci sarà “Gino è suo amico” strcmp(<stringa1>, <stringa2>) - Confronto tra due stringhe. Viene eseguito il confronto alfabetico tra le due stringhe il risultato è un numero intero = 0 se le due stringhe sono uguali, < di 0 se la prima stringa è minore secondo l’ordine alfabetico (per esempio “Gino” < “Mario”), > di 0 se la prima stringa è maggiore della seconda (per esempio “Carlo” > “Anna”). Di fatto viene restituita la differenza tra i primi due caratteri diversi. Il confronto è case sensitive ovvero le lettere maiuscole non sono uguali a quelle minuscole e, precisamente, ogni lettera maiuscola è minore di qualsiasi lettera minuscola (per esempio “Gianni” < “anna”). C’è anche la funzione strcasecmp del tutto identica a strcmp salvo per il fatto che ignora le differenze tra maiuscole e minuscole. char s1[20], s2[20]; cout << “Inserisci il primo nome: “; cin >> s1; cout << “Inserisci il secondo nome: “; cin >> s2; if (!strcmp(s1,s2)) cout << "I nomi sono uguali!" << endl; else if (strcmp(s1,s2)<0) cout << s1 << " e' < di " << s2 << endl; else cout << s2 << " e' < di " <<s1 << endl; strstr(<stringa1>,<stringa2>) - Cerca stringa2 all’interno di stringa1 e, se la trova, restituisce un puntatore che indica la posizione in memoria dove è stata trovata altrimenti restituisce NULL che è il puntatore vuoto. Anche questa funzione è case sensitive e quindi distingue tra 85 Corso sul Linguaggio C++ Edizione 2010 maiuscole e minuscole, se non si vuole fare questa distinzione si può usare l’equivalente strcasestr. char s1[100] = "Questa e' una prova", s2[20]; cout << "Inserisci la parola da cercare: "; cin >> s2; if (strstr(s1,s2)!= NULL) cout << strstr(s1,s2)<< endl; Se per esempio viene inserito in input “una” il risultato sarà “una prova”, se viene inserito “a”, il risultato sarà “a e’ una prova”, quindi, in ogni caso viene visualizzata la stringa s1 a partire dalla posizione in cui viene trovata s2. Da notare che per cercare caratteri invece di sottostrighe esiste anche la funzione strchr. Problema: Dato un numero di massimo tre cifre trasformarlo in lettere. Per esempio se in input si inserisce 51 in output si deve avere “cinquantuno”. Si tratta di concatenare le parole con cui vengono letti tutti i numeri in modo opportuno. Esaminiamo il programma passo passo. Prima di tutto bisogna includere le intestazioni compresa quella per le stringhe #include <iostream.h> #include <stdlib.h> #include <string.h> Conviene inserire, sia all’inizio che nel corpo del programma, commenti con le informazioni principali sul programma e con le spiegazioni più importanti (il significato delle variabili, breve spiegazione dell’algoritmo, ecc.) /* Autore = Prof. Giovanni Calabrese data = 7/11/2006 Versione = 1.0 Nome Programma = numinlet Breve descrizione = Trasforma un numero di tre cifre in lettere Principali dati utilizzati nel programma: costanti: par[] = tutte le parole con cui si leggono i numeri da 0 a 19 uni[] = la posizione della parola per i numeri da 0 a 19 nel vettore par[] par1[] = le parole per leggere le decine deci[] = posizione della parola per le decine nel vettore par2[] variabili: num = numero letto in input u = unità d = decine ris[] = stringa risultato */ Poi dichiariamo tutte le costanti: const char par[]="zero\0uno\0due\0tre\0quattro\0cinque\0sei\0sette\0” “otto\0nove\0dieci\0undici\0dodici\0tredici\0quattordici\0” “quindici\0sedici\0diciassette\0diciotto\0diciannove\0"; const short int uni[] = {0,5,9,13,17,25,32,36,42,47,52,58,65,72,80,92,101,108,120, 129}; const char par1[]="venti\0trenta\0quaranta\0cinquanta\0sessanta\0” “settanta\0ottanta\0novanta\0"; const short int deci[] = {0,0,0,6,13,22, 32, 41,50,58}; Invece di inserire tutte le parole in un unico vettore di caratteri si potrebbe utilizzare un vettore di stringhe, cioè un vettore i cui elementi sono vettori di caratteri, in questo modo non sarebbero più necessari i vettori uni[] e deci[] e il programma si semplificherebbe. Provate voi a modificarlo dopo aver letto il paragrafo sui vettori di vettori. 86 Corso sul Linguaggio C++ Edizione 2010 A questo punto inizia il programma con la dichiarazione delle variabili: int main(){ int num=-1, i, u, d; char ris[50]="",tmp[50]=""; Nella lettura del numero in input il programma controlla che sia di tre cifre e accetta solo numeri di massimo tre cifre. Se l’utente sbaglia visualizza un messaggio di errore e richiede il numero. In pratica ripete l’operazione di chiedere il numero fino a quando non viene inserito un numero valido. // legge il numero in input while (num <0 || num >999) { cout << "Inserisci il numero: "; cin >> num; if (num <0 || num >999) cout << "Inserire un numero tra 0 e 999" << endl; } se il numero inserito è 0 basta copiare la prima parola del vettore delle parole par[] (“zero”) nel risultato: if (num == 0) strcpy(ris,par); // se num è zero ha finito altrimenti trova le cifre del numero: u = unità, d=decine e quello che resta in num sono le centinaia, per farlo basta eseguire più volte l’operazione resto della divisione per 10 (num % 10) e risultato della divisione per 10 (num /=10): else { u = num % 10; num /=10; d = num %10; num /= 10; Dopo aver trovato le tre cifre comincia a trasformare in parole quella delle centinaia, che è rimasta in num, copiando nel risultato una delle parole “due”, “tre”, “quattro” ecc. a seconda del valore di num, e poi aggiungendo la parola “cento” // trasforma le centinaia if (num > 0) { if (num > 1) strcpy(ris,par+uni[num]); strcat(ris,"cento"); } a questo punto trasforma la cifra delle decine, se è minore di due abbiamo già la parola completa perché nel vettore costante par[] ci sono tutte le parole da “uno” a “diciannove”, altrimenti deve unirci la parola per le unità. Inoltre se la cifra delle unità è 1 o 8 elimina l’ultimo carattere prima di attaccare la parola delle unità, infatti numeri come 21 o 38 si leggono “ventuno” e “trentotto” e non “ventiuno” e “trentaotto”. Per eliminare l’ultimo carattere basta inserire il carattere di terminazione una posizione più a sinistra (ris[strlen(ris)-1]= '\0';): if (d+u > 0) // trasforma le decine if (d <2 ) strcat(ris,par+uni[d*10+ u]);//trasforma insieme decine e unità else { strcat(ris,par1+deci[d]); // aggiunge parola per decine // elide l'ultimo carattere se l'unità è 1 o 8 if (u == 1 || u == 8) ris[strlen(ris)-1]= '\0'; if (u) strcat(ris,par+uni[u]); // aggiunge parola per unità } } Scrive il risultato ed ha finito. cout << "risultato = " << ris << endl ; system("PAUSE"); return 0; } 87 Corso sul Linguaggio C++ Edizione 2010 5.7 Le funzioni printf, scanf, puts e gets Le istruzioni per l’input/output che abbiamo utilizzato finora (cin e cout) e che si possono utilizzare solo includendo il file di intestazioni <iostream>, sono incluse nelle librerie standard del C++ ma non in quella del linguaggio C. Per utilizzare le istruzioni di input/output del C bisogna includere il file di intestazioni <stdio.h> che è l’abbreviazione di standard input output. In un programma C++ si possono includere entrambi i file di intestazione e quindi utilizzare contemporaneamente sia le istruzioni standard del C che quelle aggiunte nel C++. Inoltre l’istruzione cin del C++ non funziona bene per l’input delle stringhe. Se scriviamo: char frase[100]; cout << "Inserisci una frase: "; cin >> frase; e, durante l’esecuzione, inseriamo in input una frase del tipo “Ciao mondo” ci accorgeremo che nella stringa frase è andata a finire solo la parola “ciao” perché, per l’oggetto cin, il dato in input termina appena trova uno spazio. Quindi cin non è molto comoda per far leggere le stringhe. Le istruzioni per l’I/O standard del C corrispondenti a cin e cout del C++ sono scanf e printf, inoltre, proprio per favorire l’I/O di stringhe, sono state inserite nella libreria standard le funzioni gets e puts. In realtà le funzioni disponibili sono molte di più, chi volesse studiarle tutte può far riferimento alla documentazione inserita sul server del laboratorio a cui si è fatto già riferimento nel paragrafo 3.7. printf La funzione printf si richiama con la seguente sintassi: Sintassi di Printf printf , ( espressione schema ) Printf, come cout serve per inviare dati all’unità di output che può essere il video, la stampante oppure un file su disco. Lo schema è una stringa che serve per dire al computer come deve visualizzare l’elenco di dati che eventualmente la seguono. Lo schema contiene caratteri vari che vengono visualizzati così come inseriti e comandi per indicare come formattare ciascuno dei dati elencati di seguito all’interno delle parentesi. Tutti i comandi cominciano col carattere speciale %. Per esempio int a=10, b=20; printf(“primo numero = %d secondo numero %d”, a,b); visualizza primo numero 10 secondo numero 20 il computer costruisce l’output utilizzando la stringa fornita come schema e sostituendo i comandi (la serie di caratteri che inizia con %) con i dati forniti come parametri formattati nel modo indicato dal comando. Per esempio il comando “%d” significa che il dato deve essere scritto come numero decimale. La stringa utilizzata come schema può essere anche una stringa variabile, per esempio le istruzioni viste sopra si potevano anche scrivere: int a=10, b=20; char s[]=“primo numero = %d secondo numero %d”; printf(s, a, b); I comandi che si utilizzano nelle stringhe di formato hanno in genere la seguente sintassi: % [flags] [dimensione] [ . precisione ] tipo [conversione] 88 Corso sul Linguaggio C++ Edizione 2010 per esempio nel comando %-10.8ld, - è un flag, 10 è la dimensione minima del campo in output, .8 è il numero di cifre che saranno visualizzate, l indica il tipo del dato nel caso sia diverso dal tipo in ci deve essere convertito, d indica il tipo di conversione che verrà effettuato. Se non si specifica la dimensione verrà assegnato per il dato esattamente il numero di caratteri che servono. Come si vede tutte le parti del comando sono opzionali escluso il tipo, quindi il modo più breve per indicare un comando nella stringa di formato è %tipo, per esempio il comando %d dice che in quella posizione deve essere scritto un numero decimale in free-format ovvero utilizzando il minimo spazio possibile. Sotto si riporta una tabella con i principali tipi di conversione che si possono utilizzare nelle stringhe di schema nella funzione printf. Tabella 4 Elenco dei codici di conversione utilizzati in printf Conversione Descrizione %d %i utilizzato per i numeri interi, visualizza il numero con uno spazio davanti se positivo e con il segno - davanti se negativo %o Utilizzato con i numeri interi li visualizza in ottale %x %X Visualizza i numeri interi in esadecimale %u visualizza i numeri interi senza mettere il segno %f visualizza il numero con la virgola nel formato virgola fisso (es. 341.46). se si specifica la precisione per questo comando si intende il numero di cifre dopo la virgola %e %E visualizza il numero con la virgola nel formato virgola mobile, detto anche formato esponenziale (es 3.4146e+2). Anche qui la precisione indica il numero di cifre che verranno visualizzate dopo la virgola. %g %G scrive il numero con la virgola in formato virgola fissa se il numero di cifre di precisione lo consente, altrimenti lo scrive in virgola mobile (formato esponenziale) %c visualizza un carattere %s visualizza una stringa %p visualizza un puntatore in esadecimale %% scrive il carattere % I principali simboli che si possono usare come flag sono - e +. - significa che il dato verrà allineato a sinistra nello spazio ad esso destinato invece che a destra. + significa che deve essere sempre inserito il segno per il numero, anche se è positivo, mentre normalmente prima dei numeri positivi viene visualizzato uno spazio. scanf Questa è la funzione principale per l’input di dati da un file o dalla tastiera del linguaggio C. Ha la stessa sintassi di printf ovvero scanf(<schema>, [&variabile] …). Non approfondiremo la sintassi, basta sapere che lo schema utilizza più o meno gli stessi codici di printf visti nella Tabella 4 che, nel caso di scanf servono per dire al computer come deve interpretare i dati letti dal file o dalla tastiera. Invece delle espressioni bisogna fornire un elenco di indirizzi di variabili di memoria, dove verranno depositati i dati letti. Il risultato restituito dalla funzione è il numero delle assegnazioni fatte correttamente, se il file di input termina prima che vengano riempite tutte le variabili viene restituto EOF. Per esempio per leggere un dato in una variabile intera si scrive: printf(“Inserisci la base: ”); scanf(“%d”,&base); 89 Corso sul Linguaggio C++ Edizione 2010 Se vogliamo leggere i tre coefficienti di un’equazione di secondo grado possiamo scrivere: printf("Inserisci i coefficienti: "); scanf(“%f %f %f”,&a, &b, &c); L’utente li deve inserire tutti sulla stessa riga, inserendo almeno uno spazio tra uno e l’altro. Se invece vogliamo che l’utente li immetta inserendo una virgola tra uno e l’altro dobbiamo scrivere: scanf(“%f,%f,%f”,&a, &b, &c); Se scriviamo: printf("Inserisci i coefficienti: "); printf("sono stati letti %d coefficienti\n", scanf("%f %f %f",&a, &b, &c)); printf("i coefficienti letti sono: %10.3f %10.3f %10.3f\n", a,b,c); si avrà un output del tipo: La visualizzazione dei coefficienti è stata fatta in formato virgola fissa, su dieci posizioni complessive di cui 3 decimali (%10.3f). Input e output di stringhe di caratteri Le funzioni puts e gets sono quelle più semplici e efficienti per inviare e leggere una stringa al video e dalla tastiera. Nel seguito una piccola panoramica delle principali funzioni utili per l’input-output di caratteri e stringhe: puts(<stringa>) invia il contenuto della stringa passata come parametro al video aggiungendo un carattere ‘\n’ (precisamente al file stdout = standard output); fputs(<stringa>,<filestream>) funzione come puts solo che bisogna fornire come parametro oltre alla stringa anche il puntatore al flusso di output (per inviare al video bisogna indicare stdout o stderr, per utilizzare un file su disco vedi 7.2)e non invia il carattere ‘\n’; gets(<stringa>) legge dalla taastiera (precisamente dal file stdin =standard input) un’intera riga, quindi tutti i caratteri fino a quello di invio, e li trasferisce nella stringa fornita come parametro senza il carattere di invio. Se trova un errore restituisce il puntatore NULL altrimenti restituisce il puntatore alla stringa letta; fgets(<stringa>,<num>, <filestream>) legge max num-1 caratteri da un file (tutti quelli presenti dalla posizione corrente fino a fine riga, compreso il fine riga) e li trasferisce nella stringa fornita come primo parametro (deve poter contenere almento num caratteri), alla fine viene aggiunto il carattere di terminazione ‘\0’. Inserisce nella stringa anche l’eventuale carattere di fine riga ‘\n’, vedi 7.2; getchar() legge il prossimo carattere dal buffer di input di stdin e restituisce un numero intero pari al codice del carattere; se non ci sono più caratteri o se c’è un errore restituisce la costante predefinita EOF (che normalmente è = -1); fgetc(<filestream>) come getchar() ma richiede il puntatore al flusso di input che per mette di leggere anche da un file su disco; Problema: trasformare tutti i caratteri di una frase in maiuscolo Il programma legge una frase qualsiasi usando gets, esamina ad uno ad uno tutti i caratteri della stringa letta e, se sono caratteri minuscoli (compresi tra ‘a’ e ‘z’) li trasforma in maiuscolo sottraendo 32 che è la distanza tra un qualsiasi carattere maiuscolo e il corrispondente minuscolo. Nel programma si utilizzano solo le librerie standard del C. 90 Corso sul Linguaggio C++ Edizione 2010 #include <stdio.h> #include <string.h> int main() { //Programma Maius. trasforma tutti i caratteri in maiuscolo char frase[100]; puts("Inserisci una frase: "); gets(frase); for (int i=0; i< strlen(frase); i++) if (frase[i] >= 'a' && frase[i] <= 'z') frase[i] -= 32; puts("\nLa frase trasformata in maiuscolo e':"); puts(frase); fputs("Batti invio per continuare ...",stdout); getchar(); return 0; } 5.8 I puntatori Abbiamo già visto nel paragrafo 4.4 che tutte le variabili sono informazioni memorizzate nella memoria centrale del computer e si trovano, normalmente, o nel segmento dati o nello stack. Ogni variabile occupa una quantità di spazio diverso a seconda del tipo di dati in essa contenuti e i vettori sono gruppi di variabili che occupano spazi consecutivi. Abbiamo anche visto che se nel programma premettiamo l’operatore & al nome di una variabile diciamo al compilatore che in quel punto vogliamo l’indirizzo della variabile invece del valore della variabile. Anche l’indirizzo di una variabile è un’informazione ed è distinta dal valore della variabile. Con il linguaggio C è possibile memorizzare l’indirizzo di una variabile in un'altra variabile. Le variabili che possono contenere l’indirizzo di memoria di un’altra variabile appartengono ad una specie particolare: le variabili puntatore. Quando si definisce una variabile di tipo puntatore bisogna specificare anche il tipo di dati a cui punta, quindi una variabile puntatore ad un intero è diversa da una variabile puntatore ad un double. Anche se entrambe contengono un indirizzo di memoria sono variabili di tipo diverso, non esiste un unico tipo puntatore. Infatti, come vedremo è possibile fare varie operazioni con i puntatori e queste operazioni comportano istruzioni diverse in linguaggio macchina a seconda del tipo di dati puntato dal puntatore. La sintassi per la definizione di una variabile di tipo puntatore è simile a quella per la definizione di una qualsiasi variabile basta mettere un asterisco prima del nome della variabile. Per esempio: int *p, a, *q; // p e q sono puntatori a int mentre a è una var. normale int *g=&a; // la definizione può anche inizializzare il puntatore Supponiamo che la variabile int a=20 sia collocata in memoria all’indirizzo 37286420 e che sia stato definito una variabile di tipo puntatore a int di nome punt collocata, invece, all’indirizzo 37286504, con l’istruzione: punt = &a; viene assegnato a punt l’indirizzo di a ovvero il numero 37286420. 91 Corso sul Linguaggio C++ Edizione 2010 Memoria centrale Variabile a 20 37286420 Variabile punt 37286420 37286504 Con il linguaggio C è possibile usare l’operatore di dereferenziazione che è il carattere asterisco (*) per riferirsi al valore della variabile puntata dal puntatore. Per esempio, nel caso rappresentato in figura, il valore di punt è 37286420 mentre il valore di *punt è 20. Quindi quando nel programma si scrive in una espressione il nome di una variabile puntatore preceduta da un asterisco vuol dire che in quel punto il computer non deve utilizzare il valore della variabile puntatore ma il contenuto posto nella memoria all’indirizzo che è contenuto nella variabile puntatore. Questo modi di riferirsi ai valori contenuti in memoria è detto indirizzamento indiretto. Sempre considerando l’esempio rappresentato in figura, le due istruzioni: cout << a; cout << *punt; visualizzano entrambe il numero 20 su video. Quando il compilatore traduce il programma in linguaggio macchina sostituisce i nomi delle variabili con il loro indirizzo (nei programmi in linguaggio macchina non esistono più i nomi delle variabili), quindi se traduciamo nel nostro linguaggio quello che il compilatore avrà scritto in linguaggio macchina otterremo per la prima istruzione qualcosa del tipo: scrivi in output il valore intero contenuto all’indirizzo37286420 mentre per la seconda istruzione otterremo: scrivi in output il valore intero contenuto all’indirizzo che troverai memorizzato nella posizione 37286504; Nella prima istruzione viene indicato direttamente l’indirizzo della locazione di memoria interessata dall’operazione mentre nella seconda viene indicato indirettamente, attraverso un’altra locazione di memoria, quella del puntatore. L’operazione * è detta dereferenziazione o indirizzamento indiretto. Come abbiamo visto negli esempi precedenti, per assegnare un valore alle variabili puntatore si può utilizzare l’operatore di assegnamento come si fa con le altre variabili, solo che alle variabili puntatore si possono assegnare solo indirizzi di memoria di altre variabili del tipo specificato al momento della creazione del puntatore. Per esempio se punt è un puntatore ad un valore intero definito con int *punt e num è una variabile intera definita con int num si può assegnare l’indirizzo di num a punt con l’istruzione: 92 Corso sul Linguaggio C++ Edizione 2010 punt = &num; a destra dell’uguale ci può essere una qualsiasi espressione che abbia come risultato un indirizzo di memoria di una variabile intera e sappiamo che l’operatore & produce come risultato l’indirizzo dove è memorizzato il valore della variabile. Se poi si scrive a = *punt * 10; l’istruzione sarà equivalente a a = num * 10; perché *punt indica il valore puntato da punt che corrisponde al valore della variabile num. Se punt1 è un altro puntatore ad un valore intero possiamo trasferire il valore di punt in punt1 con una normale assegnazione: punt1 = punt; perché in questo caso le due variabili sono dello stesso tipo. Con il linguaggio C++ è possibile definire anche puntatori a void: void* data; Il tipo di puntatore a void è un tipo speciale di puntatore. Nel C++, void rappresenta l’assenza di tipo, così che i puntatori a void sono puntatori ad un valore senza tipo e quindi ad un tipo indeterminato. Questo permette ai puntatori void di puntare a qualsiasi tipo di dati, dai tipi numerici elementari ai vettori. Ma, per contro, essi hanno una grave limitazione: il dato puntato non può essere direttamente dereferenziato (visto che non si conosce il tipo), per questo motivo sarà sempre necessario eseguire il cast dei puntatori a void verso un altro tipo di puntatore che punta ad un dato concreto prima di dereferenziarli. 5.9 Puntatori e vettori. Operazioni sui puntatori Se nel nostro programma abbiamo definito un vettore, come già detto nel paragrafo 5.1, il nome del vettore in realtà è un puntatore al primo elemento del vettore, però è un puntatore costante che non può essere modificato. Per esempio se abbiamo definito il vettore di caratteri char frase[50], l’identificatore frase rappresenta la posizione di memoria del primo elemento del vettore e quindi corrisponde a &frase[0]. Questo significa che se abbiamo creato un puntatore ad un valore dello stesso tipo degli elementi del vettore, per assegnare a questo puntatore l’indirizzo del primo elemento del vettore basta scrivere dopo l’uguale, nell’istruzione di assegnazione, il nome del vettore senza parentesi quadre. Per esempio, con le seguenti istruzioni: float num[50]; float *p; p=num; viene definito il vettore di numeri num e il puntatore p ad un valore float, poi si assegna la posizione del primo elemento del vettore al puntatore p. Da questo momento in poi, per riferirmi 93 Corso sul Linguaggio C++ Edizione 2010 ad un elemento del vettore posso utilizzare sia il nome num che il nome p, quindi num[k] e p[k] sono la stessa cosa. Un altro fatto importante è che è possibile eseguire varie operazioni aritmetiche sui puntatori: • Incremento e decremento con gli operatori ++ e --. Quando si applicano questi operatori l’indirizzo di memoria contenuto nella variabile puntatore viene aumentato o diminuito di tanti byte quanti sono quelli occupati dalla variabile a cui punta, quindi se il puntatore punta ad un elemento di un vettore, dopo l’incremento o il decremento punterà rispettivamente all’elemento successivo o precedente nello stesso vettore. • Somma o sottrazione di una costante intera. Ad un puntatore si può aggiungere o sottrarre un numero intero utilizzando le operazioni + e -, il risultato sarà un puntatore dello stesso tipo spostato rispettivamente in avanti o indietro di tante posizioni quanto specificato nella costante. Per esempio se aggiungiamo 4 ad un puntatore che punta ad un elemento di un vettore otterremo un puntatore che punta all’elemento dello stesso vettore che si trova 4 posizioni più avanti. Inoltre se p è un puntatore al primo elemento del vettore, scrivere p[k] o *(p+k) è la stessa cosa, entrambe le notazioni indicano il valore dell’elemento k del vettore. Gli operatori [] in realtà eseguono l’addizione tra il puntatore e il valore contenuto tra le parentesi e dereferenziano il puntatore ottenuto come risultato della somma. • Differenza tra due puntatori. Il risultato indica quante variabili del tipo puntato dai puntatori sono presenti tra le due posizioni puntate. Per esempio se a punta al primo elemento del vettore e b punta al quinto, b-a darà come risultato 4. • Confronto tra puntatori. Si possono utilizzare i normali operatori relazionali <, <=, ==, ecc. Il risultato indicherà se la posizione in memoria puntata dal primo puntatore viene prima, è uguale o viene dopo di quella puntata dal secondo puntatore. Ad un puntatore si può anche assegnare la costante NULL per dire che il puntatore è vuoto, ovvero che non punta a niente. Al posto della costante NULL si può utilizzare il carattere ‘\0’. Per inciso si osserva che si possono definire anche puntatori a una funzione, in questo caso dereferenziando il puntatore, si manda in esecuzione la funzione. Problema: Sostituire in una frase tutte le occorrenze di una parola con un'altra parola data in input. Si può usare la funzione strstr per cercare una stringa in un’altra stringa, se la troviamo trasferiamo prima tutti i caratteri fino alla posizione trovata e poi la nuova parola. Questi passi li ripetiamo fino a quando non troviamo più la stringa. #include <iostream.h> #include <stdlib.h> #include <string.h> int main() { // programma Sost. Cerca una sottostringa in una stringa char s1[100] = "Oggi Gino è andato al mare ma domani Gino andrà in montagna"; char ris[100]="", s2[20], s3[20], *p; int i=0, k, l; cout << "Inserisci la parola da cercare: "; cin >> s2; l=strlen(s2); cout << "Inserisci la parola da sostituire: "; cin >> s3; p=s1; // p è il puntatore che ci dice dove siamo arrivati nella ricerca while (strstr(p,s2)!=NULL) { k=strstr(p,s2)-p; // k = numero di caratteri prima della parola s2 strncat(ris,p,k); // trasferisce nel risultato esattamente k caratt. strcat(ris,s3); // trasferisce la nuova parola p+=k+l; 94 Corso sul Linguaggio C++ Edizione 2010 } strcat(ris, p); cout << "il risultato e': " << endl << ris << endl; system("PAUSE"); return 0; } 5.10 Sottoprogrammi, vettori e puntatori I vettori, così come gli altri tipi di dati, possono essere passati come parametri ad un sottoprogramma ma il passaggio avviene sempre per riferimento. Infatti abbiamo visto che il nome di un vettore è un puntatore costante al primo elemento del vettore e quindi è un indirizzo di memoria per cui al sottoprogramma viene sempre passato l’indirizzo del vettore e non il valore dei suoi elementi. Per esempio se vogliamo scrivere un sottoprogramma per leggere n numeri double in un vettore di massimo 100 elementi possiamo definire come parametri il vettore e il numero intero n. La definizione del sottoprogramma potrebbe essere la seguente: #define MAX 100 void leggi(double num[MAX], int n) { for (int i=0; i< n; i++) { cout << “inserisci l’elemento “ << i+1 << “: “; cin >> num[i]; } } Ma l’intestazione del sottoprogramma poteva anche essere: void leggi(double *num, int n) { perché in entrambi i casi diciamo al compilatore che il primo parametro sarà un puntatore a variabili di tipo double. Quindi quando si passa un vettore ad un sottoprogramma in realtà gli si sta passando un puntatore e quindi il passaggio avviene sempre per riferimento. Problema: Assegnato nome e cognome generare i primi 6 caratteri del codice fiscale Le regole per generare i primi 6 caratteri del codice fiscale sono le seguenti: 3 caratteri alfabetici per il cognome. Si prendono le prime tre consonanti del cognome. Se il cognome contiene solo due consonanti si prendono queste due e la prima vocale, se contiene solo una consonante si prende questa e le prime due vocali. Se il cognome contiene meno di tre lettere si prendono tutte le lettere e si aggiunge una X per ogni lettera mancante. I cognomi costituiti da più parole si considerano come un’unica ininterrotta successione di caratteri. 3 caratteri alfabetici per il nome. Si prende la prima, la terza, e la quarta consonante del nome. Se il nome contiene solo tre consonanti si prendono queste tre. Se ne contiene meno si procede come per il nome. Quindi sia per il nome che per il cognome bisogna separare le vocali dalle consonanti, conviene scrivere un sottoprogramma per fare questa separazione. Conviene anche trasformare tutto in maiuscolo. Il programma è il seguente: #include <iostream.h> #include <stdio.h> 95 Corso sul Linguaggio C++ Edizione 2010 #include <stdlib.h> #include <string.h> // programma codfis. Genera i primi sei caratteri del codice fiscale void maiusc(char *frase) { // trasforma in maiuscolo tutti i caratteri di una stringa for (int i=0; i< strlen(frase); i++) if (frase[i] >= 'a' && frase[i] <= 'z') frase[i] -= 32; } void separa(char *parola, char *cons, char *voc) { /* Separa le vocali dalle consonanti, salta i caratteri non alfabetici Tutti i caratteri devono essere maiuscoli altrimenti vengono ignorati il risultato in cons[] = consonanti e voc[]=vocali */ for (int i=0; i<strlen(parola); i++) if (parola[i]=='A' || parola[i] == 'E' || parola[i] == 'I' || parola[i] == 'O' || parola[i] == 'U') {*voc=parola[i]; voc++;} else if (parola[i] > 'A' && parola[i] <= 'Z') {*cons = parola[i]; cons++;} *cons='\0'; *voc='\0'; // chiude le due stringhe cons e voc } int main(){ char cognome[30], nome[30], c[30], v[30], ris[7]=""; cout << "Inserisci il Cognome: "; gets(cognome); maiusc(cognome); cout << "\nInserisci il Nome: "; gets(nome); maiusc(nome); // crea una stringa con tutte le consonanti+ tutte le vocali + "XXX" // per il cognome separa(cognome,c,v); strcat(c,v); strcat(c,"XXX"); strncpy(ris,c,3); // mette in ris i tre caratteri del cognome // tratta il nome separa(nome,c,v); if (strlen(c) > 3) {strncat(ris,c,1); strncat(ris,c+2,2);} else {strcat(c,v); strcat(c,"XXX"); strncat(ris,c,3);} cout << "\n il risultato è: "<< ris << endl; system("PAUSE"); return 0; } 5.11 Tipi particolari di puntatori E’ possibile definire variabili puntatori che puntano ad una costante, per esempio const int* ptc; definisce un puntatore variabile che punta ad una costante intera. Lo specificatore const si riferisce al dato puntato dal puntatore e non al puntatore stesso che resta una variabile e, quindi, nel programma il suo valore può essere modificato mentre non è ammesso modificare il valore della variabile puntata: const int cc=10; int vv = 20; ptc= &cc; //istruzione ammessa *ptc=20; // istruzione non ammessa, la variabile puntata è costante! ad un puntatore ad un costatnte si può assegnare anche l’indirizzo di una variabile perché è ammessa la conversione da puntatore a variabile a puntatore a costante ptc=&vv; 96 Corso sul Linguaggio C++ Edizione 2010 I puntatori a costante si utilizzano soprattutto per passare dei puntatori come parametri ad una funzione con la sicurezza che le variabili puntate non potranno essere modificate durante l’esecuzione della stessa. Si possono definire anche dei puntatori costanti utilizzando *const al posto di *. Per esempio float ff; float *const ptc= &ff; in questo caso ptc è un puntatore costante ad una variabile float. E’ possibile modificare la variabile dereferenziando il puntatore ma non è possibile modificare il puntatore stesso: *ptc = 2.3415; // ammesso perché il puntatore punta ad una variabile float f1=0.1254e+10; ptc=&f1; // non ammesso perché ptc è costatnte Il classico tipo di puntatore costante è il nome di un vettore. Ripetendo due volte const si possono anche definire dei puntatori costanti a costante: const char dato=’A’; const char ptr *const= &dato; in questo caso ptr è un puntatore costante ad una costante e quindi non si può modificare né il puntatore né il dato puntato. Si possono definire anche puntatori ad una variabile puntatore ripetendo due volte l’asterisco: int **ppt; // ppt è un un puntatore a un puntatore ad una var intera Puntatori e funzioni Il valore restituito da una funzione può essere di qualsiasi tipo, anche del tipo puntatore: int* funz(); // dichiara una funzione che restituisce un puntatore a int Nel linguaggio C esistono anche i puntatori a funzione. Questi servono quando un programma deve mandare in esecuzione una funzione che viene scelta durante l’esecuzione in base all’input dell’utente e che quindi non è nota a priori. I puntatori a funzione devono essere dichiarati senza definire il blocco della funzione. Successivamente verrà assegnato al puntatore l’indirizzo di una funzione esistente. Per esempio: int * (*pfunz)(dounle,*char); dichiara un puntatore di nome pfunz ad una funzione che ha come parametri un numero double e un puntatore a char e che restituisce un puntatore ad un intero. Da notare che le parentesi intorno al nome della funzione sono necessarie, infatti in assenza il compilatore interpreterebbe la dichiarazione come il prototipo di una funzione che restituisce un puntatore ad un puntatore intero. Successivamente, nel corso del programma, il puntatore deve essere inizializzato con l’indirizzo di una funzione effettiva e precedentenemente dichiarata nel programma con lo stesso numero e tipo di parametri e con lo stesso tipo restituito. Per esempio: int* funz1(double , char* ); int* funz2(double , char* ); ………… if ( ......... ) pfunz = funz1 ; else pfunz = funz2; Per richiamare la funzione puntata da pfunz si può dereferenziare o meno il puntatore: *pfunz(12.6,”Gino”) oppure pfunz(12.6,”Gino”) i due modi sono equivalenti. Si possono anche creare dei vettori di puntatori a funzioni con istruzioni del tipo: float (*apfunz[10])(int); 97 Corso sul Linguaggio C++ Edizione 2010 definisce un vettore di 10 puntatori a funzioni che hanno un parametro int e che restituiscono un valore float. I vettori di puntatori a funzioni possono essere utili, per esempio, se si realizza un programma a menù e si vuole far eseguire una particolre funzine in base alla scelta dell’utente senza utilizzare istruzioni IF o Switch. Per esempio: double (*apfunz[5])(int) = {f1, f2, f3, f4, f5} dichiara un vettore di puntatori a funzione di 5 elementi e lo inizializza con gli indirizzi delle funzioni f1, f2, f3, f4, f5 che devono essere funzioni precedentemente dichiarate nel programma con istruzioni del tipo double f1(int x){ …}, ecc. Per assegnare l’indirizzo di una funzione ad un elemento del vettore può anche utilizzare una normale istruzione di assegnazione: apfunz[2]=f3; Per richiamare una delle funzioni puntate si utilizza: apfunz[i](n); // non è necessario dereferenziare il puntatore dove, per esempio, la variabile i potrebbe contenere il numero della voce del menù scelta dall’utente. Come ultima cosa faccio notare che è anche possibile utilizzare come parametro in una funzione un puntatore ad un'altra funzione. Per esempio: void fsel(int (*pfunz)(float)) //dichiarazione della funzione fsel { .... n = pfunz(r); .....} (dove n é di tipo int e r é di tipo float) int funz1(float); int funz2(float); fsel(funz1); //chiamate di fsel fsel(funz2); 98 Corso sul Linguaggio C++ Edizione 2010 5.12 Esercizi 84) Supponiamo che sia presente in memoria il seguente vettore creato con intA[5] 5 1 3 4 6 2 Viene anche creato un vettore B con int B[5]. Dopo l’esecuzione delle seguenti istruzioni cosa conterrà il vettore B[]? for (i = 1; i<5; i++) B[i]=A[i-1]+A[i]; B() = 85) Dire quale dei seguenti frammenti di programma calcola in j la media del vettore di interi positivi v contenente n elementi, posto che j sia inizializzato a zero: a) for(i=0; i<n; i++) j = v[i]; j = j/n; b) for(i=0; i<n; i++) j += v[i]; j = j/n; c) for(i=0; i<n; i++) j += v[i]; 86) Dire quale dei seguenti frammenti di programma calcola in j l'indice del primo elemento positivo del vettore di interi v contenente n elementi, posto che j sia inizializzato a zero (potete assumere che il vettore contenga sempre almeno un elemento positivo): a) for(i=0; i<n; i++) if (v[i] > 0) j=i; b) for(i=n-1; i>=0; i--) if (v[i] > 0) v[j]=v[i]; c) for(i=n-1; i>=0; i--) if (v[i] > 0) j=i; 87) Considerate il seguente frammento di programma: t=-1; for (i=1; i<=n; i++) if (f(i)) t=i; if (t>=0) cout << t; Quale delle seguenti affermazioni è corretta? a) Il programma cerca e stampa il più piccolo intero x fra 1 e n tale che f(x)!=0; se tale intero non esiste, il programma entra in un ciclo infinito. b) Il programma cerca e stampa il più grande intero x fra 1 e n tale che f(x)!=0; se tale intero non esiste, il programma entra in un ciclo infinito. c) Il programma cerca e stampa il più piccolo intero x fra 1 e n tale che f(x)!=0; se tale intero non esiste, il programma non stampa nulla. d) Il programma cerca e stampa il più grande x fra 1 e n tale che f(x)!=0; se tale intero non esiste, il programma non stampa nulla. 88) Dato il seguente frammento di programma: int a[5]={10, 7, 4, 2, 1}; int b[5]={3,2,5,6,4}; int i, j, k; for(i=0; i<5; i++){ k=1; for(j=4; j>=0; j--) if (a[i]<=b[j]) 99 Corso sul Linguaggio C++ Edizione 2010 k=0; if (k) printf("%d ", a[i]); } Indicare i valori stampati in uscita: a) 10 7 b) 10 7 4 c) 10 7 4 2 d) 10 7 4 2 1 e) 2 1 f) 1 g) nessuna delle risposte precedenti è corretta 89) Qual è il ciclo corretto per trovare l'indice del primo elemento negativo in un vettore dichiarato come float v[MAX] ? a) while(i<MAX) if(v[i]>=0) i++; b) while(v[i]>=0 && i<MAX) i++; c) while(i<MAX || v[i]<0) i++; d) while(i<MAX && v[i]>=0.0) i++; 90) Dato il seguente pezzo di programma t=x[1]; for (i = 2 ; i <=n ; i++) t*=x[i]/i ; Quale dei seguenti calcoli viene effettuato? x(1) + x(2) + ..... + x(n) n x(1) * x(2) * ..... * x(n) b) t = n x(1) * x(2) * ..... * x(n) c) t = n! x(3) x ( n) x ( 2) d) t = x(1) * + x(1) * + ...... + x(1) * n 3 2 a) t = 91) Scrivere il pezzo di programma per calcolare la seguente somma supponendo presente in memoria e già valorizzato il vettore float X[…] con N elementi. 1 1 1 1+ + + ...... + x(1) X (2) X (N ) 92) Sia dichiarato un array int a[10]. Cosa succede dopo l'esecuzione del seguente codice: for (i=0; i<=10; i++) a[i]=2; a) b) c) d) Tutto l'array viene inizializzato al valore 2; Si superano i limiti dell'array. L'errore viene segnalato in fase di compilazione; Si superano i limiti dell'array. L'errore viene segnalato all'esecuzione del codice; Si superano i limiti dell'array. L'errore non viene segnalato, ma si hanno conseguenze imprevedibili; 100 Corso sul Linguaggio C++ Edizione 2010 e) Si superano i limiti dell'array. All'accesso all'undicesimo elemento, il sistema operativo assegna altro spazio all'array a e l'esecuzione prosegue. 93) Supponete che i e j siano variabili intere, e che s e t siano vettori di interi. Assumete che, in un dato istante, i=j=0 e che il contenuto dei due vettori sia il seguente: s = { 0, 1, 2, 3} t = { 3, 2, 1, 0} Se viene eseguita la seguente istruzione: s[i++]=t[++j]+1; Quali valori avranno alla fine le variabili i e s? a) i= 0, s= [3,2,1,0] b) i = 1, s= [2,1,0,3] c) i= 1, s=[4,3,2,1] d) i=1, s=[3,1,2,3] e) i=0, s=[3,1,2,3] f) nessuno dei precedenti 94) Quale sarà il contenuto del vettore V al termine del ciclo se è V={1,21,31,4,51,6} e n=6? t=n/2; for (i=0; i<t ;i=i+1) { temp=V[i]; V[i]=V[n-i-1]; V[n-i-1]=temp; } Programmi con i vettori 95) Calcolare lo scarto quadratico medio di un vettore numerico. Se x(i), con i da 1 a n, sono gli elementi del vettore e se M è la media aritmetica degli stessi, per definizione lo scarto quadratico medio, normalmente indicato con σ, si ottiene con la seguente formula: n σ= ∑ ( x(i) − M ) 2 i =1 n 96) Moltiplicare 2 vettori (il risultato è la somma dei prodotti di tutti gli elementi con lo stesso indice nei due vettori). 97) Scrivere un programma per far leggere n numeri (n<=100) e visualizzarli al contrario. 98) Prendendo spunto dal programma descritto nel paragrafo 5.3 realizzare un programma per trasformare in lettere un qualsiasi numero intero fino a un miliardo. 99) Dato un vettore numerico (max 4 cifre ogni numero) contare quanti elementi sono compresi tra 0 e 99, quanti fra 100 e 199, quanti fra 200 e 299 ecc. fino a contare quanti sono compresi fra 9900 e 9999. 100) Dato un vettore qualsiasi individuare la moda ovvero l’elemento che si ripete più volte. 101) Far caricare da tastiera in un vettore gli n+1 coefficienti di un polinomio P(x) di grado n: 101 Corso sul Linguaggio C++ Edizione 2010 P(x) = a0 + a1x + a2x2 + a3x3 + ....... + an-1xn-1 + anxn poi far calcolare e visualizzare il valore del polinomio per K valori di x a partire da un numero A con un incremento di S (K,A,S in input). 102) Far determinare i numeri primi da 1 a 1000 con il crivello di Eratostene (inserire i numeri da 1 a 1000 in un vettore poi partendo dal primo cancellare tutti i multipli, andare avanti nel vettore considerando gli elementi non cancellati e per ognuno cancellarne i multipli, alla fine visualizzare i numeri non cancellati). 103) Far determinare i primi N numeri primi mettendoli in un vettore. (Mettere 2 nel primo elemento del vettore poi, considerando solo i numeri dispari da 3 in poi controllare che non siano multipli di nessun elemento presente nel vettore e inferiore alla radice quadrata del numero, se questa condizione si verifica si può inserire l'elemento nel vettore perché é primo). 104) Far visualizzare le prime n righe del triangolo di Tartaglia: 1 1 1 2 1 1 3 3 1 1 4 6 4 1 . . . . . . . (la prima riga è costituita da due uno, il primo e ultimo numero di ogni riga è sempre 1, ogni altro numero è uguale alla somma di due numeri della riga precedente: quello che si trova proprio sopra il numero da determinare e quello immediatamente a sinistra). 105) Fare un programma che assegnato una frase isoli e metta in un vettore tutte le parole in essa contenute. 106) Fare un programma per caricare un testo in un vettore di stringhe. 107) Fare un programma che assegnata in input una parola ne generi tutti gli anagrammi possibili (anche quelli senza significato). 108) Far leggere una serie di nomi con il relativo indirizzo e numero di telefono e far stampare la lista in ordine alfabetico. 109) Dato un vettore ordinare gli elementi di posto dispari in ordine crescente e quelli di posto pari in ordine decrescente. 110) Dati due vettori numerici ordinati, riunirli in un terzo sempre ordinato (fusione). 111) Dato un vettore far visualizzare l'elemento mediano (cioè quello che si trova al centro del vettore dopo l'ordinamento). 112) Far inserire in input le N squadre del campionato e i punti totalizzati dopo di che far visualizzare la classifica. 102 Corso sul Linguaggio C++ Edizione 2010 113) Assegnare ad un vettore di 40 elementi, ognuno di due caratteri, delle sigle per simulare un mazzo di carte napoletane, per esempio "1C","2C","3C" ecc. potrebbero indicare le carte asso di coppe, due di coppe, 3 di coppe ecc., "1S","2S" ...potrebbero indicare asso di spade, due di spade e analogamente per tutte le altre carte. Fare un programma per mischiare le carte usando la funzione rand. 114) Problema assegnato alla selezione regionale del 2001. È venerdì, e il cassiere Camillo ha davanti a sé una lunga fila di clienti della sua banca venuti a ritirare contante per il weekend. Per fare presto, Camillo decide di usare per ogni cliente il numero minimo possibile di banconote. Sapreste scrivere un programma per evitargli il mal di testa, considerato che ha a disposizione banconote da 100.000, 10.000, 5.000, 2.000 e 1.000 in quantità illimitata e che l'entità di ogni prelievo è un multiplo di 1.000 lire? Puntatori 115) Quale delle seguenti istruzioni non ha lo stesso effetto sulle due variabili a e b? a) int a,b; a=b; b) int a,b, *p,*c; p=&a; c=&b; *p=*c; c) int a,b, *p,*c; p=&a; c=&b; *p=b; d) int a,b, *p,*c; p=&b; c=&a; *c=b; e) int a,b, *p,*c; p=&a; c=&b; p=c; f) nt a,b, *p,*c; p=&a; c=p; *c=b; 116) Si consideri il seguente frammento di codice: void foo( int *a, int b) { int temp = *a; *a = b; b = temp; }; int main( ) { int a = 1, b = 5; foo( &a, b ); } Quanto valgono le variabili a e b alla fine dell'esecuzione? Risposte esercizi Esercizio 84) B() = 6 4 7 10 8 Esercizio 85) Risposta = b) Esercizio 86) Risposta = c) Esercizio 87) Risposta = d) Esercizio 88) Risposta = a) Esercizio 89) Risposta = d) Esercizio 90) Risposta = c); Esercizio 92) Risposta = d) Esercizio 93) Risposta =d) Esercizio 94) Risposta = 6, 51, 4, 31, 21, 1 Esercizio 114) Il cassiere Camillo – problema assegnto alla selezione regionale 2001 Si vuole realizzare un programma che data in input la somma da pagare calcoli quante banconote da 100000, quante da 10000, quante da 5000, quante da 2000 e quante da 1000 sono necessarie. In pratica un numero in input e 5 numeri in output. Per utilizzare il numero minimo 103 Corso sul Linguaggio C++ Edizione 2010 di banconote basta utilizzare se possibile i tagli più grandi ovvero per pagare 100000 bisogna utilizzare una banconota da 100000 e non 10 da 10000. Si possono inserire i 5 tagli in un vettore numerico e poi in un ciclo clalcolare per ogni taglio quante banconote servono cominciando dal più grande. Per questo calcolo basta dividere la somma che resta da pagare per il taglio delle banconote il quoziente è il numero di banconote che bisogna utilizzare per quel taglio e il resto è la somma da pagare con i tagli più piccoli. Il programma è il seguente: #include <stdio.h> #include <stdlib.h> // programma cam.cpp - Cassiere Camillo- selezioni regionali 2001 int main(){ int tb[]={100000, 10000, 5000, 2000, 1000}; // i 5 tipi di banconote int somma, i; // somma = l'importo in input int nb[5]; // nb[] = numero banconote per ogni taglio (dati in output) printf("Inserisci la somma da pagare: "); scanf("%d", &somma); for (i= 0; i <5; i++) { nb[i] = somma / tb[i]; // numero banconote del taglio i somma %= tb[i];// quello che resta dopo aver dato il taglio i printf("Servono %d banconote da %d \n", nb[i], tb[i]); } system("PAUSE"); return 0; } Esercizio 115) Risposta = e) Esercizio 116) a=5; b=5 104 6 Capitolo Corso sul Linguaggio C++ Edizione 2010 6. Strutture, union, matrici, tabelle e l’allocazione dinamica della memoria 6.1 Strutture Se vogliamo creare uno spazio di memoria che possa contenere più dati di tipo eterogeneo, non possiamo utilizzare i vettori (che, invece, si utilizzano per un gruppo di dati tutti dello stesso tipo) ma dobbiamo utilizzare un tipo particolare detto struct. Il tipo struct serve per definire uno spazio di memoria adatto per contenere più variabili di tipo diverso. In altri linguaggi questo tipo di dato viene detto record perché viene utilizzato principalmente per contenere una registrazione di un archivio, per esempio le informazioni su un cliente o su un prodotto. La sintassi per dichiarare una struct è: Definizione di una struct struct nome struct { Tipo dato Nome elemento ; } Nome variabile , Per esempio, volendo creare un struttura per contenere le informazioni su un prodotto in magazzino: struct tipo_prodotto { char codice[10]; char desc[50]; double prezzo; float quant; float sconto; double costo; }; La struttura rappresenta un tipo di dati, per utilizzarla all’interno di un programma bisogna dichiarare delle variabili di questo nuovo tipo: tipo_prodotto prod1, prod2; Sia la variabile prod1 che la variabile prod2 possono contenere più informazioni, tutte quelle definite come parte della struct tipo_prodotto, queste informazioni si chiamano campi. Nell’esempio prod1 e prod2 possiedono i seguenti campi: codice, desc, prezzo, quant, sconto, costo. Come avviene per i vettori ogni campo è una variabile indipendente e i vari campi sono situati in memoria consecutivamente, ognuno in un proprio spazio. Per riferirsi nel program105 Corso sul Linguaggio C++ Edizione 2010 ma ad un campo di una struct si utilizza il nome della variabile seguita da un punto e dal nome del campo. Per esempio per far inserire da tastiera la descrizione del prodotto contenuta in prod1 possiamo scrivere: gets(prod1.desc); utilizziamo gets invece di cin perché la descrizione potrebbe contenere spazi. Per visualizzare in output il prezzo del prodotto prod1 possiamo scrivere cout << prod1.prezzo; Una variabile di tipo struct, come qualsiasi altra variabile, può essere passata come parametro ad un sottoprogramma sia per valore che per riferimento. Per esempio il seguente programma legge i dati di un prodotto e li stampa: #include <iostream.h> #include <stdlib.h> #include <stdio.h> struct tipo_prodotto { char codice[10]; char desc[50]; double prezzo; float quant; float sconto; double costo; }; void leggiprodotto(tipo_prodotto &p){//passaggio per riferimento cout << "Inserisci il codice:\t"; gets(p.codice); cout << "Inserisci la descrizione:\t"; gets(p.desc); cout << "Inserisci il prezzo:\t"; cin >> p.prezzo; cout << "Inserisci la quantita':\t"; cin >> p.quant; cout << "Inserisci lo sconto:\t"; cin >> p.sconto; cout << "Inserisci il costo:\t"; cin >> p.costo; } void visualizza(tipo_prodotto p){ cout << "il prodotto letto è: " << endl; printf("%10s %25s %10s %10s %6s %6s\n", "Codice","Descrizione","Prezzo","Quantita'","Sconto","Costo"); printf("%10s %25s %10.2f %10.2f %6.2f %6.2f\n", p.codice, p.desc, p.prezzo, p.quant, p.sconto,p.costo); } int main(){ tipo_prodotto articolo; leggiprodotto(articolo); visualizza(articolo); system("PAUSE"); return 0; } mandandolo in esecuzione il risultato è Si possono anche definite dei puntatori ad una variabile di tipo struct, per esempio: tipo_prodotto *p; 106 Corso sul Linguaggio C++ Edizione 2010 in questo caso, per far riferimento ai campi della variabile puntata da p, si usa una sintassi particolare: <nome puntatore>-><nomecampo> Invece del punto (.) si usa l’operatore -> Per esempio: cout << p->codice << ‘\t’ << p->desc << ‘\t’ << p->prezzo; 6.2 Le union Le union sono particolari strutture e si definiscono con la stessa sintassi delle strutture usando, però, la parola chiave union invece di struct. Quindi anche le union sono un insieme di campi ma a differenza delle struct tutti i campi delle union condividono lo stesso spazio di memoria. Questo significa che se si modifica un campo di una union anche gli altri campi saranno interessati dalla modifica. In una union i campi non sono indipendenti perché occupano lo stesso spazio di memoria, c’è in pratica un solo spazio di memoria che può essere interpretato in diversi modi. Problema: rappresentazione interna di una variabile Vogliamo visualizzare la rappresentazione interna di una variabile di memoria di tipo qualsiasi. Sappiamo che qualsiasi dato immesso in memoria viene trasformato in una serie di bit ovvero in una serie di zeri e uno, vogliamo visualizzare il contenuto binario della memoria. Si può utilizzare una union per far condividere lo spazio utilizzato dalla variabile con un vettore di char e poi visualizzare la rappresentazione interna di ogni elemento del vettore con un ciclo in cui si esegue l’and bit a bit dell’elemento del vettore con una maschera contenente tutti zeri e un solo bit = 1, quello che si vuole visualizzare. Se l’and tra l’elemento del vettore e la maschera è 0 il bit sarà zero altrimenti è 1. Il programma è il seguente. #include <iostream.h> #include <stdlib.h> int main() { //programma rapint. Rappresentazione interna di una variabile // vc.x =variabile di cui si vuole conoscere la rappresentazione interna const unsigned char msk1=0x80; // corrisponde al numero binario 10000000 // viene assegnato lo stesso spazio alla variabile x e al vettore lt union cond {long double x; char lt[12];}; cond vc; unsigned char mask; int sz,i,j; cout << "Inserisci il valore di cui vuoi vedere la rappresentazione: "; cin >> vc.x; cout << "Rappresentazione interna = \n"; sz = sizeof(vc.x); for (i=0; i<sz; i++) // controlla un byte alla volta {mask=msk1; for (j=0; j<8; j++) // controlla un bit alla volta e lo stampa {if (vc.lt[i] & mask) cout << "1"; else cout << "0"; mask = mask >> 1; } cout << ' '; } system("PAUSE"); return 0; } 107 Corso sul Linguaggio C++ Edizione 2010 Questo programma si può utilizzare per visualizzare la rappresentazione interna di qualsiasi variabile, se per esempio volessimo visualizzare la rappresentazione interna di una variabile int basta sostituire nella union la riga long double x; con int x; 6.3 Le matrici e le tabelle Gli elementi di un vettore possono essere di qualsiasi tipo, non necessariamente un tipo elementare. Questo significa che gli elementi di un vettore possono essere a loro volta vettori o struct (ovvero record), o union. Anche gli elementi di una struct possono essere di qualsiasi tipo compreso altre struct, in questo caso di parla di struct annidate. Possiamo, per esempio, creare vettori di vettori che normalmente vengono chiamati matrici, oppure vettori di struct (vettori di record) che normalmente vengono chiamati tabelle. È anche possibile creare struct con dei campi che sono vettori. Inserendo vettori, struct, union in altri vettori, struct e union si possono creare strutture di dati di elevata complessità e tali da rispondere a tutte le esigenze dei programmatori. Matrici Esaminiamo in particolare i vettori i cui elementi sono a loro volta dei vettori, questo tipo di strutture di dati viene chiamato matrice o vettore a due dimensioni. Per esempio se abbiamo un vettore di dimensione 3 i cui elementi non sono variabili semplici ma sono a loro volta dei vettori di dimensione 4, otterremo un vettore che contiene in totale 3 * 4 = 12 elementi. Un vettore di questo tipo si dice che ha due dimensioni (3 e 4) e può essere rappresentato graficamente nel seguente modo (3 righe e 4 colonne): per definirlo con il linguaggio C bisogna specificare le due dimensioni tra parentesi quadre. Per esempio: int a[3][4]; // 3 vettori ciascuno di 4 elementi interi char testo[10][80]; // 10 stringhe ciascuno con massimo 79 caratteri Si può inizializzare una matrice come si fa con un vettore elencando semplicemente tutti i valori da inserire: int a[3][4]={1,2,3,4,5,6,7,8,9,10,11,12}; oppure, per comodità di lettura, distinguendo i vari vettori che la compongono: int a[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}}; Normalmente quando si visualizza una matrice si visualizza un vettore per riga e poi, si incolonnano gli elementi che nei vari vettori si trovano nella stessa posizione. Per esempio, per la matrice di tre vettori ciascuno di 4 numeri inizializzata nell’esempio precedente prima si ottiene: 1 2 3 4 5 3 7 8 9 10 11 12 quindi la prima dimensione riguarda il numero di righe mentre la seconda indica il numero di colonne. Le righe sono numerate da 0 a 2 mentre le colonne da 0 a 3. Il numero degli elementi di una matrice è pari al prodotto del numero delle righe per il numero delle colonne. Per far riferi108 Corso sul Linguaggio C++ Edizione 2010 mento ad un elemento della matrice bisogna specificare il numero della riga e quello della colonna, per esempio l’elemento con valore 7 appartiene alla riga 1 colonna 2, per far riferimento ad esso si utilizza la scritta: a[1][2] il primo indice indica la riga mentre il secondo la colonna. L’inizializzazione della matrice poteva anche essere fatta con le seguenti istruzioni: a[0][0]=1; a[0][1]=2; a[0][2]=3; a[0][3]=4; a[1][0]=5; a[1][1]=6; a[1][2]=7; a[1][3]=8; a[2][0]=9; a[2][1]=10; a[2][2]=11; a[2][3]=12; Per visualizzare una matrice su video si utilizza un doppio ciclo for come nell’esempio seguente che segue che visualizza la matrice 3 x 4 dell’esempio precedente: for (int i=0; i<3; i++) {for (int j=0; j<4; j++) cout << a[i][j] << '\t'; cout << endl; } Se vogliamo far inserire i dati per caricare una matrice da tastiera possiamo, per esempio, utilizzare le seguenti istruzioni (valide per una matrice numerica di dimensione n x m): cout << "inserisci la matrice una riga per volta separando i” “numeri con spazi" << endl; for (int i=0; i<n; i++) { cout << "Riga " << i<< '\t'; for (int j=0; j<m; j++) cin >> a[i][j] ; } Si possono creare anche vettori a 3 o più dimensioni definendoli utilizzando tre o più volte le parentesi quadre, per esempio supponiamo di voler memorizzare le temperature rilevate in una certa località ogni ora del giorno, per ogni giorno del mese, in ogni mese dell’anno: float temp[12][31][24]; definisce un vettore a tre dimensioni costituito da 12 matrici ognuna delle quali ha 31 righe e 24 colonne. Una matrice per ogni mese dell’anno. In memoria verranno create 12 x 31 x 24 = 8928 variabili di tipo float, alla prima ci si riferisce con temp[0][0][0], alla seconda con temp[0][0][1] e così via fino alla 24° temp[0][0][23], mentre la 25° si ottiene con temp[0][1][0], l’ultima avrà indici temp[11][30][23]. La temperatura rilevata il 15 aprile alle ore 13 sarà memorizzata nell’elemento temp[4-1][15-1][13] ovvero temp[3][14][13] Problema: Scrivere un programma per caricare due matrici n x n, calcolarne il prodotto righe per colonne e visualizzare la matrice risultato. Una volta scritti i sottoprogrammi per leggere una matrice (leggi), visualizzare una matrice (scrivi) e per fare il prodotto (prodotto), il programma principale si riduce a richiamare il sottoprogramma leggi due volte per far leggere le due matrici, richiamare il sottoprogramma prodotto e quindi richiamare il sottoprogramma scrivi per visualizzare la matrice prodotto. #include <iostream.h> #include <stdlib.h> /* Programma prodotto. Legge due matrici nxn, fa il prodotto e visualizza il risultato n = dimensione di tutte le matrici mat1[][] = prima matrice mat2[][] = seconda matrice ris[][] = matrice prodotto */ void leggi(float a[][10], int n){ cout << "inserisci la matrice una riga per volta separando “ 109 Corso sul Linguaggio C++ Edizione 2010 “i numeri con spazi" << endl; for (int i=0; i<n; i++) { cout << "Riga " << i<< '\t'; for (int j=0; j<n; j++) cin >> a[i][j] ; } } void scrivi(float a[][10], int n) { for (int i=0; i<n; i++) {for (int j=0; j<n; j++) cout << a[i][j] << '\t'; cout << endl; } } void prodotto(float a[][10], float b[][10], float c[][10], int n){ // calcola il prodotto righe per colonne int i,j,k; float s; for (i=0; i<n; i++) for (j=0; j<n; j++) { s=0; for (k=0; k<n; k++) s+= a[i][k]* b[k][j]; c[i][j]=s; } } void main(){ int n=20; float mat1[10][10], mat2[10][10], ris[10][10]; while (n>10) { cout << "Inserisci la dimensione delle matrici (max 10): "; cin >> n; } cout << endl <<"Inserisci la prima matrice." << endl; leggi(mat1,n); cout << endl <<"Inserisci la seconda matrice." << endl; leggi(mat2,n); prodotto(mat1, mat2, ris, n); cout << endl << "La matrice prodotto e':" << endl; scrivi(ris,n); system("PAUSE"); } da notare che quando si passa una matrice come parametro ad un sottoprogramma la prima dimensione può essere omessa ma non la seconda perché una matrice è un vettore di vettori e quindi gli elementi del primo vettore sono altri vettori di cui il sistema deve conoscere la dimensione per poterne calcolare la grandezza. 6.4 Allocazione dinamica della memoria Tutte le variabili definite nei programmi precedenti con le istruzioni di dichiarazione che abbiamo visto nel paragrafo 2.2, una volta create, hanno un loro ciclo di vita e non possono essere eliminate dalla memoria in anticipo. Le variabili globali e quelle locali definite con static restano in memoria fino alla fine del programma (nel segmento dati) mentre quelle locali normali vengono create nel segmento stack e cancellate quando termina l’esecuzione delle istruzioni del blocco nel quale sono state definite (vedi paragrafo 4.5). Inoltre il programmatore deve deciderne il numero e assegnargli un nome in fase di progettazione del programma. Potrebbe essere utile permettere ad un programma di creare nuove variabili durante l’esecuzione, in base alle esigenze determinate dall’input dell’utente, senza averle dichiarate precedentemente. Inoltre, dato che lo spazio di memoria assegnato dal sistema operativo ad un programma in esecuzione è limitato, potrebbe essere utile poter eliminare variabili che non servono più durante l’esecuzione del programma per far spazio ad altre. Questo è un problema 110 Corso sul Linguaggio C++ Edizione 2010 soprattutto per i programmi che devono elaborare grandi quantità di informazioni e che hanno bisogno di molta memoria. Per esempio abbiamo visto che per dichiarare i vettori è necessario indicarne la dimensione utilizzando una costante e questo ci costringe a dichiararli con un numero di elementi in genere superiore a quello che verrà effettivamente utilizzato nella maggior parte dei casi. Per esempio se sto scrivendo un programma per elaborare i voti degli alunni di una scuola qualsiasi e mi serve un vettore per memorizzare i voti, devo dimensionare il vettore in modo che sia sufficiente per contenere i voti della scuola che ha il maggior numero possibile di alunni e, quindi, in genere, quando il programma verrà effettivamente impiegato, buona parte delle caselle del vettore resteranno inutilizzate. Con il linguaggio C++ è possibile creare una variabile durante l’esecuzione e cancellarla quando non serve più. Questo modo di gestire la memoria libera si dice gestione dinamica. Le variabili create con questo sistema vengono inserite in uno spazio di memoria particolare detto heap distinto sia dal segmento dati, dove vanno le variabili globale e locali statiche, che dal segmento stack dove vanno le varibili locali normali. Il linguaggio C++ mette a disposizione per queste operazioni gli operatori new e delete. Per creare una nuova variabile di qualsiasi tipo (anche vettori, struct, ecc.) si usa la sintassi: • • • <nome puntatore> = new <tipo dato> [[<numero elementi>]] [(<valore iniziale>)] <tipo dato> é il tipo dell'oggetto (o degli oggetti) da creare; <numero elementi> é il numero degli oggetti, che vengono sistemati nella memoria heap consecutivamente (come gli elementi di un array); se questo operando viene omesso, sarà costruito un solo oggetto; se é presente, l'indirizzo restituito da new punta al primo oggetto; <valore iniziale> é il valore con cui l'area allocata viene inizializzata (deve essere dello stesso tipo di <tipo>); se é omesso l'area non é inizializzata. Questa istruzione permette di creare sia variabili elementari che vettori e di assegnargli anche dei valori iniziali (le parti tra parentesi quadre sono come al solito facoltative). Per esempio se in un programma mi serve un vettore di n elementi dove n è una variabile del problema, posso crearmelo esattamente di n elementi: int n, i, *p=NULL; cout << "Quanti elementi: "; cin >> n; p= new int[n]; cout << "Inserisci i numeri separati da spazi:\n"; for (i=0; i<n; i++) cin >> p[i]; L’operatore new riserva in memoria uno spazio sufficiente a creare la struttura di dati e assegna l’indirizzo iniziale dell’area alla variabile puntatore. Potrebbe capitare che non sia possibile assegnare la memoria necessaria per creare la struttura di dati richiesta, in questo caso alla variabile puntatore dovrebbe essere assegnato il valore NULL (corrispondente a ‘\0’). Purtroppo il DEV C++ in questo caso interrompe semplicemente l’esecuzione senza segnalare nessun errore. Successivamente è possibile eliminare le variabili create con new utilizzando l’operatore delete con la sintassi: delete <nume puntatore> oppure delete [] <nome puntatore> 111 Corso sul Linguaggio C++ Edizione 2010 se si tratta di un vettore. Per esempio per eliminare il vettore creato nell’esempio precedente basta scrivere: delete [] p; L’operatore delete rende disponibile lo spazio precedentemente assegnato alla variabile puntata dal puntatore ma non elimina il puntatore dalla memoria. Lo stesso puntatore potrà essere ancora utilizzato per puntare ad altre variabili eventualmente create con un’altra new. Le variabili inserite nella memoria heap possono essere utilizzate solo attraverso i puntatori. L’operatore delete è l’unico modo per deallocare una variabile nella memoria heap. Se si cancella il valore contenuto nel puntatore, per esempio con un’istruzione del tipo p=NULL; oppure gli si assegna un altro valore prima di utilizzare delete per rilasciare lo spazio occupato dalle variabili puntate, queste ultime non potranno più essere cancellate e continueranno ad occupare spazio in memoria, pur non essendo più accessibili, fino a quando il programma termina l’esecuzione. Problema: Gestione di una pila. La pila o stak è una struttura dinamica di dati dove è possibile fare solo due operazioni : inserimento di nuovi dati (operazione push) ed estrazione di dati (operazione pop). I dati inseriti vengono immagazzinati con la tecnica LIFO (Last In First Out) ovvero, quando si estrae un dato viene estratto l’ultimo inserito. In pratica, quando inseriamo i dati è come se li mettessimo uno sopra l’altro formando appunto una pila, quando poi li andiamo a riprendere partiamo da quello in cima alla pila che è l’ultimo inserito. Questa è una struttura dinamica perché si espande man mano che aggiungiamo dati e si riduce man mano che li estraiamo. Per simulare una pila si deve utilizzare della memoria per inserire i dati contenuti nella pila, possiamo utilizzare un vettore oppure utilizzare la memoria heap per creare e cancellare dinamicamente le variabili durante il funzionamento del programma. Il programma seguente mostra come si può gestire una pila di numeri interi utilizzando la memoria heap: // Programma pila. Simula la gestione di una pila di numeri interi struct elem {// elem=un qualsiasi elemento inserito nella pila int dato; elem *next; }; elem *pila=NULL; // puntatore al primo elemento della pila int pop(){ // funzione che preleva un elemento dalla pila if (pila == NULL) return 0; else { int ris =pila->dato; elem *tmp=pila->next; delete pila; pila = tmp; return ris; } } void push(int num){ // sottoprogramma per inserire un numero nella pila elem *tmp= new elem; tmp->next=pila; tmp->dato = num; pila=tmp; } void main(){ char comando; int num; cout << "Gestione Pila. I per inserire, E per estrarre, F per fine:\n" ; do { cout << "Comando: "; cin >> comando; // trasforma in maiuscolo il comando 112 Corso sul Linguaggio C++ Edizione 2010 if (comando >='a' && comando <= 'z') comando -=32; switch (comando) { case 'I': cout << "Inserisci il numero: "; cin >>num; push(num); break; case 'E': if (pila == NULL) cout << "Pila vuota\n" ; else cout << "Elemento estratto= " << pop() << endl; break; case 'F': break; default: cout << "Sono ammessi solo I o E o F.\n"; } } while (comando != 'F'); system("PAUSE"); } 6.5 Assegnare pseudonimi ai tipi di dati Una delle caratteristiche del linguaggio C è la possibilità di creare tipi di dati complessi la cui definizione può essere anche molto lunga. Per scrivere meno si può utilizzare la parola chiave typedef che permette di assegnare un nome scelto da noi per qualsiasi tipo di dati. La sintassi è: typedef Dichiarazione di tipo Nome alternativo Per esempio spesso nei programmi troviamo dichiarazioni del tipo: typedef unsigned long int ulong; viene assegnato lo pseudonimo ulong al tipo unsigned lond int. Questo permette successivamente nel programma di utilizzare il nuovo nome di tipo ulong al posto di unsigned long int in tutte le dichiarazioni successive, per esempio: ulong a,b,c; Un altro utilizzo comune è quello per definire i puntatori. Se per esempio si scrive: typedef double* pdouble; nel seguito del programma si potranno definire variabili puntatori a double usando il nome pdouble : pdouble punt1,punt2; invece di scrivere: double *punt1, *punt2; 6.6 Esercizi 117) La variabile dichiarata come char d[N][M]; è... a) ...un vettore di N stringhe di lunghezza M-1 b) ...un vettore di M stringhe di lunghezza N c) ...una matrice NxM di stringhe d) ...una matrice NxM di stringhe lunghe un carattere ciascuna Esercizio 117) Risposta = a) 118) Data la seguente funzione che inizializza i valori di un array bidimensionale "matrice": #define N 5 void inizializza( ){ 113 Corso sul Linguaggio C++ Edizione 2010 int matrice[N][N]; int riga, colonna; for ( riga = 0; riga < N; riga++ ) { for ( colonna = 0; colonna < N; colonna++ ) { if ( riga == colonna ) matrice[riga][colonna] = 1; else if ( riga + colonna == N - 1 ) matrice[riga][colonna] = 1; else if ( riga < colonna ) matrice[riga][colonna] = 0; else matrice[riga][colonna] = matrice[colonna][riga]; } } for ( riga = 0; riga < N; riga++ ) { for ( colonna = 0; colonna < N; colonna++ ) printf( "%d ", matrice[riga][colonna] ); printf( "\n" ); } } Indicare quale tra le seguenti configurazioni vengono stampate dalla procedura "inizializza". Risposte: a) 1 1 0 1 0 0 0 0 1 1 1 0 1 0 1 1 0 0 1 1 1 0 0 0 1 114 Corso sul Linguaggio C++ Edizione 2010 b) 1 0 0 1 0 0 0 1 1 0 0 0 1 0 0 0 1 0 1 0 1 0 0 0 1 115 Corso sul Linguaggio C++ Edizione 2010 c) 1 0 0 1 0 0 0 1 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 116 Corso sul Linguaggio C++ Edizione 2010 d) 1 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 Esercizio 118) Risposta = b) Programmi con le matrici 119) Far assegnare in modo casuale i valori in una matrice di dimensione N x M (N e M dati in input) e visualizzarla. 120) Creare una matrice unitaria di ordine N. 121) Far visualizzare l'elemento più piccolo e quello più grande di una matrice. 122) Scambiare di posto gli elementi di una matrice per ottenere la matrice trasposta. 123) Descrivere un algoritmo per verificare se una matrice quadrata é simmetrica. 124) Descrivere un algoritmo per vedere se in una matrice c'é una riga contenente tutti 1. 125) Un quadrato magico é una matrice quadrata con la seguente proprietà: la somma degli elementi di ciascuna riga, colonna e diagonale dà sempre lo stesso numero. Per esempio: 4 3 8 9 5 1 2 7 6 Quadrato magico d'ordine3 (la somma é sempre 15) 11 24 7 29 3 4 12 25 8 16 17 5 13 21 9 10 18 1 14 22 23 6 19 2 15 Quadrato magico d'ordine 5 (la somma é sempre 65) Un algoritmo per ottenere un quadrato magico di ordine dispari é il seguente: - si parte mettendo 1 nella casella sotto il centro della matrice; - i numeri seguenti (2, 3, 4 .... n*n) sono inseriti ogni volta nella casella intersezione della riga e colonna successiva (riga sotto e colonna a destra, con la convenzione che la successiva dell'ultima riga o colonna é la prima); - se una casella é stata già riempita si inserisce il numero nella colonna precedente una riga più sotto (si assume che la colonna precedente la prima sia l'ultima). Scrivere un programma che assegnato un numero N dispari qualsiasi visualizzi il quadrato magico di ordine N e il numero magico somma di tutte le righe, colonne e diagonali (N*(N2+1)/2). 117 Corso sul Linguaggio C++ Edizione 2010 126) Creare una matrice di ordine NxM nella memoria heap, caricarla con numeri a caso e visualizzarla. 127) Creare e visualizzare una matrice triangolare superiore con numeri a caso (una matrice triangolare superiore è quella dove tutti i numeri sotto la diagonale principale sono = 0) 128) Assegnata una matrice quadrata di ordine n qualsiasi far scambiare le righe in modo che: per ogni J>I sia abs(A(I,I)) > abs(A(J,I)) cioè in modo che gli elementi della diagonale principale siano maggiori, in valore assoluto, di tutti gli elementi che seguono nella stessa colonna. Per esempio partendo dalla matrice: 7 75 -10 5 3 -2 4 22 -6 20 -1 7 4 -10 5 20 -5 -5 7 75 -6 4 +2 si ottiene 4 22 7 -4 -4 3 -2 -1 +2 129) Far visualizzare il polinomio prodotto di due polinomi di grado m e n. (se si mettono i coefficienti dei due polinomi in due vettori a[i] e b[i], e consideriamo la matrice c[i][j]= a[i]*b[j] la somma degli elementi delle diagonali secondarie della matrice ci darà i coefficienti del polinomio prodotto). 118 7 Capitolo Corso sul Linguaggio C++ Edizione 2010 7. I file su disco 7.1 Input output in generale La maggior parte dei programmi legge dati (operazione di input) e produce dei risultati che devono essere visualizzati o scritti da qualche parte (output). Per fare queste operazioni il programma comunica con le unità di input e di output collegate al sistema di elaborazione. Possiamo immaginare il trasferimento di dati, da e alle periferiche, come flussi di byte che vanno e vengono, in inglese “flusso” si traduce con “stream”. Nel linguaggio C, la gestione dei flussi di dati in input e output è demandata ad un gruppo di funzioni presenti nella libreria standard e inclusi nel file di intestaizioni <stdio.h> (standard input output), mentre nel linguaggio C++, oltre a <cstdio>, sono stati introdotti una serie di oggetti, definiti nel file di intestazioni <iostream> (input output stream). Fra questi oggetti ci sono anche cout e cin, proprio quelli che abbiamo utilizzato finora nei nostri programmi per l’input e l’output. Nel linguaggio C le istruzioni corrispondenti a cin e cout (del C++) sono scanf e printf definite in <stdio.h>. I dati possono essere anche presi o inviati da o a un file presente su disco, oltre che da o a una periferica. Nel linguaggio C++ i flussi di dati da e verso le memorie di massa vengono trattati come quelli da e verso le periferiche. Per flusso (stream) intendiamo il canale di collegamento tra il programma e l’oggetto che fornisce o riceve i dati. Per ragioni storiche, nel linguaggio C base, questi canali di collegamento vengono chiamati FILE invece di stream (nel linguaggio C++ si chiamano correttamente stream), normalmente il file è uno degli oggetti che può ricevere o fornire i bytes del flusso, non il canale di collegamento. Prima di effettuare qualsiasi operazione di input-output è necessario creare il flusso, ovvero il collegamento programma <──> periferica o file, questa operazione viene definita apertura del flusso. L’apertura del flusso mette in collegamento il programma con il particolare file o periferica e crea il buffer (area di memoria dove vengono depositati temporaneamente i byte presi o da inviare al file) e tutte le variabili di lavoro che servono per gestire questo collegamento, per esempio la variabile intera che indica la posizione nel file del prossimo byte che verrà letto o scritto. Quando il programma ha finito di utilizzare il flusso lo può chiudere per canellare tutte le variabili e liberare gli spazi di memoria utilizzati per la sua gestione. Quando il programma parte, se abbiamo incluso stdio (#include <stdio.h>) o iostrem (#include <iostream.h>), vengono automaticamente creati tre flussi: stdin, stdout, e stderr; il primo è sinonimo di standard input ed è collegato normalmente alla tastiera ma il sistema operativo lo può collegare anche ad un file o ad un'altra periferica; il secondo sta per standard output ed è collegato normalmente al video ma può essere collegato anch’esso ad un file o ad una altra 119 Corso sul Linguaggio C++ Edizione 2010 periferica; il terzo sta per standard error ed è collegato in genere al video. Alcune istruzioni come cin, cout, scanf e printf, utilizzano automaticamente proprio questi flussi per l’input e l’output. Quindi non è necessario aprire questi tre flussi perché sono già aperti quando il programma parte. 7.2 I file su disco In questo paragrafo vedremo la gestione standard dei file su disco nel linguaggio C attraverso le funzioni definite in <stdio.h>, quindi nelle dichiarazioni iniziali bisogna inserire: #include <stdio.h> Come abbiamo già detto queste funzioni sono disponibili sia nel linguaggio C base che nel linguaggio C++ e sono quelle che servono per risolvere i problemi assegnati nelle gare delle olimpiadi dell’informatica. Non verrà trattata in questo testo la gestione attraverso le classi istream, ostream e iostream presenti nella libreria del C++ (accessibili attraverso <iostream>). Se vogliamo leggere o scrivere da un file su disco bisogna prima di tutto creare una variabile puntatore ad una struttura particolare definita in <stdio.h> e chiamata FILE, per esempio: FILE *fleggi, *fscrivi; crea due puntatori che possono essere utilizzati per collegare il programma a due file diversi. Prima di utilizzare il flusso è necessario aprirlo e assegnare il valore al puntatore precedentemente definito, questo si fa con la funzione fopen(). Per esempio se vogliamo creare un flusso che collega il programma al file “prova1.txt” per l’input dei dati e un altro flusso per collegarlo al file “prova2.txt” per l’output è necessario utilizzare le seguenti istruzioni: fleggi = fopen(“prova1.txt”,”r”); fscrivi = fopen(“prova2.txt,”w”); La funzione fopen crea il flusso e restituisce un puntatore alla struttura di tipo FILE che serve per gestire il flusso. Richiede due parametri di tipo stringa: - il primo deve contenere il nome, ed eventualmente il percorso, per individuare il file nel disco; - il secondo indica il tipo di flusso che verrà creato, è possibile scegliere fra i seguenti valori: “r” il flusso serve solo per leggere i dati dal file, se il file non esiste viene segnalato un errore; la direzione dei bytes è solo quella dal file al programma; “w” il flusso serve solo per scrivere i dati nel file. Se il file esiste viene cancellato, se non esiste viene creato vuoto; la direzione dei bytes è solo quella dal programma al file; “a” il flusso può essere utilizzato solo per aggiungere nuovi dati nel file. Se il file esiste i dati precedenti in esso contenuti non verranno cambiati, i nuovi dati vengono aggiunti alla fine del file, se non esiste viene creato; “r+” si può sia leggere che scrivere nel file, la posizione iniziale da cui si legge o in cui si scrive è quella del primo byte del file, il contenuto del file non viene modificato; “w+” si può sia leggere che scrivere nel file, se il file esiste viene completamente cancellato; “a+” si può sia leggere che scrivere nel file, se il file esiste non viene cancellato e la posizione iniziale è impostata dopo l’ultimo byte contenuto nel file; Al secondo parametro (il tipo di apertura) si possono aggiungere altre lettere, per esempio la lettera ‘b’ per dire che il flusso è binario, per esempio “rb”(solo lettura binario) oppure “wb”. Normalmente i flussi vengono considerati nel linguaggio C come flussi di testo costituiti da 120 Corso sul Linguaggio C++ Edizione 2010 una serie di righe separate dal carattere ‘\n’, a meno che non si inserisca la lettera ‘b’ nel secondo parametro, nel qual caso diventano flussi binari costituiti da una serie di byte qualsiasi. Per leggere e scrivere in un file su disco si possono utilizzare le funzioni riportate nella Tabella 3 a pagina 63, precisamente per leggere o scrivere un byte alla volta abbiamo le funzioni: - int fgetc (FILE *stream) legge il prossimo carattere dal flusso fornito come parametro, il risultato è un numero intero da 0 a 255, se si verifica un errore o se il file è finito viene restituita la costante EOF che normalmente è = -1; - int fputc (int c, FILE *stream) converte c in un carattere e lo scrive nel file alla posizione corrente; il risultato è il carattere c oppure EOF se si verifica un errore; per leggere o scrivere un stringa di caratteri nel file: - int fputs (const char *s, FILE *stream) scrive la stringa s nel file senza aggiungere il carattere di terminazione o il carattere di nuova riga, vengono scritti solo i caratteri contenuti nella stringa, per esempio: fputs ("Ciao. ", stdout); fputs ("Sei ", stdout); fputs ("stanco?\n", stdout); scrive su video la frase Ciao. Sei stanco? e va a capo. - char * fgets (char *s, int count, FILE *stream) legge dal file esattamente count-1 caratteri oppure fino alla fine della riga incluso il carattere ‘\n’ se la linea contiene meno di count1 caratteri; inserisce i caratterei letti nella stringa s fornita come primo parametro; la stringa deve poter contenere almeno count caratteri perché alla fine viene inserito il carattere di terminazione ‘\0’; se è stata raggiunta la fine del file viene restitutito NULL altrimenti viene restituito un puntatore alla stringa s; se il file contiene un carattere ‘\0’ il risultato è imprevedibile; lettura e scrittura di numeri (input e output formattato): - int fprintf (FILE *stream, const char *schema, ...) funziona come printf (vedi pag.88) ma scrive nel flusso passato come primo parametro; Per esempio ricordando che %d si usa per scrivere numeri interi, %f per numeri con la virgola e %s per scrivere stringhe: fprintf(out, fprintf(out, fprintf(out, dec fprintf(out, // copia nel scluso fprintf(out, “%d\n”, num); // scrivere num nel file “out” e va a capo “%d %d”, a, b); // scrive a e b separati da 1 spazio “%10.3f”, a); // scrive a utilizzano 10 spazi con 3 cifre “%c/n”, c); //scrive il carattere c e va a capo file la stringa frase fino al carattere di terminazione e“%s”, frase); - int fscanf (FILE *stream, const char *template, ...) funziona coma scanf (vedi pag.89) ma legge dal flusso passato come primo parametro; Per esempio: 121 Corso sul Linguaggio C++ Edizione 2010 fscanf(in,"%d\n",&n); // legge un numero seguito da un a capo nella variab. n // legge due numeri separati da uno spazio nelle variabili a e b fscanf(in,”%d %d”, &a, &b); legge una serie di caratteri fino ad uno spazio o fino alla fine della riga e li mette nella stringa str che deve essere abbastanza capiente per contenerli: fscanf(in, “%s”, str); Quando si legge e scrive in un file il sistema memorizza in una variabile interna di tipo long int la posizione corrente nel file. Ogni volta che si legge o scrive questa variabile viene automaticamente aggiornata. E’ possibile cambiare la posizione di lettura o scrittura nel file (accesso random) utilizzando la funzione fseek(): - int fseek (FILE *stream, int spostamento, int tipo) cambia la posizione corrente nel flusso fornito come primo parametro. La nuova posizione dipende da spostamento che dice di quanti byte bisogna spostarsi e da tipo. Il valore di quest’ultimo parametro deve essere una delle costanti SEEK_SET, SEEK_CUR, SEEK_END per indicare se lo spostamento è relativo all’inizio del file, alla posizione corrente nel file o alla fine del file. - long int ftell(FILE *stream) restituisce l’attuale posizione all’interno del file (se c’è un errore viene restituito -1); - size_t fread (void *data, size_t size, size_t count, FILE *stream) questa funzione legge count oggetti tutti di dimensione size dal file e li inserisce nel buffer puntato da data. Serve per leggere dati di qualsiasi tipo da un file binario. Si può utilizzare per leggere i file di record. - size_t fwrite (const void *data, size_t size, size_t count, FILE *stream) serve per scrivere count oggetti di dimensione size nel file prendendoli dal buffer data. Permette l’accesso diretto ai file binari per la scrittura. Attenzione! Quando si esegue il debug di un programma che utilizza i file su disco, con il Dev C versione 4, bisogna sapere che la directory corrente durante il debug è la cartella bin contenuta nella cartella di installazione del Dev C e, quindi, se non si utilizzano i percorsi assoluti, bisogna mettere una copia dei file in input in questa cartella altrimenti verrà generato un errore dopo l’apertura del file. Esempio 1 copiare il contenuto di un file in un altro file. Supponiamo di voler copiare un file qualsiasi in un altro con nome “prova.txt”. Il file iniziale deve esistere prima di avviare il programma. Possiamo crearlo nella stessa cartella del programma con Blocco note inserendoci frasi a caso. Il programma completo potrebbe essere il seguente: #include <stdio.h> #include <stdlib.h> /* programma "copiafile" copia un file con nome qualsiasi dato in input in un altro di nome "prova.txt" */ int main(){ FILE *fleggi, *fscrivi; char c, nomef[40]; // chiede il nome del file da copiare printf("Inserisci il nome del file da copiare:"); gets(nomef); fleggi = fopen(nomef,"r"); // apre il file if (fleggi == NULL) 122 Corso sul Linguaggio C++ Edizione 2010 // scrive un messaggio di errore se non riesce ad aprire il file printf("Errore! impossibile aprire il file!\n"); else { fscrivi = fopen("prova.txt","w"); // apre la copia while ((c=fgetc(fleggi)) != EOF) fputc(c,fscrivi); printf("Il file e' stato copiato!\n"); } system("PAUSE"); return 0; } Dopo l’esecuzione dovrebbe apparire, nella cartella dove si trova il programma, il file “prova.txt” contenente una copia esatta del file il cui nome è stato inserito in input. I nomi dei file passati come parametri alla funzione fopen possono contenere un percorso assoluto o relativo. Se il nome comincia con la lettera del drive o con “\” allora si tratta di percorso assoluto altrimenti è un percorso relativo alla cartella corrente che è quella che contiene il programma, per esempio: “c:\temp\prova.txt” percorso assoluto – drive c, sottocartella temp, cerca il file prova.txt “\prog\cpp\prova.txt” percorso assoluto nel drive corrente cerca il file nella cartella \prog\cpp “prova.txt” percorso relativo, in questo caso non si sposta dalla cartella del programma “tmp\prova.txt “ va nella sottocartella tmp della cartella corrente e lì cerca prova.txt Attenzione, se si esegue il programma utilizzando il debug la cartella corrente non è più quella che contiene l’eseguibile ma è la cartella bin del percorso di installazione del Dec C++! Esempio2 – Stringhe strane. Problema della selezione regionale 2002. Dato un insieme di stringhe di lunghezza 6 formate dai caratteri ‘A’, ‘B’, ‘C’ o ‘D’ bisogna distinguere le stringhe “buone” da quelle “cattive”. Una stringa è buona se soddisfa tutti i seguenti requisiti: 1. Non contiene una sequenza di 3 o piu’ caratteri che sono uguali (ad esempio le stringhe contenenti AAA o BBBB sono “cattive”) 2. Non contiene una sequenza di 3 o piu’ caratteri consecutivi che sono in ordine crescente ( ad esempio le stringhe che contengono ABC o ACD sono “cattive”) Se una stringa non rispetta anche uno solo dei due requisiti precedenti è “cattiva”. File di Input Il file di input di nome input.txt ha il seguente formato: • la prima riga contiene un numero intero N, che rappresenta il numero di stringhe; • le successive N righe contengono ciascuna una stringa di lunghezza 6 formata dai caratteri ‘A’, ‘B’, ‘C’ o ‘D’. Il primo carattere della riga contiene il primo carattere della stringa e i caratteri della stringa sono consecutivi (cioè non sono separati da spazi). File di Output Il file di output di nome output.txt è costituito da N righe, una riga per ciascuna stringa data in input; l’ultima riga termina con un a-capo. Ogni riga è costituita esattamente da 1 carattere In particolare, l’i-esimo carattere è • ‘C’ se la stringa i-esima è “cattiva” • ‘B’ se la stringa i-esima è “buona” Assunzioni • Il file di input non contiene altri caratteri oltre a quelli indicati nel testo. • Il file di output non deve contenere altri caratteri oltre a quelli contenuti nel testo; in particolare non ci devono essere linee di separazione fra le linee di output. • Il programma non deve produrre alcun altro input/output oltre a quelli indicati: deve limitarsi a leggere il file di input e a scrivere i risultati sul file di output. 123 Corso sul Linguaggio C++ Edizione 2010 Esempio 1 File di input File di output 8 AABBCC ABABAC AAABBD AABBBB ABBABC DCBDAB ADDBBA AABAAB B B C C C B B B Soluzione Con un ciclo leggiamo tutte le stringhe nel file i input e per ognuna di esse controlliamo tutti i caratteri a partire dal secondo al sesto nel seguente modo: 1) prima di iniziare il controllo supponiamo che la stringa sia “buona” e mettiamo ‘B’ in ris; inizializziamo anche il contatore dei caratteri uguali con 1 (cu = 1) e quello dei caratteri crescenti con1 (scc=1); 2) cominciamo il ciclo per controllare i caratteri dal secondo al sesto 3) se il carattere è uguale al precedente incrementiamo il contatore dei caratteri uguali (cu++) altrimenti mettiamo 1 in questo contatore. 4) se il carattere è > del precedente incrementiamo il contatore della sequenza caratteri crescenti (scc++) altrimenti mettiamo 1 in questo contatore; 5) se cu>2 o scc >2 allora la stringa è cattiva e possiamo terminare il controllo dei caratteri; 6) passiamo al carattere successivo. 7) dopo aver terminato il controllo scriviamo ris nel file di output Il programma completo è il seguente: #include <stdio.h> // programma stringhe.cpp - Assegnato alla selezione regionale 2002 int main(){ int cu, scc, n, i, j; char str[10], ris; FILE *in, *out; in = fopen("input.txt","r"); out = fopen("output.txt", "w"); fscanf(in,"%d\n",&n); for (i=0; i<n; i++) { fscanf(in,"%s",str); // legge una nuova stringa /* si poteva usare anche fgets() per leggere la stringa fgets(str, 10, in); in questo caso vengono letti anche gli spazi fin a massimo 9 caratteri compreso il carattere ‘\n’ (con fscanf() la lettura si ferma al primo spazio oppure al carattere ‘\n’) */ ris = 'B'; cu = 1; scc=1; for (j = 1; j < 7; j++) { if (str[j] == str[j-1]) cu++; else cu=1; if (str[j] > str[j-1]) scc++; else scc=1; if (cu>2 || scc >2) {ris = 'C'; break;} } fprintf(out,"%c\n",ris); } fclose(in); fclose(out); 124 Corso sul Linguaggio C++ Edizione 2010 return 0; } 125 8 Capitolo Corso sul Linguaggio C++ Edizione 2010 8. Raccolta test delle olimpiadi informatica (solo test linguaggio C) Le olimpiadi internazionali di informatica sono una gara patrocinata dall'UNESCO a cui partecipano circa 70 nazioni in tutto il mondo. In Italia, il ministero dell'istruzione, ha affidato all'AICA il compito di gestire e organizzare la partecipazione del nostro paese. Alla gara possono partecipare tutti i giovani con le seguenti caratteristiche: • essere iscritto ad uno dei primi quattro anni della scuola superiore; • non aver ancora compiuto 18 anni oppure averli compiuti dopo il 30 giugno dell’anno in corso; • essere disponibile, qualora superi le prime prove di selezione, a frequentare i corsi di formazione che si terranno prima della competizione internazionale; • essere disponibile, qualora superi l'ultima selezione, a recarsi nella nazione scelta per ospitare la competizione finale nel periodo stabilito per detta gara. Ogni anno, più o meno a metà novembre, le scuole che partecipano operano una prima selezione somministrando un test preparato a livello nazionale dall’AICA, costituito per metà da domande di logica e per l’altra metà da domande su un linguaggio a scelta tra il Pascal e il C++. I due alunni che, in ogni scuola, ottengono il punteggio più alto in questa prova (il secondo solo se il suo punteggio è superiore alla media nazionale) partecipano alla selezione regionale che si svolge circa ad aprile dell’anno successivo. La prova della selezione regionale consiste nel risolvere al computer una serie di problemi di programmazione utilizzando sempre uno dei due linguaggi di programmazione ammessi: il C++ o il Pascal. I migliori in ogni regione più i migliori a livello nazionale nella prova regionale faranno parte del gruppo di 60 alunni che partecipano alla selezione nazionale. In base a quest’ultima prova vengono scelti i probabili olimpici che parteciperanno alla prova internazionale. Gli alunni che hanno seguito fin quì questo corso dovrebbero aver raggiunto un livello di conoscenza del linguaggio C++ sufficiente per rispondere correttamente ai test della selezione scolastica delle olimpiadi dell’informatica. Si tratta di un livello elevato e quindi per raggiungerlo non è sufficiente aver ascoltato le lezioni con attenzione, è necessario anche passione e un lavoro autonomo di sperimentazione a casa. Ho riportato in questo capitolo una piccola raccolta dei test di programmazione per il linguaggio C++, somministrati nelle selezioni scolastiche delle ultime olimpiadi italiane dell’informatica e alcuni problemi assegnati nelle selezioni regionali. Mi aspetto che tutti provino a risolvere almeno quelli delle selezioni scolastiche e, nel caso non ci riescano, vadano a 126 Corso sul Linguaggio C++ Edizione 2010 ristudiarsi i capitoli del libro relativi alle nozioni dimenticate. Il test finale del corso verrà preparato sulla falsariga di questi test. L’elenco completo dei test somministrati alle selezioni scolastiche e regionali delle olimpiadi dell’informatica italiane si può trovare su internet all’indirizzo: http://www.olimpiadi-informatica.it/. 8.1 Selezione scolastica I test completi comprendono alcune domande di logica e altre di programmazione, per aumentare la selezione viene concesso un tempo di 60 minuti che è in genere insufficiente per rispondere a tutte le domande. In questo libro ho inserito solo le domande di programmazione relative al linguaggio C++ per risolvere le quali il tempo concesso è di circa 30 minuti. Anno 2008 (test somministrato il 23/11/2007) Domanda N°1 La risposta esatta vale 3 punti. Si consideri la seguente funzione int ES1( int a, int b ) { int j; int k = 1; int p = 0; while ( k <= a ) {k++; j = 0; while ( j < b ) {j++; p = p+j;} } return 2* p / b; } Dire cosa calcola la funzione nell'ipotesi che a e b siano sempre positivi e che il programma non generi mai un “overflow” durante le operazioni aritmetiche. Risposte: a) a*b b)a*(b+1) c)(a+1)*(b-1) d) nessuna delle precedenti Domanda N°2 La risposta esatta vale 3 punti. Si consideri il seguente programma: #include <stdio.h> #define DMAX 5 void ES2(int M[][DMAX], int R, int C){ int I, K; int V[DMAX]; for( I = 0; I < R; I++) V[I] = M[I][0]; for( I = 0; I < C-1; I++) for( K = 0; K < R; K++) M[K][I] = M[K][I+1]; for( I = 0; I < R; I++) M[I][C-1] = V[I]; } int main (){ int M[DMAX][DMAX]; int I; M[0][0] = 1; M[0][1] = 2; M[0][2] = 3; M[0][3] = 4; M[0][4] = 5; M[1][0] = 1; M[1][1] = 2; M[1][2] = 3; M[1][3] = 4; M[1][4] = 5; M[2][0] = 1; M[2][1] = 2; M[2][2] = 3; M[2][3] = 4; M[2][4] = 5; M[3][0] = 1; M[3][1] = 2; M[3][2] = 3; M[3][3] = 4; M[3][4] = 5; M[4][0] = 1; M[4][1] = 2; M[4][2] = 3; M[4][3] = 4; M[4][4] = 5; for ( I = 0; I < 3; I++) ES2( M, 5, 5 ); return 0; } La matrice M inizialmente contiene tutti 1 nella prima colonna, tutti 2 nella seconda colonna e così via. Indicare il contenuto della matrice Mal termine del programma. Risposte: a) 4 5 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 1 2 3 127 Corso sul Linguaggio C++ Edizione 2010 b) 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 c) 5 4 3 2 1 5 4 3 2 1 5 4 3 2 1 5 4 3 2 1 5 4 3 2 1 d) 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 5 5 5 5 5 Domanda N°3 La risposta esatta vale 1 punto. Si consideri la seguente funzione int ES3( int x ){ if ( x <= 1 ) return 0; else return 1 + ES3( x/2 ); } Dire cosa restituisce la chiamata ES3( 10 ). Risposte: a)1 b)2 c) 3 d) nessuna delle precedenti Domanda N°4 La risposta esatta vale 2 punti. Si consideri la seguente funzione int ES4( int x ){ if (x/10 == 0) return x; else return x%10 + ES4( x/10 ); } Dire cosa restituisce l'invocazione ES4( ES4( 731 ) ). Risposte: a) 2 b) 9 c) 36 d) nessuna delle precedenti Domanda N°5 La risposta esatta vale 2 punti. Si consideri la procedura void ES5( int n, int d ){ while ( n != 1 ) if( n % d == 0 ){printf( "%d ", d );n = n/d;} else ++d; } Dire cosa stampa su schermo la chiamata ES5( 210, 2 ). Risposte: a) 2 b) 2 3 4 5 6 7 c) 2 3 5 7 8 d)nessuna delle precedenti Domanda N°6 La risposta esatta vale 2 punti. Si consideri la funzione int ES6( int x, int y ){ if ( x == 0 && y == 0 ) return 0; else if ( x%10 < y%10 ) return ES6( x, y/10 ); else if ( x%10 > y%10 ) return ES6( x/10, y ); else return x%10 + 10 * ES6( x/10, y/10 ); } Dire cosa restituisce la chiamata ES6( 3467, 5678 ). Risposte: a) 18 b) 67 c) 2367 d) nessuna delle precedenti Domanda N°7 La risposta esatta vale 3 punti. Si considerino le seguenti procedure void mf( int n, int k, int t[], int e){ 128 Corso sul Linguaggio C++ Edizione 2010 int i; if ( e < n-1 ) for ( i = t[e]+1; i <= k; i++ ){t[e+1] = i; mf( n, k, t, e+1 );} } void ES7( int n, int k ){ int t[10]; int i; for( i = 0 ; i < n; i++ ){t[0] = i+1; mf( n, k, t, 0);} for ( i = 0; i < n; i++ ) printf( "%d ", t[i] ); printf( "\n" ); } Si assuma che sempre si verifichi n <= 10. Dire cosa stampa su schermo la chiamata ES7( 3, 4 ). Risposte: a) 1 2 4 b) 2 3 4 c) 3 4 4 d) nessuna delle precedenti Domanda N°8 La risposta esatta vale 2 punti. Si consideri la funzione int ES8( int m){ int a, b, c, g, t; a = m * m; b = a/2; c = 4 * b * a; t = m; g = m + a + t + t + a; while ( g < b + a + c + c + a ) { g = g + a + t + t + a; c = t + a + t + a; } return g; } Dire cosa restituisce la chiamata ES8( 2 ). Risposte: a) 19 b) 32 c) 38 d) nessuna delle precedenti Domanda N°9 La risposta esatta vale 2 punti. Si considerino le funzioni mutuamente ricorsive int foo( int n ); int ES9( int n ){ if ( n%2 == 1 ) return foo( n - 3 ); else return n; } int foo(int n){ if ( n%2 == 0 ) return ES9( 2*n ); else return n; } Dire cosa restituisce la chiamata ES9( 51 ). Risposte: a) 48 b) 96 c) 102 d) nessuna delle precedenti Anno 2007 (test somministrato il 16/11/2006) 1) La risposta esatta vale 1 punto. Quale dei seguenti valori di a e b produce il valore vero per la condizione: (a > 0) and ((b < 0) or (b > 1)) Risposte: a) a = 5; b = 0 b) a = 5; b = 2 c) a = -1; b = 5 d) a = 1; b = 1 ======================================================================= 2) La risposta esatta vale 2 punti. Si consideri la seguente funzione: #define N 5 int vett1[N]; int vett2[N]; void calcola() { int temp, i; for (i = 0; i < N; i++) vett1[i] = N - i - 1; for (i = 0; i < N; i++) {temp = vett1[i]; vett2[temp] = i;} } 129 Corso sul Linguaggio C++ Edizione 2010 Indicare il contenuto del vettore vett2 al termine dell’esecuzione della funzione “calcola”. Risposte: a)[0,1,2,3,4] b)[3,1,4,2,0] c)[4,3,2,1,0] d)nessuna delle precedenti ======================================================================= 3) Ogni risposta esatta vale 2 punti; punteggio totale da -4 a 8 punti. Considerate le seguenti quattro funzioni, con argomento N (un intero non negativo). Ciascuna di esse calcola un qualche valore scelto tra: N, 2N , N2, N3, 2N , N! Dovete stabilire qual e il valore calcolato da ogni funzione (potrebbero esserci doppioni: più funzioni potrebbero calcolare lo stesso valore; notate inoltre che N! denota il fattoriale di N ed e definito come N! = 1x2 x 3 …. x N; quindi 2!= 2, 3! = 6 e 5!= 120; inoltre si assume che 0! = 1). int A(int N) { if (N > 0) return 2 * A(N - 1) ; else return 1; } int B(int N) { if (N > 0) return B(N - 1) + 2 * N - 1; else return 0; } int C(int N) { int i, R; R = 1; for (i = 0; i < N; i++) R = R + R; return R; } int D(int N) { int i, R; R = 1; for (i = 1; i <= N; i++) R = R * i; return R; } Risposte: A: ……… B: ……… C: ……… D: ……… ======================================================================= 4) La risposta esatta vale 3 punti. Si consideri la seguente funzione: int f(int n) { if (n == 0) return 0; if ( (n%2) == 0) return 2 * f(n/2); else return 2 * f(n/2) + 2; } Indicare qual e’ il valore restituito dall’ invocazione f(f(2)). Risposte: a) 2 b) 4 c) 6 d) nessuna delle precedenti ======================================================================= 5) La risposta esatta vale 3 punti. Cosa stampa il seguente programma, con input 5? int main(){ float x,y,z; x= 0; y= 0; scanf("%f", &z); do { x = x + 1; y = y + 1/x;} while (!(y > z)); printf ("%f \n",x); } Risposte: a)5 b)12 c)25 d)nessuna delle precedenti ======================================================================= 6) La risposta esatta vale 3 punti. Cosa stampa il seguente programma? #include <stdio.h> #include <stdlib.h> int calcola(int n) { 130 Corso sul Linguaggio C++ Edizione 2010 if(n == 1) {return 1;} else if(n == 2) {return 3;}else if(n==3) {return n + calcola(n-1);} else {return n + calcola(n-1) + calcola(n-2);} } int main(){ printf("%d\n",calcola(6)); return 0; } Risposte: a) 41 b) 45 c) 49 d) nessuna delle precedenti ======================================================================= 7) La risposta esatta vale 5 punti. Cosa stampa il seguente programma? #include <stdio.h> #include <stdlib.h> int mistero(int m, int n) { if ( m == 0 ) return n; else if ( n==0 ) return mistero( m-1, 1 ); else return mistero( n-1,mistero( m-1, n-1 ) ); } int main(){ printf( "%d %d %d %d\n", mistero(0,3), mistero(1,3), mistero(2,3), mistero(3,3)); return 0; } Risposte: a) 3 1 2 0 b) 3 1 1 0 c) 3 1 0 1 d) nessuna delle precedenti Anno 2006 (test somministrato il 18/11/2005) 1) La risposta esatta vale 1 punto. Dopo l'esecuzione della seguente porzione di codice: #include <stdio.h> void funzione(int *a,int b) { int temp=*a; *a=b; b=temp; }; main(){ int a=2; int b=5; funzione(&a,b); } Quanto valgono a e b? Risposta aperta 2) La risposta esatta vale 1 punto. Si consideri la seguente funzione. int funzione ( ){ int contatore = 0; int sum = 0; 131 Corso sul Linguaggio C++ Edizione 2010 while (contatore <= 4){ contatore = contatore + 1; sum = sum + contatore; } return sum; } Quale valore restituisce la funzione? Risposte: a) 10 b) 15 c) 16 d) Nessuna delle risposte precedenti 3) La risposta esatta vale 2 punti. Si consideri la seguente funzione: void calcola(int* vett, int n) { int x,y; int i, j; for ( i=0; i<n; i++) { y = vett[0]; for (j=0; j<n-1; j++) { x = vett[j+1]; vett[j+1] = y; y = x; } vett[0] = y; } } Assumendo che vett contenga il vettore [10,9,8,7,6,5,4,3,2,1], quali sono gli elementi di vett dopo l'esecuzione di calcola (usando 10 come secondo parametro)? Risposte: a) [1,2,3,4,5,6,7,8,9,10] b) [10,9,8,7,6,5,4,3,2,1] c) [1,3,5,7,9,2,4,6,8,10] d) nessuna delle precedenti 4) La risposta esatta vale 3 punti. Cosa stampa il seguente programma? #include <stdio.h> int funzione(int arr[], int dim) { int i = 0; int t=0; if(dim % 2==1) { while(i<dim){ t=arr[i]; arr[i]=arr[dim-i-1]; arr[dim-i-1]=t; i=i+1; }; } else { 132 Corso sul Linguaggio C++ Edizione 2010 while(i<dim/2){ t=arr[i]; arr[i]=arr[dim-i-1]; arr[dim-i-1]=t; i=i+1; }; } return arr[0]; }; main() { int arr1[10]={1,2,3,4,5,6,7,8,9,10}; int arr2[11]={1,2,3,4,5,6,7,8,9,10,11}; int a=funzione(arr1,10); int b=funzione(arr2,11); printf ("a=%d,b=%d\n",a,b); } Risposte: a) a=10,b=1 b) a=1,b=1 c) a=10,b=11 d) a=1,b=11 ================================================================= 5) La risposta esatta vale 3 punti. Cosa stampa il seguente programma? #include <stdio.h> int funzione1(int arr[]) { int i = 1; while(arr[i] != -1) i = i * 2; return i; }; int funzione2(int arr[], int f, int k) { int i = 0; int m; while(i <= f) { m = (i + f) / 2; if(arr[m] == k) return m; if((arr[m] == -1) || (arr[m] > k)) f = m - 1; else i = m + 1; }; return -1; }; main() { int arr[10]={1,2,4,8,-1,-1,-1,-1,-1,-1}; int f=funzione1(arr); int a=funzione2(arr,f,4); int b=funzione2(arr,f,7); printf ("a=%d,b=%d\n",a,b); } 133 Corso sul Linguaggio C++ Edizione 2010 Risposte: a) a=2,b=4 b) a=2,b=-1 c) a=-1,b=4 d) a=-1,b=-1 6) La risposta esatta vale 3 punti. Data la seguente funzione che inizializza i valori di un array bi-dimensionale "matrice": #define N 5 void inizializza( ){ int matrice[N][N]; int riga, colonna; for ( riga = 0; riga < N; riga++ ) { for ( colonna = 0; colonna < N; colonna++ ) { if ( riga == colonna ) matrice[riga][colonna] = 1; else if ( riga + colonna == N - 1 ) matrice[riga][colonna] = 1; else if ( riga < colonna ) matrice[riga][colonna] = 0; else matrice[riga][colonna] = matrice[colonna][riga]; } } for ( riga = 0; riga < N; riga++ ) { for ( colonna = 0; colonna < N; colonna++ ) printf( "%d ", matrice[riga][colonna] ); printf( "\n" ); } } Indicare quale tra le seguenti configurazioni vengono stampate dalla procedura "inizializza". Risposte: a) 1 1 0 1 0 0 0 0 1 1 1 0 1 0 1 1 0 0 1 1 1 0 0 0 1 b) 1 0 0 1 0 0 0 1 1 0 0 0 1 0 0 0 1 0 1 0 1 0 0 0 1 c) 1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 134 Corso sul Linguaggio C++ Edizione 2010 0 1 0 0 0 1 0 0 0 0 d) 1 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 7) La risposta esatta vale 3 punti. Sia data la seguente funzione ricorsiva: int mistero(int m, int n) { if ( m == 0 ) return n; else if ( n==0 ) return mistero( m-1, 1 ); else return mistero( mistero( m-1, n-1 ), n-1 ); } Calcolare quale tra le seguenti risposte corrisponde ai valori restituiti invocando: printf( "%d %d %d %d\n", mistero(0,3), mistero(1,3), mistero(2,3), mistero(3,3) ); Risposte: a) 3 1 2 0 b) 3 2 1 0 c) 0 1 2 3 d) Nessuna delle risposte precedenti Anno 2005 1) La risposta esatta vale 6 punti. Si consideri il seguente frammento di programma. int calcola(int vett[], int n){ int i,y,x; y=0; x=vett[0]; for (i=0; i < n-1; i++) if (x < vett[i+1]){ y=vett[i+1]-x+y; x=vett[i+1]; } else if (x-y > vett[i+1]) y=x-vett[i+1]; return y; } 135 Corso sul Linguaggio C++ Edizione 2010 Dire che cosa restituisce la funzione "calcola" assumendo che venga invocata passando un vettore di lunghezza n con n maggiore di 2. Risposta aperta ================================================================ 2) La risposta esatta vale 4 punti. Si consideri il seguente frammento di programma. bool verifica(int vett[], int n){ int i,j=0; int appoggio[n]; for (i=0; i < n; i++) if (j==0){ appoggio[j]=vett[i]; j++; } else if (appoggio[j-1] ==vett[i]) j--; else { appoggio[j]=vett[i]; j++; } if (j==0) return true; else return false; } Si considerino i seguenti tre vettori {5,8,1,1,2,4,4,8,8,2,3,9,7,7,9,3,8,2,2,5} {6,7,9,9,5,4,3,3,4,5,7,6,2,2,2,2,4,5,4,5} {3,3,3,2,2,3,1,5,5,1,3,5,7,6,6,7,5,3,1,1}. Si supponga di invocare tre volte la funzione verifica passando nell'ordine i tre vettori (assumendo che il secondo parametro sia 20 in tutti e tre i casi). Quali tre valori saranno restituiti nell'ordine dalle tre invocazioni? Risposte: a _ true-false-true b _ true-true-true c _ true-false-false d _ nessuna delle precedenti ================================================================ 3) La risposta esatta vale 6 punti. Si consideri il seguente frammento di programma. int B(int n); 136 Corso sul Linguaggio C++ Edizione 2010 int A(int n){ int m; if (n==0) return 0; else if (n%2 == 0) return n+B(n); else return B(n); } int B(int n){ int m; if (n==0) return 0; else if (n%2 == 1) return n+A(n-1); else return A(n-1); } Dire che cosa calcola la funzione A assumendo che venga invocata passando un intero positivo. Risposta aperta 4) La risposta esatta vale 4 punti. Si consideri la seguente funzione: int calcola(int n) { if(n == 1) { return 1; } else if(n == 2) { return n * calcola(n-1); } else { return n * calcola(n-1) * calcola(n-2); } } Quale valore restituisce se viene richiamata con parametro 5? Risposte: a _ 120 b _ 1400 c _ 414720 d _ nessuna delle precedenti ================================================================== 5) La risposta esatta vale 4 punti. Si consideri la seguente funzione: 137 Corso sul Linguaggio C++ Edizione 2010 bool calcola(int numero, int* vettore, int i, int j) { int m = (i + j) / 2; int off = vettore[m] - numero; if(off == 0) { return true; } else if(i == j) { return false; } else if(off > 0) { return calcola(numero, vettore, i, m - 1); } else { return calcola(numero, vettore, m + 1, j); } } Determinare quale fra i seguenti problemi è risolto dall'algoritmo implementato: Risposte: a _ determinare se il parametro numero si trova all'interno dell'array vettore fra gli indici i e j b _ determinare se il parametro numero si trova all'interno dell'array vettore fra gli indici i e j quando i valori in vettore sono ordinati dal più piccolo al più grande c _ determinare se il parametro numero si trova all'interno dell'array vettore fra gli indici i e j quando i valori in vettore sono ordinati dal più grande al più piccolo d _ nessuna delle precedenti ================================================================== 6) La risposta esatta vale 4 punti. Si consideri la seguente funzione: int trova(int bersaglio, int *valori) { int contatore = 0; while(valori[contatore++] != bersaglio); return contatore-1; } Essa serve a determinare l'indice in cui si trova un certo valore (rappresentato dal parametro bersaglio) in un vettore (rappresentato dal parametro valori). La funzione, però, funziona sempre solo se vale un vincolo specifico rispetto ai dati in ingresso, quale? Risposta aperta Anno 2004 1) La risposta esatta vale 4 punti. Si consideri il seguente frammento di programma: void quack(int *a, int *b, int *c, int l){ int i = 0, j = 0, k = 0; 138 Corso sul Linguaggio C++ Edizione 2010 while(k < 2*l) { if((a[i] < b[j] && i < l) || j == l) { c[k] = a[i]; i++; } else { c[k] = b[j]; j++; } k++; } } Che problema risolve la procedura/funzione quack? Risposte: a _ riempie c con gli elementi di a e b ordinati in modo crescente indipendentemente dall'ordine degli elementi in aeb b _ riempie c con gli elementi di a e b ordinati in modo crescente se gli elementi in a e b sono ordinati in modo crescente c _ riempie c in modo da riportare tutti i valori negativi di a all'inizio di c e quelli positivi di b alla fine di c d _ nessuna delle precedenti ======================================================== 2) La risposta esatta vale 6 punti. Si consideri il seguente frammento di programma: void bang(int *a, int l){ int i = 0, t = 0, loop = 1; while(loop) { if(a[i] > 0) { a[t] = a[i]; t++; } if(i < l-1) i++; else loop = 0; } } Si supponga che si richiami bang passando come primo paramento il seguente vettore: 33 3 -22 -18 -30 27 1 -42 -14 -19 -1 23 -40 26 15 30 6 40 -12 -34 e come secondo parametro il numero 20. Scrivere l'insieme dei valori in a al termine dell'esecuzione di bang. Risposta aperta ======================================================= 3) La risposta esatta vale 4 punti. Si considerino le due seguenti funzioni: int A(int n){ if (n > 0) return n+A(n-1); } else return 0; int B(int n){ return (n*(n+1)/2); } Quale delle seguenti affermazioni è vera? 139 Corso sul Linguaggio C++ Edizione 2010 Risposte: a _ la funzione A calcola il fattoriale di un numero mentre la funzione B calcola la sommatoria di tutti i numeri compresi fra 1 ed n. b _ sia la funzione A che la funzione B calcolano la sommatoria di tutti i numeri compresi fra 1 ed n, assumendo n maggiore o uguale a 1. c _ la funzione A e la funzione B calcolano esattamente la stessa funzione. d _ nessuna delle precedenti affermazioni è vera. ======================================================= 4) La risposta esatta vale 4 punti. Si consideri la seguente funzione conta che prende in input un vettore di numeri di lunghezza n (assumendo che n sia compreso fra 2 e 100): int conta(int vett[], int n){ int i, k=0; int j = 1; for (i = 0; i < n-1; i++) if (vett[i] < vett[i+1]) j++; else{ if (k < j) k = j; j = 1; } return k; } Quale delle seguenti affermazioni è vera? Risposte: a _ la funzione restituisce il numero di interi presenti nel vettore b _ la funzione restituisce il massimo valore presente nel vettore c _ la funzione restituisce la lunghezza della più lunga sottosequenza ordinata (in modo strettamente crescente) all'interno del vettore d _ nessuna delle precedenti affermazioni è vera ======================================================= 5) La risposta esatta vale 6 punti. Si consideri la seguente funzione: int A(int n, int m){ if (n == 0) return 1; else if (n%2 == 0) return A(n/2,m)*A(n/2,m); else return m*A(n-1,m); } Dire quale sarà il valore tornato dalle chiamate A(3,2) A(4,3) A(5,4) 140 Corso sul Linguaggio C++ Edizione 2010 Risposta aperta 6) La risposta esatta vale 6 punti. Si consideri la seguente funzione: int A(int a, int b){ int p = 0; while (a > 0) { if (a%2 == 1) p = b+p; a = a/2; b = 2*b; } return p; } Dire quale sarà il valore tornato dalle chiamate A(4,3) A(7,4) A(35,25) Risposta aperta 8.2 Selezione regionale La selezione regionale si volge in modo molto diverso rispetto alla selezione scolastica, tipicamente la procedura è la seguente: - ad ogni alunno viene assegnato un computer con gli ambienti Dev C e Dev Pascal installati; - viene data circa mezzora per prendere confidenza ed esaminare la macchina, poi i docenti accompagnatori vengono fatti uscire, agli alunni viene consegnata la traccia che in genere consiste in tre problemi di difficoltà diversa (difficoltà indicata nella traccia con un numero intero da 1 a 4), per risolvere i quali vengono concesse tre ore; - non è possibile portare con se né libri né appunti né dischetti, pen drive, telefonini o altri apparecchi elettronici; - gli alunni devono realizzare i programmi al computer memorizzandoli in una cartella, il nome da assegnare al programma è indicato nella traccia (il nome breve del problema) e li consegnano facendo l’upload della cartella o del sorgente e dell’eseguibile su un server della scuola che ospita la competizione; - i programmi devono leggere i dati da un file di testo di nome “input.txt” e scrivere i risultati in un file, sempre di testo, di nome “output.txt”, senza specificare il percorso (quindi per provare i programmi bisogna creare il file input.txt con un editor di testo come notepad e metterlo nella stessa cartella del programma). Non devono leggere e scrivere niente su video pena la loro non valutazione. La correzione viene fatta automaticamente da un programma apposito che copia l’eseguibile in una cartella, ci mette un file di input, manda in esecuzione il programma e controlla se il file di output prodotto è corretto; ripete questa operazione per 10 volte, con file di input diversi, preparati appositamente dai responsabili e non noti a priore (probabilmente i programmi verranno testati sia con dati in input tipici che con quelli più estremi). Ad ogni programma sorgente realizzato e non completamente banale, viene assegnato, come minimo, un punteggio pari al suo coefficiente di difficoltà. Detto D il coefficiente di difficoltà, se oltre al sorgente c’è anche l’eseguibile corrispondente (ovvero se non ci sono errori di 141 Corso sul Linguaggio C++ Edizione 2010 sintassi) il punteggio sarà 2*D. Inoltre per ognuna delle 10 prove effettuate, se l’output risultante è corretto, vengono aggiunti D punti, quindi il punteggio massimo raggiungibile per la soluzione di un problema di difficoltà D è 12 * D. Per esempio se la difficoltà di un problema è 2 il punteggio massimo che si può ottenere per la sua soluzione è 24. Nelle scorse edizioni sono stati pochi quelli che, nelle tre ore concesse, sono riusciti a risolvere più di un problema. In genere se si riescono a risolvere correttamente almeno due dei tre problemi si ha una certa garanzia di arrivare alla selezione nazionale. Nel seguito propongo alcune delle tracce degli anni scorsi, le altre si possono trovare al sito www.olimpiadi-informatica.it, mentre nel sito http://allenamenti.olimpiadi-informatica.it/ è possibile trovare, oltre alle tracce, anche varie soluzioni sia dei problemi delle regionali che di quelli delle olimpiadi nazionali e internazionali. Prove regionali 2001 – il cassiere Camillo (CAM) Livello di difficoltà D=3 È venerdì, e il cassiere Camillo ha davanti a sé una lunga fila di clienti della sua banca venuti a ritirare contante per il weekend. Per fare presto, Camillo decide di usare per ogni cliente il numero minimo possibile di banconote. Sapreste scrivere un programma per evitargli il mal di testa, considerato che ha a disposizione banconote da 100.000, 10.000, 5.000, 2.000 e 1.000 in quantità illimitata e che l'entità di ogni prelievo è un multiplo di 1.000 lire? Dati in input Il file input.txt contiene l'importo del prelievo. Il file è costituito da un'unica riga di testo, contenente un numero (senza puntini o virgole che raggruppano le cifre a tre a tre!). Dati in output Il programma, dopo aver letto il file di input, deve calcolare il numero di banconote necessario per ognuno dei tagli disponibili, e scriverlo su un file di nome output.txt. Più precisamente, il file output.txt deve contenere cinque righe, che corrispondono (in ordine, dalla prima all'ultima) alle banconote da 100.000, 10.000, 5.000, 2.000 e 1.000. Ogni riga deve contenere un unico numero intero, che rappresenta il numero di banconote di quel taglio necessarie. Assunzioni 1. Il file di input non contiene altri caratteri oltre a quelli precisati. 2. L'entità del prelievo è in ogni caso inferiore a 1 miliardo di lire. 3. Importante! Il programma non deve scrivere nulla sul video, e non deve interagire con l'utente. Deve solo leggere il file di input e scrivere il file di output. 4. L'esecuzione del programma deve terminare entro 5 secondi. Esempio 1 File di input File di output 10000 0 1 0 0 0 Esempio 2 File di input 152000 File di output 1 5 0 1 0 Esempio 3 File di input 2001000 File di output 20 0 0 0 1 La biblioteca degli smemorati (BIB) olimpiade regionale 2001 Livello di difficoltà D=3 La vostra biblioteca rionale ha qualche problema nello stabilire da quanto tempo gli utenti tengono i libri. Dovete aiutarli scrivendo un programma che, prese in input due date del 2001, stabilisca quanti giorni intercorrono tra le due date. Dati in input Il file input.txt è formato da una riga che contiene la data iniziale e la data finale del prestito. Più precisamente, la riga contiene quattro interi: la prima coppia specifica la data iniziale, la seconda la data finale. Ogni data è formata da due numeri, e cioè il giorno del mese e il numero del mese. 142 Corso sul Linguaggio C++ Edizione 2010 Dati in output Il programma, dopo aver letto il file di input, deve stabilire quanti giorni intercorrono tra le due date, e scrivere il numero di giorni su un file di nome output.txt. Più precisamente, il file output.txt deve contenere un'unica riga. Su questa riga dovrà comparire il numero intero corrispondente ai giorni che intercorrono tra le due date in input. Assunzioni 1. Il file di input non contiene altri caratteri oltre a quelli precisati. 2. Trenta giorni a novembre, con april, giugno e settembre; di ventotto ce n'è uno: tutti gli altri ne han trentuno. 3. La seconda data non precede mai la prima. 4. Il numero di giorni considerato non comprende quello iniziale: quindi, ad esempio, tra il 2 gennaio e il 2 gennaio intercorrono 0 giorni, tra il 30 gennaio e il 2 febbraio intercorrono tre giorni, e così via. 5. Importante! Il programma non deve scrivere nulla sul video, e non deve interagire con l'utente. Deve solo leggere il file di input e scrivere il file di output. 6. L'esecuzione del programma deve terminare entro 5 secondi. Esempio 1 File di input File di output 2 3 2 3 0 Esempio 2 File di input File di output 30 1 2 2 3 Esempio 3 File di input 1 5 2 7 File di output 62 Lo struzzo Simone (SIM) – olimpiade 2001 Livello di difficoltà D=2 Lo struzzo Simone si sposta solo nelle direzioni dei quattro assi cardinali (Nord, Sud, Est, Ovest). Ogni suo passo misura 1 metro. Dovete scrivere un programma che, data una sequenza di spostamenti di Simone, misuri quant'è la distanza fra il punto di partenza e il punto di arrivo. Dati in input Il file input.txt contiene la sequenza degli spostamenti. Tale file è costituito da un'unica riga di testo, contenente una sequenza di S, N, E, O (che indicano gli spostamenti nelle direzioni Sud, Nord, Est, Ovest rispettivamente). La sequenza è terminata da un *. Ad esempio, il file di input NNESO* dice che Simone si sposta di due metri a Nord, poi di un metro verso Est, poi di un metro verso Sud, e quindi di un metro a Ovest. Dati in output Il programma, dopo aver letto il file di input, deve calcolare la distanza in metri fra il punto di partenza e il punto di arrivo, e scriverla su un file di nome output.txt. Più precisamente, il file output.txt deve contenere un'unica riga. Su questa riga dovrà comparire il numero intero corrispondente al quadrato della distanza. Assunzioni 1. Il file di input non contiene altri caratteri oltre a quelli precisati. 2. Il numero complessivo di spostamenti contenuti nel file di input è minore o uguale a 100000. 3. Importante! Il programma non deve scrivere nulla sul video, e non deve interagire con l'utente. Deve solo leggere il file di input e scrivere il file di output. 4. L'esecuzione del programma deve terminare entro 5 secondi. Esempio 1 File di input File di output NNSEEESNOENNS* 13 Esempio 2 File di input File di output NNESOS* 0 Esempio 3 File di input OSOS* File di output 8 Foto Stellare – olimpiadi 2004 Livello di difficoltà D = 4 Il dottor Hubb è un appassionato di astronomia. Possiede una carta astronomica planare, composta dai due assi del piano cartesiano aventi l'origine in posizione (0,0). La carta è suddivisa in quattro quadranti delimitati dagli assi cartesiani, dove a ciascun quadrante viene assegnata una lettera distinta come mostrato in figura: A 143 B Corso sul Linguaggio C++ Edizione 2010 D C Le stelle nella carta planare sono rappresentate mediante coordinate intere e un valore di intensità. Precisamente, ciascuna stella è rappresentata da una tripla x, y, k, per individuare le coordinate (x, y) del centro della stella e la sua intensità espressa mediante un intero k e { 1,2,3 }. Al dottor Hubb piace molto scattare delle foto digitali al cielo stellato. Ciascuna foto digitale è composta da zeri e da uni. Lo sfondo del cielo è rappresentato dagli zeri, mentre una stella x, y, k è rappresentata dagli uni in accordo al valore di k: k= 1 k=2 k=3 1 1 111 1 111 11111 1 111 1 In tal caso, la coordinata (x, y) si riferisce all'uno in posizione centrale. Le stelle possono parzialmente sovrapporsi nelle foto ma le loro coordinate sono sicuramente distinte come coppie di valori. Purtroppo il dottor Hubb è un pasticcione. La sua carta astronomica contiene N stelle, e lui ha scattato una foto digitale di taglia M x M, ma non ricorda in quali quadranti. Per l'elevata tecnologia adottata, non ci sono distorsioni ottiche: basta sovrapporre la foto digitale alla carta planare in una posizione opportuna, per farle combaciare e poter cosi ricostruire il quadrante (o i quadranti) di appartenenza. Le risposte ammissibili sono A, B, C, D, AD, AB, BC, CD, ABCD, mentre AC, BD, ABC, ecc., non sono considerate valide. Suggerimento: Quadrettare a griglia il piano cartesiano con le coordinate intere, assegnando le coordinate (x, y) al quadratino avente (x, y) come coordinata dello spigolo inferiore sinistro. Dati in input Il file di input contiene una sequenza di righe. La prima riga contiene il valore di N e M. Le N righe successive rappresentano le N stelle nella carta astronomica, ciascuna riga contenente una tripla x, y, k, interpretata in accordo a quanto descritto sopra. Le M righe finali rappresentano la foto digitale, ogni riga contenente una sequenza di M valori scelti tra zeri e unì. Dati in output Il file di output contiene una sola delle risposte ammissibili, A, B, C, D, AD, AB, BC, CD, ABCD, in base alla posizione della foto digitale rispetto ai quadranti della carta astronomica. Assunzioni I numeri sono rappresentati con il segno. Le coordinate (x, y) sono coppie di numeri, dove - 1000 < x, y < 1000. Per la conformazione della carta stessa e della foto c'è esattamente una risposta da fornire (non è possibile che la foto appaia due o più volte in posizioni distinte). La foto non taglia le stelle: una stella è interamente catturata dalla foto oppure non è catturata affatto. Infine, se una stella ha centro nel quadrante A ma la sua intensità è tale che induca un uno nel quadrante B, per esempio, allora la stella viene considerata a cavallo dei due quadranti A e B (non conta solo il centro, ma anche l'intensità). 144 Corso sul Linguaggio C++ Edizione 2010 Esempio 1 File di input File di output AB 9 7 -3 7 1 -2 6 1 -1 2 2 -2 1 1 1 7 1 1 3 3 2 3 2 2 -1 1 -3 -1 1 1000100 0100000 0000100 0001110 0011111 0111110 0110100 La dieta di Poldo – olimpiadi 2004 Livello di difficoltà D=3 Il dottore ordina a Poldo di seguire una dieta. Ad ogni pasto non può mai mangiare un panino che abbia un peso maggiore o uguale a quello appena mangiato. Quando Poldo passeggia per la via del suo paese da ogni ristorante esce un cameriere proponendo il menù del giorno. Ciascun menù è composto da una serie di panini, che verranno serviti in un ordine ben definito, e dal peso di ciascun panino. Poldo, per non violare la regola della sua dieta, una volta scelto un menù, può decidere di mangiare o rifiutare un panino; se lo rifiuta il cameriere gli servirà il successivo e quello rifiutato non gli sarà più servito. Si deve scrivere un programma che permetta a Poldo, leggendo un menù, di capire qual è il numero massimo di panini che può mangiare per quel menù senza violare la regola della sua dieta. Riassumendo, Poldo può mangiare un panino se e solo se soddisfa una delle due condizioni: I) il panino è il primo che mangia in un determinato pasto; 2)il panino non ha un peso maggiore o uguale all'ultimo panino che ha mangiato in un determinato pasto. Dati in input La prima linea del file input.txt contiene il numero m di panini proposti nel menu. Le successive m linee contengono un numero intero non negativo che rappresenta il peso del panino che verrà servito. I panini verranno serviti nell'ordine in cui compaiono nell'input. Dati in output Il file output.txt contiene il massimo numero di panini che Poldo può mangiare rispettando la dieta. Assunzioni I pesi di panini sono espressi in grammi, un panino pesa al massimo 10 Kg. Un menù contiene al massimo 100 panini 145 Corso sul Linguaggio C++ Edizione 2010 Esempio 1 File di input File di output 8 389 207 155 300 299 170 158 65 6 Esempio 2 File di input 3 22 23 27 File di output 1 Esempio 3 File di input 22 15 14 15 389 201 405 204 130 12 50 13 26 190 305 25 409 3011 43 909 987 1002 900 File di output 6 Giardinaggio – olimpiade 2004 Livello di difficoltà D=2 Pippo ha deciso di spendere il proprio tempo libero facendo giardinaggio. Poiché il numero di fiori piantati nelle sue aiuole gli pare eccessivo, decide di ridurne il numero eliminando quelli tra loro più vicini. A tale scopo, essendo appassionato di informatica, decide di scrivere un programma che gli permetta di risolvere il problema. Di ogni fiore viene identificata la posizione e questa viene memorizzata in un file indicando le coordinate, in centimetri, rispetto l'angolo in basso a sinistra dell'aiuola; ogni posizione è contenuta in una riga diversa del file e codificata con due numeri interi separati da uno spazio. Oltre alle righe che contengono le posizioni dei fiori, il file contiene anche, nella prima riga, il numero di fiori che deve essere eliminato. In sostanza il programma deve cercare la coppia di fiori con distanza minima ed eliminare, dei due, il fiore più in alto (cioè quello la cui ordinata è maggiore); ripetere, quindi, l'operazione tante volte quanti sono i fiori da eliminare. Nel caso in cui l'ordinata dei due fiori fosse uguale, il fiore da eliminare è quello con ascissa maggiore. Dati in input I dati di ingresso sono contenuti nel file input.txt che contiene: - nella prima riga, un numero intero che rappresenta il numero di fiori da eliminare; - dalla seconda riga al termine, coppie di numeri interi separati da uno o più spazi bianchi, il primo rappresenta l'ascissa ed il secondo l'ordinata della piantina. Dati in output Il risultato deve essere scritto nel file di testo output.txt indicando le coordinate dei punti eliminati nello stesso formato del file in ingresso (esclusa la prima riga). I punti devono essere ordinati, in modo crescente, rispetto all'ascissa, e nel caso in cui avessero la stessa ascissa, rispetto l'ordinata. Assunzioni 1. Non ci sono due o più fiori esattamente nello stesso punto; 2. la distanza è calcolata usando la formula euclidea; 3. il numero di piante da eliminare, indicato nel file di ingresso, è sempre minore o uguale al numero di piante contenute nello stesso file; 4. il file di ingresso contiene almeno due piante; 5. l'aiuola ha dimensioni 1000 x 1000 cm. 146 Corso sul Linguaggio C++ Edizione 2010 Esempio 1 File di input File di output 2 30 50 10 40 10 20 80 60 60 100 40 60 50 80 Le pesate di bilancino (bilancino) olimpiade – 2006 Livello di difficoltà D = 3 Bilancino è un bambino con una passione maniacale, quella di mettere gli oggetti in ordine crescente di peso. I suoi genitori posseggono un'antica e rara bilancia con due bracci uguali: posti due oggetti, uno per braccio, la bilancia permette di stabilire quale dei due oggetti è più pesante, ma non permette di trovarne il peso assoluto. Oggi Bilancino vuole mettere in ordine crescente di peso N oggetti e, a tale scopo, ha già effettuato una serie di M pesate, trascrivendone i risultati. Infatti, numerati tali oggetti da 1 a N, egli ha pesato M coppie di oggetti distinti x e y, dove 1 <= x, y <= N, scrivendo i due interi x e y in quest'ordine su una riga per indicare che x è più leggero di y e, invece, scrivendo y e x in quest'ordine per indicare che y è più leggero di x. Da notare che non esistono due oggetti con lo stesso peso (siano essi stati pesati o meno da Bilancino) e che la stessa coppia di oggetti non può essere pesata più di una volta. Esaminate le M pesate finora eseguite da Bilancino e aiutatelo a decidere quale, tra le seguenti alternative, consente di stabilire l'ordine crescente di peso tra gli N oggetti: • le M pesate sono sufficienti; • è necessaria un'ulteriore pesata; • sono necessarie due o più pesate. Dati di input Il file “input.txt” è composto da M+1 righe. La prima riga contiene due interi positivi separati da uno spazio: il primo intero rappresenta il numero N di oggetti da ordinare in base al peso mentre il secondo intero rappresenta il numero M di pesate effettuate da Bilancino. Le successive M righe contengono coppie di interi positivi: la j-esima di tali righe è composta da due interi distinti a e b separati da uno spazio, a rappresentare la j-esima pesata effettuata da Bilancino, in cui egli scopre che l'oggetto a è più leggero dell'oggetto b (dove 1 <= j <= M e 1 <= a, b <= N). Da notare che la stessa pesata non può apparire in più di una riga. Dati di output Il file “output.txt” è composto da una riga contenente un solo intero come dalla seguente tabella. 0 : nessuna ulteriore pesata è necessaria per stabilire l'ordine crescente di tutti gli oggetti. 1 : serve e basta un'ulteriore pesata per stabilire l'ordine crescente di tutti gli oggetti. 2 : due o più pesate sono ulteriormente necessarie per stabilire l'ordine crescente di tutti gli oggetti. Assunzioni 1 < N < 100. 1 <= M <= N(N-1)/2. I dati in “input.txt” garantiscono sempre che esiste almeno un ordinamento degli oggetti compatibile con tutte le pesate trascritte da Bilancino. Esempio 1 File di input File di output 3 2 1 2 3 1 0 Esempio 2 File di input 4 2 1 1 2 4 3 4 3 1 File di output 1 147 Esempio 3 File di input 4 2 1 2 3 3 4 1 File di output 2 Corso sul Linguaggio C++ Edizione 2010 Teste di serie (serie) – olimpiade 2006 Un torneo è composto da K gironi, con N squadre partecipanti in ciascun girone (per un totale di KxN squadre nel torneo). Dopo le eliminatorie, passa soltanto la prima classificata di ogni girone. A ogni squadra è associato un "coefficiente di bravura", ovvero un intero positivo che è tanto maggiore quanto più la squadra è forte. Per rendere più vivace il torneo, gli organizzatori vogliono far gareggiare le squadre più forti tra loro soltanto dopo le eliminatorie: in altre parole, le K squadre con i coefficienti di bravura più alti devono giocare in gironi distinti. Aiutate gli organizzatori a verificare che la composizione del torneo rispetti il loro volere: prese le K squadre con il più alto coefficiente di bravura, ciascun girone deve contenere esattamente una di esse (da notare che due o più squadre possono avere lo stesso coefficiente). Dati di input Il file “input.txt” è composto da K+1 righe. La prima riga contiene due interi positivi separati da uno spazio: il numero K di gironi e il numero N di squadre per girone. Le successive K righe contengono i coefficienti di bravura delle squadre: la j-esima di tale righe contiene N interi positivi separati da uno spazio che sono i coefficienti di bravura delle N squadre nel j-esimo girone, per 1 <= j <= K. Dati di output Il file “output.txt” è composto di una riga contenente un solo intero: 1 se il torneo rispetta i vincoli imposti dagli organizzatori, 0 altrimenti. Assunzioni 1 < N <= 100. 1 < K <= 100. Esempio 1 File di input File di output 3 2 2 2 4 2 2 1 1 3 1 4 2 1 1 Esempio 2 File di input 3 3 3 43 4 5 7 9 6 78 90 78 71 32 File di output 0 Ritrovo a Brambillia (brambillia) – olimpiade 2006 Livello di difficoltà D = 2 Nell'isola di Brambillia, vi sono N città numerate da 1 a N e collegate attraverso una ferrovia circolare, le cui tratte sono anch'esse numerate da 1 a N e possono essere percorse in entrambe le direzioni: la tratta ferroviaria j collega direttamente la città j alla città j+1 (e, percorsa nella direzione opposta, collega j+1 a j) dove j = 1, 2, ..., N-1; la tratta N collega la città N alla città 1 (e, percorsa nella direzione opposta, collega 1 a N). Il biglietto ferroviario per ciascuna tratta ha un costo prestabilito. Date due qualunque città p e q, è possibile andare da p a q attraverso due percorsi ferroviari alternativi (ipotizzando che 1 <= p < q <= N, un percorso attraversa le tratte p, p+1, ..., q-1 mentre l'altro attraversa, nella direzione opposta, le tratte p-1, p-2, ..., 1, N, N-1, ..., q; per andare da q a p, attraversiamo tali percorsi ma in direzione opposta). Il biglietto ferroviario per ciascuno dei percorsi ha un costo pari alla somma dei costi delle singole tratte che lo compongono. Gli abitanti di Brambillia intendono utilizzare la ferrovia circolare per ritrovarsi in occasione della sagra annuale dell'isola e devono scegliere la città presso cui organizzare tale sagra minimizzando il costo totale dei biglietti. Per questo motivo hanno contato, per ogni città, quante persone vogliono parteciparvi, visto che è necessario acquistare un biglietto ferroviario per persona al costo descritto sopra (per gli abitanti della città che verrà scelta, il costo sarà nullo perché non dovranno prendere il treno). In base a tale conteggio, individuate la città in cui organizzare la sagra, tenendo presente che le persone possono giungervi attraverso uno dei due percorsi a loro disposizione nella ferrovia circolare. Dati di input Il file “input.txt” è composto da 2N+1 righe. La prima riga contiene un intero positivo che rappresenta il numero N delle città. Le successive N righe contengono ciascuna un intero positivo: quello nella j-esima di tali righe rappresenta il costo del biglietto ferroviario per la tratta j, dove 1 <= j <= N. 148 Corso sul Linguaggio C++ Edizione 2010 Le ulteriori N righe contengono ciascuna un intero positivo o nullo: quello nella j-esima di tali righe è il numero delle persone della città j che intendono partecipare alla sagra, per 1 <= j <= N. Dati di output Il file “output.txt” è composto da una riga contenente un solo intero j che rappresenta la città j presso cui organizzare la sagra. Come osservato in precedenza, tale città rende minimo il costo totale, ottenuto sommando i costi dei biglietti ferroviari di tutti i partecipanti. Assunzioni 1 < N < 100. I dati in “input.txt” garantiscono che la soluzione è unica (esiste una sola città in cui organizzare la sagra). Esempio 1 File di input File di output 4 45 34 18 40 3 0 4 2 4 Mappa antica – olimpiadi 2008 Difficoltà D=2 Topolino è in missione per accompagnare una spedizione archeologica che segue un'antica mappa acquisita di recente dal museo di Topolinia. Raggiunta la località dove dovrebbe trovarsi un prezioso e raro reperto archeologico, Topolino si imbatte in un labirinto che ha la forma di una gigantesca scacchiera quadrata di NxN lastroni di marmo. Nella mappa, sia le righe che le colonne del labirinto sono numerate da 1 a N. Il lastrone che si trova nella posizione corrispondente alla riga r e alla colonna c viene identificato mediante la coppia di interi (r, c). I lastroni segnalati da una crocetta '+' sulla mappa contengono un trabocchetto mortale e sono quindi da evitare, mentre i rimanenti sono innocui e segnalati da un asterisco '*'. Topolino deve partire dal lastrone in posizione (1, 1) e raggiungere il lastrone in posizione (N, N), entrambi innocui. Può passare da un lastrone a un altro soltanto se questi condividono un lato o uno spigolo (quindi può procedere in direzione orizzontale, verticale o diagonale ma non saltare) e, ovviamente, questi lastroni devono essere innocui. Tuttavia, le insidie non sono finite qui: per poter attraversare incolume il labirinto, Topolino deve calpestare il minor numero possibile di lastroni innocui (e ovviamente nessun lastrone con trabocchetto). Aiutate Topolino a calcolare tale numero minimo. Dati di input Il file input.txt è composto da N+1 righe. La prima riga contiene un intero positivo che rappresenta la dimensione N di un lato del labirinto a scacchiera. Le successive N righe rappresentano il labirinto a scacchiera: la r-esima di tali righe contiene una sequenza di N caratteri '+' oppure '*', dove '+' indica un lastrone con trabocchetto mentre '*' indica un lastrone sicuro. Tale riga rappresenta quindi i lastroni che si trovano sulla r-esima riga della schacchiera: di conseguenza, il c-esimo carattere corrisponde al lastrone in posizione (r, c). Dati di output Il file output.txt è composto da una sola riga contenente un intero che rappresenta il minimo numero di lastroni innocui (ossia indicati con '*') che Topolino deve attraversare a partire dal lastrone in posizione (1, 1) per arrivare incolume al lastrone in posizione (N, N). Notare che i lastroni (1, 1) e (N, N) vanno inclusi nel conteggio dei lastroni attraversati. Assunzioni 1 ≤ N ≤ 100; 1 ≤ r, c ≤ N. 149 Corso sul Linguaggio C++ Edizione 2010 E' sempre possibile attraversare il labirinto dal lastrone in posizione (1, 1) al lastrone in posizione (N, N); inoltre tali due lastroni sono innocui Esempio 1 File di input File di output 4 *+++ +**+ +*+* +*** 8.3 5 Soluzioni delle selezioni scolastiche Test 2008 Esercizio 1 2 3 4 5 6 7 8 9 Punti 3 3 1 □ x □ x □ □ □ □ □ 2 2 2 3 2 2 a a a a a a a a a Risposta b □ b □ b x b □ b □ b □ b x b x b □ c c c c c c c c c □ □ □ □ x □ □ □ □ Risposta x b □ □ b x B: N2 C: □ b □ □ b □ □ b □ □ b x c c 2N c c c c □ d □ d D: N! x d x d x d □ d x □ □ □ □ x □ □ x d d d d d d d d d Test 2007 Esercizio 1 2 3 4 5 6 7 Punti 1 2 2/R 3 3 3 5 □ a □ a A: 2N □ a □ a □ a □ a Test 2006 Esercizio: Punti: 1 1 2 1 3 2 4 3 5 3 6 3 7 3 8 3 Risposta: □ □ x □ □ x □ a a a a a a a x x □ x x □ x a = 5 b b b b b b b 150 □ □ □ □ □ □ □ b = 5 c c c c c c c □ □ □ □ □ □ □ d d d d d d d Corso sul Linguaggio C++ Edizione 2010 Test 2005 Esercizio: Punti: 1 6 2 3 4 5 6 4 6 4 4 6 Risposta: La differenza fra l’elemento massimo e l’elemento minimo presenti nel vettore. X a □ b □ c □ d La somma di tutti gli interi positivi minori od uguali al parametro. □ a □ b □ c X d □ a X b □ c □ d Che il valore cercato esista effettivamente nel vettore di input. Test 2004 Esercizio: Punti: 1 4 Risposta: □a xb 2 6 3 4 4 4 5 6 6 6 33, 3, 27, 1, 23, 26, 15, 30, 6, 40, -1, 23, -40, 26, 15, 30, 6, 40, -12, -34 □a xb □c □d □a □b xc □d ma se il vettore è ordinato K=0, quindi anche ‘d’ è giusta 8, 81, 1024 12, 28, 875 □c □d 8.4 Soluzione di alcuni problemi delle selezioni regionali Cassiere Camillo – 2001 Il problema è stato già proposto e risolto a pag 103. La soluzione deve, però, essere adattata ai vincoli imposti nella selezione regionale e, quindi, i dati in input devono essere letti dal file input.txt e i dati in output devono andare nel file output.txt. Questo significa che bisogna aprire i due flussi e poi bisogna utilizzare per l’input fscanf() invece di scanf e, per l’output, fprintf invece di printf . Il programma modificato è il seguente: #include <stdio.h> // programma cam.cpp - Cassiere Camillo- selezioni regionali 2001 int main(){ FILE *in, *out; // in=flusso per l'input e out = flusso per l'output int tb[]={100000, 10000, 5000, 2000, 1000}; // i 5 tipi di banconote int somma, i; // somma = l'importo in input int nb[5]; // nb[] = numero banconote per ogni taglio (dati in output) in = fopen("input.txt","r"); // apre i due file out = fopen("output.txt","w"); fscanf(in,"%d", &somma); // legge la somma da pagare for (i= 0; i <5; i++) { nb[i] = somma / tb[i]; // numero banconote del taglio i somma %= tb[i];// quello che resta dopo aver dato il taglio i 151 Corso sul Linguaggio C++ Edizione 2010 fprintf(out,"%d\n", nb[i]); } fclose(in); fclose(out); return 0; } Dieta di Poldo – olimpiadi 2004 Si tratta di individuare la più lunga serie decrescente in una serie di numeri. L’algoritmo proposto utilizza due vettori. il vettore peso[] che contiene i pesi degli m panini caricati in input e il vettore np[] dove, per ogni i l’elemento np[i] contiene il numero massimo di panini che si possono prendere partendo dalla portata i. Sappiamo che se si parte dall’ultima portata (la portata m-1) np[m-1]=1. Se conosciamo np[j] per ogni j>i allora per calcolare np[i] basta trovare il massimo per tutti gli np[j] con j>i e con peso[j]<peso[i] e aggiungerci 1 oppure se tutti i panini seguenti hanno peso >= peso[i] allora np[i]=1. Il massimo del vettore np[i] sarà il numero massimo di panini che Poldo può prendere. #include <stdio.h> // Programma Poldo - Olimpiade 2004 int main(){ FILE *fi, *fo; int m , peso[100], np[100], i, j, n, max; fi = fopen("input.txt","r"); // apre i due file fo = fopen("output.txt","w"); fscanf(fi,"%d ", &m); // legge il numero m dei panini del menù for (i=0; i<m; i++) fscanf(fi,"%d",&peso[i]); // carica i pesi max=0; if (m>0) {np[m-1]=1; max=1;} /* calcola per ogni i il numero massimo di panini np[i] che può prendere partendo dalla portata i */ for (i=m-2; i>=0; i--) { n=1; // n=numero max di panini partendo dalla portata i for (j=i+1; j<m; j++) if (peso[j] < peso[i] && np[j] >= n) n=np[j]+1; np[i]=n; if (max < n) max=n; } fprintf(fo,"%d", max); fclose(fi); fclose(fo); return 0; } Testa di serie – olimpiadi 2006 Riporto qui l’algoritmo proposto da Casciato Amedeo, l’alunno del nostro istituto che ha partecipato alla selezione regionale delle olimpiadi del 2006 e che, risolvendo questo problema, si è classificato ad uno dei primi posti nella classifica regionale. Vengono definiti due vettori, Best[] e Best2[], il primo di K elementi e il secondo di 2*K elementi (K = numero di gironi). Carica nel vettore Best[] il coefficiente di bravura della migliore squadra di ogni girone (in totale K numeri), mentre carica nel vettore Best2[] il coefficiente delle due migliori squadre di ogni girone (in totale 2*k numeri). Ordina i due vettori in modo decrescente. Se i primi K elementi dei due vettori ordinati coincidono vuol dire che le k migliori squadre di ogni girone (tutte presenti nel vettore Best[]) sono le K migliori squadre in assoluto e quindi il torneo rispetta i vincoli imposti altrimenti non li rispetta. Questo algoritmo ha il vantaggio di caricare in memoria solo 3*K coefficienti di bravura e non tutti i dati in input che potrebbero anche essere 100*100= 10000 e di essere molto veloce (i vettori da ordinare sono piccoli). #include <stdio.h> /* N = numero di squadre per ogni girone K = numero di gironi 152 Corso sul Linguaggio C++ Edizione 2010 Best[]= vettore contenenente i coefficienti di bravura della migliore squadra di ogni girone( in totale K coefficienti) Best2[]= vettore conenente contenente i coefficienti di bravura delle due migliorisquadre di ogni girone, quindi in totale 2 * K coefficienti. */ void ordina(int vett[],int n) { int i,j,a; //ordina in maniera decrescente il vettore vett[] di n elementi for (i=0; i<n; i++) for (j=i+1; j <n; j++) if (vett[i]<vett[j]) {a=vett[i]; vett[i]= vett[j]; vett[j]=a;} } int main(){ FILE *fi, *fo; int j,i,max1, max2,N,K, coeff; int Best[100], Best2[200]; fi = fopen("input.txt","r"); // apre i due file fo = fopen("output.txt","w"); fscanf(fi,"%d %d", &K, &N); // legge N e K // legge i dati in input e carica i vettori Best[] e Best2[] for (i=0; i<K; i++) { // ciclo sui gironi fscanf(fi,"%d %d", &max1, &max2); // legge le prime due squadre if (max1 < max2) {coeff=max1; max1=max2; max2=coeff;} for (j=2; j<N; j++) { // ciclo sulle restanti squadre del girone fscanf(fi, "%d ", &coeff); if (coeff > max1) { max2 = max1; max1 = coeff; } else if (coeff > max2) max2 = coeff; } Best2[2*i]=Best[i]=max1; Best2[2*i+1]=max2; } ordina(Best, K); // ordina i due vettori ordina(Best2,2*K); /* confronta i due vettori, se i primi k elementi non sono uguali vuol dire che il torneo non rispetta i vincoli */ for (i = 0; i<K; i++) if (Best[i] != Best2[i]) break; if (i<K) fprintf(fo,"%d", 0); else fprintf(fo,"%d", 1); fclose(fi); fclose(fo); return 0; } Mappa antica – olimpiadi 2008 La mappa viene rappresentata con una matrice (mappa) di interi di ordine n+2, ossia si utilizza un bordo sentinella intorno alla matrice originale in modo da garantire che ogni cella interna sia attorniata sempre da 8 celle (in questo modo si evitano di controllare ogni volta se la cella si trova sul bordo). Il valore di una cella va così interpretato: - se nullo, si tratta di un "trabocchetto" e quindi non va attraversato; - se > 0, è il numero ottimo di lastroni attraversati da (1,1) per raggiungerlo. Durante la lettura da file le celle della mappa vengono poste a zero per le celle "+" (trabocchetto) e ad un valore "infinito" (basta anche n*n) per le celle "*" (sicure). Il bordo sentinella viene posto a zero come se fossero celle trabocchetto. 153 Corso sul Linguaggio C++ Edizione 2010 La cella (1,1) viene inizializzata ad 1 che è il numero dei lastroni da attraversare per passare da (1,1) a (1,1). Poi considerato che, se conosciamo il numero di lastroni ottimo da attraversare per raggiungere la cella (r,c) per tutte le celle adiacenti (che sono 8) al massimo il numero dei lastroni sarà 1 in più, possiamo propagare le modifiche a tutte le celle collegate. Per propagare le modifiche esaminiamo tutta la matrice impostando al massimo 1 in più per le 8 celle adiacenti ad ogni cella non trabochetto a meno chè in questa cella ci sia già un numero inferiore (quindi le celle con trabocchetto e quelle che si possono raggiungere attraverso un altro percorso più corto venono escluse dall’aggiornamento). L’aggiornamento si ripete fin quando non ci sono più modifiche. Per ripetere il minor numero di volte l’aggiornamento esaminiamo la matrice alternativamente dall’alto in baso e dal basso in alto. Il risultato sarà il valore finale memorizzato in mappa[n, n]. #include <stdio.h> const int NMAX = 102; const int INFINITO = 10000; const int TRAB = 0; int mappa[NMAX][NMAX]; int n; int main() { FILE *input, *output; char s; input = fopen("input.txt","r"); // apre i due file output = fopen("output.txt","w"); fscanf(input,"%d", &n); // legge il numero di righe e colonne int r, c, h; for (c=0; c<n+2; c++) mappa[0][c]= 0; // prima riga sentinella // legge la mappa for (r=1; r<=n; r++) {mappa[r][0]=0; for (c=1; c<=n; c++) { do fscanf(input, "%c", &s); while(s !='+' && s != '*'); if (s=='*') mappa[r][c]= INFINITO; else mappa[r][c]=TRAB; } mappa[r][n+1]=0; } for (c=0; c<n+2; c++) mappa[n+1][c]= 0; // ultima riga sentinella //imposta il valore dalla prima casella che è conosciuto mappa[1][1] = 1; int ripeti=1; /* propaga le modifiche nella mappa fin quando non ci sono più modifiche da propagare */ while (ripeti) { /* per evitare di ripetere molte volte il cilco propaga le modifiche alternativamente dall'alto il basso, da sinistra a destra e dal basso in alto, da destra a sinistra */ if (ripeti > 0) {ripeti=0; for (r=1; r<=n; r++) for (c=1; c<=n; c++) if (mappa[r][c] > TRAB) { // se non è un trabochetto h = mappa[r][c] + 1; /* -- stessa riga a sx -- */ if (mappa[r][c-1] > h ) {mappa[r][c-1] = h; ripeti=-1;} /* -- stessa riga a dx -- */ if (mappa[r][c+1] > h) mappa[r][c+1] = h; /* -- riga prec. -- */ for (int ic=c-1; ic<=c+1; ic++) if (mappa[r-1][ic] > h) {mappa[r-1][ic] = h; ripeti=-1;} /* -- riga succ. -- */ 154 Corso sul Linguaggio C++ Edizione 2010 for (int ic=c-1; ic<=c+1; ic++) if (mappa[r+1][ic] > h) mappa[r+1][ic] = h; } } else { // propagazione dal basso in alto e da destra a sinistra ripeti = 0; for (r=n; r>0; r--) for (c=n; c>0; c--) if (mappa[r][c] > TRAB) { // se non è un trabochetto h = mappa[r][c] + 1; /* -- stessa riga a sx -- */ if (mappa[r][c-1] > h ) mappa[r][c-1] = h; /* -- stessa riga a dx -- */ if (mappa[r][c+1] > h) {mappa[r][c+1] = h; ripeti=1;} /* -- riga prec. -- */ for (int ic=c-1; ic<=c+1; ic++) if (mappa[r-1][ic] > h) mappa[r-1][ic] = h; /* -- riga succ. -- */ for (int ic=c-1; ic<=c+1; ic++) if (mappa[r+1][ic] > h) {mappa[r+1][ic] = h; ripeti=1;} } } } // scrive il risultato in output fprintf(output,"%d", mappa[n][n]); fclose(input); fclose(output); return 0; } 155 9 Capitolo Corso sul Linguaggio C++ Edizione 2010 9. Appendici e Link 9.1 Conclusioni Il linguaggio C è un linguaggio di alto livello, per tutti gli usi (general-purpose) progettato per permettere una efficiente traduzione in linguaggio macchina in modo da creare programmi eseguibili il più possibile simili a quelli scritti direttamente in linguaggio macchina ma, nello stesso tempo, indipendenti dal particolare processore. Permette l’accesso di basso livello alla memoria e anche ai registri interni del processore ma contemporaneamente è progettato per permettere il riutilizzo del codice anche su macchine con sistemi operativi e processori diversi. Per ottenere programmi eseguibili il più possibile veloci il compilatore riduce al minimo il supporto run-time, per esempio caratteristiche specifiche di una particolare funzione, che non sono comuni a tutte le piattaforme, non sono incluse nel linguaggio (ma sono incluse in librerie aggiuntive esterne), non vengono inseriti controlli di run-time per evitare errori che non dovrebbero essere presenti in un programma corretto (per esempio i controlli sugli indici dei vettori per evitare che superino i limiti). La traduzione in linguaggio macchina è sempre fatta nel modo più semplice e diretto. Fra le sue principali caratteristiche ricordiamo che permette la programmazione strutturata, permette la ricorsione, permette la definizione di variabili con visibilità limitata ad un particolare pezzo di programma, tutto il codice è contenuto all’interno di funzioni, ha un sistema di tipi di dati molto flessibile che permette la creazione di praticamente qualsiasi struttura di dati, supporta in modo completo l’utilizzo dei puntatori (attraverso i quali si può avere anche l’accesso di basso livello alla memoria), ecc. Tra le caratteristiche assenti ricordiamo il controllo di run-time per evitare che gli indci dei vettori superino i limiti, le operazioni su interi vettori (inclusi però nelle classi aggiuntive String e Vector del C++). Dopo la sua creazione molte versioni di questo linguaggio sono state sviluppate. Per garantire uniformità nelle varie implementazioni e, quindi, permettere la migrazione del codice da una piattaforma all’altra, è internvenutta prima l’ANSI (American National Standards Institute) per definire un primo standard denominato ANSI C89, poi l’ISO (International Organization for Standardization) che ha prodotto le specifiche denominate ISO C99. Mentre le specificheper il linguaggio C sono rimaste ferme al 1999 quelle per il linguaggio C++ sono in continua evoluzione. Anche per questo linguaggio l’ISO ha formato una commissione (ISO/IEC 14882:1998) che ha prodotto vari aggiornamenti nello standard in particolare nel 2003 e nel 2005. 156 Corso sul Linguaggio C++ Edizione 2010 9.2 Come approfondire. Libri • Ferdinando Sampietro e Ornella Snpietro, Il linguaggio di programmazione C++, Editore Tramontana • Piero Gallo e Fabio Salerno, Il linguaggio C++, Editore Minerva Italica • Bruce Eckel, Thinking in C++ 2° Edition, primo e secondo volume, http://www.mindview.net/, il primo volume disponibile anche in italiano all’indirizzo http://www.umbertosorbo.it. Il DEV-C++ http://www.bloodshed.net/dev/devcpp.html Sito ufficiale del progetto open source Dev-C++. Il Dev-C++ include il compilatore C g++ realizzato nell’ambito del progetto gnu (www.gnu.org). http://sourceforge.net/projects/dev-cpp/ Un sito in inglese dove si possono trovare informazioni sulle ultime versioni del DEV-C++ e della relativa documentazione e da dove si può scaricare il materiale; Il Dev C++ è stato fondato originariamente dal programmatore Colin Laplace e la sua compagnia, Bloodshed Software, utilizza il famoso compilatore gcc (Gnu Compiler Collection http://gcc.gnu.org/) che è stato scritto per Unix. Per farlo funzionare sul sistema operativo Windows utilizza mingw (http://www.mingw.org/) una altro progetto open source che ha come scopo il passaggio del software scritto per Linux sul sistema Windows. Per la documentazione relativa ad alcune delle funzioni implementate nel compilatore si veda http://www.gnu.org/software/libc/manual. Lo sviluppo del Dev-C++ è stato abbandonato nel 2005 e l’ultima versione è la 4.9.9.2 che è una beta e riscontra qualche problema di incompatibilità con Windows Vista che però può essere facilmente raggirato settando in modo appropriato il programma. Una caratteristica in più di Dev-C++ rispetto agli altri sistemi di sviluppo è l’uso dei DevPaks, estensioni sull'ambiente di programmazione con librerie addizionali, template e strumenti. I DevPaks solitamente contengono strumenti grafici, inclusi kit di sviluppo come GTK+, wxWidgets, e FLTK. Altri DevPaks includono librerie per usi delle funzioni più avanzate. Il progetto è stato ripreso da un team di sviluppo che lo ha migliorato aggiungendo le funzionalità di wxWidgets per la creazione rapida di applicazioni con interfaccia grafica. La nuova versione è stata denominata wxDev-C++ e si può scaricare liberamente all’indirizzo: http://wxdsgn.sourceforge.net/ Olimpiadi dell’informatica http://www.olimpiadi-informatica.it/ sito ufficiale dell’AICA che gestisce in Italia le olimpiadi internazionali dell’informatica. Si possono trovare tutte le tracce delle prove assegnate nelle edizioni precedenti. http://allenamenti.olimpiadi-informatica.it/ un sito contenente la possibilità di svolgere un test on line con correzione automatica, le soluzioni di molti dei problemi dati alle selezioni regionali, materiale didattico vario. 157 Corso sul Linguaggio C++ Edizione 2010 Siti per programmatori http://www.cpiupiu.tk/ In italiano, link a risorse disponibile + esercizi vari Guide on line http://www.webmasterpoint.org/c/home.asp http://www.hyperbook.it/c_book/c2.htm Hyperbook della Mc Grow Hill (bisogna prima registrarsi) http://alpha.science.unitn.it/~fiorella/guidac/indexc.html http://www.tuttogratis.it/costume_e_societa/manuali_c_e_c_gratis.html http://www.cppreference.com/ Un guida al linguaggio C++ on line gratuita in inglese http://msdn.microsoft.com/ La MSDN della Microsoft contiene molte informazioni sul lin guaggio C++, purtroppo in lingua inglese; http://en.wikipedia.org/wiki/C%2B%2B_Standard_Library anche su wikipedia si può trovare un manuale di riferimento per le librerie standard del C++, purtroppo solo in inglese; http://www.silicontao.com/ProgrammingGuide/GNU_function_list/index.html un elenco completo delle funzioni delle librerie g++, in inglese; Link per documentazione http://www.cplusplus.com/ Un sito contenente un completo manuale di riferimento per il linguaggio C e C++ (tutte le funzioni, I tipi di dati e gli oggetti definiti delle librerie), con la possibilità di effettuare ricerche e tutorial (purtroppo in inglese). http://www.manuali.it/manuali.asp?cr=51 Un elenco di manuali per il C http://pensareincpp.altervista.org/ Il sito di un progetto per la traduzione di un manuale gratuito inglese (solo 1° Vol. Pensare in C) www.bruceeckel.com Sito dove si trova l’originale inglese di 2 vol. http://ennebi.solira.org/ Sito del Prof. Nunzio Brugaletta, ITC di Ragusa 9.3 Tabelle presenti nel testo Tabella 1. Sequenze di escape.................................................................................................... 7 Tabella 2. Gli operatori disponibili nel linguaggio C++ raggruppati in ordine di precedenza da quelli con precedenza maggiore fino a quelli con precedenza minore (le linee più marcate segnano la divisione tra un gruppo e l’altro).................................................................... 28 Tabella 3 Elenco di alcune funzioni e costanti definite nelle librerie standard del C e utilizzate in questo libro................................................................................................................... 63 Tabella 4 Elenco dei codici di conversione utilizzati in printf ................................................ 89 158