close

Enter

Log in using OpenID

3 C++ - Atletica Carpenedolo

embedDownload
ISTITUTO SUPERIORE “E.FERMI”
Tecnico Settore Tecnologico e Liceo Scientifico delle Scienze Applicate
Strada Spolverina, 5 – 46100 MANTOVA
Tel. 0376 262675 Fax 0376 262015
www.fermimn.gov.it
e-mail: [email protected]
Argomenti:
 Doppi cicli
 Modularità
 Vettori
 Matrici
 Stringhe
 Bitmap
DOPPI CIClI
Nested loops
Introduzione
Il doppio ciclo è stato incontrato nel corso precedente come strumento per inserire un ciclo di elaborazione
all’interno di una richiesta ripetuta o di una sequenza di numeri casuali.
Questo gruppo di problemi prevede un doppio ciclo solo per la soluzione, senza contare il ciclo per la
ripetizione. Sono quindi problemi più complessi, nei quali ci saranno giochi di variabili più impegnativi e più
istruzioni da mettere al posto giusto.
L’obiettivo è di migliorare la corretta impostazione dei cicli ed il riconoscimento dei tempi del programma1.
Verso il doppio ciclo
Riprendiamo la sequenza base più semplice, quella dove va scritta una sequenza di numeri:
for ( i = 1; i <= n; i++) cout << i;


//
con n = 6:
123456
L’istruzione all’interno del ciclo vale per scrivere un numero solo (azione singola).
Il ciclo serve per moltiplicare l’azione, quindi per una sequenza di numeri (azione ripetuta).
La sequenza viene scritta su una sola riga per semplicità di visualizzazione: comunque non bisogna mai
dimenticare che i numeri sono scritti uno per volta, in una sequenza temporale che corrisponde a quella dei
valori crescenti della variabile (indice del ciclo).
Se una cout da sola serve per un numero, e il for serve per più numeri, possiamo estendere il ragionamento
inserendo lo snippet in un altro ciclo: in questo modo sarà l’intera riga ad essere ripetuta, quindi avremo
come risultato una sequenza di sequenze.
Se andiamo troppo in fretta, però, il nostro programma non avrà molto successo. Possiamo provare così:
for ( i = 1; i <= n; i++) cout << i;
for ( i = 1; i <= n; i++) cout << i;
Il risultato di questo tentativo sarà piuttosto sconsolante:
vedere cosa manca alla nostra soluzione:


123456123456,
ma almeno ci permette di
le righe non sono nel numero desiderato;
manca un “a capo” tra le righe (che diventano quindi una sola).
La soluzione vera è un po’ più elaborata, e, come sempre, necessita di una variabile in più:
for ( i = 1; i <= n; i++)
{
for ( j = 1; j <= n; j++) cout << j;
cout << endl;
}
ATTENZIONE !!!





Risultato:
123456
123456
123456
123456
123456
123456
Ogni riga necessita di un ciclo (da 1 a n: sequenza base);
Per far ripetere le righe, occorre un ciclo (anche questo da 1 a n: altra sequenza base);
Il ciclo di ripetizione delle righe deve contenere una riga completa, compreso endl;
Fondamentale: tutto quello che va ripetuto va inserito nel ciclo esterno;
Il doppio ciclo ha l’effetto di MOLTIPLICARE il numero di operazioni singole: il numero totale di
cout numerici risulterà n2 (n volte n), il numero di “a capo” solo n, visto che è fuori dal ciclo interno.
I due cicli hanno indici diversi: usare lo stesso indice è un
errore gravissimo (d-t).
1
I tempi sono fondamentali perché spesso le sequenze che risultano evidenti nei risultati non lo sono
altrettanto nella progettazione.
Figure piane
Un ciclo va bene per una scrittura lineare (una riga di numeri, solitamente variabili).
Un doppio ciclo va bene per una scrittura nel piano (una serie di righe di numeri forma una figura piana).
for (i=1; i<=n; i++)
{
for (j=1; j<=n; j++) cout << j;
cout << endl;
}
La riga va ripetuta "n" volte (ciclo)
Scrittura di "n" numeri (ciclo)
In ogni riga si deve andare a capo
La figura risultante è un quadrato (anche se, essendo i caratteri più alti che larghi, quello che si vede sullo
schermo è un rettangolo).
Alterando opportunamente i cicli e i punti di partenza e arrivo, si possono produrre schemi di varie forme. La
più immediata è quella triangolare, che si può ottenere facilmente modificando il ciclo interno in modo che al
posto di n usi i.
La logica è semplice: l’indice i del ciclo esterno rimane costante per tutta la durata della ripetizione (per
intenderci, dalla graffa aperta alla graffa chiusa), ma varia tra una ripetizione e l’altra, o, per dirlo in altro
modo, tra una riga e l’altra.
12345
1234
123
12
1
for (i=n; i>=1; i--)
{
for (j=1; j<=i; j++) cout << j;
cout << endl;
}
12345
2345
345
45
5
for (i=1; i<=n; i++)
{
for (j=i; j<=n; j++) cout << j;
cout << endl;
}
1
12
123
1234
12345
for (i=1; i<=n; i++)
{
for (j=1; j<=i; j++) cout << j;
cout << endl;
}
5
54
543
5432
54321
for (i=n; i>=1; i--)
{
for (j=n; j>=i; j--) cout << j;
cout << endl;
}
5
45
345
2345
12345
for (i=n; i>=1; i--)
{
for (j=i; j<=n; j++) cout << j;
cout << endl;
}
Solitamente i due indici
vengono chiamati i e j.
i è l’iniziale di “index”, j è la
lettera successiva .
Questi indicatori sono sempre
stati usati anche in algebra.
C’è anche un retaggio storico,
dato che nelle prime versioni
del FORTRAN (archetipo della
maggior parte dei linguaggi)
le variabili intere venivano
distinte per la loro iniziale,
che poteva essere solo una
delle seguenti:
i, j, k, l, m, n.
I tempi del programma
Con schemi di organizzazione più complessa è possibile costruire dei progetti più completi, lavorando sui
tempi del programma. Spesso, in un programma, c’è qualcosa che viene prima e qualcosa che viene dopo.
Detta così sembra un’idiozia, e forse lo è: il problema è che spesso, soprattutto da principianti, risulta
problematico distinguere le fasi successive di un algoritmo. Esempio con n=5:
123454321
....4
....3
....2
....1
All’inizio c’è la prima riga; poi ci sono le altre (bella scoperta). Il problema è separare bene le due fasi.
Riga 1
Una “striscia” di numeri corrisponde ad un ciclo. In questo caso ci sono 2 sequenze di numeri: una da 1 a n,
l’altra da n-1 a 1. I due cicli sono uno di seguito all’altro, seguiti da “a capo”. L’effetto è quello di sommare
le operazioni: dopo la prima sequenza c’è la seconda (di seguito), I numeri scritti in totale sono circa il
doppio di “n” (in realtà, 2*n-1, visto che una sequenza è incompleta).
Righe da 2 a n
Un “disegno piano” corrisponde ad un doppio ciclo, come visto in precedenza. In questo caso un ciclo ne
contiene un altro (e anche “a capo”). L’effetto è quello di moltiplicare le istruzioni: ogni volta che si ripete il
ciclo, si esegue di nuovo tutto quello interno. Ogni riga inizia con una sequenza di punti, prosegue con un
numero e termina con un “a capo”.
È consigliabile sapere quanti sono i simboli da scrivere, ma solo in modo algebrico (attraverso le variabili).
Per esempio, non si dovrebbe mai pensare che le righe sono 4 e i punti iniziali sono 4, perché con un altro
valore di n lo schema sarebbe diverso. Quello che va considerata è la relazione tra i numeri, non il fatto che
ci siano 4 righe o 4 simboli. Il ragionamento corretto deve andare oltre: se con n uguale a 5 righe e punti
sono 4, bisogna chiedersi cosa dovrebbe succedere con un altro valore di n, ad esempio 7. Possiamo anche
pensare ad una tabella di corrispondenza che esprima la relazione tra il valore di n e il numero di punti:
1234567654321
......6
......5
......4
......3
......2
......1
Valore di n
n. punti
3
5
7
8
2
4
6
7
Una volta verificato (non è difficile) che con n uguale a 7 righe e punti sono 6, si arriva alla conclusione che
in realtà il valore da usare per contare non è 4, non è 6, ma n-1. I punti saranno in totale (n-1)2 (ogni riga
ne contiene n-1, le righe sono n-1).
Alla fine di ogni riga, prima di andare a capo, c’è un numero progressivo (decrescente). In una situazione
del genere, la soluzione più semplice è fare in modo che il ciclo esterno della struttura sia discendente.
for (j=1; j<=n; j++) cout << j;
for (j=n-1; j>=1; j--) cout << j;
cout << endl;
for (i=n-1; i>=1; i--)
{
for (j=1; j<=n-1; j++) cout << ".";
cout << i;
cout << endl;
}
//
Riga 1 (2 cicli + a capo)
//
Righe 2-n (doppio ciclo)
//
//
//
Un punto per volta
L’indice di riga
A capo
And then some
Possiamo provare a vedere cosa serve per fare uno schema simile, ma con variazioni diverse.
7654321234567
......2
......3
......4
......5
......6
......7
Le uniche variazioni nel nuovo schema sono nella variabilità (cicli ascendenti e discendenti sono scambiati).
Nella prima riga il primo ciclo è discendente e il secondo ascendente, partendo da 2; nella seconda parte il
movimento è ascendente e il primo valore è 2. Bisogna sempre considerare se questi 2 siano sempre validi o
se siano red herrings (false tracce): in questo caso ci sono pochi dubbi, perché al centro della prima riga c’è
necessariamente 1, quindi è corretto che il secondo ciclo e il doppio ciclo inizino da 2.
Con poche modifiche si mette tutto a posto:
for (j=n; j>=1; j--) cout << j;
for (j=2; j<=n; j++) cout << j;
cout << endl;
for (i=2; i<=n; i++)
{
for (j=1; j<=n-1; j++) cout << ".";
cout << i;
cout << endl;
}
// Rovesciare i cicli
// Usare un 2 – fino a
n
// Rovesciare il ciclo
// Usare un 2
Variabilità all’interno dei doppi cicli
Alcuni schemi più variabili presentano maggiori difficoltà nell’impostazione dei cicli. Si può vedere con
alcune lettere dell’alfabeto con linee diagonali: i cicli di punti sono variabili (in più, in meno, variazione di 1,
di 2, ecc.). La scrittura di un ciclo corretto di punti (ciclo interno) può indurre all’errore, per il mancato
riconoscimento dei tempi del programma: nella sequenza delle righe, occorre individuare le variazioni che
avvengono TRA UNA RIGA E L’ALTRA. Ecco alcuni esempi di schemi che necessitano di una
progettazione particolarmente precisa, tra cui una clessidra e un’improbabile Q:
654321
....2
...3
..4
.5
654321
6.........6
.5.......5
..4.....4
...3...3
....2.2
.....1
........1 (n=5)
.......2.2
......3...3
.....4.....4
....543212345
...4.........4
..3...........3
.2.............2
1...............1
6....6
55...5
4.4..4
3..3.3
2...22
1....1
123454321 (n=5)
.2.....2
..3...3
...4.4
....5
...4.4
..3...3
.2.....2
123454321
123456
2....5
3....4
4....3
5....2
654321
111111
2....2
3....3
4....4
5....5
666666
11111111 (n=8)
2......2
3......3
4......4
5......5
6....6.6
7.....77
88888888
........9
666666 (n=6)
5....5
4....4
3....3
2....2
111111
1....1 (n=4)
2....2
3....3
444444
3....3
2....2
1....1
Vedremo che, come sempre, molti programmi si somigliano (c’è poco da inventare) .
Progettazione di schemi variabili
Esempio 1
.....1
....2
...3
..4
.5
6
Esempio 1 con n=6
Lo schema è composto da "n" righe (ciclo)
Composizione di ogni riga: punti (ciclo) + numero + a capo
punti presenti nella prima riga: n-1 (verificare con altri n)
Il ciclo delle righe va da 1 a "n". Al suo interno va un ciclo di punti, poi la riga si conclude con un altro
numero e andando a capo. I numeri da scrivere corrispondono alla riga, quindi si può usare la variabile “i”
che conta i giri del ciclo.
ATTENZIONE. Il problema di programmare i punti nelle varie righe è spesso risolto in modo errato.
L’errore più frequente è scrivere un ciclo discendente, applicando scorrettamente la soluzione “tra una riga e
la seguente, i punti sono uno di meno”. Questo è l’errore che generazioni di studenti hanno fatto e
probabilmente continueranno a fare:
for (i=1; i<=n; i++)
{
for (j=n-1; j>=0; j--) cout << ".";
...
ERRORE GRAVISSIMO !!! (d-t) NON È QUESTA LA SOLUZIONE
Con questo “metodo”, il numero di punti È SEMPRE LO STESSO IN TUTTE LE RIGHE (in questo
caso n-1, con n che non cambia per tutta la durata del doppio ciclo). L’istruzione “j--“ non fa cambiare il
numero di punti, è solo un’illusione.
I punti diminuiscono tra una riga e l’altra. Applicare un ciclo discendente all’interno della stessa riga è
inutile. Occorre applicare un diverso ragionamento. Osservando lo schema, bisogna considerare che nella
prima riga abbiamo n-1 punti, nella seconda n-2 e così via, quindi sempre di meno (tra una riga e l’altra,
non all’interno della stessa). Come sempre:
INTRODUCIAMO UNA NUOVA VARIABILE CHE DEFINISCE IL NUMERO DI PUNTI
Possiamo chiamarla p. All’inizio del ciclo le assegniamo n-1 (il numero di punti della prima riga), ma questo
vale solo per il primo giro; se alla fine di ogni riga la variabile viene diminuita, al prossimo giro del ciclo
esterno (la riga successiva) il ciclo interno fa un giro in meno. In questo modo ogni riga è più corta di quella
precedente, perché la variabile che conta i punti è minore.




CON LA NUOVA VARIABILE IL CICLO INTERNO È UN CICLO BASE.
LA NUOVA VARIABILE HA UNA VITA PROPRIA ALL’INTERNO DEL CICLO ESTERNO.
AD OGNI RIGA TOGLIAMO 1, IN MODO DA PREPARARE LA RIGA SUCCESSIVA.
ALL’ULTIMA RIGA I PUNTI DIVENTANO ZERO (PROBLEMA?).
p = n-1;
for (i=1; i<=n; i++)
{
for (j=1; j<=p; j++) cout << ".";
cout << i << endl;
p--;
}
// NUMERO INIZIALE DI PUNTI
// questo è il ciclo dei "p" punti
// preparazione riga SUCCESSIVA
Possiamo pensare il procedimento in questo modo:
Prendiamo a calci la variabile per modificarla
Esempio 2
.....6
....5.5
...4...4
..3.....3
.2.......2
1.........1
Esempio 2: schema più complesso (doppio) con n=6
La prima riga è indipendente dalle altre
Dalla seconda in poi sono simili (DOPPIO CICLO)
Composizione di ogni riga:
punti + numero + punti + numero (servono 2 cicli)
Questa volta il ciclo per la prima riga (uno solo, la figura non è piana) è indipendente, mentre il doppio ciclo
successivo delle righe va da "n-1" a 1. Al suo interno ci sono due cicli di punti seguiti ciascuno da un
numero, poi si va a capo. I numeri da scrivere corrispondono alla riga, quindi si può usare la variabile “i” che
conta i giri del ciclo. I DUE CICLI HANNO UN NUMERO DIVERSO DI PUNTI, SERVONO DUE
VARIABILI PER DESCRIVERLI (le chiameremo p1 e p2). Lo schema di ogni riga è il seguente:
punti - numero - punti – numero - (a capo)
Nella prima riga ci sono n-1 punti (verificabile con altri valori di n), più lo stesso numero n.
Nella seconda riga ci sono n-2 punti (uno in meno che nella prima) nel primo ciclo e 1 nel
secondo. Le altre righe procedono togliendo 1 punto dal primo ciclo e aggiungendone 2 al secondo,
perché la figura si apre da due parti: è facile verificare che i punti sono 1, 3, 5, eccetera.
ATTENZIONE !!! il ciclo delle RIGHE è discendente perché lo sono i numeri scritti in ogni riga. I cicli con
“p1” e “p2” NON DEVONO ESSERE DISCENDENTI: normalmente, si conta a partire da 1.
for (j=1; j<=n-1; j++) cout << ".";
prima riga (punti + numero)
cout << n << endl;
il numero scritto è “n”
p1 = n-2;
numero iniziale di punti (CICLO 1)
p2 = 1;
numero iniziale di punti (CICLO 2)
for (i=n-1; i>=1; i--)
ciclo discendente come i numeri
{
for (j=1; j<=p1; j++) cout << "."; primo ciclo
I due cicli interni possono
cout << i;
numero (1.volta)
usare lo stesso indice j perché
for (j=1; j<=p2; j++) cout << "."; secondo ciclo
non sono concatenati: tanto
cout << i;
numero (2.volta)
vale riciclare la variabile.
cout << endl;
a capo
p1--;
questa è la diminuzione
p2+=2;
questo è l'aumento
}
I due cicli di punti sono sequenze base: è inutile andare alla rovescia (anche se è possibile). Rende più
difficile la comprensione del programma. Quello che si deve capire è che, nella stessa riga:
è richiesto un numero di punti pari a p1 (1.ciclo)
è richiesto un numero di punti pari a p2 (2.ciclo)
La diminuzione non è la soluzione. È inutile scrivere: for (j=p2; j>=1; j--) cout << ".";
Esempio 3
Può servire molto anche l’esempio rovesciato, soprattutto per sapere come iniziare a contare i punti dei due
cicli e come si deve proseguire nelle righe successive:
1.........1
.2.......2
..3.....3
...4...4
....5.5
.....6
All’inizio, i punti del primo ciclo
sono zero. Esiste il ciclo vuoto:
basta porre p1=0.
Ad ogni riga aumentano di 1.
Per vedere la larghezza della prima riga:
1.........1
12345654321
La larghezza totale è 2*n, meno 1 (c’è la
punta). I punti della prima riga sono quindi
2*n-3, perché 2 spazi sono occupati.
Ad ogni riga diminuiscono di 2 (non di 1)
Esempio 4
Proviamo a seguire passo per passo la costruzione di una lettera N. Come sempre in questi esercizi, a volte
risulta utile vedere sulla carta più casi (con valori di n diversi), per evitare di seguire red herrings (false
tracce) e proporre calcoli fallaci; possibilmente, meglio provare un valore pari e uno dispari, sempre per
diminuire le sorprese:
6....6 (n=6)
55...5
4.4..4
3..3.3
2...22
1....1
7.....7 (n=7)
66....6
5.5...5
4..4..4
3...3.3
2....22
1.....1
La prima cosa da osservare è che le righe possono essere considerate discendenti: questo viene suggerito dai
numeri del disegno, e sarà opportuno riportare il concetto nel ciclo delle righe (quello esterno).
Ma prima di mettere mano al doppio ciclo, dobbiamo distinguere la prima (n) e l’ultima (1) riga, che hanno
uno schema diverso dalle altre. Sono righe singole, quindi non presentano un doppio ciclo, ma uno singolo
per la sequenza di punti. Quindi una prima suddivisione del problema ci porta a tre fasi:
riga n
una riga
righe da "n-1" a 2 un ciclo di righe
riga 1
una riga
Incidentalmente, la struttura delle righe marginali (n e 1) è simile a quella della struttura generale, come
vediamo da un esame della riga n:
variabile n
punti
variabile n
un simbolo
un ciclo di simboli
un simbolo
Il prossimo passo è il conteggio dei punti nelle varie fasi del programma. Nelle righe marginali, se
consideriamo la larghezza del disegno (esattamente n simboli), arriviamo immediatamente alla conclusione
che i punti da scrivere nel ciclo sono n-2. La riga n e la 1 risultano quindi:
Riga 1
Riga n
cout << n;
for (j=1; j<=n-2; j++) cout << ".";
cout << n;
cout << endl;
La riga 1 sarà fatta allo stesso
modo, con 1 al posto di n.
Anche le righe nel doppio ciclo sono n-2; per contarle potrà bastare un ciclo discendente da n-1 a 2. Le
righe del doppio ciclo sono fatte tutte allo stesso modo:
variabile i
punti
variabile i
punti
variabile i
a capo
un simbolo
primo ciclo di simboli
un simbolo
secondo ciclo di simboli
un simbolo
Si può usare l’indice i
(quello che conta le righe).
I due cicli variano in modo diverso tra una riga e l’altra (uno aumenta, l’altro diminuisce). Rimane il
problema di determinare quanti sono lunghe le due sequenze (p1 e p2), che va risolto osservando l’aspetto
del disegno. Il risultato per il ciclo centrale sarà:
Punti del primo ciclo all’inizio
zero
p1 = 0;
p2 = n-3;
Punti del secondo ciclo all’inizio
n-3
for( i=n-1; i>=1; i--)
(il disegno è largo n spazi, di cui 3 occupati).
{
cout << i;
for (j=1; j<=p1; j++) cout << ".";
//
primo ciclo
Righe
cout << i;
centrali
for (j=1; j<=p2; j++) cout << ".";
//
secondo ciclo
cout << i;
cout << endl;
p1++; p2--;
//
per la prossima riga
}
Le due versioni
Questo è il risultato finale per il problema esaminato. La soluzione è suddivisa nelle tre fasi, evidenziate con
righe di commento: notare la differenza tra le righe marginali (ciclo singolo) e quelle centrali (ciclo doppio):
cout << n;
for (j=1; j<=n-2; j++) cout << ".";
cout << n;
cout << endl;
//
p1 = 0;
p2 = n-3;
for( i=n-1; i>=2; i--)
{
cout << i;
for (j=1; j<=p1; j++) cout << "."; //
cout << i;
for (j=1; j<=p2; j++) cout << "."; //
cout << i;
cout << endl;
p1++; p2--;
}
6....6
55...5
4.4..4
3..3.3
2...22
1....1
primo ciclo
secondo ciclo
//
cout << 1;
for (j=1; j<=n-2; j++) cout << ".";
cout << 1;
Questa è la soluzione del problema con un disegno simile, ma rovesciato. La struttura generale del
programma non varia: c’è uno scambio di snippet per le righe marginali e un rovesciamento (che potremmo
anche chiamare raddrizzamento) del doppio ciclo delle righe centrali.
cout << 1;
for (j=1; j<=n-2; j++) cout << ".";
cout << 1;
cout << endl;
//
p1 = 0;
p2 = n-3;
for( i=2; i<=n-1; i++)
{
cout << i;
for (j=1; j<=p1; j++) cout << "."; //
cout << i;
for (j=1; j<=p2; j++) cout << "."; //
cout << i;
cout << endl;
p1++; p2--;
}
//
cout << n;
for (j=1; j<=n-2; j++) cout << ".";
cout << n;
1....1
22...2
3.3..3
4..4.4
5...55
6....6
primo ciclo
secondo ciclo
modulI
Importanza della programmazione modulare
Già nei primi esercizi, appena le richieste si complicano un po', si nota che spesso vanno riscritte intere
sequenze di istruzioni, con perdita di tempo e possibilità di errori: anche il “copia e incolla” ha i suoi limiti,
perché spesso vanno usate variabili diverse, e nel cambiare nome alle variabili alcune ricorrenze possono
sfuggire. In generale, quando i programmatori (quelli veri) affrontano problemi complessi, non possono
limitarsi a scrivere lunghe sequenze di istruzioni: per rendere più produttivo il proprio lavoro, devono
identificare soluzioni parziali che possano essere usate in problemi analoghi: in questo modo sono in grado
di ridurre i nuovi problemi ad altri già risolti.
Per venire incontro a questa esigenza, i linguaggi di programmazione offrono una soluzione agevole per
raggruppare gruppi di istruzioni sotto un unico nome, permettendone così l'attivazione in modo semplice e
(soprattutto) in programmi diversi. Si usano parecchi nomi per definire questi “gruppi di istruzioni”:
sottoprogrammi, procedure, funzioni, function, moduli. Si dice programmazione modulare la pratica di
combinare più moduli per costruire programmi complessi; l’obiettivo principale è di mettere a propria
disposizione una collezione di moduli intercambiabili tra un progetto e l’altro, riducendo i tempi di sviluppo
con l’uso di moduli già esistenti.
Oltre a fornire strumenti per la produzione, i moduli permettono anche di ridurre gli errori, perché un modulo
funzionante non ha solitamente bisogno di correzioni, essendo già stato controllato in situazioni precedenti:
anche se spuntassero errori inaspettati a distanza di tempo, è più facile affrontare un programma breve e
circoscritto che andare ad infilarsi in migliaia di istruzioni alla ricerca dei punti da correggere.
Partiamo da una situazione reale, di quelle affrontate nei problemi precedenti. Più volte si sono incontrate
sequenze di punti da inserire nelle righe:
for (j=1; j<=n-1; j++) cout << ".";
for (j=1; j<=p; j++) cout << ".";
for (j=1; j<=p1; j++) cout << ".";
Si tratta di sequenze base, solitamente facili da identificare per chi legge il programma, ma vanno
riconosciute all’interno di altre strutture, e comunque bisogna capire il conteggio dalla lettura di una riga
magari sepolta tra altre. La saggezza storica insegna che è più comodo avvicinarsi al linguaggio (umano)
reale e poter scrivere, in una forma più colloquiale:
Scrivi "n-1" punti
Scrivi "p" punti
Scrivi "p1" punti
La soluzione offerta dal C++ (e, con piccole variazioni, da quasi tutti i linguaggi) è associare un intero
snippet con un nome e con il numero richiesto di punti, più o meno come si associa alla parola cout ad una
variabile numerica. Questo modo di operare viene detto “chiamata di un modulo”:
punti(n-1);
analogamente: cout << n-1;
Il valore tra parentesi, detto argomento,
punti(p);
cout << p;
viene passato al modulo.
punti(p1);
cout << p1;
Lo snippet viene scritto sotto forma di modulo con intestazione, graffe e variabili (come il modulo main). Il
modulo deve avere un tipo, non necessariamente int (l’esempio usa un particolare tipo “vuoto” void), un
nome, e due parentesi, tra le quali possono esserci zero o più parametri, detti a volte dati di ingresso (input):
void punti( int p)
//
void = nessun valore in uscita
{
int i;
for( i=1; i<=p; i++) cout << ".";
}
Un modulo void esegue delle azioni, a volte anche dei calcoli, ma non forniscono valori per il programma
chiamante. Più avanti troveremo anche moduli int o double, che servono a calcolare valori di output,
analogamente alle funzioni matematiche comprese nel linguaggio, quali il “valore assoluto”: x=abs(n).
I moduli attivati devono essere noti al modulo che li chiama: in pratica, devono essere scritti prima, in modo
che il compilatore li abbia già incontrati al momento in cui trova la chiamata. Il risultato finale sarà quindi
con il main per ultimo:
void punti( int p)
//
ATTENZIONE !!! i parametri NON
{
//
vanno dichiarati nelle variabili
int i;
for( i=1; i<=p; i++) cout << ".";
}
int main()
{
int n;
n = 18;
punti(4);
//
"Chiamata", detta anche
punti(n);
//
attivazione, call, evoke (antiquato)
getchar();
}
È possibile incontrare un altro modo (più antiquato) di organizzare i moduli, con l’elenco delle intestazioni
(prototipi) precedente al main, e i moduli completi scritti dopo: nelle intestazioni non è necessario il
nome dei parametri, solo il tipo. L’uso dei prototipi non è consigliato, essendo necessario solo in casi
particolarmente complessi, ma ogni tanto capita di trovarli quando si va in giro per la rete a cercare
soluzioni. Con i prototipi, la struttura del programma diventa come la seguente:
void punti( int);
int main()
{
... (corpo del main)
}
void punti( int p)
{
... (corpo del modulo)
}
Look ma, no parameters
Esistono situazioni in cui un sottoprogramma non necessita di parametri, solitamente perché compie una
sequenza di operazioni sempre uguali.
Negli esempi finora incontrati una delle poche situazioni che si possono semplificare con un modulo senza
parametri è l’istruzione per andare a capo. Non è una gran cosa, ma serve per capire il meccanismo delle
parentesi, che non possono mancare in un modulo, anche in assenza di parametri:
Istruzioni da inserire nel modulo (una sola, in realtà):
cout << endl;
Le inseriamo in un modulo che chiameremo lf (line feed,
un’espressione dei vecchi tempi). Come risulta il modulo:
void lf()
{
cout << endl;
}
LE PARENTESI SONO VUOTE – è proprio così
Come risulta un main:
int main()
{
int n;
n = 18;
punti(4);
lf();
punti(n);
getchar();
}
ATTENZIONE !!!
I moduli esaminati sono molto elementari. Sono stati scelti proprio per la loro semplicità e
per la conseguenza con gli argomenti precedenti, con l’obiettivo di fare i primi passi nella
programmazione modulare. Il risultato più importante è quello di snellire e semplificare il
modulo main.
Passaggio dei parametri
È fondamentale comprendere come avviene il passaggio del controllo delle operazioni e dei dati dal
programma chiamante al programma chiamato.
Seguiamo i movimenti del programma nello snippet visto in precedenza:
punti(4);
punti(n);
//
il valore di "n" è 18
La sequenza delle operazioni è la seguente, con il responsabile dell’azione evidenziato in nero:
punti(4); // il main cede il controllo al modulo, passandogli 4 (costante)
void punti( int p) // il modulo entra in azione
sostituisce il parametro con l’argomento (p diventa 4)
esegue con il nuovo valore di p
il modulo termina (graffa chiusa)
restituisce il controllo al chiamante
il chiamante prosegue con l’istruzione successiva
punti(n); // il main cede il controllo al modulo, passandogli 18 (variabile n)
void punti( int p) // il modulo entra in azione
sostituisce il parametro con l’argomento (p diventa 18)
esegue con il nuovo valore di p
il modulo termina (graffa chiusa)
restituisce il controllo al chiamante
il chiamante prosegue con l’istruzione successiva
Il trasferimento del controllo (nella prima chiamata) può essere visualizzato con un diagramma:
CONCLUSIONI
Regole d’oro da tenere sempre presenti, soprattutto quando i problemi diventano complessi:




La programmazione modulare serve a dividere gli algoritmi in parti più semplici;
Un modulo deve risolvere un problema specifico (1 problema = 1 modulo);
La variabilità delle operazioni di un modulo è data dal valore degli argomenti passati;
Un modulo serve se può essere usato più volte, anche (anzi, soprattutto) in programmi diversi.
Parte dell’abilità di un programmatore risiede nell’individuare soluzioni simili anche in situazioni
apparentemente diverse, preparandosi una batteria di moduli pronti da usare nei programmi più disparati.
Moduli per calcolo di valori
Il linguaggio FORTRAN (il primo fra tutti) propone due nomi diversi per i moduli:
 subroutine: sottoprogrammi non destinati a fornire valori al chiamante, analoghi a quelli void visti
negli esempi precedenti;
 function: sottoprogrammi che forniscono valori al chiamante, analoghi agli operatori matematici del
linguaggio (uno dei quali è il “valore assoluto” x = abs(n) ).
Il termine subroutine fu sostituito da procedure nel linguaggio PASCAL, mantenendo la distinzione con la
function. La terminologia è ora meno rigida, e in alcuni linguaggi si usa il termine standard function, che ci
sia un valore da restituire in uscita oppure no2.
Nel linguaggio C++ quello che fa testo è il tipo (void, int, double o qualsiasi altro tipo ammesso). Tutti i
linguaggi, comunque, fanno uso della stessa istruzione return per far uscire un valore dal modulo e
mandarlo al programma chiamante. Non a caso si dice anche “restituire” un valore.
Un esempio molto semplificato può essere un modulo che calcola il doppio di un numero. Anche se non di
grande utilità, esso consente di vedere il processo di trasformazione dei dati di ingresso (input) in quelli di
uscita (output):
int doppio( int n)
//
"n" = dato di ingresso
{
Si può anche essere più sbrigativi, a
int d;
//
Variabile interna
patto che il modulo non perda in
d = 2 * n;
//
Trasformazione
semplicità (KISS):
return d;
//
dato di uscita
int doppio( int n)
}
{
int main()
return 2 * n;
{
}
int n;
n = 18;
x = doppio(4);
//
Chiamata e assegnazione
z = doppio(n);
//
"x" diventa 8; "z" diventa 36
getchar();
}
Il modulo è stato presentato in modo da evidenziare la possibilità di usare variabili interne, indipendenti dagli
altri moduli: la variabile "d" usata nel modulo è usata solo in esso, rimanendo sconosciuta al main. Si
consiglia di usare le variabili interne, evitando quelle del chiamante, per evitare rischi di modifiche non
previste a variabili con lo stesso nome.
La differenza fondamentale con un modulo
void è la presenza di un valore di output,
che rientra nel programma chiamante,
esattamente come qualsiasi altro valore
int, (o double, dove serve), e può essere
assegnato ad una variabile.
Quindi il programma chiamante, prima di
proseguire con l’istruzione successiva, può
elaborare il dato che viene restituito dal
modulo.
Nel diagramma a fianco, il valore calcolato
(8) viene assegnato alla variabile x.
2
Il termine procedure è sopravvissuto in un'altra espressione: vanno tuttora sotto il nome di programmazione
procedurale (o imperativa) le tecniche tradizionali per la scrittura di programmi, il cui paradigma consiste nella scrittura
di istruzioni guidate da una sequenza di controllo (come avviene in un main). La programmazione ad oggetti
(OOP=Object Oriented Programming), ora più considerata, ha modalità più elastiche, anche se non così diverse da
quella procedurale come certi affermano. L’OOP non è oggetto di questo corso, se non in modo molto marginale.
Programmazione modulare con i “numeri interessanti”
Per la scrittura di programmi complessi, diventa necessario scomporre i programmi in “moduli” piccoli e
maneggevoli (leggi: facilmente modificabili). Un modo per iniziare è approfondire lo studio delle proprietà
dei numeri interi, che non richiedono operazioni particolarmente difficili, ma sequenze ben precise di calcoli.
È questa necessità di precisione che può aiutare a suddividere un problema in componenti meno complesse.
La difficoltà sta nel mettere assieme i moduli per costruire un progetto completo.
Si considera “matematica ricreativa” una collezione di enigmi, giochi, proprietà dei numeri, proprietà
geometriche tra lo strano, il curioso e l’inutile. Il loro studio, spesso usato come passatempo, ha tuttavia
portato, talvolta, allo studio di aree matematiche con applicazioni inaspettate (come è successo con i numeri
di Fibonacci, i numeri primi, o con molti problemi studiati da Eulero).
Tra i numeri interi si trovano parecchi esempi di proprietà curiose, come le seguenti:
1 Numeri di Carmichael: sono quelli i cui fattori primi, diminuiti di 1, sono divisori del numero stesso
diminuito di 1. 561 è il primo numero che soddisfa questa proprietà: infatti, i suoi divisori primi (3, 11, 17),
una volta diminuiti di 1, risultano tutti divisori di 560.
2 Numeri di Smith: sono quelli in cui la somma delle cifre è uguale alla somma delle cifre di tutta la sua
fattorizzazione (compresi i dattori primi ripetuti). Il numero “originale” è 4937775 = 3×5×5×65837 (la
somma delle cifre è 42), ed è stato così chiamato perché era il numero di telefono di un tale Smith. Anche
202 è numero di Smith: la somma delle sue cifre è 2+0+2=4, come la somma delle cifre dei suoi fattori
(2×101).
Nella base 10, i primi 20 numeri di Smith sono
4 22 27 58 85 94 121 166 202 265 274 319 346 355 378 382 391 438 454 483
Nella programmazione, questi numeri “interessanti” offrono proprietà che, pur essendo molto semplici da
intuire, presentano una serie di problemi coordinati, che si rivelano molto utili nello studio della modularità.
Solo negli esempi sopra ricordati, troviamo: divisibilità, suddivisione delle cifre, proprietà dei numeri primi,
scomposizione degli interi in fattori. Si possono così affrontare semplici problemi di progettazione,
abituandosi a scrivere sottoprogrammi e ad assemblarli nella costruzione di soluzioni più complesse.
Esempio: scomposizione di un numero nelle sue cifre
Un problema forse meno “interessante” dei precedenti dal punto di vista numerico, ma utile per la semplicità
dei problemi, riguarda l’estrazione delle cifre di un numero. Per prima cosa si tratta di tradurre le espressioni
“estrarre una cifra” e “togliere una cifra” nelle corrispondenti operazioni: la soluzione, aritmeticamente
semplice e (presumibilmente) nota a tutti è fare dei resti e delle divisioni per 10. Non bisogna trascurare il
dettaglio della ripetizione, ma anche questa è di facile comprensione.
u = n % 10
calcola la cifra delle unità
n = n / 10
toglie la cifra delle unità dal numero (che viene modificato)
while ( n > 0
{
u = n %
cout <<
n = n /
}
)
10;
u;
10;
Come sempre, occorre un ciclo per fare ripetere le
operazioni. Il ciclo opera togliendo la cifra delle unità
(con /10), ed ha termine quando il numero è stato
completamente “distrutto” (aritmeticamente, quando
diventa ZERO)
Esercizio 1
Inserire lo snippet precedente in un modulo void ed attivare il tutto da un main.
Esercizio 2
Modificare il modulo precedente trasformandolo in un int che restituisca il conteggio delle cifre.
Esercizio 3
Modificare il modulo precedente trasformandolo in un int che restituisca la somma delle cifre.
Calculemus – problemi di matematica ricreativa
“Calculemus” lo diceva Leibniz (Gottfried Wilhelm von Leibniz, 1646-1716). Quello che
aveva in mente era un modo per risolvere ogni discussione mettendola in numeri: una specie
di intelligenza artificiale ante litteram. Più modestamente, in questo capitolo esamineremo le
proprietà dei numeri attraverso dei programmi.
Con i numeri interi si possono fare infiniti tipi di calcolo, dai più semplici ai più intricati. Ad
esempio, la collezione dei problemi che hanno a che fare con la suddivisione delle cifre è
vastissima, ed esplora un gran numero di proprietà dei numeri interi: alcune possono trovare
applicazioni pratiche, altre fanno parte della cosiddetta “matematica ricreativa”, ovvero null’altro che
l’esame di una serie di proprietà curiose ed interessanti3.
Nei seguenti esempi, vedremo come alcuni matematici passano il loro tempo.
Numero di Kaprekar
Si prende un numero e se ne calcola il quadrato. Si divide il quadrato in due “metà”, che vengono
sommate. Se il numero di partenza è uguale a questa somma, viene detto “numero di Kaprekar”.
Esempi:
452=2025; le due metà, 20 e 25, sommate, danno di nuovo 45;
22232=4941729; le due metà, 494 e 1729, sommate, danno di nuovo 2223;
152=225; le due metà, 2 e 25, sommate, NON danno 15.
Non sono vere metà aritmetiche: il quadrato viene diviso in due parti con lo stesso numero di cifre,
anche se a volte la metà di sinistra ha una cifra in meno (se le cifre sono dispari)
Numero Gapful
Si prende un numero e se ne calcola un altro mettendo assieme la prima e l’ultima cifra. Se il numero di
partenza divisibile per il risultato, viene detto “numero Gapful”.
Esempi:
1729 è divisibile per 19;
5001 non è divisibile per 51.
Numero Harshad
Se un numero è divisibile per la somma delle sue cifre, viene detto “numero Harshad”.
Esempi:
1729 è divisibile per 19;
5001 non è divisibile per 6.
Dattaraya Ramchandra Kaprekar
Matematico indiano (1905-1986). Non era un accademico, ma un insegnante delle superiori. Appassionato di
teoria dei numeri, diventò uno dei più noti matematici “ricreativi”, pubblicando innumerevoli lavori di
ricerca. Alle proprietà dei numeri dava spesso nomi fantasiosi, come “Devlali” (il nome della sua città),
“Harshad” (“che porta gioia” in Sanscrito), “Demlo” (la stazione ferroviaria dove gli era venuta l’idea).
3
Forse potranno servire a qualcosa, forse no, ma per molti sono un gradevole passatempo scientifico. C’è un caso
famoso di proprietà sfruttate dopo secoli: usando i numeri primi, alcune considerazioni di Pierre de Fermat (1601?1665) si sono rivelate utili per costruire, nel 1978, algoritmi di crittografia per rendere sicure le transazioni digitali.
Moduli per la divisibilità
Un problema nel quale lo stesso procedimento va applicato più di una volta è quello dei numeri “amicali”.
Così vengono detti due numeri tali che la somma dei divisori di ciascuno è uguale all’altro. La più piccola
coppia di tali numeri è formata da 220 e 284:


Somma dei divisori propri di 220: 1+2+4+5+10+11+20+22+44+55+110 = 284
Somma dei divisori propri di 284: 1+2+4+71+142 = 220
Soluzione sconsigliabile
cin >> n >> z;
s1 = 0;
for( i=1; i<n; i++)
if ( n % i == 0 ) s1+=i;
Questa non è la soluzione migliore: si
notano le istruzioni “doppie”, che fanno le
stesse operazioni su variabili diverse. In
questo caso sono poche, ma comunque
tolgono
chiarezza
e
leggibilità
al
programma
s2 = 0;
for( i=1; i<z; i++)
if ( z % i == 0 ) s2+=i;
if ( s1 ==
{
cout
cout
}
else
{
cout
cout
}
z && s2 == n )
<< n << " e " << z;
<< " sono amicali";
<< n << " e " << z;
<< " NON sono amicali";
Soluzione modulare
int sommaDivisori( int x)
{
int i, s;
s = 0;
for ( i = 1; i < x; i++)
if ( x % i == 0 ) s+=i;
return s;
}
cin >> n >> z;
if ( sommaDivisori(n) == z &&
sommaDivisori(z) == n )
{
cout << n << " e " << z;
cout << " sono amicali";
}
else
{
cout << n << " e " << z;
cout << " NON sono amicali";
}
In questo modo le istruzione per la soluzione sono scritte solo una volta, nel modulo chiamato
sommaDivisori. Non c’è bisogno di ripeterle nel main, è sufficiente mettere il nome del modulo per
attivarlo. Bisognerà comunque avere cura di indicare tra parentesi l’argomento del modulo, che andrà a
sostituire il parametro x, che diventa quindi un segnaposto (la sostituzione è analoga a quella che si fa in
algebra).
Numeri primi
Con un procedimento analogo al precedente, risolviamo il problema di determinare se un numero è primo.
int primo( int n)
{
int i, c;
c = 0;
for ( i = 2; i < n / 2; i++)
if ( n % i == 0 ) c++;
if ( c == 0 && n > 1 )
return 1;
else
return 0;
}
n = parametro del modulo; alla chiamata ci si aspetta un
numero intero tra parentesi
i = variabile che prova i possibili divisori di “n”
c = contatore per i divisori
return 1 = VERO
return 0 = FALSO
ATTENZIONE !!!
Ogni volta che un modulo deve verificare se una proprietà è vera o
falsa per un dato numero, dovrà comportarsi in modo analogo:


return 1 = VERO
return 0 = FALSO
Ricerca di numeri primi
Con il modulo appena scritto, si semplifica anche la ricerca di numeri primi: per esempio, risolviamo il
problema di trovare il numero primo successivo a uno dato. Nel caso il parametro n sia già primo, viene
restituito lo stesso valore4.
int prossimoPrimo( int n)
{
while ( ! primo(n) ) n++;
return n;
}
Non ci sarà bisogno di ripetere in questo modulo le
istruzioni per verificare quando la variabile “n” contiene
un numero primo: i dettagli sono nascosti nel modulo
“primo”. Anche il programma che chiama questo modulo
risulta semplificato:
z = prossimoPrimo(n);
Un vantaggio da non trascurare è che, oltre a semplificare il nuovo modulo, non c’è bisogno di esplicitare il
procedimento di verifica se un numero è primo: il problema è già risolto, dovremo solo applicare la
soluzione al nuovo problema. Questo si applica anche a moduli che chiamano altri moduli.
Esercizio 1
Trovare quanti numeri primi sono compresi tra due numeri dati.
La soluzione è un ciclo con filtraggio dei valori dell’indice: il ciclo per la verifica se un valore è primo è
nascosto nel modulo primo.
Esercizio 3
Elencare i fattori primi di un numero dato.
Procedimento simile al precedente, con la differenza che i possibili fattori primi del numero dato possono
essere cercati con il modulo prossimoPrimo.
Esercizio 2
Trovare quanti numeri primi sono fattori comuni di due numeri dati.
Estensione dell’esercizio precedente, con uso di una doppia condizione.
4
prossimoPrimo(15) restituisce 17, prossimoPrimo(17) restituisce di nuovo 17.
Estrazione delle cifre di un numero intero
Molti problemi di teoria dei numeri prevedono l’estrazione delle cifre di un numero intero. Il problema più
generale è: estrarre da un numero “n” la cifra in posizione “p”. Le cifre partono da DESTRA, quindi la
prima cifra è quella delle unità. Esempi:
n
p
cifra
45
1
5
Il caso più semplice: la PRIMA cifra (n%10)
4631
2
3
664581
3
5
1239245
4
9
8641
7
0
il numero può essere scritto come: 0008641
35425
-1
0
non ci sono cifre a destra della prima
35425
-3
0
idem
35425
0
0
idem
Gli ultimi 4 casi sono un po’ particolari, ma non presentano problemi aritmetici. Nel primo si usano cifre
nulle non significative; negli altri la posizione nulla o negativa non viene presa in considerazione. In teoria si
dovrebbero usare i decimali, ma questo verrà fatto in altri corsi.
Il caso più semplice è quando la posizione “p” è 1, dove il risultato “z” si ottiene immediatamente:
z = n % 10;
Per risolvere gli altri casi, è sufficiente ricondursi al caso precedente:
n
p
nuovo "n"
unità del nuovo "n"
4631
2
463
3
664581
3
6645
5
1239245
4
1239
9
Osservando i numeri, si può notare che il primo viene diviso per 10, il secondo per 100, il terzo per 1000
(rispettivamente 1,2,3 volte divisioni per 10). Il caso generale è:
1. Dividere il numero n per 10, p-1 volte (è una sequenza base);
2. Alla fine, la cifra delle unità è quella desiderata.
Il metodo risolve tutti i casi “normali” (cifre in posizioni da 1 in avanti). Per le posizioni inesistenti
(negative), si può inserire un caso a parte, con una clausola “if”.
for(i=1;i<=p;i++) z/= 10;
u = z % 10;
//
ora u è la cifra desiderata
Una cifra per volta
Quando le cifre servono tutte, prima della demolizione si estrae la cifra delle unità (questo equivale a usare
una cifra diversa ogni volta che si passa per il ciclo:
...
while( z > 0 )
{
u = z % 10;
//
estrazione della cifra delle unità
d = z / 10 % 10;
//
o forse anche delle decine
...
//
viene fatto qualcosa con la cifra
z/= 10;
//
ELIMINAZIONE della cifra delle unità
}
...
PERICOLO D-T !!! Ancora una volta: la modifica del numero “z” (o qualunque sia il
numero che viene controllato) DEVE essere l’ultima istruzione del ciclo. Se venisse messa
prima, il valore della variabile “z” verrebbe alterato e i calcoli corrispondenti sarebbero
quasi sicuramente falsati.
Applicazioni
Ora applichiamo il procedimento di scomposizione di un numero nelle sue cifre, scrivendo un modulo che
visualizzi tutte le cifre di un numero.
void scrittura( int
{
int u;
while ( n > 0
{
u = n %
cout <<
n/= 10;
}
}
n)
)
10;
u;
Con un procedimento analogo, contiamo le cifre di un numero.
int cifre( int n)
{
int c;
c = 0;
while ( n > 0 )
{
c++;
u = n % 10;
n/= 10;
}
return c;
}
Per estrarre da un numero una cifra in una determinata posizione (0 è quella delle unità, 1 quella delle
decine, e così via, da destra a sinistra).
int cifra( int n, int z)
{
int c;
c = 0;
for( i=1; i<=z; i++)
{
n/= 10;
}
return n%10;
}
Dividendo il numero per 10 z volte, si eliminano le
prime z cifre: le unità del numero così modificato sono
la cifra in posizione z.
Come sempre in aritmetica, si parte da destra.
Con i due moduli precedenti, la somma delle cifre di un numero diventa:
int sommaCifre( int n)
{
int s;
s = 0;
for( i=0; i<cifre(n); i++)
s+= cifra(n,i);
return s;
}
Altre soluzioni
Somma cifre elevate a potenza
La soluzione precedente al problema della somma delle cifre può essere resa più generica, aggiungendo
come parametro un esponente al quale elevare le cifre di un numero durante la somma. Questo può risultare
utile per verificare alcune proprietà dei numeri.
Aggiungiamo un secondo parametro, che rappresenta l’esponente da assegnare ad ogni cifra elevata a
potenza. Esso ha un valore detto di default. Questa espressione definisce quello che un programma deve fare
in mancanza di indicazioni (default=mancanza). In pratica, se nel programma chiamante non viene scritto
un secondo argomento, il modulo supplirà il valore indicato dopo l’uguale: in questo caso, il parametro 1 fa
in modo che vengano sommate le cifre, esattamente come nella soluzione precedente (senza parametro). Un
richiamo con sommaCifre(n,0), invece, equivale al conteggio delle cifre del primo argomento.
int sommaCifre( int n, int e=1)
//
nessuna indicazione: usa 1
{
int s;
s = 0;
for( i=0; i<cifre(n); i++)
s+= (int)round( pow( cifra(n,i), e));
return s;
}
Scambio di variabili
Un altro modulo che risulterà utile è quello per scambiare il valore di due variabili. Piuttosto che scrivere
ogni volta le tre istruzioni necessarie, sarà opportuno “nasconderle” in un modulo, rendendo così immediata
la soluzione al problema dello scambio.
void swap( int &a, int &b)
{
int c;
c = a;
a = b;
b = c;
}
int main ()
{
int x,y;
x = 10;
y = 12;
cout << "prima " << x << " " << y << endl;
swap( x, y);
cout << "dopo " << x << " " << y << endl;
}
Scomposizione di un numero in fattori primi
Questo esercizio prevede la ricerca dei fattori primi di un numero, con l’aggiunta che, per ogni fattore, viene
calcolato l’esponente, ossia quante volte esso compare nella scomposizione del numero. Questa è una
versione ridotta, che può essere usata per verificare il procedimento. Per risolvere il problema del numero di
Smith, andrà completata con altre istruzioni che sommino le cifre del numero stesso ed altre che sommino le
cifre di tutti i suoi divisori primi. Alla fine andrà la verifica se queste somme sono uguali.
void scomposizione( int n)
{
divisore = 2;
while ( n > 1 )
{
while ( n % divisore == 0 )
{
cout << n << " " << divisore;
n/= divisore;
}
divisore = prossimoPrimo( divisore);
}
}
Costruzione di una libreria di moduli
Come già osservato in precedenza, esistono moduli che vengono usati in più situazioni. Per renderli sempre
disponibili occorre un meccanismo di “inclusione” analogo a quello che viene usato per rendere disponibili
le operazioni del linguaggio. Per fare questo, si possono scrivere tutti i moduli utili in un unico file, nel quale
mettere anche le solite istruzioni “include” delle componenti del linguaggio. NON VA SCRITTO IL
MODULO main(), perché questa è solo una raccolta di moduli. Questo file potrà essere salvato con
estensione H (header), oppure CPP.
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <time.h>
#include <iostream>
#include <limits>
using namespace std;
int primo( int n)
{
...
}
int prossimoPrimo( int n)
{
...
}
int cifre ( int n)
{
...
}
int cifra ( int n, int z)
{
...
}
int sommaCifre ( int n)
{
...
}
void swap ( int &a, int &b)
{
...
}
ATTENZIONE !!!
In questa libreria andrebbero solo i moduli che
vanno usati in più situazioni, vale a dire quelle
maggiormente usate.
È inutile memorizzare qua TUTTE LE
SOLUZIONI DEGLI ESERCIZI.
Per “guadagnarsi” la presenza in libreria, un modulo
deve essere usato in più programmi.
A questo punto, ogni nuovo programma può avere accesso a questa libreria: è sufficiente una direttiva di
“include” per incorporare la libreria in un altro programma. L’unica differenza con le solite “ include” è che
il nome della libreria va delimitato da virgolette, perché le parentesi angolate “ <>” indicano una cartella
particolare contenente le componenti del linguaggio. Per usare #include <moduli.h> sarebbe necessario (e
sconsigliabile) copiare il file in tale cartella5
#include "moduli.h"
int main()
{
...
z = prossimoPrimo(x);
...
}
5
N.B.
Se le include delle librerie C++ sono già scritte nella
libreria moduli.h, non è necessario ripeterle: sono già
disponibili attraverso le direttive scritte dall’altra parte.
La cartella può essere individuata con “Tools-Compiler Options-Directories”. Solitamente è:
C:\Programmi\Dev-Cpp\include
Problemi da risolvere con moduli
1
Numero di Kaprekar
Si prende un numero e se ne calcola il quadrato. Si divide il quadrato in due metà, che vengono sommate. Se
il numero di partenza è uguale a questa somma, viene detto “numero di Kaprekar”.
Esempi:
173442=300814336; le due metà, 3008 e 14336, sommate, danno di nuovo 17344;
7032=424209; le due metà, 424 e 209, sommate, danno di nuovo 703;
452=2025; le due metà, 20 e 25, sommate, danno di nuovo 45;
22232=4941729; le due metà, 494 e 1729, sommate, danno di nuovo 2223;
152=225; le due metà, 2 e 25, sommate, NON danno 15.
Il modo per ottenere le due metà del quadrato (anche quando ha un numero dispari di cifre) è di usare il
numero di cifre del numero di partenza, PARTENDO DA DESTRA.
Con numeri di 1 o 2 cifre il problema non è difficile: basta dividere il quadrato (e trovare il resto) per 10
oppure 100. Con altri numeri è necessario calcolare quante cifre ha “n” (vedi più avanti).
2
Dato un numero (z), contare quante cifre ha.
La soluzione è: dividere ripetutamente il numero per 10. Il ciclo termina quando il numero si annulla.
Contando il numero di passaggi nel ciclo, si ha il risultato richiesto.
3
Dato un numero (z), contare quante sue cifre sono uguali alla prima.
N.B.: “prima” si intende da destra, quindi è quella delle unità.
Possibili risultati:
z=485
0
z=45485
1
z=5555
3
(nessuna cifra uguale alla prima)
(una cifra uguale alla prima)
(tre cifre uguali alla terza)
4
Dati un numero (z) e una posizione (p), contare quante cifre di “z” sono uguali a quella che è nella
posizione ”p”.
N.B.: Possiamo usare due cicli, uno per trovare la cifra indicata, l’altro per contare.
Possibili risultati:
z=485
p=2
0
(nessuna cifra uguale alla seconda)
z=45485
p=3
1
(una cifra uguale alla terza)
z=5555
p=4
3
(tre cifre uguali alla terza)
5
Dato un numero (z), verificare se le sue cifre sono in numero pari.
Possibili risultati:
6
z=485
z=45485
z=5555
no
no
si
Numero Gapful (da Kaprekar)
Si prende un numero e se ne calcola un altro, di due cifre mettendo assieme la prima e l’ultima cifra. Se il
numero di partenza divisibile per il risultato, viene detto “numero Gapful”.
Esempi: 1729 è divisibile per 19; 5001 non è divisibile per 51.
Anche in questo caso occorre calcolare quante cifre ha “n”. La cifra iniziale viene poi calcolata dividendo
“n” per 10 elevato al numero di cifre diminuito di 1.
1729 ha 4 cifre; 1729 diviso 1000 fa 1 (cifra delle decine del numero da calcolare).
7
Numero Harshad (da Kaprekar)
Se un numero è divisibile per la somma delle sue cifre, viene detto “numero Harshad”.
Esempi: 1729 è divisibile per 19; 5001 non è divisibile per 6.
ARRAYS
Some words and expressions used in this chapter:
Set (noun)
Insieme
Shape
Forma
to Collapse
Crollare
Tabular
In forma di tabella
Ubiquitous
Onnipresente
Grade
Voto
Cumbersome
Scomodo
Uncouth
Rozzo, poco curato
Anche di persona
Drawback
Svantaggio
Think: draw back (tirare)
Feature
Caratteristica
Trial
Tentativo
Processo (tribunale);
Gara (ciclismo: time trial)
Jargon
Gergo
Also: parlance
Loan
Prestito
Loan shark (usuraio)
Dash
Linea (-)
100 meters dash: 100 piani
Risaltare
Think: outstanding
to Stand out
Ampersand
6
6
Mathematics
Think: spreadsheet
&
Glance
Sguardo
Idiom: at first glance
Hash marks
Linee (campo sportivo)
Hash sign: #
Slack
Temporaneo, usa-e-getta
Also: slack pants
Touchstone
Pietra di paragone
Trooper
Agente (USA)
Brit.: cavalryman
Yours truly
Sinceramente vostro
Lo scrivente
Speeding
Eccesso di velocità
Turn signal
Freccia (auto, moto)
Suitable
Adatto
AAAC
Avoid At All Costs
To get one’s feet wet
Fare le prime esperienze
The name has a story. Centuries ago, the symbol & was learnt by schoolchildren as a 27th letter of the alphabet,
following Z. The alphabet was recited thus: “A, B, C,… X, Y, Z, and, per se, and”. This meant that the last symbol, by
itself (per se in Latin), stood for the word and. Over time, the expression “and, per se, and” gave place to ampersand.
What, if anything, is an ARRAY ?
An array is a set of elements (numbers or other data) organized in rows and columns. Arrays can have any
number of dimensions, but they typically come in two shapes, linear and rectangular. Rectangular arrays are
normally called matrices (singular: matrix), and can be square when the number of rows and columns is the
same; linear arrays can be considered as matrices collapsed to one single row or column. Having multiple
elements and an organization (linear, tabular, or otherwise), arrays are an example of data structures.
Linear (mono-dimensional) arrays
Rectangular (two-dimensional) arrays
In computer science, arrays are ubiquitous: in scientific applications they are involved in complex
calculations7, but the tabular form is also the typical arrangement in data processing8.
Clearly, the simplest organization is the mono-dimensional array9, sometimes called, for obvious reasons, a
linear list. Examples of linear lists could be:
 all the names of the students in a class (names are alphanumeric data);
 all the grades of one specific student (a typical numeric array);
 the mean temperatures of the days in a given period.
An array has a global name, shared by all its elements, which are further identified by their position. The
length of an array is usually unspecified, so it should be stored into an integer variable (say, n). The initial
position (numbered with zero), is normally empty, but can be used in some specific cases, often related to
algebra; in most cases, the array elements in use are those numbered from 1 through n, with another empty
position on the far side, identifiable with n+1. The elements at both ends of the array are called sentinels: in
our course, sentinels will be used to store the results of calculations or searches.
In the following example, the value of n is 8, the content of the sentinels has been set to zero, and the
elements contain random two-digit values:
In algebra, array elements are identified by an index, written as a subscript: if the name of the array were a,
the elements would be identified as: a1, a2, a3, and so on. In programming languages, this typographic
convention is next to impossible, so the index is enclosed between square brackets, as in: a[0], a[1], and
so on, up to a[n+1]10.
It should also be noted that in many modern applications (often Web-related), the need for sentinels is
negligible. The reason is that “new” languages (such as JavaScript and PHP) come equipped with scores of
array-related functions, which free the developer from the burden of writing complicated algorithms
requiring sentinels.
7
Fields where matrix operations are present range from aerodynamics to relativity.
Databases are collections of data associated to various entities, laid out in tables.
9
The term commonly used in Italian (vettore) should not be mistaken with the homonymous entity known from
physics: there is a relation, though, as the name refers to the fact that the data structure has components.
10
This, at least, is the notation used in C/C++; other languages use parentheses, as in: a(5). Another difference may be
in the numbering of elements: in some languages the first position is 1, in some others even negative positions are
allowed. In at least one known case (VB), numbering from 1 is activated with the directive option base 1.
8
Declaration and usage of “Array” variables
The simplest way of declaring an array in C/C++ is as follows:
int a[20];
//
Array named a, with 20 elements
Array a, thus declared, contains a sequence of 20 positions, numbered from 0 through 19 (a[0] to a[19]).
This declaration, however, does not help modular programming, as including it in each function header is
rather cumbersome, not to say uncouth. Another obvious drawback is that the length of the array is fixed,
while it would be preferable to have elastic structures, with variable length. But this last feature, alas, is built
into the language, and there is little that can be done about it.
One way out would seem to be the following (where variable n should hold a value greater than zero):
int a[n];
//
VLA (Variable Length Array)
Unfortunately, VLAs can be declared only locally, that is, they do not transport between functions, thus
hindering the development of modular projects. The main problem is the length of an array, which cannot be
detected outside the function where the array has been declared: the only way to be modular is to include
array lengths in each call to the subprograms in a project. It is possible to live with this, but it is not nice.
The reason for adding some variability to our programs is to avoid giving the idea of all arrays having one
given, immutable length. One common practice is to randomly vary the length of arrays between different
programs (better yet, even in different trials of the same program). In order to simplify the checking of
program outcomes, we will use the rand() function to choose a length between 5 and 10. It would be of
little use processing hundreds or thousands of elements, since the solutions would not change; plus, a handful
of values is much easier to check. The only requirement will be declaring a maximum length (20 is as good a
choice as any other), using only part of the total memory thus reserved (or, in technical jargon, allocated).
One feature of the language that can simplify things (not much, but still better than nothing) is a globally
recognizable declaration of the structure type, valid for any specific array variable. This is done through the
following directive:
typedef int array[20];
//
typedef = type definition
All the variables declared as “array” (of course, following this definition), will contain a sequence of 20
elements, numbered from 0 through 19. The declaration of single instances uses the familiar C/C++ syntax:
array a, b;
int i; double x;
//
//
User-defined (or custom) types
The usual suspects: predefined (or native) types
Since programs are often executed a number of times, a time-saving device can avoid wasting our youth
keying in array values with the cin flow: the rand() function can automate this menial task.
As previously stated, array cells are singled out by their index, enclosed between square brackets, as in:
a[1], a[5]. Normally, though, the index will be a variable, as in: a[i], a[j], b[i], b[x]. Any index is
good, provided it is integer (on loan from algebra, commonly used index names are i and j).
Before using an array, a recommended practice is to initialize its elements, as the actual memory content is
unpredictable. Normally, we can start by choosing the length at random, then proceed by assigning random
values to each element. The following snippet exemplifies the two operations:
n = rand() % 6 + 5;
//
for(i=1; i<=n; i++) a[i] = rand()%20 + 10; //
Length between 5 and 10
Randomly chosen values
Of course, the typedef directive and these statements should be present in every single program that
processes arrays. As we know from previous studies, the best practice is to place the commonly needed
pieces in a library, so that any program that includes that library can recognize and initialize a custom
“array” variable. It should also be noted that, if we ever needed longer arrays, we would only have to modify
the typedef length to a new value: all the programs would automatically be aware of the change.
While the typedef directive stands by itself, the initialization of array elements belong to a function
(we don’t like stray statements). Other functions will contain the common operations on arrays.
A library for arrays
The most basic functions should at least allow programs to recognize, initialize, and show arrays (the rest is
left to us). As previously mentioned, the length and the content of the array are chosen at random, in a
function named (not surprisingly) values. The show function, which displays the array structure, contains a
couple of tricks. The first feature is the printf statement, which makes it possible to determine the exact
number of screen positions for every number, thus matching each element with its position; the second
feature is the horizontal dash (extended ASCII code 196) used to sketch a continuous line. The effect of this
introductory function is quite simple: for one thing, the sentinels do not stand out (more on this later).
typedef int array[20];
void zeroize( array a, int n)
{
Simple display outcome
int i;
for (i=0; i<=n+1; i++) a[i] = 0;
0 1 2 3 4 5 n+1
}
───────────────────────
void values( array a, int &n)
0 16 34 17 40 30 0
{
int i;
srand(time(NULL));
n = rand() % 6 + 5;
There is no need to analyze the details
zeroize( a, n);
of the printf function: just use it.
for (i=1; i<=n; i++) a[i] = rand() % 31 + 10;
}
void show( array a, int n)
{
int i;
for (i=0; i<=n; i++) printf("%3d", i);
//
Indexes
printf(" n+1");
//
display “n+1”
printf("\n");
for (i=1; i<=3*(n+2)+2; i++) printf("%c", 196);
//
Separator
printf("\n");
for (i=0; i<=n+1; i++) printf("%3d", a[i]);
//
Content
printf("\n\n");
//
2 new lines
}
All the above statements should be in a library. The following is a sample which should be in another source
file containing an include directive to that library.
int main()
{
int i,j,k,n,z;
array a,b,c;
values( b, z); //
show( b, z);
//
getchar();
}






Initialize array b
Show array b
This main is the model for every solution: it goes through the steps of setting up an array, showing it,
processing it, then showing the final result (as in all “before” and “after” pictures). We should note that
each operation is detailed to a specific function. Most functions are void because they do not return one
single value (the language does not allow for returning an array);
Parameter “by reference”: the ampersand operator (&), meaning “address of”, instructs the program to
affect the actual storage area of the calling module, so as to modify the argument (z, not n);
Parameter a: data structures are always passed by reference, so the address operator is optional; the name
a is as good as any other, being only a placeholder for the actual array in the calling module;
zeroize “cleans” anything present in the array storage area (an operation sometimes called zapping);
The value for the array length, once assigned in the values function, usually remains unchanged;
Of course, the names of the variables of the main module should be kept consistent with their choice.
This section contains a more complete and scenic version of function show (made available along with this
text). Its intended effect is to separate structure and data with hash marks. There is also another quite similar
function, named show2, which can be used where extended ASCII characters are not supported. As we can
see even with a quick glance, the “original” show uses ASCII codes 196, 179 and 197 (from the extended
set) to draw the hash marks, while the alternate version show2 uses codes 45 e 124.
void show( array a, int n, int o=196, int v=179)
{
int i,x;
x = (v==179?197:124);
/*
First row: positions
*/
printf("%4d", 0);
printf(" %c", v);
for (i=1; i<=n; i++) printf("%3d", i);
printf(" %c", v);
printf(" n+1");
printf("\n");
/*
Second row: separators
*/
for (i=1; i<=5; i++) printf("%c", o);
printf("%c",x);
for (i=1; i<=3*n+1; i++) printf("%c", o);
printf("%c",x);
for (i=1; i<=5; i++) printf("%c", o);
printf("\n");
/*
Third row: values
*/
printf("%4d", a[0]);
printf(" %c", v);
for (i=1; i<=n; i++) printf("%3d", a[i]);
printf(" %c", v);
printf("%4d", a[n+1]);
printf("\n\n");
}
void show2( array a, int n)
{
show(a,n,45,124);
}
//
EXTENDED ASCII
//
//
//
//
//
0
separator
index
separator
n+1
//
//
//
//
//
horizontal separators
vertical separator
horizontal separators
vertical separator
horizontal separators
//
//
//
//
//
//
sentinel
separator
content
separator
sentinel
2 new lines
//
//
NO EXTENDED ASCII
minimalist version
The outcomes of the two functions are the following:
show2
show
To sum it up, the library for arrays should contain these elements:
typedef int array[20];
void zeroize( array a, int n)
void values( array a, int &n)
void show( array a, int n)
void show2( array a, int n)
//
//
//
//
//
Type definition
Clean up array and sentinels
Random initialization
Show with extended ASCII
Simplified version
Normally, every other program should only contain the main module and an individual solution (named at
will), comprising one function in the simpler cases, or possibly more in advanced problems.
Working with arrays
The first program we met is nothing special: the only things it does are initializing an array and showing it.
#include "functions.h"
int main()
{
int n,z;
//
array a;
values( a, n);
//
show( a, n);
//
getchar();
}
declarations
initialization
showing the values
Of course, we should do something more, that is, process the array according to specific requests. The
processing, needless to say, is best done in a function. How this function should be declared depends on the
problem, which can belong to one of two categories:


Problems that require changing the content of the array (void);
Problems that require the calculation of a result (int or double).
The reason for having an entire class of void solutions (actually, the vast majority) is that arrays usually
contain more than one value, while a return statement only yields a single result: we will just modify the
array, then take a look at the new values with a second call to the show function.
#include "functions.h"
void solve( a, n);
{
...
}
int main()
{
int n,z;
array a;
values( a, n);
show( a, n);
solve( a, n);
show( a, n);
}
//
processing the array
The SOLVE function normally has two
parameters: the array and its length.
//
declarations
//
//
//
//
initialization
showing the values before the cure
processing the array
showing the values after the cure
Occasionally, though, we will meet problems requiring the calculation of one single value. In those cases, the
solving functions will contain a return statement for an int (or double) value; there also will be no need
to show new array content, as one cout will suffice:
#include "functions.h"
int solve( a, n);
//
{
...
}
int main()
{
int n,z;
array a;
values( a, n);
show( a, n);
z = solve( a, n);
cout << z;
}
sometimes double
The presence of a list of elements, plus their linear organization, usually calls for at least one processing
loop. In most cases, the array will be traversed (or visited) with the basic “1 through n” structure:
for (i=1; i<=n; i++)
{
...
}
//
visiting the array
//
processing a[i]
Before the visit, we could initialize counters or other slack (temporary) variables. Some sample snippets:
s=0;
for(i=1; i<=n; i++) s+= a[i];
//
summing all the values
for(i=1; i<n; i+=2)
{
a[i]+= a[i+1];
}
//
index jumping by 2
//
summing adjacent cells
for(i=n; i>1; i-=2)
{
a[i] = -a[i];
}
//
visit in reverse order
//
change sign to every other element
c=0;
//
for(i=1; i<=n; i++) if (a[i]%2!=0) c++;
Each snippet should be in
its void or int solving
function.
counting odd values
These example show that array elements are treated like any other variable, with the only difference that it is
necessary to include their position (or the program would not be able to locate them).
Find the largest element in an array.
A general comparison of all elements is out of the question, if not for the simple reason that we do not know
the length of the array: the solution is comparing pairs of elements, choosing the one that fits our needs, and
casting off the other. As is always the case, this takes a variable, which could store the value of the useful
element, or, better, its position.
Focusing on our problem, let’s consider the first two elements: if the values of a[1] and a[2] were 31 and
35, we would keep track of the larger one, a[2], by storing 2 (not 35) in a variable named p. We could then
use a[p] as a touchstone for further comparisons. Why the position and not the value? Easy: knowing where
an element is implies knowing what its value is, while the reverse does not hold. To quote a trooper who
once stopped yours truly for speeding, “Use positions11: that’s what they are there for”.
The complete solution identifies the searched element (as yet unknown) with position p. Initially, setting p to
1 is as good a choice as any other: after all, we have to start somewhere (and if the array had only one
position, that’s where the largest element would be). But this view must be broadened: it is necessary to
activate a loop which compares the element in position p with all the others, one at a time. When a more
suitable element (i.e.: one larger than a[p]) is found, it will be its position to be stored into p, discarding the
former “largest element” with a better one. In other words: p keeps track of where the largest element is.
This technique is called “false position”, or regula falsi. The word “position” has nothing to do with the
structure of the array. It means postulating an arbitrary value to the solution, then correcting it, if needed.
The ancient Egyptians solved mathematical problems with the regula falsi (we now use equations); in the
digital age, some computer algorithms still work smoothly with it.
WARNING: this is one problem which returns an integer value, instead of altering the array.
int largest( array a, int n)
{
int p;
//
p=1;
//
for (i=2; i<=n; i++)
//
if (a[i]>a[p]) p=i;
//
return p;
//
}
11
He actually said “Use your turn signals”.
slack variable (position of largest element)
it could be in the first position
start checking from the second position
anything better? new "largest element"
return the position of the largest element
This collection of brief exercises should get our feet wet. The first two were chosen to exemplify a rather
common error in referring to elements: some care must be taken with those square brackets.
void substractOne( array a, int n)
{
int i;
for ( i=1; i<=n; i++) a[i] = a[i]-1;
}
void moveBy1( array a, int n)
{
int i;
a[0] = a[n];
for ( i=n; i>=1; i--) a[i] = a[i-1];
}
// WRONG “SOLUTION”
void allEqual( array a, int n)
{
int i;
for ( i=2; i<=n; i++) a[i] = a[i-1];
}
void doubleThem( array a, int n)
{
int i;
for ( i=1; i<=n; i++) a[i]*= 2;
}
void sumPairs( array a, int n)
{
int i;
for ( i=1; i<=n; i+=2) a[i]+= a[i+1];
}
// VERY WRONG “SOLUTION” (D-T)
void sumPairsDT( array a, int n)
{
int i;
for ( i=1; i<=n; i+=2) a[i]+= a[i++];
}
Subtract 1 from all the elements of the array.
The solution is: a[i]-1, which is not a[i-1], as we
will see in the next exercise.
a[i-1] is the element TO THE LEFT of the one
indexed by i. What happens here is that each element
ends up with the value of the one which preceded it. The
0 sentinel holds the value of the last element of the
array, which would otherwise be lost; this also avoids
the additional error of leaving a[1] either empty or
equal to a[2], depending on the loop.
Starting from the right avoids the error shown in the
next exercise.
This solution is wrong: the second element takes the
value of the first, but its value is lost. What happens
later is that the third element also takes the value of the
first one, then the fourth, and so on: the final
consequence is that ALL the elements will contain the
value of a[1].
Just like the first problem, only doubling all the
elements.
Each element is summed with its neighbor (think: which
one? To the right or to the left?)
Why move the index by 2?
Try using i++ instead of i+1, then see what happens
The error occurs because i++ SURREPTITIOUSLY
MODIFIES THE INDEX i, which ends up jumping
by 3 positions instead of 2 (1 with i++, 2 with i+=2).
The moral: do not touch the index inside the loop.
Position and content are totally different beasts (positions are structural data), so they don’t
mix. After all, if they were the same thing, they would have the same name.
This is a totally useless sum:
s=0;
for (i=1; i<=n; i++) s+= i;
//
NO SENSE IN SUMMING POSITIONS
When they should compare elements, some derelict IRs compare positions:
for (i=2; i<=n; i++)
if ( i > p ) p = i;
//
NO SENSE IN COMPARING POSITIONS
To paraphrase the immortal Satchel Paige , “positions don’t move”.
12
12
“Home plate don’t move”. Leroy Robert “Satchel” Paige (1906-1982), American baseball player.
The anatomy of swapping
It is often necessary to move the elements of an array to different positions: this may happen when adding or
deleting elements, or when arranging elements in ascending or descending order (an entire chapter is
dedicated to this problem, called sorting). This is frequently done by swapping pairs of elements.
Suppose that elements a[3] and a[4], for obscure reasons, need to be changed with one another:
Position 3
Position 4
Before
5
1
After
1
5
Swapping two values requires THREE statements, as can easily be seen with the following examples. The
wrong solution has the only effect of making both elements equal to whichever one was copied first; the
correct solution temporarily stores one of the values into a slack variable (z), before erasing it with the other,
which is itself overwritten with the value previously saved:
TEMPTING, BUT WRONG (D-T)
CORRECT
z = a[3];
a[3] = a[4];
a[4] = z;
a[3] = a[4];
a[4] = a[3];
THE WRONG RESULT
THE CORRECT RESULT
Position
3
4
Position
3
4
Final Value
1
1
Final Value
1
5
Of course, swapping any two variables is best done with a function:
// Swap two integer values
void swap( int &a, int &b)
{
int c;
c = a;
a = b;
b = c;
}
Note the ampersand (&) operator in the header,
representing the actual address of a variable: without
it, only copies of the variables would be exchanged,
thus nullifying the purpose of the function.
Calling this function greatly simplifies other modules, grouping the three statements into one call, and
cleaning up the source code:
...
swap( a[3], a[4]);
...
//
Solving the given example
In general, swap operations are using VARIABLE indexes (presumably the indexes of some loops):
...
swap( a[i], a[j]);
...
//
Swap ANY two elements of the array
Expand the preceding example to the entire length of the array, swapping all the adjacent elements:
Before
After
0 │ 1 2 3 4 5 │ n+1
─────┼────────────────┼─────
0 │ 38 20 32 21 11 │
0
0 │ 1 2 3 4 5 │ n+1
─────┼────────────────┼─────
0 │ 20 38 21 32 11 │ ?
When the length of the array is odd, care must be taken so as not to swap the last element with the sentinel.
In some problems, which require separate calculations, one useful trick for the solution can be storing partial
(or final) results into the sentinels, thus disposing with the need for additional variables.
Example
Let’s consider this problem: split the array into two halves, then sum the elements in each half. Since it is not
possible to return two values, this is one case where the sentinels can help, each holding one of the results.
One other consideration is that, when the length of the array is odd, we must compromise, choosing one
“half” as slightly shorter that the other.
Before
After
0 │ 1 2 3 4 5 │ n+1
─────┼────────────────┼─────
0 │ 38 20 32 21 11 │
0
0 │ 1 2 3 4 5 │ n+1
─────┼────────────────┼─────
58 │ 38 20 32 21 11 │ 64
void sums( array a, int n)
{
int i, m, s;
m = n / 2;
s = 0;
for (i=1; i<=m; i++) s+= a[i];
a[0] = s;
s = 0;
for (i=m+1; i<=n; i++) s+= a[i];
a[n+1] = s;
}
//
//
Mid-point (more or less)
First half
//
Second half
Problem 1
Sum separately the odd and the even elements. Use a[0] and a[n+1], respectively.
Before
After
0 │ 1 2 3 4 5 │ n+1
─────┼────────────────┼─────
0 │ 38 11 32 21 11 │
0
0 │ 1 2 3 4 5 │ n+1
─────┼────────────────┼─────
43 │ 38 11 32 21 11 │ 70
Problem 2
Sum separately the elements in odd and even positions. Use a[0] and a[n+1], respectively.
Before
After
0 │ 1 2 3 4 5 │ n+1
─────┼────────────────┼─────
0 │ 38 20 32 21 11 │
0
0 │ 1 2 3 4 5 │ n+1
─────┼────────────────┼─────
81 │ 38 20 32 21 11 │ 41
Problem 3
Sum separately the elements smaller and larger than the mean of the values (the mean is DOUBLE).
Before
After (mean: 25.8)
0 │ 1 2 3 4 5 │ n+1
─────┼────────────────┼─────
0 │ 23 40 18 18 30 │
0
0 │ 1 2 3 4 5 │ n+1
─────┼────────────────┼─────
59 │ 23 40 18 18 30 │ 70
So far, our problems have processed all the elements of an array, with basic for loops stretching its entire
length. When searching for something, the loop becomes a little hazy, because we do not know where to
stop: this situation calls for a while loop, the indefinite version of traversing a data structure.
Let’s consider this problem: find the first prime number of the array. The method is quite straightforward:
start from the left end of the array, then move to the right in search of a prime number. One of two things
will happen: either we find a prime number (the object of our search), or we reach the end of the array,
without having found one (search unsuccessful). These events are mutually exclusive. Note that the same
method can be applied for any search problem (just by changing the search condition).
The loop must take two concepts into account: “moving” the index to the right (i++), and a reversed
condition with respect to the search (!prime(a[i])). The result of the search is verifiable by checking the
value of the traversing index: i<=n means that the visit was interrupted before the end of the array (success);
i>n means that the whole array has been scanned, but no numbers have been found (no success). There are
two possible solutions: the first returns an integer index, representing the position of the sought-after element
(where -1 stands for an unsuccessful search); the second returns a boolean value, indicating only the result
of the search, without location. The latter is not quite useful in itself: it just shows an alternate search
method, which separates the two ways of terminating the loop. Sometimes, in complex programs, this
solution can help clarify things.
Return value: 4
Return value: -1
0 │ 1 2 3 4 5 │ n+1
─────┼────────────────┼─────
0 │ 38 20 32 11 21 │
0
0 │ 1 2 3 4 5 │ n+1
─────┼────────────────┼─────
0 │ 38 20 32 81 15 │ 0
int firstPrime( array a, int n)
{
int i;
i = 1;
while (i<=n && !prime(a[i])) i++;
if (i>n)
return -1;
else
return i;
}
Shorthand:
return( i<=n ? i : -1);
bool firstPrime( array a, int n)
{
int i; bool found;
i = 1;
found = false;
while (i<=n && !found)
{
if (prime(a[i]))
found = true;
else
i++;
}
return found;
}
One note on the “shorthand” statement shown in the first solution. It is called a conditional assignment, and
evaluates one of two expressions, depending on the result of a condition: the first expression is chosen then
the condition is true; the second when the condition is false. The condition and the two expressions (which
make this a ternary operator) are separated by ? and :. A couple of examples:
a = ( i%2==0 ? i : i*2);
a = ( a>0 ? a : -a); (same as a=abs(a);)
Problem 1
Find the position of the first odd element.
Problem 2
Find the position of the last odd element. Hint: just reverse the loop.
Problem 3
Find the first pair of adjacent elements with a difference smaller than 3.
In some situations, an array is used to contain a certain sequence of data. This would call for a VLA, but
there is no need to resort to that, so we will keep the habitual scheme. The array still needs to be initialized
with the values function, using it as a prop to give the array a length: after that, any values present in the
array will be overwritten, and only the final state of the structure will matter13. One further step that could
help would be to preemptively zap the array with the zeroize function: a uniform initial value can be useful
when trying to meet specific conditions (as in problem 3 below).
Let’s consider this (quite unusual) problem: fill the array with elements matching their position. The method
is quite straightforward: activate a direct traversing loop, copying the index value into each element.
A related problem could be to fill the array in reverse order. This example is given in two version, one quite
down-to-earth involving a variable, another a little more sophisticated, involving a formula.
Result (ascending)
Result (descending)
0 │ 1 2 3 4 5 │ n+1
─────┼────────────────┼─────
0 │ 1 2 3 4 5 │
0
0 │ 1 2 3 4 5 │ n+1
─────┼────────────────┼─────
0 │ 5 4 3 2 1 │ 0
void ascending( array a, int n)
{
int i;
for( i=1; i<=n; i++) a[i]=i;
}
void descending( array a, int n)
{
int i;
for( i=1; i<=n; i++) a[i]=n+1-i;
}
void descending( array a, int n)
{
int I,z;
z=n;
for( i=1; i<=n; i++)
{
a[i]=z;
z--;
}
}
Problem 1
Fill the array with a Fibonacci sequence. The Fibonacci sequence starts with 0 and 1, and each subsequent
element is the sum of the previous two:
0 │ 1 2 3 4 5 6 7 8 │ n+1
─────┼─────────────────────────┼─────
0 │ 0 1 1 2 3 5 8 13 │
0
Problem 2
Fill the array with a sequence of prime numbers. Hint: use nextPrime, referring to the previous element.
0 │ 1 2 3 4 5 6 7 8 │ n+1
─────┼─────────────────────────┼─────
0 │ 2 3 5 7 11 13 17 19 │
0
Problem 3
Fill the array with random numbers, each not smaller than the preceding one, not exceeding 99. This means
that 99 would be the ultimate value, should it be reached midway. Example:
0 │ 1 2 3 4 5 6 7 8 │ n+1
─────┼─────────────────────────┼─────
0 │ 42 43 55 55 90 99 99 99 │
0
There is at least one solution that can be simplified by previously zapping the array with a call to zeroize.
13
This means that the first call to the show function is quite useless.
ARRAY Sorting
Some words and expressions used in this chapter:
to Arrange
Sistemare
to Single out
Individuare
Misplaced
Fuori posto
mis=prefix for wrong
to Swap
Scambiare
Swap meet=mercatino
Score (noun)
Ventina
Neatly
In modo pulito, ordinato
It is safe to assume
Possiamo stare certi
Pick
Scegliere, prendere
Pickpocket=borseggiatore
Neighbor
Vicino (“vicino di casa”)
Brit.: Neighbour
Cauldron
Calderone
Sizable
Consistente
to Shift
Spostare
Bubble
Bolla
Previously
Prima, precedentemente
Ripple
Piccola onda, ondulazione
Bogus
Falso, spurio
to Christen
Battezzare, chiamare
to Implement
Mettere in esecuzione,
attuare
Please...
Boundary
Confine
Also: border
To and fro
Avanti e indietro
Also: back and forth
There ain’t no such thing as a free lunch – Non si ottiene mai niente per niente
The anatomy of sorting
It is often necessary to arrange the elements of an array in ascending or descending order (imagine a random
telephone directory). Such a process is called sorting (the verb has several other meanings, including
“choosing” and “singling out”).
The process of sorting requires moving values to different places, as misplaced elements must be stored into
their correct position. They also usually require double loops, since a single loop is able to put only one
element in its right place.
When an array is populated at random, disorder is almost total. A few numbers could be already in order, but
only by chance, as in the following example:
30 12 15 8 29 20 17 45 42 11 20
Only some elements are in the correct order with respect to their neighbors (as
There is a long way to the sorted version of the array:
12 and 15, or 8 and 29).
8 11 12 15 17 20 20 29 30 42 45
This algorithm is often reported as an example of how not to do things, as the name
implies (Bozo the Clown has been for decades a popular TV character in the USA).
Yet the method is rather simple to explain. Choose any two elements at random; if they
are not ordered, swap them; repeat the operation until the entire array is ordered.
We can note that the end of the algorithm is quite hazy, as there is no certainty as to when to stop. While this
obviously calls for a while loop, the problem with having multiple elements is that the condition cannot be
stated simply (as, for example, checking for a prime number or any other property of integers), so a function
is needed to check whether the array is ordered or not. Still, this new function can be useful as a blueprint for
any other problem that requires verifying a given condition for the entire array.
int sorted( array a, int n)
{
int i,j;
Suppose that, with the following array, i
i=1;
and j were 2 and 6.
while (i<n && a[i]<a[i+1]) i++;
return (i>=n);
Swapping the corresponding positions
}
would make the situation a little better:
void bozo( array a, int n)
30 12 15 8 29 11 20 16
{
int i,j;
30 11 15 8 29 12 20 16
while (!sorted(a,n))
{
i = rand()%n + 1;
j = rand()%n + 1;
if ( i<j && a[i]>a[j] ) swap( a[i], a[j]);
//
See inset
}
}
The main problem of this method is that there is no pattern: it is rather like groping in the dark. Besides,
relying on random choices often repeats the same steps, so the efficiency is quite low: this is a worst-case
example of the so-called brute force methods.
Is there anything worse? Actually, there is. The Bozo sort is an optimization (if we can call it this way) of the
Bogo sort algorithm (from bogus), where the array is randomly reshuffled until it is sorted. This methods has
an almost zero probability of succeeding.
Clearly, a project with some insight is needed.
Sort in action
When the number of elements to be sorted lies in the millions (or even more), time becomes a key factor:
sorting algorithms are rated by their speed14. The more advanced methods are quite complex indeed, and
some use sophisticated programming techniques, beyond the scope and the purposes of this course.
Basic (or naive) sorting algorithms rely on the concepts of “source sequence” and “destination sequence”,
and on the process of moving one element at a time from the former to the latter:


Source sequence
Destination sequence
the part of an array which has not yet been sorted
the part of an array which is in the correct order
It is safe to assume that a random array is one source sequence, with no certainty about order:
30 12 15
8 29 20 17 45 42 11 20
The first step towards an ordered array is to make sure that the destination sequence has one element, which
should be the smallest:
8 12 15
x 29 20 17 45 42 11 20
The red x standing in the place formerly occupied by the element 8 must not be lost, or the whole process of
sorting would be useless. One possibility is to store the destination sequence into another array, but most
sorting algorithms are designed to work in situ15 (without using extra storage). Depending on the method, the
array can be modified in different ways, placing the “wrong” element 30 (and possibly some others) in a
different position. The following results are produced by two of the methods we will encounter later:
8 12 15 30 29 20 17 45 42 11 20
8 30 12 15 29 20 17 45 42 11 20
selection sort
insertion sort
Once the destination sequence (in blue) has been started, the next step is to expand it with a second
element, then with a third, and so on, until the entire array is sorted. In those cases when the “right” element
is already in place, no changes are made (see step 3 and 5, proceeding with the insertion sort algorithm):
8
8
8
8
11
11
11
11
12
12
12
12
15
15
15
15
30
30
30
17
29
29
29
30
20
20
20
29
17
17
17
20
45
45
45
45
42
42
42
42
20
20
20
20
after
after
after
after
step
step
step
step
2
3
4
5
While the process is in action, the array is divided into two separate sequences:
Destination sequence
Source sequence
8 11 12 15 17 30 29 20 45 42 20
One by one, the elements are transferred from the source sequence (which keeps shrinking) to the destination
sequence (which keeps expanding). This is done by swapping two or more elements.
When an element is repeated (as 20 in the example), there should be no preferences, nor any limitations, on
which occurrence is transferred first.
When all is said and done, the final result is a neatly ordered array, comprising one complete destination
sequence:
8 11 12 15 17 20 20 29 30 42 45
Now it is time to see some of the simplest classic algorithms (plus one only recently entered into the
literature).
14
15
Arguably the fastest algorithm is one called quicksort, devised by Sir Charles Anthony Richard Hoare (1934-).
Latin for “in the same place”
Selection sort
First round
Pick the smallest element in the array, then swap it with the first one. This way, the smallest element will be
placed where it should be. In the following example, the element to be moved is in position 6:
.
5
8
6
3
4
1
8
9
The result after swapping the values will be:
.
1
8
6
3
4
5
8
9
It is safe to say that the destination sequence has now one element, the smallest of the entire array. The
following apply:



Finding the smallest element requires examining all the elements of the array (a loop is necessary);
Only one element is guaranteed to be in the right place (the smallest);
There are no considerations on the element which was formerly in the first position: it may be already in
place, it may not.
As it always happens, the only way to expand the destination sequence is to repeat the process: this can be
done by inserting the partial algorithm into another loop. But let us examine what happens next.
Second round
Single out the smallest element in the source sequence of the array (this excludes the destination sequence),
then swap it with the second one. This way, the destination sequence will comprise two elements. In the
example, the destination sequence is written in blue, and the element to be moved is in position 4.
.
.
1
1
8
3
6
6
3
8
4
4
5
5
8
8
9
9
(before)
(after)
The destination sequence has now two elements.
Third round
By now, it should be clear that the trick is to repeatedly apply the process only to the source sequence, like
this:
.
.
1
1
3
3
6
4
8
8
4
6
5
5
8
8
9
9
(before)
(after)
The destination sequence now comprises three elements. And so on…
The need for a double loop should be evident: searching for the smallest
element (however long the source sequence is) requires a loop, repeating
this search requires another loop.
This method is called “selection sort” because, in each step of the outer
loop, the smallest element in the source sequence is located (selected),
then placed into its correct position, thus expanding the destination
sequence by one element at a time. Some final considerations:




If the chosen element is already in place, there is no need to swap it with itself (this can save processor
time);
The other swapped element is not sure to reach its correct position, but it is not lost, only thrown back
into the cauldron of the source sequence, there to wait its turn;
After each step, the source sequence is shorter, so there are fewer comparisons to be done;
The process takes n-1 steps, not n: when the source sequence is reduced to one element, the entire array
is already sorted.
Insertion sort
This method is better described if we consider a sizable destination sequence already in place. In the
following example, five elements are already sorted, and the method describes how to move the sixth one
into the desired position.
12 15 18 19 20 17 45 42 11 20
It is clear that 17 should be placed somewhere to the left. The method consists in shifting by one position to
the right all the destination sequence elements which are larger than the new entry: in the example, 20, 19,
and 18 (in this order) all make room for 17.
12 15 18 19 20 20 45 42 11 20
5
12 15 18 19 19 20 45 42 11 20
4
12 15 18 18 19 20 45 42 11 20
3
12 15 18 18 19 20 45 42 11 20
2
(do not copy 15, as it is smaller than 17)
There is no need for swapping: the trick is to move the elements in the correct sequence (moving
backwards), thus requiring fewer operations (swapping two elements requires three statements).
At this point, the position occupied by the last element copied (18) is the place where the new element 17
must be placed. The only provision is that the new value of the destination sequence (17) must be previously
stored into another variable (or in a sentinel), lest we lose it when it is overwritten.
The result is as follows:
12 15 17 18 19 20 45 42 11 20
We can note that the next element (45) will not require to be inserted, being already the largest in the
destination sequence. Element 42 will move only element 45, while 11 and 20 will cause more of a
disturbance (especially 11, which will end up in the first position). Normally, we need not care about this:
only in arrays with millions of elements, time would be a factor (but such cases would be handled with quite
more sophisticated algorithms).
The usual considerations:




This sequence requires a loop (short or long, depending on the position of the element to be inserted);
The first loop could start from the second position (after all, the first position is already ordered with
itself);
As usual, extending the operation to the whole length of the array requires another loop;
This method is analogous to what most card players do when they sort their hand, picking one card from
the right and inserting it among the others to the left.
Bubble sort
Beginning from the end of the array, each element is compared to its neighbor. If they are inverted16, they are
swapped, thus restoring a least part of the correct sequence.
As usual:




the process requires a loop;
his loop is repeated through another loop;
the destination sequence is expanded by one element at a time;
th
when the (n-1) element is in place, there is no need to sort the source sequence again.
First round
5 8 6 3
5 8 6 3
5 8 6 3
Details
7
Actions
start
4-7 OK
3-4 OK
inversion
6-3 becomes 3-6
inversion
8-3 becomes 3-8
inversion
5-3 becomes 3-5
4
4
4
7
7
7
5
8
3
6
4
7
5
3
8
6
4
7
3
5
8
6
4
3
5
8
6
4
7
End of visit
destination sequence: 3
Details
destination sequence: 3 4
Second round
3 5 8 6
3 5 8 6
3 5 8 4
3 5 4 8
3 4 5 8
4
4
6
6
6
7
7
7
7
7
Actions
start
4-7 OK
3-4 OK
inversion
inversion
3
6
7
End of visit
4
5
8
5
5
5
5
5
5
5
3
8
8
8
8
8
3
3
5
6
6
6
3
3
8
8
8
3
3
3
6
6
6
6
6
4
4
4
4
4
4
4
4
7
7
7
7
7
7
7
7
before
after
before
after
before
after
Synthesis of the remaining rounds:
3
3
3
3
3
4
4
4
4
4
Round 3
5 8 6
5 8 6
5 6 8
5 6 8
5 6 8
7
7
7
7
7
3
3
3
3
4
4
4
4
Round 4
5 6 8
5 6 7
5 6 7
5 6 7
7
8
8
8
3
3
3
4
4
4
Round 5
5 6 7
5 6 7
5 6 7
8
8
8
STOP
This method is called bubble sort because, at each passage in the outer loop, the smallest
element of the source sequence “rises” towards the top of the array, just like bubbles do in
water17.
One variation of this method is called ripple sort, which, instead of comparing and swapping
neighboring elements (as in: a[j],a[j+1]), works at a distance, using two separate indexes
(as in: a[i],a[j]).
16
17
An inversion occurs when an element is larger than another one following it (i.e.: to the right).
Considering the names they choose, it may be true that computer scientists have a wicked sense of humor.
Gnome sort
This method was christened by the Dutch computer scientist Dick Grune, who likened
it to the behavior of tuinkabouters when they sort out flower vases18. It has similarities
with the ”insertion sort” and the “bubble sort”, and is peculiar for being implemented
with a single loop.
The kabouter/gnome, according to Grune, must sort a sequence of flower vases. He
starts at the left end of the sequence and proceeds by comparing the vase in front of
him with the preceding one: if they are in the correct order, he leaves them in place,
then moves forward; if not, he swaps them, then moves backward19.
There are two boundary rules20:
1. If there is no “preceding” vase (i.e.: at the beginning of the sequence, where i==1), just move forward;
2. After reaching the end of the sequence (i.e.: when i>n), stop: the job is done.
void gnome( array a, int n)
{
int i;
i = 1;
while (i <= n)
{
if (i == 1 || a[i-1] <= a[i])
i++;
else
{
swap( a[i], a[i-1]);
i--;
}
}
}
//
Boundary rule #2
//
Boundary rule #1; see Consideration 2 ***
//
See Consideration 1
Consideration 1
The two statements between the red braces:
swap( a[i], a[i-1]);
i--;
could be synthesized into one:
swap( a[i--], a[i-1]);
Many developers resort to similar shortcuts, but too concise a program could become unreadable, so it is not
always a good practice (it goes against the KISS principle). The longer solution has the advantage of
showing explicitly the sequence of operations: swap first, then move backwards.
Consideration 2
The first boundary rule *** could be simplified by using the sentinel in the ZERO position: if it contained a
very large negative number, the condition a[0]<=a[1] would always be true. Thus, there would be no
need for the clause i==1, and the test would be reduced to a[i-1]<=a[i].
The largest negative number in the C/C++ language requires the following directives:
#include <limits>
using namespace std;
and can be stored into the sentinel with the following statement (a bit awkward, indeed):
a[0] = numeric_limits<int>::min();
18
Tuinkabouter = garden gnome
Of course, there ain’t no such thing as a free lunch: since the index of the loop is altered during the process, there can
be a lot of going to and fro along the array.
20
Boundary rules: rules to be applied at either end of a sequence (or at the borders of a region).
19
MATRICI
DEFINIZIONE DEL TIPO “MATRICE” – USO DI MATRICI
Una matrice (o “array bidimensionale”) è un insieme di numeri identificati con uno stesso nome, distinti per
posizione e distribuiti in forma di tabella rettangolare, suddivisa in righe e colonne. Si dice quadrata una
matrice nella quale il numero di righe è uguale a quello delle colonne: è quadrata la maggior parte delle
matrici che si usano nei problemi elementari. Analogamente a quanto avviene nei vettori, possiamo
identificare posizioni, contenuti e sentinelle.
Il numero di righe viene solitamente indicato con m e quello di colonne con n. Per semplicità, i moduli che
verranno in seguito presentati, trattando con matrici quadrate, useranno una sola dimensione variabile n. Le
sentinelle, avranno riga e/o colonna uguali a 0 oppure n+1.
In una matrice quadrata individuiamo:



4*n + 4 sentinelle;
Diagonale principale: posizioni dove riga == colonna;
Diagonale secondaria: posizioni dove riga + colonna == n+1.
La “matrice” è usata in modo analogo al vettore, con l’aggiunta di una dimensione (e quindi di un ciclo). Le
dichiarazione è:
typedef int matrix[20][20]; //
typedef = definizione di tipo
matrix a,b;
//
tipi user-defined
All’interno del programma, le singole celle del vettore sono usate con i corrispondenti indici: ad esempio
a[1][5], ma normalmente con indici variabili come a[i][j]. I programmi di base sono analoghi a quelli
dei vettori, con una differenza fondamentale: come già visto altre volte, il fatto che la matrice sia piana
anziché lineare si traduce nell’uso di doppi cicli.
Normalmente si procede “righe per colonne”: da sinistra a destra e dall’alto al basso:
...
for (i=1; i<=n; i++)
{
...
for (j=1; j<=n; j++)
{
...
// elaborazione di a[i][j]
}
}
Le righe precedenti i cicli verranno usate per inizializzare contatori o altre variabili di servizio.
Come per i vettori monodimensionali, anche per le matrici i moduli zeroize, values e show dovranno
essere sempre disponibili.
ATTENZIONE !!! Come per i vettori, anche questa versione del modulo vedi è semplificata. Nella pagina
seguente sono riportati i moduli completi vedi e vedi2 (da usare quando i caratteri estesi non sono visibili).
void zeroize( matrix a, int n)
{
int i;
for (i=0; i<=n+1; i++)
// Valori nulli (sentinelle comprese)
for (j=0; j<=n+1; j++) a[i][j]=0;
}
void values( matrix a, int &n)
{
n = rand() % 5 + 3;
// Dimensioni tra 3 e 7
for (i=1; i<=n; i++)
// Valori casuali
for (j=1; j<=n; j++) a[i][j]=rand()%20+1;
}
void show( matrix a, int n)
{
printf("
%c ",179);
for ( j=1; j<=n; j++) printf("%3d",j); //
Scrittura delle posizioni
printf("\n");
for ( j=1; j<=3; j++) printf("%c",196);//
Una riga di separazione
printf("%c",197);
for ( j=1; j<=3*n+2; j++) printf("%c",196);
printf("\n");
for ( i=1; i<=n; i++)
{
printf("%2d %c ",i,179);
for ( j=1; j<=n; j++)
printf( "%3d", a[i][j]);
//
Scrittura dei contenuti
printf("\n");
}
VISUALIZZAZIONE
printf("\n\n");
}
int main()
│ 1 2 3 4 5
{
────┼────────────────
int i,j,k,n,z;
1 │ 15 23 8 12 19
matrix a,b,c;
2 │ 7 3 25 16 8
values( a, n);
//
Inizializzazione
3 │ 21 16 13 17 4
show( a, n);
//
Visualizzazione
4 │ 12 5 24 2 27
getchar();
5 │ 1 23 5 27 19
}
Osservazioni analoghe a quelle sui vettori:



Questo main è il tipico programma di elaborazione di una matrice: prepara una matrice, la visualizza,
richiama un modulo che opera sulla matrice, visualizza la matrice modificata (“prima delle cura” e “dopo
la cura”). È lo schema che va seguito per tutti gli esercizi con una matrice;
modulo main: dichiara e inizializza le matrici, poi esegue altri compiti (sempre attraverso moduli)
il valore di n viene determinato casualmente dal modulo valori, poi non viene solitamente più variato.
Una visualizzazione più completa
Valgono le stesse considerazioni riportate per i vettori.
void separatoriOrizzontali( int n, int o, int v)
{
int i;
for (i=1; i<=5; i++) printf("%c", o);
printf("%c",v);
for (i=1; i<=5; i++) printf("%c", o);
printf("%c",v);
for (i=1; i<=3*n+1; i++) printf("%c", o);
printf("%c",v);
for (i=1; i<=5; i++) printf("%c", o);
printf("\n");
}
void valoriOrizzontali( matrix a, int n, int i, int v)
{
int j;
if (i==n+1)
printf(" n+1");
else
printf("%4d", i);
printf(" %c", v);
printf("%4d", a[i][0]);
printf(" %c", v);
for (j=1; j<=n; j++) printf("%3d", a[i][j]);
printf(" %c", v);
printf("%4d", a[i][n+1]);
printf("\n");
}
void show( matrix a, int n, int o=196, int v=179) //
{
int i,j,x;
x = (v==179?197:124);
printf("
%c", v);
printf("%4d", 0);
printf(" %c", v);
for (i=1; i<=n; i++) printf("%3d", i);
printf(" %c", v);
printf(" n+1\n");
separatoriOrizzontali(n,o,x);
valoriOrizzontali( a, n, 0, v);
separatoriOrizzontali(n,o,x);
for(i=1; i<=n; i++)
valoriOrizzontali( a, n, i, v);
separatoriOrizzontali(n,o,x);
valoriOrizzontali( a, n, n+1, v);
printf("\n\n");
}
void show2( matrix a, int n)
{
show(a,n,45,124);
}
show
show2
//
Separa le sentinelle
//
//
//
//
//
//
//
separatori
separatore
separatori
separatore
separatori
separatore
separatori
//
Riga con valori
//
sentinella finale
//
//
//
//
//
//
//
sentinella
separatore
sentinella
separatore
contenuti
separatore
sentinella
orizzontali
verticale
orizzontali
verticale
orizzontali
verticale
orizzontali
VISUALIZZAZIONE 1
//
//
//
//
//
//
//
//
//
//
//
Indici di colonna
Angolo NORD-OVEST
0
separatore
indici
separatore
n+1
Prima delle sentinelle
Sentinelle "top"
Dopo le sentinelle
Righe con i valori
//
//
//
Separatori
Sentinelle "bottom"
a capo 2 volte
//
//
VISUALIZZAZIONE 2
Versione per i poveri
Esercizi di somma
Come esempio di doppio ciclo, consideriamo la somma di elementi. L’obiettivo del primo esempio è
sommare tutti gli elementi di ciascuna riga. Il problema è analogo alla somma degli elementi di un vettore,
considerando ciascuna riga come un vettore. Le somme possono essere memorizzate nelle sentinelle, con il
seguente risultato (con la matrice “prima della cura” e “dopo la cura”):
Un modulo che risolve il problema potrà somigliare al seguente:
void sommaTotale( matrix a, int n)
{
int i,j,s;
for( i=1; i<=n; i++)
{
s=0;
for( j=1; j<=n; j++) s+=a[i][j];
a[i][n+1]=s;
}
}
//
Ciclo delle righe
//
Ciclo delle colonne
Come variazione, trasformiamo la matrice in “triangolare superiore” ed effettuiamo le somme DALLA
DIAGONALE IN POI. Come già visto in altre occasioni (doppi cicli e schemi numerici), basterà qualche
modifica per trasformare il doppio ciclo. In questo caso il discriminante è la DIAGONALE ( i==j):
void sommaTriangolare( matrix a, int n)
{
int i,j,s;
for( i=1; i<=n; i++)
{
s=0;
for( j=1; j<i; j++) a[i][j]=0;
for( j=i; j<=n; j++) s+=a[i][j];
a[i][n+1]=s;
}
}
//
//
Annulla PRIMA della diagonale
[i] è l’elemento sulla diagonale
Altri problemi
Un problema con un ciclo singolo, ma con soluzione non ovvia, consiste nell’individuare un elemento a caso
della matrice e scambiare tra di loro la riga e la colonna cui l’elemento appartiene. Nella figura sottostante,
l’elemento (che non va spostato) è evidenziato in verde, mentre i numeri evidenziati in blu vanno scambiato
con quelli evidenziati in rosso (lungo una specie di croce). In questo caso non serve un doppio ciclo, in
quanto si tratta di effettuare due percorsi lineari lungo la matrice, ma contemporaneamente. Occorre anche
notare che i due indici sono indipendenti, non vengono modificati nello stesso modo e nello stesso
momento: per ognuno dei due bisogna saltare la posizione corrispondente al “perno” della croce.
Nella seguente soluzione, sono indicate con r e c le coordinate del perno:
void scambioACroce( matrix a, int
{
int i,j;
for( i=1,j=1; i<=n; i++,j++)
{
if ( i==r ) i++;
if ( j==c ) j++;
swap( a[r][j], a[i][c]);
}
}
n, int r, int c)
//
Ciclo unico con due indici
// Si salta la riga del perno
// Si salta la colonna del perno
// Scambio degli elementi blu con quelli rossi
Proviamo ora ad assegnare nuovi valori agli elementi della matrice, secondo uno schema regolare. Ad
esempio, possiamo creare un triangolo di Tartaglia21 (o di Pascal, fuori dall’Italia), limitato dalle dimensioni
della matrice. Il primo 1 viene messo in una sentinella:
void fontana( matrix a, int n)
{
int i,j;
zeroize(a,n);
a[0][1]=1;
for (i=1; i<=n; i++)
for (j=1; j<=n; j++)
a[i][j] = a[i-1][j-1]
}
21
//
//
//
Modulo che annulla tutta la matrice
Costruzione della prima riga
Ogni riga deriva da quella precedente
+ a[i-1][j];
Niccolò Fontana (1500?-1557), matematico bresciano, chiamato “Tartaglia” per un difetto di pronuncia.
Problemi
Alcuni esercizi per la navigazione (dove la chiave è lo spostamento corretto degli indici) e il riempimento
(dove la chiave è la scelta delle variabili da attribuire agli elementi della matrice.
Dove c’è da elaborare gli elementi, di fianco al testo c’è un esempio di come potrebbe essere la matrice
all’ingresso e all’uscita del modulo; nei problemi di inserimento c’è solo il risultato finale, l’unico che conta.
ATTENZIONE !!!
Gli esempi presentati negli esercizi hanno pochi elementi, e possono offrire la tentazione di
cavarsela con un paio di cicli (magari uno per la prima e uno per la terza riga). In realtà le soluzioni
dovrebbero essere progettate per un numero qualsiasi di elementi della matrice, anche se
contenesse centinaia di righe.
Esercizi elementari
1 Sommare a coppie come nell’esempio (nel primo elemento ci va la somma del primo e del secondo, il
quale viene annullato; nel terzo elemento la somma del terzo e del quarto, il quale viene annullato; e così
via).
. 1
. 2
. 3
.15
5
3
4
5
5
9
8
6
2
3
6
7
. 6
. 5
. 7
.20
0 7
0 12
0 14
0 13
0
0
0
0
2 Scambiare la prima riga con la seconda, la terza con la quarta, e così via.
. 1
. 2
. 3
.15
5
3
4
5
5
9
8
6
2
3
6
7
. 2
. 1
.15
. 3
3
5
5
4
9
5
6
8
3
2
7
6
3 Inserire nella matrice numeri corrispondenti alle righe (alla rovescia).
. 1
. 2
3
4
1
2
3
4
1
2
3
4
1
2
3
4
4 Inserire nella matrice numeri corrispondenti alle righe (alla rovescia).
. 4
3
. 2
1
4
3
2
1
4
3
2
1
4
3
2
1
Esercizi più impegnativi
5 Annullare tutti gli elementi che ne hanno sulla destra almeno uno uguale (anche a distanza).
.
.
.
.
1
2
7
5
5
3
4
6
5
3
8
5
2
3
7
6
.
.
.
.
1
2
0
0
0
0
4
0
5
0
8
5
2
3
7
6
6 Inserire nella matrice numeri progressivi, per diagonali (triangolo inferiore). Gli altri vanno annullati. I
punti di partenza del ciclo interno sono: (1,1) (2,1) (3,1) (4,1)
. 1
. 5
. 8
.10
0
2
6
9
0
0
3
7
0
0
0
4
Consigli per l’approfondimento
Tutti questi esercizi possono anche essere variati, con soluzioni simmetriche o rovesciate: in questo modo è
possibile familiarizzare con tutte le situazioni di navigazione all’interno di matrici. Gli esercizi delle
prossime pagine sono presentati e analizzati più in dettaglio.
Sommare gli elementi secondo percorsi a forma di L (prima in basso, poi a destra, con cicli sempre più
brevi). Le somme vanno nelle sentinelle sulla destra. L’esempio riporta una matrice 5x5 all’ingresso del
modulo, che dovrebbe uscire dal modulo con le sentinelle occupate, come se fosse 5x6. I colori mostrano i
differenti percorsi lungo la matrice.
PRIMA
.
.
.
.
.
1
2
3
5
7
5
3
4
6
3
5
3
8
5
4
2
3
6
6
2
DOPO
5
8
7
2
2
1
2
3
5
7
5
3
4
6
3
5
3
8
5
4
2
3
6
6
2
5
8
7
2
2
5
13
29
31
29
La prima considerazione è che le somme sono esattamente n: questo implica necessariamente un ciclo. Per
analizzare la struttura interna del ciclo, occorre osservare come ogni passo sia composto da due movimenti
distinti (basso+destra): questo implica che il ciclo delle somme deve contenere due cicli in successione (non
un doppio ciclo, non un ciclo, non tre, né altre curiose invenzioni).
Nello schema seguente sono visualizzati i primi tre cicli (con colori corrispondenti all’esempio), con i
concetti relativi alla variazione tra un passo e l’altro:
Non ci sono grosse restrizioni su come terminare il primo ciclo interno ed iniziare il secondo: è comunque
leggermente più semplice tenere il primo ciclo più corto del secondo (i numeri sommati sono sempre in
numero dispari, quindi è impossibile avere due cicli della stessa lunghezza), a causa della gestione dei
relativi indici.
L’aspetto della struttura del modulo dovrebbe essere come il seguente:
for (h=1; h<=n; h++)
{
somma=0;
for (i...)
for (j...)
a[h][n+1] = somma;
}
//
Indice dei singoli percorsi
//
//
//
In basso
A destra
Sentinella
Sommare gli elementi secondo percorsi diagonali (basso/destra). Le somme vanno nelle sentinelle in basso e
sulla destra. L’esempio riporta una matrice 5x5 all’ingresso del modulo, che dovrebbe uscire dal modulo con
le sentinelle occupate, come se fosse 6x6. I colori mostrano i differenti percorsi lungo la matrice.
Per semplicità, il problema può essere suddiviso in due parti, data la differenza nella gestione dei punti di
partenza e di arrivo (le sentinelle) dei percorsi. Come nel precedente esempio, la prima parte sarà quella con
lunghezza minore (anche se non ci sono grosse restrizioni nell’operare la scelta opposta)
1 – Sentinelle in basso
PRIMA
.
.
.
.
.
1
2
3
5
7
5
3
4
6
3
5
3
8
5
4
2
3
6
6
9
2 – Sentinelle a destra
DOPO
5
8
7
2
5
.
.
.
.
.
.
1
2
3
5
7
0
5
3
4
6
3
7
PRIMA
5 2 5
3 3 8
8 6 7
5 6 2
4 9 5
3 13 20
.
.
.
.
.
.
1
2
3
5
7
0
5
3
4
6
3
7
5 2 5
3 3 8
8 6 7
5 6 2
4 9 5
3 13 20
DOPO
.
.
.
.
.
.
1
2
3
5
7
0
5
3
4
6
3
7
5 2 5
3 3 8
8 6 7
5 6 2
4 9 5
3 13 20
0
5
10
15
16
23
Nello schema seguente sono approssimativamente visualizzati i differenti percorsi:
Nelle due “metà” della soluzione ci sono rispettivamente n e n+1 cicli. I percorsi della prima metà partono
da righe diverse (progressive), mentre hanno la stessa colonna iniziale (1); nella seconda metà queste
considerazioni vengono rovesciate. Per questo è opportuno suddividere il problema in due parti: un unico
doppio ciclo è possibile, ma a prezzo di complicarne eccessivamente la struttura.
L’aspetto della struttura della prima metà del modulo dovrebbe essere come il seguente:
for (h=1; h<=n-1; h++)
{
i = h;
j = 1;
while (...)
{
...
i++; j++;
}
}
//
Indice dei singoli percorsi
//
//
//
Riga iniziale progressiva
Colonna iniziale = 1
Controllare se siamo nella matrice
//
Basso/destra
L’aspetto della struttura della seconda metà del modulo dovrebbe essere come il seguente:
for (h=1; h<=n; h++)
{
i = 1;
j = h;
...
}
//
Indice dei singoli percorsi
//
//
Riga iniziale = 1
Colonna iniziale progressiva
STRINGHE
Cosa sono le stringhe
Quasi tutte le applicazioni informatiche richiedono la visualizzazione o la richiesta di dati di tipo testuale:
durante l’input, messaggi di invito (prompt) vengono usati al momento della richiesta di un dato per
facilitare il lavoro dell'utente; durante l’output, ogni informazione presentata su uno schermo o in stampa
deve essere corredata da opportuni didascalie o sigle che ne descrivono il significato.
Esempio di un testo tratto dall’Eneide (2.6.8-255):
Questa visualizzazione è stata fatta con un’applicazione didattica, che nell’ultimo riquadro sulla destra
visualizza la lunghezza del testo.
Gli spazi vengono contati come caratteri a tutti gli effetti, quindi il seguente testo è diverso dal precedente:
Esistono applicazioni interamente dedicate alla gestione dei testi: la videoscrittura e il controllo ortografico,
le traduzioni, la messaggistica nelle varie forme, anche i software per l’enigmistica. I motori di ricerca
nell’Internet fanno uso di giganteschi database di testi che raccolgono il contenuto delle pagine web. Ha a
che fare con le stringhe anche il dizionario T9 usato nella scrittura degli SMS.
I nostri dati anagrafici costituiscono una enorme massa di testo: nome, cognome, indirizzo, codice fiscale,
occupazione, colore degli occhi,... Anche la nostra altezza, pur essendo rappresentata con un numero, non
viene solitamente elaborata come tale (tranne, che nelle statistiche etnografiche), ma solo scritta così com’è.
Lo stesso vale per i numeri presenti in una targa o in un documento.
Nell'informatica le informazioni di tipo testuale vengono definite stringhe. Le stringhe sono dati costituiti da
sequenze di caratteri di lunghezza varia. Esiste anche la stringa nulla, lunga zero caratteri.
La lunghezza massima di una stringa dipende dal linguaggio e dalla distribuzione: in wxDevC++, ad esempio,
è di 1.073.741.820.
ATTENZIONE !!!
La gestione degli spazi è una cosa piuttosto importante e delicata. Non essendo facilmente visibili
sullo schermo, è facile dimenticarsi della loro presenza: quindi è opportuno usare alcune funzioni
del linguaggio per assicurarsi che una stringa contenga solo gli spazi necessari.
Questo significa che, per essere elaborata correttamente, una stringa inserita maldestramente con spazi inutili
deve essere normalizzata (senza spazi all’inizio o alla fine, solo uno spazio tra una parola e l’altra). Questo
può facilitare la ricerca delle parole, ma soprattutto prepara la stringa in una situazione standard, senza il
problema di contare ogni volta gli spazi incontrati.
Stringa non normalizzata
Stringa normalizzata
" it’s twelve o’clock
at night
"
“^
^
^^^
^^^
"it’s twelve o’clock at night"
L’eliminazione degli spazi iniziali e finali è chiamata trimming, (to trim=tagliare). Alcuni linguaggi hanno
funzioni predefinite per eseguirla: il C++ non è tra questi. D’altra parte si è sempre distinto per una gestione
discutibile delle stringhe (scomoda e macchinosa).
A closer look
Vengono dette “alfanumeriche” le stringhe contenenti un misto di lettere e cifre, o anche solo cifre. In questo
caso vengono anche dette “stringhe numeriche”, così come “alfabetiche” quando contengono solo lettere
(compresi apostrofi e spazi). Una stringa numerica non è un numero22.



Alfanumerica
Numerica
Alfabetica
CG213LA
14250001
four candles
L’esempio di stringa numerica è preso dai programmi di contabilità23.
Da ricordare:





ogni stringa ha una lunghezza variabile;
il primo carattere di una stringa è in posizione ZERO;
per questo motivo, l’ultimo carattere della stringa “a” è in una posizione indicata da lunghezza – 1;
uno spazio bianco (BLANK) occupa una posizione esattamente come gli altri caratteri;
gli spazi all’inizio o alla fine della stringa fanno parte della stringa (se non si usano, è consigliabile
eliminati);
 lo stesso vale per sequenze di due o più spazi: se non servono, è meglio che lo spazio sia uno solo
(normalizzazione);
 una stringa può avere lunghezza ZERO (stringa vuota o stringa nulla), quando viene inizializzata con ""
(due virgolette dello stesso tipo, attaccate);
 "" è diverso da " ": la stringa nulla NON è la stessa cosa di una stringa composta da uno o più spazi.
Il simbolo del BLANK ha un suo senso: in tempi andati, le istruzioni venivano preparate a mano sui “fogli di
programmazione”, dove diventava problematico vedere quanti spazi potevano essere compresi in una stringa.
I simboli sono certamente più visibili degli spazi “veri”. Anche adesso si usano, talvolta, appunti manoscritti,
dove può essere utile capire quanti spazi vadano inseriti in qualche stringa.
Lo storico simbolo del BLANK
(un ibrido tra b e k)
Tra le operazioni che si possono fare su una stringa troviamo:

concatenazione (o somma) di stringhe (“abito” + BLANK + “blu” diventa “abito blu”);

modifica dei caratteri presenti (“garibaldi” può essere trasformato in “Garibaldi” );

cancellazione di parte della stringa (“armadillo“ può diventare “armadio“);

inserimento di stringhe (“musica gradevole” può diventare “musica non gradevole”);

sostituzione di parti di stringhe (“fresh crab legs” può diventare “fresh chicken legs”)24;

ricerca di stringhe all’interno di altre (“su“ esiste all’interno di “consumo”, “crab“ non esiste).
22
Altrimenti non si chiamerebbe “numero”, non “stringa”.
In tali programmi tutte le componenti del bilancio di un’azienda (materiali, clienti, disponibilità di denaro) sono
spesso indicate con tre numeri organizzati in modo gerarchico: ad esempio, 1425 potrebbe indicare tutti i clienti
dell’azienda, 0101 tutti i conti correnti bancari o postali. La struttura completa prende il nome di piano dei conti.
24
Evidentemente, la sostituzione può essere considerata come cancellazione + inserimento
23
Le stringhe nel linguaggio C
Il linguaggio C (precursore del C++) mette a disposizione il tipo char, che può contenere un singolo
carattere. Un char occupa un byte in RAM. In effetti corrisponde ad un numero intero senza segno (detto
unsigned), con valori compresi tra 0 e 255, corrispondenti alla codifica ASCII estesa.
char c;
c = 'a';
// dichiarazione
// assegnamento, contrassegnato da due apici, singoli
Queste variabili si possono usare per le cosiddette escape sequence25: si tratta dei caratteri di controllo della
console, indicati dalla barra retroversa (backslash) seguita da un carattere che non è quello che verrà
visualizzato, ma corrisponde all’effetto che l’output del carattere (cout << c;) provoca:
c
c
c
c
c
=
=
=
=
=
'\r';
'\n';
'\t';
'\b';
'\a';
//
//
//
//
//
CR: Carriage Return (ritorno carrello)
LF: line feed (avanzamento di linea)
tabulazione
backspace: cancella l’ultimo carattere visualizzato
alarm: la “campanella” del PC (emette un beep)
N.B.: i caratteri tra gli apici sono 2, in realtà l'effetto è unico.
Ad esempio, per far emettere un “beep” al computer le istruzioni necessarie sono le seguenti:
char c;
c = '\a';
cout << c;
Molti di questi casi sono un retaggio delle storiche console TTY26 (quelle senza display).
Con quei dispositivi, l’unico segnale acustico era un campanello analogo a quello delle
macchine da scrivere; anche con i primi computer l’altoparlante poteva emettere solo un
tono, del quale si poteva modificare solo la frequenza (niente mp3 a quei tempi). Gli altri
caratteri di controllo servivano per gestire l’ output, che avveniva su un modulo cartaceo
continuo.
Ormai le escape sequence sono praticamente inutili, dato che le interfacce sono cambiate di
molto da quei tempi: le uniche usato normalmente sono il LF ('\n') e il NULL ('\0').
In altri casi la sequenza serve a indicare caratteri particolari usati dal linguaggio, oppure direttamente i codici
ASCII dei caratteri:
c
c
c
c
c
c
=
=
=
=
=
=
'\\';
'\'';
'\"';
'\0';
'0x21';
(char)65;
//
//
//
//
//
//
per scrivere veramente una barra inversa (UNA SOLA !!!)
singolo apice
doppi apici
NULL (carattere nullo: bit tutti a zero)
codice ASCII esadecimale (hex); 21 16(3310) = '!'
codice ASCII decimale. Il numero “diventa” char (65=A)
L’ultima assegnazione viene detta type casting27. L’espressione deriva dal verbo to cast, che indica il
dare una forma agli oggetti tramite uno stampo: è un’esemplificazione un po’ ardita del processo di
trasformare un tipo di dato in un altro28.
Come vedremo immediatamente, per costruire una stringa bisogna allocare diversi char, fino alla lunghezza
desiderata.
25
Così chiamate perché inizialmente potevano essere assegnate tramite il tasto ESC (escape).
TTY = TeleTYpe (telescriventi)
27
A volte typecasting o anche solo casting28
Si può fare anche con altri tipi, ad esempio trasformando (temporaneamente) delle variabili double in int:
w = (int)a % (int)b;
26
Gli improbabili array of char
Il tipo stringa è derivato dal tipo elementare char: in pratica è un vettore di char. Nell'esempio la stringa
viene delimitata con una coppia di virgolette29. Si è anche praticamente costretti a fare una cosa
sconsigliabile: inizializzare una variabile nel momento della sua dichiarazione:
char s[] = "procul este"; // la lunghezza (12) viene calcolata dal C
La lunghezza è 12 perché in memoria verrà riservato spazio per 11 caratteri più il carattere terminatore NULL
(il '\0' visto prima). Questo “tappo” è necessario perché il C non prevede un posto dove memorizzare la
lunghezza della stringa, ma la determina scorrendola alla ricerca del primo NULL che incontra. Questo
implica l’ulteriore rischio che la presenza di un NULL all’interno della stringa la fa apparire come più corta di
quello che è. Un altro strano effetto è che la stringa nulla, assegnata con virgolette vuote "", occupa non zero
byte ma uno.
Come se tutto questo non bastasse, il programmatore che deve gestire le stringhe in C si trova davanti un
lavoro piuttosto arduo, data l'assenza di operatori semplici e intuitivi.
Le alternative disponibili sono ugualmente sconsolanti:
 trattare le stringhe esattamente come “array of char”, manovrando indici in lunghe sequenze di cicli tutti
simili tra loro e ardui da comprendere;
 usare alcune funzioni incluse nella libreria string.h, macchinose e difficili perfino da pronunciare.
Nel primo caso ci si dovrebbe improvvisare tutti gli operatori da zero, una “mission impossible”. Nel
secondo caso, la libreria string.h gli operatori li ha, ma sono un vero tormento. Un paio di esempi:
Al posto dell’assegnamento (=) va scritto:
strcpy( stringa, "scis quid vis");
Per aggiungere una stringa ad un’altra:
strcat( stringa, ", Phoebe, suspicari?”);
Altre funzioni dai nomi improbabili:
strtok, strcspn, strpbrk, …
Le stringhe in C++
Il C++ deriva dal linguaggio C. È stato progettato per consentire ai programmatori un livello di astrazione
maggiore grazie all'aggiunta di tipi di dati evoluti e costrutti avanzati per la loro manipolazione semplificata:
l'obiettivo era quello di aumentare la produttività dello sviluppatore, per poter eseguire un lavoro maggiore
nella unità di tempo. Questa esigenza è particolarmente sentita nel caso di progetti particolarmente
impegnativi, che si misurano in anni-uomo30: ma anche un programmatore che fa cose meno eclatanti è
avvantaggiato dal potersi concentrare sulla soluzione algoritmica del suo problema senza che la complessità
del linguaggio distolga di continuo la sua attenzione.
Per ovviare alla miserabile gestione delle stringhe del C, il C++ mette a disposizione un tipo di dato nativo
per definirle, anziché un tipo di dato derivato (quello nativo essendo il char). In realtà è molto di più: si
tratta di una classe, ovvero uno “stampo” per definire delle variabili chiamate oggetti, la cui caratteristica è
di essere in grado di rispondere automaticamente alle situazioni con moduli specifici. In effetti il C++ è un
linguaggio ad oggetti31, che consente di praticare il cosiddetto OOP (Object Oriented Programming).
Un’analisi completa della programmazione ad oggetti è al di fuori degli obiettivi di questo corso. Potremo
limitarci alle istruzioni principali per dichiarare e manipolare le stringhe in C++, con operatori di tipo
matematico più semplici da usare delle improbabili funzioni del C.
29
Un char è delimitato da apici semplici; una stringa è delimitata da apici doppi. Questa è un’altra delle strane
complicazioni del linguaggio nella gestione delle stringhe.
30
Da non confondere con gli anni del calendario: un progetto da 50 anni-uomo, ad esempio, potrebbe essere assegnato
ad un “team” di 25 programmatori per essere terminato in 2 anni.
31
Si dice anche “orientato agli oggetti”.
Esempi
Per rendere disponibili le funzioni dedicate alle stringhe è necessario includere l’apposita libreria:
#include <string>
//
ATTENZIONE !!! NON string.h
Per dichiarare e inizializzare una variabile stringa si può procedere normalmente:
string s1;
s1 = "in the lobby ";
string s2 = "of the hotel";
//
//
//
Dichiarazione di una stringa nulla
Inizializzazione
Dichiarazione e inizializzazione
Le classi hanno un metodo speciale per creare i corrispondenti oggetti, detto costruttore: si tratta di una
funzione che viene attivata automaticamente al momento della dichiarazione di una variabile. Come si
intuisce dal nome, il costruttore permette di inizializzare la variabile passando eventuali parametri necessari
per specificarne le caratteristiche iniziali. Il costruttore di un oggetto semplice come la stringa non ha grandi
possibilità: l’unico parametro che solitamente si passare è una stringa (costante o variabile). Si può passare
come secondo parametro un numero, che diventerà la lunghezza della stringa, ma è rischioso: può servire per
troncare una stringa troppo lunga, ma senza conoscere la lunghezza del primo parametro il pericolo è di
usare una lunghezza superiore, finendo per inglobare, e presumibilmente danneggiare, anche altre parti del
programma (certamente non una buona idea). Esempi di uso del costruttore, consigliabili e non:
string
string
string
string
s3("at midnight");
s4(s3);
s5("at midnight",20); //
s6("at midnight",2); //
Don’t try this at home
Cui prodest???
L’input e l’output possono essere effettuati come per ogni altro tipo di dato, tramite i flussi (stream) cin e
cout e gli operatori funnel (imbuto) << (insert into stream) e >> (extract from stream):
string messaggio, s;
messaggio = "Scrivere un testo:";
cout << messaggio;
//
equivale a cout << "Scrivere un testo:"
cin >> s;
Il problema con il cin è che l’input viene considerato concluso non solo con l’Enter, ma anche al primo
spazio inserito, anche se precedente al termine dell’operazione. Per consentire la scrittura di frasi con più
parole si può usare la funzione getline, specificando tre argomenti: il flusso di input, la stringa da inserire
e il codice ASCII del carattere terminatore, corrispondente al tasto Enter:32
cout << "Inserire un testo: ";
getline( cin, s, (char)10); //
il codice ASCII 10 trasformato in char
C’è tuttavia un caso particolare in cui si deve per forza ricorrere ad un altro tasto, ed è quando si deve
leggere una stringa “multilinea”. In questo caso occorre usare come terminatore un carattere diverso da
Enter (che viene usato per andare a capo): uno qualunque va bene, basta che sia poco comune e che l’utente
conosca la sua funzione. Il carattere “terminatore” dell’input non verrà salvato nella stringa. Nell’esempio la
stringa viene conclusa alla pressione di un carattere '^', detto caret, solitamente poco usato nei dati.
cout << "Inserire un testo - Terminare l’input con il carattere ^ :";
getline( cin, s, '^');
È evidente quanto sia rischioso procedere in questo modo: bisogna confidare nell’efficienza dell’utente
(ahimè, non sempre affidabile) e c’è sempre il rischio che il carattere terminatore sia invece da inserire nel
testo. D’altra parte, l’interfaccia testo standard del C/C++ non consente grandi scelte: versioni più sofisticate
del linguaggio, al contrario, prevedono comode aree di testo dove l’input è libero da vincoli.
La funzione getline è sicuramente più utile quando, anziché il flusso cin, viene indicato il nome di un file
su disco dal quale leggere il testo. Questo caso verrà esaminato in una successiva parte del corso.
32
In realtà il tasto Enter produce due byte (in binario, la sequenza è 00001101 00001010): fa testo il secondo.
L’unica operazione “matematica” sulle stringhe è la concatenazione di più stringhe tra loro (detta a volte
append), che somiglia fortemente ad una somma; è quindi naturale l’uso degli operatori '+' e '+=':
string Disney("PIPPO”);
//
Uso del costruttore
Disney+= " ha un cane: PLUTO";
//
ovvero Disney = Disney + "…";
Una stranezza comunque c’è, ed è l’impossibilità di concatenare stringhe costanti con l’operatore + se non
dopo una variabile. Come si nota negli esempi, la variabile potrebbe essere solamente il primo o il secondo
addendo in una somma di stringhe:
s2 = s1 + "size" + " 48";
s2 = "size" + s1 + " 48";
s2+= s1 + "size" + <" 48";
s = "A" + "B";
s2 = "size" + " 48" + s1;
s+= "A" + "B";
//
//
//
//
//
//
OK
OK
OK
ERRORE
ERRORE
ERRORE
Anche nel caso dei confronti tra stringhe, si può ricorrere ai sei operatori relazionali ==,!=, <, <=, >, >=. Ad
esempio:
if ( s1 == s2 ) cout << "le due stringhe sono uguali";
Gli operatori di maggioranza e minoranza sono utili soprattutto per l’ordine alfabetico. In questi casi occorre
fare attenzione alle maiuscole e alle minuscole: essendo il codice ASCII della “a” minuscola (97) superiore
a quello della “Z” maiuscola (90), l’errore è sempre alle porte.
Metodi
La stringa assomiglia molto ad una variabile comune, ma in realtà è molto di più: essendo un oggetto, ha a
disposizione funzioni specifiche, chiamate metodi nella terminologia OOP. Tali funzioni sono destinate al
calcolo di alcune caratteristiche degli oggetti: ad esempio, una stringa può esprimere la propria lunghezza.
Si dice proprio così, perché si ha l’illusione che sia l’oggetto stesso a provvedere alla bisogna, come si può
vedere dall’esempio:
s1 = "twice a year";
cout << s.length();
cout << length(s);
//
//
risultato: 12
NO!!! Questo è un errore
Un metodo viene attivato scrivendo il nome dell’oggetto seguito da un punto e dal nome del modulo. È una
modalità OOP che viene definita “mandare un messaggio all’oggetto”. Come con ogni sottoprogramma, non
va dimenticata la coppia di parentesi, che in questo caso è vuota, non essendoci bisogno di altre
informazioni: è la stringa chiamata che risponde, “esaminando” la propria struttura dati.
Si sente la mancanza di un metodo length nella gestione degli array. Per ragioni oscure, tuttavia, pare che
questo non sia previsto dagli sviluppatori del linguaggio.
L’oggetto string rimane comunque un vettore di caratteri, quindi possiamo usare l’operatore di accesso
"[]" ai singoli caratteri, come in tutti gli array, ricordando che il primo carattere è in posizione zero. Per
effettuare la visita di una stringa si può quindi procedere con una sequenza “quasi” base; nell’esempio si
visualizzano i caratteri uno per riga:
for( i=0; i<s.length(); i++) cout << s[i] << endl;
L’operatore di accesso funziona anche per la gestione dei singoli caratteri, che possono essere raggiunti
anche con un metodo:
s[1] = s[4] = 'A';
//
stile array
s.at(1) = s.at(4) = 'A';
//
con il metodo "at"
Metodo che fornisce la dimensione massima disponibile per una stringa:
cout << s.max_size ();
//
con wxDevC++: 1.073.741.820 (1 Gbyte)
Metodo che controlla se una stringa è vuota:
if ( s.empty() ) cout << "La stringa è vuota";
Altri esempi
I problemi più semplici sono quelli che visitano una stringa un carattere alla volta, con l’operatore [] o con il
metodo .at(). Alcuni di essi trattano la gestione delle lettere maiuscole e minuscole; possono essere quindi
utili delle funzioni char per le rispettive conversioni applicate ai singoli elementi di una stringa:
char toUpper(char c)
{
if ( c>='a' && c<='z' ) c-=32;
return c;
}
char toLower(char c)
{
if ( c>='A' && c<='Z' )c+=32;
return c;
}
Costruzione di stringhe totalmente maiuscole o minuscole
In ciascun sottoprogramma viene applicato il modulo char a tutti i caratteri della stringa.
string toUpper(string s)
{
int i;
ATTENZIONE !!!
for( i=0; i<=s.length()-1; i++)
s.at(i) = toUpper(s.at(i));
I moduli hanno lo stesso nome dei
return s;
precedenti, ma il C++ non rischia di
confonderli: essi sono differenziati dal
}
tipo dei parametri: quindi il modulo
string toLower(string s)
toUpper applicato ad un char non è
{
quello applicato ad una string.
int i;
for( i=0; i<=s.length()-1; i++)
s.at(i) = toLower(s.at(i));
return s;
}
Costruzione di una stringa rovesciata rispetto a quella data
Non tutti gli esempi sono utili “in sé”; alcuni sono solo esercizi di abilità, come questo. Il problema si risolve
con un ciclo alla rovescia, che estrae i caratteri dall’ultimo al primo, aggiungendoli ad una nuova stringa.
string rovescia(string s)
{
int i; string t;
t = "";
for( i=s.length()-1; i>=0; i--) t+=s.at(i);
return t;
}
N.B.
La nuova stringa viene
inizializzata come null,
analogo allo ZERO di una
somma numerica.
Costruzione di una stringa con caratteri minuscoli e maiuscoli alternati
Questo esempio mostra come i metodi di una stringa possano essere “composti”, ossia applicati in sequenza:
il risultato del primo metodo viene passato a quello successivo (e la cosa potrebbe proseguire con altri
metodi). In questo caso, prima si ricava un carattere della stringa con il metodo at(), poi lo si passa al
metodo toLower() o toUpper() per trasformarlo in minuscolo o in maiuscolo:
string alternata(string s)
{
int i; string t;
t = "";
for( i=0; i<s.length(); i++)
if ( i%2 == 0)
t+= toLower(s.at(i));
else t+= toUpper(s.at(i));
return t;
}
Operazioni stringa avanzate
Metodo per l’estrazione di una sottostringa (substring)
La gestione “char by char” è decisamente limitata, in quanto tratta le stringhe come array. L’oggetto
string è dotato di metodi più potenti, come quelli per l’estrazione e la gestione di sequenze di caratteri
(dette “sottostringhe”, la più corta delle quali è la stringa nulla, la più lunga l’intera stringa).
Una sottostringa si estrae con il metodo substr, che prevede due parametri: la posizione iniziale della
sottostringa da estrarre e la sua lunghezza. Se non si specifica la lunghezza, l’estrazione termina alla fine
della stringa. Vediamo alcuni esempi:
s = "I had a great meal":
s.substr(2,3)
"had"
s.substr(0,1)
"I"
(ATTENZIONE!!! Non è un char, è una stringa)
s.substr(14)
"meal"
Costruzione di un “nome proprio” (proper)
In alcuni linguaggi esiste una funzione proper, che presenta una stringa con l’iniziale maiuscola e il resto
minuscolo, come nei nomi propri. In questo esempio possiamo anche vedere l’applicazione del metodo
substr senza il secondo parametro, visto che la parte da rendere minuscola è tutta la stringa escluso il
carattere iniziale (che è in posizione ZERO: l’estrazione partirà da 1). Nella function applichiamo i seguenti
metodi e moduli: at(), substr(), toLower() e toUpper().
string proper(string s)
{
return toUpper(s.at(0)) + toLower(s.substr(1));
}
Per attivare la function si potrà scrivere, in un modulo chiamante, una delle seguenti combinazioni:
cout << proper(s);
t = proper(s);
cout << t;
//
//
Conversione diretta
Assegnazione ad altra variabile (STRING!!!)
N.B.: questo funziona su una stringa formata da un’unica parola; se la stringa è composta da più parole,
occorre suddividerle, come si vedrà in un esempio successivo.
Altre operazioni: inserimento, cancellazione, sostituzione




insert
erase
replace
clear
inserisce testo in una stringa alla posizione indicata
rimuove parte di una stringa indicando posizione di partenza e lunghezza
rimuove (come erase) e sostituisce con un nuovo testo
cancella tutto il contenuto della stringa
Alcuni esempi, che si consiglia intervallare con dei cout per verificare i risultati:
s = "Houston, we’ve had a problem";
s.insert(15,"not ");
s.erase(15,4);
s.insert(s.length()," here");
s.erase(28);
s.replace(11,7," have");
s.clear()
//
//
//
//
//
//
//
Jim Lovell (14/04/1970)33
Diventa "we’ve not had”
Cancella i 4 caratteri appena inseriti
Fatta in fondo, equivale alla somma
Cancella fino alla fine (da " here")
La frase modificata nel film Apollo 13
Cancella tutto senza pietà
ATTENZIONE !!!
Evitare di fare operazioni indicando una posizione esterna alla stringa (negativa o superiore alla lunghezza
della stringa). Il programma andrà quasi sicuramente in errore e terminerà in modo anomalo34.
33
Qualche secondo prima, Fred Haise chiama “Houston” e Jack Swigert aggiunge “I believe we’ve had a problem
here”. Lovell ripete (quasi per intero) su richiesta da Houston (http://apollo13.spacelog.org).
34
Una volta si diceva “andrà in abend”, da “abnormal end“.
Ricerca di sottostringhe
Se non sappiamo in che posizione si trova una sottostringa, la possiamo cercare usando il metodo find:
s
i
j
b
=
=
=
=
"Bond. James Bond ";
s.find("John");
s.find("James");
s.find("Bond");
//
//
//
//
007
risulta -1 (=NON TROVATO)
risulta 6
risulta 0
Per cercare ulteriori sottostringhe, la ricerca può partire da una posizione successiva. Quindi il secondo
“Bond” viene individuato così:
b = s.find("Bond",b+1);
//
risulta 12
Si può anche cercare l’ultima ricorrenza della sottostringa con rfind (right find):
b = s.rfind("Bond");
b = s.rfind("Bond",b-1);
//
//
risulta 12
risulta 0
Le ricerche ripetute servono più che altro per trovare le parole, con un ciclo che sposta il punto di ricerca fino
a quando il risultato diventa -1 (sottostringa non trovata).
b = " ";
p = s.find(b);
while( p>=0 )
{
cout << p << endl;
p = s.find(b,p+1);
}
ATTENZIONE !!!
Questa ricerca non è completa se non ci
assicuriamo che la stringa è normalizzata
(priva di spazi inutili). Negli esercizi
seguenti ci sono le soluzioni corrette.
NORMALIZZAZIONE DEGLI SPAZI
Una stringa può contenere spazi inutili: all’inizio, alla fine, spazi doppi. Essi possono provocare problemi
nella gestione automatica delle stringhe, soprattutto quelli iniziali, che alterano l’ordine alfabetico (lo spazio
è considerato minore di tutti gli altri simboli) e per di più risultano difficili da individuare. Inoltre, spazi
doppi tra le parole possono stravolgere le ricerche di sottostringhe contenenti spazi singoli.
Viene detta normalizzata (o, in gergo, purgata) una stringa senza spazi inutili. Per quelli all’inizio e alla
fine si controlla, rispettivamente, il primo e l’ultimo carattere: se risulta blank, viene eliminato. Potendocene
essere più di uno, entrambe le operazioni vanno in un ciclo. Per quanto riguarda gli spazi multipli, si può fare
la ricerca di un doppio spazio: se esiste all’interno della stringa, essa viene ricostruita eliminando uno dei
due spazi (ovviamente, non importa quale). Mettendo anche questa operazione in un ciclo, si possono
eliminare tutti gli spazi doppi, e di conseguenza anche quelli multipli.
Con il seguente modulo si ha l’effetto desiderato. In esso viene usata una variabile char c contenente uno
spazio e una variabile string d contenente due spazi (ottenuti con due nuove function chiamate space), in
modo da rendere il più possibile chiare le istruzioni successive difficili da interpretare. Osservando le
istruzioni del modulo, è probabilmente più immediato capire le espressioni c e d che vedere due spazi tra
virgolette, facilmente confondibili con spazi di lunghezza diversa:
string purge( string s)
string space(int n)
{
{
int i; char c; string d;
int i; string s;
if ( s.length() > 0 )
s = "";
{
for(i=1;i<=n;i++) s+=" ";
c = space();
//
Uno spazio (char)
return s;
d = space(2);
//
Due spazi (string)
}
while(s.at(0)==c)
s.erase(0,1);
while(s.at(s.length()-1)==c) s.erase(s.length()-1,1);
char space()
while(( i=s.find(d))>=0)
s.erase(i,1);
{
}
return ' ';
return s;
}
}
SUDDIVISIONE IN PAROLE
Le parole sono parti di stringa comprese tra spazi. Per estrarre o elaborare le parole, occorre sapere dove
sono le coppie di spazi consecutivi (supponendo di avere normalizzato la stringa, facendo così in modo
questi spazi “consecutivi” siano separati da caratteri non vuoti).
Una soluzione è quella di usare il metodo di ricerca find, usando due indici per individuare le parole:
verranno chiamati i e f (inizio e fine).
Ad esempio, consideriamo la stringa normalizzata ”I had a dream”, e consideriamo la sequenza ideale dei
due indici:
Parola
i
f
1
0
1
2
2
6
3
7
8
4
9
14 ???
All’inizio il problema è minimo: trovare il primo spazio per definire f, dato che i è sicuramente zero. Una
volta elaborata la prima parola, il nuovo indice i può essere posto oltre la fine della stessa (quindi assume il
valore di f+1), dopo di che il prossimo spazio sarà individuato con una nuova ricerca: questa però non deve
partire dall’inizio della stringa, ma dalla nuova posizione i, procurando un nuovo valore dell’indice f.
In questo modo i due indici si inseguono, con f sempre avanti a i: ogni parola inizierà alla posizione i e
terminerà alla posizione f. Per estrarla si applica il metodo substr partendo dall’indice i, per una
lunghezza pari a f-i:
t = s.substr(i,f-i);
L’unico guaio si incontra all’ultima parola, dove l’indice f assume il valore di -1, non essendoci spazi
successivi: sarebbe più semplice invece il valore 14 proposto nella tabella. Per evitare complicazioni e
trattare tutte le parole allo stesso modo, la soluzione è un “uovo di Colombo”: basta aggiungere uno spazio
alla stringa. In questo modo anche l’ultima parola inizia all’indice i e ha lo spazio all’indice f:
"I had a dream"
stringa normalizzata
"I had a dream "
stringa con spazio slack
L’apparente contraddizione di normalizzare la stringa e poi di aggiungere uno spazio in coda è un artificio
per semplificare la soluzione: lo spazio viene poi eliminato implicitamente quando si esce dal
sottoprogramma. Questo dato aggiuntivo “usa e getta” viene chiamato in gergo slack (se fosse una variabile,
sarebbe una slack variable),
Per scrivere le parole separate risulta:
void scriviParole( string s)
{
int i,f; string b;
b = space(1);
s = purge(s) + b;
i = 0;
f = s.find(b);
while( f>0 )
{
cout << s.substr(i,f-i) << endl;
i = f+1;
f = s.find(b,i);
}
}
Musings
Come convivere con le vecchie stringhe del C
Alcune importanti funzioni del linguaggio accettano solo le vecchie stringhe del C: tra queste la printf.
Quindi per usarle serve un meccanismo di conversione verso il passato che ci garantisca la retrocompatibilità. Esso viene fornito dal metodo c_str(), come si vede in questi esempi:
printf("%s",s);
//
ERRORE
const char *old;
old = s.c_str();
printf("%s",old);
//
//
//
pointer to char
conversione
printf %s vuole una stringa vecchia maniera
Si può anche fare tutto senza passaggi intermedi (e senza puntatori):
printf("%s\n", s.c_str());
Come concatenare stringhe e variabili numeriche
È noto che l’interfaccia standard è composta da 25 righe di 80 colonne l’una (residuo del vecchio DOS), e
che può essere ridimensionata agendo sul suo menù di sistema (click sull’icona a sinistra nella barra del
titolo e modifica delle proprietà). Lo stesso risultato si può ottenere attraverso il comando MODE, una volta
aperto il prompt dei comandi dal menù degli accessori di Windows. Ad esempio posso stringere la console :
MODE CON COLS=40
Ma pochi sanno che si può ottenere questo risultato anche tramite le istruzioni C dei nostri programmi, in
modo da adeguare lo schermo alla quantità di dati da visualizzare. A partire da un minimo di 1 riga e 14
colonne. Ecco come ottenere lo schermo minimo (un rettangolino) :
system("MODE CON LINES=1 COLS=14");
In pratica abbiamo fatto una richiesta al sistema operativo, con la funzione system().
Lo snippet che segue chiede all’utente quante righe e colonne desidera, poi imposta lo schermo.
N.B.
Viene fatto uso di una variabile stringstream: è uno stream provvisorio, che prepara i contenuti dei flussi
manipolando variabili e stringhe. Esso necessita della libreria <sstream>.
#include <sstream>
...
int linee, colonne;
cout << "Inserire linee e
cin >> linee >> colonne;
/*
serve una variabile
una stringa che può
*/
stringstream flow;
flow << "MODE CON LINES="
string s;
s = flow.str();
system(s.c_str());
colonne schermo (1-50, 14-100):";
di tipo "FLUSSO" per generare
essere riversata nel cout
<< linee << " COLS=" << colonne;
// diventa stringa
// impostazione schermo
Additional problems
When working with strings, you should expect three basic function categories:



int
char
string
(when counting characters or searching for substrings)
(when a certain character is requested)
(when building a new string)
The library functions.h (or moduli.h, depending on preferences) should contain the case conversion
and blank management functions, as follows:








char toUpper(char c)
char toLower(char c)
string toUpper(string s)
string toLower(string s)
string proper(string s)
char space()
string space( int n)
string purge( string s)
PROBLEMS
1 Count how many blanks are contained in a string (int)
"good morning! I’m the plumber"
should return 4
This problem requires a single basic loop with a filter. Visit the string in the normal fashion, checking each
character for the given condition.
2 Return the first capital letter encountered in a string (char)
"good morning! I’m the plumber"
should return I
"quod stat illis"
should return a null char ('\0')
This problem requires a search loop. In these cases, the filter is incorporated into the loop condition.
i = 0;
found = false;
while( i<s.length() && ! found )
{
if (s.at(i)>='A' && s.at(i)<='Z')
found = true;
else
i++;
}
At the end of this loop, you may have found the required condition or not. If you did, the index i will point
to the corresponding position, and the switch found will be “true”; if you did not, the index will point to
a location outside the string (s.length()) and the switch will be “false”. Both variables can be tested to
verify a successful search.
Keep in mind that the function should return the character, not its position.
The null character is expressed by '\0'.
3 Swap the two halves of a string (string)
"plumber"
will return "mberplu"
4 Insert the string into itself at the midpoint (string)
"plumber"
will return "pluPLUMBERmber"
These problem do not require a loop. The solution consists in just extracting the required parts of the string,
then rearrange them as directed.
solutionS
1
int countBlanks( string s)
{
int i,c;
c = 0;
for( i=0; i<s.length(); i++)
{
if( s.at(i)==' ') c++; //
}
return c;
}
or: s.at(i)==space()
2
char firstCapital( string s)
{
int i,found;
char x;
i = found = 0;
while( i<s.length() && !found )
{
if (s.at(i)>='A' && s.at(i)<='Z')
found = true;
else
i++;
}
If ( found )
//
x = s.at(i);
else
x = '\0';
//
return x;
}
3
string swapHalves( string s)
{
int h;
string t;
h = s.length() / 2;
t = s.substr(h) + s.substr(0,h);
return t;
}
4
string insertItself( string s)
{
int h;
string t;
h = s.length() / 2;
s.insert(s,h);
return s;
}
equivalent to (i<s.length())
null char
Flussi di I/O – operazioni elementari
Il C++ dispone di una libreria fstream per l’I/O su file, derivata dalla libreria iostream (quella, già
incontrata, che contiene le dichiarazioni per l’I/O su console).
I flussi vengono descritti tramite classi. Semplificando, come “classe” possiamo intendere un elenco di dati e
funzioni (rispettivamente chiamati proprietà e metodi), normalmente collegati tra loro. Variabili chiamate
oggetti, dichiarate come appartenenti35 alla classe, dispongono di tutte le proprietà e possono essere elaborate
con tutti i metodi associati.
Ad esempio, nella libreria iostream sono dichiarate le classi istream e ostream:
 cin è un oggetto di classe istream; ad esso è associata l’operazione indicata con >>
 cout è un oggetto della classe ostream; ad esso è associata l’operazione indicata con <<
Questi oggetti non vanno nemmeno dichiarati, fanno parte delle variabili globali (note in ogni parte di un
programma), per la precisione sono due flussi di dati globali (global streams).
Nel caso dell’I/O su disco, i flussi vanno invece dichiarati; inoltre vanno associati ad un nome di file
(filename) nell’operazione di apertura (altrimenti il file system non saprebbe in quale cartella registrare i
dati). È anche buona norma chiudere il flusso alla fine delle operazioni, anche se la chiusura viene effettuata
automaticamente alla chiusura del programma.
Vediamo nell’esempio come registrare (output) informazioni in formato testo (txt):
#include <fstream>
using namespace std;
int main()
{
ofstream x;
//
stream di output
x.open( "testo.txt");
//
apertura (con filename)
x << "Cochrane Foxx Bishop" << endl;
x << "Boley Dykes Simmons" << endl;
//
registrazione righe
x << "Miller Haas Grove" << endl;
x.close();
//
chiusura
getchar();
}
Nell’esempio specifico, la dichiarazione ofstream implica l’apertura in output (il file viene “creato” se
inesistente, sovrascritto se esistente). Come (quasi) sempre avviene con gli oggetti, i metodi vengono attivati
con l’operatore punto (dot operator), che segnala l’appartenenza alla classe: secondo la terminologia OOP36,
si può dire che “il metodo open appartiene all’oggetto flow“.
Il programma appena visto fa in modo che nella cartella di esecuzione venga a trovarsi un nuovo file testo, di
nome testo.txt, che può essere aperto con un apposito programma (blocco note, notepad++, …), oppure
visualizzato nello shell (prompt dei comandi) con il comando type.
Modifica con “blocco note”
35
Esame da shell con type
Appartenere ad una classe è un’espressione tecnicamente scorretta, dal momento che una classe è una descrizione,
non un contenitore: essendo però entrata nel gergo, il suo uso non provoca problemi.
36
Object Oriented Programming, o “programmazione orientata agli oggetti”. La pronuncia dell’acronimo è molto
soggettiva: su www.sitepoint.com è stato anche proposto un sondaggio, secondo il quale la maggior parte scandisce
le lettere “O O P”, un po’ meno preferisce la parola intera ”u:p”, poi seguono altre scelte come “double-O P”. Non
manca chi disdegna la sigla e usa le parole intere.
Questa visualizzazione dei dati è certamente meglio di quella nota, in tempi storici, come dump (discarica),
che agli albori dell’informatica era l’unico modo di esaminare i dati. Se volessimo metterci nei panni delle
ragazze dell’ENIAC37, il vero contenuto del file comprende dai seguenti 496 bit:
0100001101101111011000110110100001110010011000010110111001100101001000000100011001101111011110000111100000100
0000100001001101001011100110110100001101111011100000000110100001010010000100110111101101100011001010111100100
1000000100010001111001011010110110010101110011001000000101001101101001011011010110110101101111011011100111001
1000011010000101001001101011010010110110001101100011001010111001000100000010010000110000101100001011100110010
000001000111011100100110111101110110011001010000110100001010
(in rosso la "v" di "Grove", valore 118)
Un tuffo nel passato: due “ragazze dell’ENIAC”, Gloria Ruth Gordon ed Ester Gerston,
girano per il computer collegando un cavo per ogni bit.
In realtà, per esaminare il contenuto del file direttamente, in alcuni text editor38 esiste un’opzione che
permette di visualizzare l’esatto contenuto del file, byte per byte. Altri programmi, detti hex editor39, si
aprono direttamente in questa modalità, mostrando da una parte il dump esatto di ciò che è registrato sul
disco, dall’altra i corrispondenti caratteri visualizzabili. Nell’esempio è stato usato il “Free Hex Editor Neo”.
Questo esame permette di risolvere un’apparente incongruenza: il file risulta lungo 62 byte, mentre la somma
dei caratteri presenti nelle tre righe scritte è di 56. In realtà i byte mancanti sono i tre newline (a capo),
ciascuno formato da due caratteri, che hanno i valori decimali 13 e 10. Il primo indica un “carriage return”
(ritorno carrello), il secondo un “line feed” avanzamento riga), termini che ricordano le vecchie macchine da
scrivere. Non essendoci caratteri visualizzabili corrispondenti, nella parte di testo sono segnalati con dei
punti. Alcuni esempi di codifica:
37
CARATTERE
ESADECIMALE BINARIO
DECIMALE
cr/lf (newline)
blank
'a' (minuscola)
'S' (maiuscola)
0d 0a
20
61
53
13 10
32
97
83
0000
0010
0110
0101
1101 0000 1010
0000
0001
0011
Dette inizialmente “computer”, formavano un gruppo di circa 80 agguerrite scienziate, il cui contributo allo sviluppo
dell’informatica fu fondamentale.
38
Sono applicazioni con limitate possibilità di effetti grafici, normalmente usati dai programmatori per scrivere il
codice sorgente dei programmi.
39
Così chiamati perché ogni byte è visualizzato in base 16, con una cifra esadecimale per ogni nibble.
Per leggere il file testo appena registrato si può usare un flusso ifstream, come nel prossimo esempio. Il
nuovo programma richiede un ciclo, dal momento che il testo contiene varie parole. Prestiamo particolare
attenzione a come viene scritto il ciclo, per costruire un programma correttamente strutturato.
Il ciclo termina quando il metodo eof (end of file) restituisce true.
ifstream y;
È fondamentale, nella struttura del ciclo, leggere un dato
string z;
(solitamente è il primo) prima di entrare. Questo
y.open( "testo.txt");
permette di gestire ogni dato nello stesso modo: altri
y >> z;
//
1
costrutti rendono il ciclo più complesso, perché sorge la
while( ! flow.eof())
//
2
necessità di verificare se il flusso è terminato o meno.
{
cout << z << " ";
//
3
La tecnica è detta read ahead (leggere prima), ed è usata
y >> z;
//
4
anche nella gestione del traffico dei dati tra la RAM e le
}
memorie periferiche.
y.close();
Si può notare che l’operatore >> considera terminato l’input non solo al “fine linea”, ma anche ad ogni
spazio; il risultato della lettura è il seguente:
Cochrane Foxx Bishop Boley Dykes Simmons Miller Haas Grove
Per leggere le righe esattamente come sono state registrate si usa la funzione getline:, che permette di
leggere una riga completa, compreso il doppio carattere di newline:
ifstream t;
string z;
Il risultato del nuovo programma corrisponde
t.open( "testo.txt");
al modo in cui sono stati registrati i dati:
getline( t, z);
//
1
while( ! t.eof())
//
2
Cochrane Foxx Bishop
{
Boley Dykes Simmons
cout << z;
//
3
Miller Haas Grove
getline( t, z); //
2
}
t.close();
Oltre alle due classi di flusso incontrati, ne esiste una più completa, detta fstream, che può essere usata in
input o in output a seconda della modalità di apertura. Questa viene specificata come secondo parametro del
metodo open, (detto open mode). Ci sono svariate possibilità, tra le quali quelle interessanti sono 3: input,
output, append (aggiunta). La sintassi è un po’ criptica, ma si può intuire; un’altra stranezza è il prefisso che
descrive l’open mode, che può essere ios (input/output stream) oppure fstream (file stream): Le modalità
possono essere multiple, consentendo quindi più operazioni: vanno separate da un operatore |.
fstream w;
w.open("testo.txt",
w.open("testo.txt",
w.open("testo.txt",
w.open("testo.txt",
w.open("testo.txt",
w.open("testo.txt",
ios::out);
fstream::out);
//
ios, fstream: EQUIVALENTI
ios::in);
fstream::in);
ios::in | ios::out | ios::app);
fstream::in | fstream::out | fstream::app);
Ad esempio, un programma simile a quello iniziale, con l’apertura
del file testo in append e nuovi output, aggiunge i dati scritti in
coda a quelli già presenti.
Per distinguere le due registrazioni, i nuovi dati sono stati
predisposti in maiuscolo, come nell’esempio:
w << "COCHRANE FOXX BISHOP" << endl;
w << "BOLEY DYKES SIMMONS" << endl;
w << "MILLER HAAS GROVE" << endl;
Binary I/O
Le operazioni di I/O su file di solo testo sono molto rare (tranne che, per ragioni probabilmente legate alla
semplicità della correzione, nei corsi e negli esami scolastici): nella realtà i dati da scrivere, oltre ad essere
più complessi, solitamente necessitano di essere registrati in formati numerici. In particolare, osserveremo
come memorizzare i numeri interi. Nel C++ si definisce binary un flusso di dati in formato “non di
testo”, e come tale va indicato nel metodo open.
fstream x; char w; int i;
x.open("bin.dat", ios::out | ios::binary);
w = 66;
x.write( &w, 1);
Per scrivere (registrare) i dati nel file serve un
w = 'A';
apposito metodo del flusso, che sostituisce
x.write( &w, 1);
l’operatore <<, riservato per le operazioni di
i = +1000000;
testo:
x.write((char*)&i, sizeof(int));
i = -1000000;
flow.write(<dato>,<lunghezza>)
x.write((char*)&i, sizeof(int));
Le caratteristiche del metodo write sono 3:



(char*) trasforma (temporaneamente) un dato in una sequenza di char. È necessaria perché anche un
file binario è un flusso di byte. Questa operazione, detta typecasting (to cast =dare forma), induce il
programma a trattare un dato in un modo prestabilito;
& rende disponibile l’indirizzo del dato (una scelta un po’ curiosa);
sizeof(...) serve perché il metodo richiede quanti sono i byte da registrare; con questa funzione
lasciamo che sia il programma a fare i calcoli, qualunque sia il tipo di dato interessato (ad esempio, a
seconda del compilatore C++, il tipo int potrebbe essere di 2 o di 4 byte).
Osservando il dump del file, si può notare che, mentre i due char (che corrispondono alle lettere B e A) sono
registrati esattamente come scritto nello snippet; i due numeri interi (in notazione binaria complementare a
32 bit), hanno i byte registrati in ordine inverso:
ESADECIMALE
42 41
40 42 0f 00
c0 bd f0 ff
BINARIO
01000010 01000001
01000000 01000010 00001111 00000000
11000000 10111101 11110000 11111111
ASCII/DECIMALE
BA
+1000000
-1000000
Rovesciando l’ordine dei byte troviamo l’effettiva codifica dei dati int (+1000000 e -1000000) :
0000 0000
0000 1111
0100 0010
0
0
0
f
4
2
1111 1111
1111 0000
1011 1101
f
f
f
0
b
d
11111111111100001011110111000000
00000000000011110100001000111111
00000000000000000000000000000001
00000000000011110100001001000000
0100 0000
4
0
0*160+4*161+2*162+4*163+15*164
1100 0000
c
0
calcolo del valore di questo numero negativo
rovesciamento dei bit
+1
l’opposto del numero (corrisponde a +1000000)
Gestione delle immagini
A volte capita anche di dover raggruppare i dati per rispettare gli standard di
registrazione, proprio come nel caso delle immagini. Il formato che verrà preso
in esame è il più semplice: un’immagine “raster” in formato “bitmap”
monocromatico (bianco e nero, o B/W), senza compressione.
Un’immagine raster40 è una matrice di punti colorati (pixel41), che può essere
memorizzata in vari formati42. L’aspetto e la qualità dell’immagine sono
determinati dalla sua risoluzione e dal numero di colori usati: una bassa
risoluzione (o uno fattore di zoom elevato) comporta un’immagine più sgranata.
Il termine raster deriva dal latino rastrum (rastrello), ed è nato per descrivere la modalità di scansione dei
vecchi monitor CRT, nei quali un fascio di elettroni disegna l’immagine passando lo schermo riga per riga.
Nel formato bitmap, ogni pixel dell’immagine è memorizzato separatamente (proprio come in una “mappa”),
con tutte le informazioni necessarie per la sua visualizzazione.
Il numero di bit associati ad un pixel varia a seconda numero di colori:
COLORI
BIT PER PIXEL
2
16
256
65536
16M
4G
N. EFFETTIVO
1
4
8
16
24
32
16.777.216
4.294.967.296
Nel formato monocromatico è sufficiente un bit per descrivere un pixel, quindi la mappa dell’immagine
occuperà all’incirca 1/8 del numero totale di pixel (calcolato con il prodotto larghezza * altezza).
In realtà i calcoli sono leggermente diversi, dato che i dati sono memorizzati in blocchi da 4 byte; la
corrispondenza è tanto più precisa quanto la larghezza è vicina ad un multiplo di 32 pixel. In quel caso tutto
il blocco di 4 byte è significativo, altrimenti ci sarà un certo numero di bit di “tara”, il cui numero può anche
essere calcolato con precisione.
Ad esempio, calcoliamo lo spazio necessario per una riga di 173 pixel. (173+7)/8=22 (il numero di byte
necessario per i 173 bit). In 22 byte ci sono 176 bit, quindi i bit di tara per ogni blocco sono almeno 3 (ma
non è finita) 22/4+1=6, 6*4=24 (la riga contiene un multiplo di 4 byte), quindi ci sono altri 16 bit di tara. In
totale, per ogni riga ci sono 19 bit di tara, il cui contenuto non viene elaborato. Alla fine, il numero di byte
necessario per ogni riga è 24. Moltiplicando per l’altezza dell’immagine, si ottiene lo spazio totale necessario
per memorizzare i dati dell’immagine (in realtà, come vedremo nel prossimo paragrafo, il file dell’immagine
contiene anche altre informazioni di servizio).
Le formule generali sono le seguenti:
CALCOLO SPAZIO OCCUPATO (w=width; h=height)
1) Byte per riga
b = int((w+7)/8)
b = 4*((b-1)/4+1)
2) Byte totali
b = b * h
40
1 byte=8 pixel
blocchi di 4 byte
In computer graphics le immagini raster sono generalmente usate come sfondo; le figure geometriche sono in un
altro formato, detto vettoriale, che non è oggetto di questo capitolo.
41
Pixel=Pictorial element
42
I più comuni: BMP (bitmap), GIF (Graphic Interchange Format), JPG (actually JPEG, Joint Photographic Experts
Group), TIFF (Tagged Image File Format), PNG (Portable Network Graphics), MrSID (Multiresolution Seamless
Image Database).
Formato immagine BMP
Il formato più semplice è il bitmap monocromatico (B/W), che esamineremo in dettaglio.
Un file BMP di questo tipo è composto dalle seguenti parti:




File header (14 byte)
Image header (40 byte)
Palette (2*4 = 8 byte)
Dati
contiene “firma” e informazioni sulle dimensioni del file
contiene informazioni sull’immagine
2 colori nel formato RGB (solo bianco e nero)
ogni byte rappresenta 8 pixel; l’immagine è in blocchi di 4 byte
Le informazioni sono codificate con numeri interi che occupano 1, 2 o 4 byte; storicamente questi tipi di
dato sono noti come BYTE, WORD e DWORD (DOUBLE WORD), quindi sono stati usati questi nomi per
semplificare il contenuto del sorgente. Tranne dove indicato, i dati sono DWORD (la maggior parte). I valori
sono quasi tutti fissi, tranne quelli legati alle dimensioni: si possono calcolare una volta note larghezza e
altezza dell’immagine, in pixel. La dimensione della palette, ovviamente, è diversa nei formati a colori.
File header





(W) Signature (firma, valore fisso = 19778, che corrisponde alle lettere BM);
Dimensione totale (62 + dimensione immagine in byte); classificato come “unreliable”;
(W) Dato Riservato (valore fisso = 0): in teoria dipende dal software grafico usato;
(W) Dato Riservato (valore fisso = 0): in teoria come sopra;
Offset dell’immagine (valore fisso = 62).
Il numero 62 è dato dalla somma delle lunghezze delle informazioni di servizio (14+40+8).
La dimensione di 14 byte è data da 2+4+2+2+4.
Image header











Dimensione header (valore fisso = 40)
Larghezza (in pixel)
Altezza (in pixel)
(W) Piani di colore (valore fisso = 1)
(W) Profondità colori (1=B/W; 4=16 colori; 8=256 colori; 24=16M)
Compressione (0=nessuna compressione)
Dimensione immagine in byte; classificato come “unreliable”;
Bit per metro orizzontali (valore fisso = 0); classificato come “unreliable”
Bit per metro verticali (valore fisso = 0) ; classificato come “unreliable”
Numero colori (valore fisso = 0)
Numero colori “importanti” (valore fisso = 0)
La dimensione di 40 byte è data da 4+4+4+2+2+4+4+4+4+4+4.
Palette (monocromatica)




(B) Nero: valore del blu (default: 0)
(B) Nero: valore del verde (default: 0)
(B) Nero: valore del rosso (default: 0)
(B) Nero: valore non usato (default: 0)




(B) Bianco: valore del blu (default: 255)
(B) Bianco: valore del verde (default: 255)
(B) Bianco: valore del rosso (default: 255)
(B) Bianco: valore non usato (default: 0)
Con i raggi luminosi, i colori primari sono rosso,
verde, blu. Con la luce riflessa (come nei quadri),
i colori primari sono rosso, giallo, blu.
Il nero è dato dall’assenza dei colori primari
(valore 0), il bianco dalla somma di tutti, nel loro
valore massimo 255). I colori con valori uguali
dei 3 componenti formano la scala dei grigi.
Modificare i valori dei colori è sconsigliabile, non avendo un effetto uniforme: alcuni software riescono a
rispettare la nuova palette, altri no. Tra i possibili effetti malefici, ogni colore diverso dal bianco potrebbe
essere reso in nero; oppure, salvando l’immagine, la palette potrebbe tornare quella default.
Memorizzazione informazioni di servizio
È possibile registrare tutto il contenuto di una bitmap come char o string, ma non è molto consigliabile. Per
raggruppare i dati in un formato più leggibile si possono dichiarare formati composti detti struct
(strutture): una struttura dati non è normalmente formata da un dato singolo, ma da più dati associati ad essa.
Ciascun componente di una variabile strutturata viene indicato con il formato degli oggetti:
<struct>.<component>
Anche con questi accorgimenti, tuttavia, sarà necessario rispettare il formato delle istruzioni, che spesso
ricorda una gestione “old-style” dei flussi di dati come sequenze di caratteri.
Per semplificare il sorgente vengono specificate alcune abbreviazioni dei tipi di dato presenti nelle strutture.
typedef unsigned char BYTE;
typedef unsigned short int WORD;
typedef unsigned long int DWORD;
struct fileHeader
{
WORD type;
DWORD fileSize;
WORD reserved1;
WORD reserved2;
DWORD offset;
};
struct imageHeader
{
DWORD headerSize;
DWORD width;
DWORD height;
WORD planes;
WORD bitCount;
DWORD compression;
DWORD imageSize;
DWORD xPixelsPerMeter;
DWORD yPixelsPerMeter;
DWORD clrUsed;
DWORD clrImportant;
};
struct color
{
BYTE blue;
BYTE green;
BYTE red;
BYTE unused;
};
//
//
//
BYTE: 1 byte unsigned
WORD: 2 bytes unsigned
DWORD: 4 bytes unsigned
Strutture dati
Dichiarazione di variabili composte:
fileHeader h1;
imageHeader h2;
color black, white;
In questo modo le variabili h1, h2, white, black risultano
composte da più dati, corrispondenti alla definizione di struct
Ad esempio, le componenti della variabile h1 saranno:
h1.type
h1.size
h1.reserved1
h1.reserved2
h1.offset
All’interno del programma è possibile associare alle variabili
composte i valori distinti dei singoli dati atomici, evitando di
costruire stringhe scarsamente significative e ancor meno
gestibili:
white.blue = 255;
white.green = 255;
white.red = 255;
white.reserved = 0;
Essendoci nel formato BMP un header di 14 byte, bisogna
allineare la scrittura alla sua esatta dimensione (e non a 16); per
farlo occorre aggiungere la seguente direttiva (che può essere
associata alle include all’interno del programma).
#pragma pack( push, 1)
I colori sono descritti con le componenti fondamentali nel formato RGB (red, green, blue). I valori associati ai
colori sono in ordine inverso perché è così che sono memorizzate le DWORD.
Come si può immaginare, per un bitmap monocromatico basteranno le descrizioni del bianco (255,255,255)
e del nero (0,0,0). Per un’immagine a colori servirebbe una palette con tutte le descrizioni dei colori, ma, per
ovvi motivi di praticità, per più di 256 colori non viene memorizzata. Con 256 colori le informazioni di
servizio occupano uno spazio ancora gestibile di 1078 byte (54+1024 per i colori), mentre per un’immagine a
16M il carico necessario diventerebbe improponibile.
Una volta dichiarate le strutture, se ne definisce una per tipo (tranne per i colori, che possono essere due),
oltre ad una variabile di flusso per direzionare il tutto nel file desiderato:
fileHeader h1;
imageHeader h2;
color black, white;
fstream f;
I dati dei due header sono per lo più fissi; quelli variabili, come già visto, sono i seguenti:
h1.size
UNRELIABLE (valore = 62+h2.imageSize)
h2.width
h2.height
h2.imageSize
SIGNIFICATIVO
SIGNIFICATIVO
UNRELIABLE
Per definire le palette è opportuno seguire lo standard (nero=assenza di colori; bianco=tutti i colori):
black.blue = 0;
black.green = 0;
black.red = 0;
black.unused = 0;
white.blue = 255;
white.green = 255;
white.red = 255;
white. unused = 0;
Per scrivere l’intera struttura è consigliabile usare il metodo write , che ha il seguente formato:
f.write( (char*)&<struct>, sizeof(<struct>));
Per registrare le informazioni di servizio si può procedere in questo modo:
f.open( z, ios::out | ios::binary);
result = f.good();
if ( result )
{
f.write ((char*)&h1,
sizeof
f.write ((char*)&h2,
sizeof
f.write ((char*)&black, sizeof
f.write ((char*)&white, sizeof
f.close();
}
// z = nome del file
(fileHeader));
(imageHeader));
(color));
(color));
sizeof( fileHeader) 14
sizeof( imageHeader) 40
sizeof( color)
4
La variabile z è la stringa contenente il nome del file.
Il metodo .good() permette di verificare se un’operazione (in questo caso l’apertura) è andata a buon fine.
Tra le cause di possibili errori, ci potrebbe essere il disco pieno (piuttosto improbabile), oppure l’accesso
protetto alla cartella; se l’intero file system fosse danneggiato, probabilmente il programma non riuscirebbe
nemmeno a funzionare.
A questo punto qualche modulo si dovrà occupare della scrittura dei byte dati, aprendo nuovamente il file
con una nuova clausola (ios::app), che consente di aggiungere in coda (operazione nota come append):
flow.open( z, ios::out | ios::binary | ios::app);
//
append mode
Nel prossimo paragrafo potremo vedere un esempio di come raggruppare le informazioni e i moduli in una
libreria (un header file con estensione .h)
Una libreria per la gestione delle immagini
Le dichiarazioni e le funzioni necessarie alla registrazione possono essere inserite in una libreria, che
potrebbe essere scritta come riportato nel seguito.
#include <fstream>
#pragma pack(push, 1)
using namespace std;
//
byte-wise setting
typedef unsigned char BYTE;
typedef unsigned short int WORD;
typedef unsigned long int DWORD;
//
//
//
1 byte unsigned
2 bytes unsigned
4 bytes unsigned
/*
The two headers (file/image) and the palettes
*/
struct fileHeader
{
WORD type;
DWORD fileSize;
WORD reserved1;
WORD reserved2;
DWORD offset;
};
Tutte le dichiarazioni dei dati e degli header
struct imageHeader
sono già stati esaminati in precedenza.
{
DWORD headerSize;
DWORD width;
DWORD height;
WORD planes;
WORD bitCount;
DWORD compression;
DWORD imageSize;
DWORD xPixelsPerMeter;
DWORD yPixelsPerMeter;
DWORD clrUsed;
DWORD clrImportant;
};
struct color
{
BYTE
blue;
//
Reverse order
BYTE
green;
BYTE
red;
BYTE
unused;
};
/*
This function computes the number of bytes
required for each row of the bitmap
*/
int bytesPerRow( int width)
{
int b, row;
b = ((width+7)/8);
//
bytes minumum
row = 4 * ( (b-1)/4 + 1 );
//
actual bytes per row
return row;
}
Il modulo “bytesPerRow” calcola quanti byte sono occupati da
una riga di pixel; non dipende dall’altezza dell’immagine.
I calcoli sono già stati esaminati in precedenza.
/*
Stores the bitmap header and the palette:
Dimensions in bytes:
file info
14
image info
40
palette
8 (4 black + 4 white)
*/
bool monoHeaders( const char* z, int width, int height)
{
fileHeader h1;
imageHeader h2;
Il modulo ha questo nome per i seguenti motivi:
color white, black;
 mono
monocromatico
int b, i, j, row, imageSize;
char bitmap, b1, b2;
 Headers il plurale indica le 4 strutture di servizio
bool result;
fstream x;
row = bytesPerRow( width);
imageSize = row * height;
//
//
bytes per row
total bytes
h1.type = 19778;
h1.fileSize = 62 + imageSize;
h1.reserved1 = 0;
h1.reserved2 = 0;
h1.offset = 62;
//
//
(FIXED) must be 19778 (signature: BM)
---SIZE (unreliable)
//
(FIXED) header size: 14+40+(4+4)
h2.headerSize = 40;
h2.width = width;
h2.height = height;
h2.planes = 1;
h2.bitCount = 1;
h2.compression = 0;
h2.imageSize = imageSize;
h2.xPixelsPerMeter = 0;
h2.yPixelsPerMeter = 0;
h2.clrUsed = 0;
h2.clrImportant = 0;
//
//
//
//
//
(FIXED) header length
---SIZE
---SIZE
must be 1 (color planes)
1 = monochrome
//
//
//
size (unreliable)
unreliable
unreliable
black.blue = 0;
black.green = 0;
black.red = 0;
black.unused = 0;
white.blue = 255;
white.green = 255;
white.red = 255;
white.unused = 0;
Il modulo “monoHeaders” si occupa della registrazione
del dati di servizio: l’unico dato veramente necessario è il
nome del file, mentre le dimensioni sono inserite per
completezza (l’immagine risulterebbe valida anche con
questi dati posti a zero).
x.open( z, ios::out | ios::binary);
result = x.good();
if ( result )
{
x.write ((char*)&h1,
x.write ((char*)&h2,
x.write ((char*)&black,
x.write ((char*)&white,
x.close();
}
return result;
}
sizeof
sizeof
sizeof
sizeof
(
(
(
(
fileHeader));
imageHeader));
color));
color));
//
//
//
//
header 1
header 2
black = 0
white = 1
Un’immagine casuale
Quello che segue è un esempio di creazione di un bitmap monocromatico affidato completamente al caso, sia
come dimensioni dell’immagine, sia come contenuto.
Va tenuto presente che le righe sono memorizzate dal basso all’alto: quindi la prima riga, quella
bianca, risulterà quella più in basso.
bool randomImage( const char* z, int width, int height)
{
int b, i, j, row, size;
char b1, b2, bitmap;
bool result;
fstream x;
result = monoHeaders( z, width, height);
//
if ( result )
//
{
row = bytesPerRow( width);
x.open( z, ios::out | ios::binary | ios::app); //
for( i=1; i<=height; i++)
{
for( j=1; j<=row; j++)
{
bitmap = rand() % 256;
//
x.write (&bitmap, 1);
//
}
}
x.close();
}
return result;
headers/palette
Check
append mode
8 random bits
write to file
}
int main()
{
bool result;
int width, height;
srand(time(0));
width = rand() % 30 + 1;
height = rand() % 10 + 1;
result = randomImage( "test.bmp", width, height);
cout << result;
getchar();
}
Un esperimento di questo programma casuale
ha prodotto l’immagine sotto riportata (23x7):
N.B.: Scrivere un programma C++ per la gestione di immagini, o anche per la sola lettura, è completamente
fuori luogo, sia per le difficoltà pratiche, sia per la presenza di decine di programmi adeguati.
Un’immagine meno casuale
Un problema meno complesso numericamente è quello di costruire una scacchiera. C’è anche il vantaggio di
lavorare per definizione con multipli di 8: un immagine di 64x64 pixel non ha tara e si può costruire un byte
per volta.
L’impostazione generale del modulo di scrittura dell’immagine può rimanere la stessa, con una piccola
differenza. Sembrerebbe infatti sufficiente, come prima, un doppio ciclo: in realtà va tenuto conto che ogni
riga va ripetuta 8 volte, quindi il ciclo risulterà triplo. In ogni caso, la scelta del colore può essere legata agli
indici, o meglio alla loro relativa parità, come evidenziato in figura.
Il modulo monoHeaders continua ad essere valido: in questo esercizio possiamo anche permetterci di usare
delle costanti, data la regolarità del disegno.
height = width = 64;
result = monoHeaders( z, width, height);
Nel seguito del programma, potrà essere sufficiente inserire, tra i classici cicli in i e j, il terzo ciclo che
faccia ripetere 8 volte ciascuna riga di pixel, per costruire una riga della scacchiera.
for( i=1; i<=8; i++)
{
for( h=1; h<=8; h++)
{
for( j=1; j<=8; j++)
{
//
righe della scacchiera
//
8 righe di schermo per ogni riga di scacchiera
//
una riga di pixel
Author
Document
Category
Uncategorized
Views
0
File Size
2 822 KB
Tags
1/--pages
Report inappropriate content