In C++ un programma comunica con l'esterno mediante i seguenti:
Il compilatore distingue gli operatori di
flusso da quelli di shift dei bit (identificati dagli stessi
simboli) in base al contesto, cioè in base al tipo degli operandi.
Output tramite l’operatore di inserimento <<
In C++ un’operazione di output di un dato tramite l'operatore
di inserimento << consiste nell'inserire il dato nello stream di
output:
stream << dato;
cout << dato;
dove dato è una qualsiasi variabile o espressione di tipo base
(oppure una costante o una stringa).
L’istruzione significa: “inserisci dato nell’oggetto cout “ (e da questo automaticamente trasferito su stdout).
Esempio:
#include
<iostream.h>
main()
{
cout<<"Ciao Ciao!\n";
}
A differenza dalla funzione printf non è necessario usare specificatori,
in quanto il tipo delle variabili è riconosciuto automaticamente
(in realtà, come vedremo più avanti, esistono anche qui
degli specificatori, detti “manipolatori di formato”, ma servono
soltanto quando la scrittura non deve essere in formato libero).
In una stessa istruzione si possono “accodare” più operazioni
di inserimento una di seguito all’altra.
Esempio:
Programma che stampa la tabella di conversione di gradi Fahrenheit
in Celsius da 0 a 300 con incrementi di 20.
°F=9/5°C+32
°C=5/9(°F-32)
#include
<iostream.h>
main()
{
float
fahr, celsius;
int
inf, sup, step;
inf=0;
/* limite inferiore della tabella */
sup=300;
/* limite superiore*/
step=20;
/* incremento*/
fahr=inf;
while
(fahr <= sup)
{
celsius = (5.0/9.0) * (fahr-32);
cout << " A gradi Fahrenheit: "
<< fahr
<< " corrispondono gradi Celsius: "
<< celsius << "\n";
fahr =fahr + step;
}
}
In C++ un’operazione di input di un dato tramite l'operatore di estrazione
consiste nell'estrarre il dato dallo stream di input:
stream >> dato;
cin >> dato;
dove dato è una variabile (non una costante né un’espressione
!) di qualsiasi tipo base (oppure una variabile stringa).
L’istruzione significa: “estrai il valore immesso da stdin (automaticamente trasferito in cin) dall’oggetto cin e memorizzato nella variabile dato”.
#include
<iostream.h>
main()
{
int
n;
do {
cout << "Inserisci un intero >=0 \n";
cin >> n;
} while (n < 0 );
cout<<n;
}
Come le operazioni di inserimento, anche quelle di estrazione possono
essere “accodate” una di seguito all’altra in un’unica istruzione.
Esempio:
cin >> dato1 >> dato2 >> dato3;
i dati dato1, dato2, dato3 devono essere forniti nello stesso
ordine.
Come si puo' notare, cin e
cout, a differenza di printf e scanf, permettono
di trattare allo stesso modo stringhe, int, float, senza specificare
il tipo e il numero di caratteri da leggere o stampare.
Meccanismo di lettura dei dati da tastiera
La lettura dei dati da tastiera non avviene direttamente, ma tramite
un’area di memoria, detta buffer di input;
Il programma, appena incontra un’istruzione di lettura, acquisisce
i dati (che distingue l’uno dall’altro riconoscendo i terminatori) leggendoli
dal buffer di input.
Se il buffer di input si svuota prima che la lettura sia terminata
(oppure se il buffer é già vuoto all’inizio della lettura,
come dovrebbe succedere sempre), il programma si ferma in attesa di input
e il controllo passa all’operatore, che viene abilitato a introdurre dati
da tastiera fino a quando non si invia un carriage-return (indipendentemente
dal numero di dati da leggere); l’intera riga digitata dall’operatore viene
poi trasferita nel buffer di input, al quale il programma riaccede per
completare l’operazione di lettura
Se nel buffer di input restano ancora dati dopo che l’operazione di
lettura é finita, questi verranno utilizzati durante la lettura
successiva.
Come si può notare, la presenza del buffer di input crea
una specie di “asincronismo” fra operatore e programma, che può
essere causa di errore: bisogna fare attenzione a fornire ogni volta
esattamente il numero di dati richiesti.
Il meccanismo di lettura dei
dati da tastiera fa sì che il programma termina la lettura di un
dato quando incontra un blank, un carattere di tabulazione o un ritorno
a capo.
La funzione gets(argomento) trasferisce l’intero buffer di input
nella stringa passata come argomento, che deve essere stata dichiarata
(nel programma chiamante) come array di tipo char, e riconosce
come unico terminatore il carattere newline (che é sempre l’ultimo
carattere del buffer) e lo sostituisce con il carattere NULL.
Ne consegue che la stringa può contenere anche blank e tabulazioni
(a differenza dalle stringhe lette mediante cin o le altre funzioni di
input del C).
#include
<stdio.h>
#include
<iostream.h>
main()
{
char Str[30];
//
richiede il cognome
gets(Str);
cout << "Saluti " << Str << "!\n";
}
La funzione cin.get() acquisisce ciascun carattere (incluso il blank) presente nel buffer di input nella stringa.
#include <iostream.h>
main()
{
char
Str[30];
int
l,i=0;
//
richiede il cognome
Str[i]
=cin.get();
if (Str[i]!=’\n’)
{
do {
i++;
Str[i] = cin.get();
}
while (Str[i]!=’\n’);
l=i;
cout<<Str <<”\n”;
for (i=0;i<l;i++) {cout<<Str[i];}
}
}
Il programma precedente equivale al programma seguente ove risulta un
diverso uso della funzione cin.get
#include <iostream.h>
main()
{
char Str[30];
int l,i=0;
// richiede il cognome
cin.get(Str[i]);
if (Str[i]!=’\n’)
{
do {
i++;
cin.get(Str[i]);
}
while (Str[i]!=’\n’);
l=i;
cout<<Str <<”\n”;
for (i=0;i<l;i++) {cout<<Str[i];}
}
}
| setw(n) | pone pari a n l'ampiezza del campo |
| dec | conversione decimale |
| hex | conversione esadecimale |
| oct | conversione ottale |
#include
<iostream.h>
#include
<iomanip.h>
main()
{
int
i,n,m;
cout
<< "Inserisci 2 interi\n";
cin
>> n>>m;
cout<<setw(4)<<n<<setw(8)<<m<<”fine\n”;
cout<<n<<”
“<<hex<<n<<” “<<oct<<n <<”fine\n”;
}
timeelapsed% a.out
Inserisci 2 interi
16 43
16
43fine
16 10 20fine
timeelapsed%
Tipo File
Per memorizzare un dato su un supporto magnetico come un hard
disk o un nastro, o più in generale su un'unità di memoria
di massa viene utilizzata un tipo di dato chiamato file.
Un file può essere considerato come una sequenza di byte.
Poiché, come
detto, il sistema di I/O del C++ ha un'interfaccia indipendente dall'hardware,
ossia è analogo accedere a terminali, unità a disco,
a nastro eccetera, in quanto benché ogni dispositivo sia diverso
dagli altri, ciascuno viene trasformato in un dispositivo logico
chiamato stream che può essere associato alle classi cin, cout,
cerr, le operazioni sui file sono vere non solo per i
file su disco, ma anche per le periferiche.
Pertanto anche i messaggi di posta elettronica in partenza ed in arrivo,
i caratteri battuti sulla tastiera, l'output sul video del terminale, i
dati che passano da un programma all'altro possono essere utilizzati dai
programmi come file, in quanto non sono altro che sequenze di byte.
I file posso essere
di due tipi:
1) FILE DI
TESTO: è una sequenza di caratteri, che durante il
trasferimento può subire anche delle conversioni a seconda delle
necessità dell'ambiente di destinazione.
2) FILE BINARI: è una sequenza di byte avente una corrispondenza uno a uno con la sequenza ricevuta dal dispositivo esterno.
| ios::in | Apertura di un file in lettura |
| ios::out | Apertura di un file in scrittura |
| ios::binary | file binario |
| ios:: | Fallisce l'apertura se il file non esiste |
| ios::noreplace | Fallisce l'apertura se il file già esiste |
Dato che nell'apertura
di un file (ad esempio di nome f1) può verificarsi un errore
di solito si controlla con una istruzione condizionale:
if(!f1)
{
cout << "Impossibile aprire file.txt.";
exit(1);
}
Se l'apertura del file f1 fallisce,
il sistema segna nella variabile f1 il valore 1, che viene utilizzato per
chiamare la funzione exit con il valore 1 e terminare l'esecuzione del
programma.
Programma per la gestione di un file sequenziale di caratteri.
Il programma:
1. legge da tastiera i caratteri digitati e li memorizza su un
file di testo
2. stampa a video il contenuto del file memorizzato.
Nel programma il file e' prima aperto
in scrittura e poi in lettura.
#include
<iostream.h>
#include
<fstream.h>
#include
<stdlib.h>
main()
{
fstream f1;
f1.open("file.txt", ios::out);
if(!f1)
{
cout << "Impossibile aprire file.txt.";
exit(1);
}
char c;
cout
<< endl << "Introdurre un testo terminato da '.'" <<
endl << endl;
while((c
= cin.get()) != '.')
{
f1 << c;
}
f1.close();
f1.open("file.txt",
ios::in);
if(!f1)
{
cout << "Impossibile aprire file.txt.";
exit(1);
}
cout
<< endl << "Contenuto del file: " << endl << endl;
while(f1
>> c)
{
cout << c;
}
return
0;
}
"b.c" [New file] 40 lines, 821 characters
timeelapsed% g++ b.c
timeelapsed% a.out
Introdurre un testo terminato da '.'
Sto provando a scrivere un file di testo.
Contenuto del file:
Stoprovandoascrivereunfileditestotimeelapsed%
Dato che alla fine
di un file viene posta una marca di terminazione, questa può essere
usata durante la lettura per giungere fino alla fine del file.
nell'apertura di un
file (ad esempio di nome f1) può verificarsi un errore di
solito si controlla con una istruzione condizionale:
while(f1
>> c)
{
cout << c;
}
Si noti che l'operatore di estrazione
>> ignora i blank ed i caratteri di tabulazione pertanto l'output prodotto
conterrà solo caratteri diversi dai delimitatori, pertanto se si
vuole che vengano stampati anche tutti i caratteri spazio inseriti nel
testo occorre prelevarli tramite l'istruzione get().
Poiché l'operatore '<<' ignora spazi e caratteri di tabulazione
per stampare anche i caratteri spazio si potrà utilizzare:
c = f1.get();
while(!f1.eof())
{
cout<< c;
c = f1.get();
}
Programma per la gestione di file di interi .
Il programma:
1. legge da tastiera i dati e li memorizza separandoli con spazi su
un file di testo
2. stampa a video il contenuto del file e la media dei valori memorizzati.
3.Calcola e stampa il massimo valore memorizzato
#include
<iostream.h>
#include
<fstream.h>
#include
<stdlib.h>
main()
{
fstream
f1;
f1.open("file.txt",
ios::out);
if(!f1)
{
cout << "Impossibile aprire file.txt.";
exit(1);
}
int
c,i;
float
s;
cout
<< endl << "Introdurre una sequenza delimitata da 0 "
<< endl;
cin>>c;
while(c!=
0)
{
f1 << c<<” “;
cin>>c;
}
f1.close();
f1.open("file.txt",
ios::in);
if(!f1)
{
cout << "Impossibile aprire file.txt.";
exit(1);
}
cout
<< endl << "Contenuto del file: " << endl << endl;
s=0;i=0;
while(f1
>> c) /* condizione di fine file */
{
i++;
s=s+c;
cout << c<<” “;
}
cout<<"media
= "<<(s/i);
f1.close();
f1.open("file.txt",
ios::in);
if(!f1)
{
cout << "Impossibile aprire file.txt.";
exit(1);
}
cout
<< endl << "Massimo valore del file: " << endl <<
endl;
int
max;
f1>>max;
while(f1
>> c) /* condizione di fine file */
{
max= (c>max) ? c : max;
}
cout<<max;
f1.close();
return 0;
}
Si noti che nel programma è stato utilizzato il frammento:
cin>>c;
while(c!=
0)
{
f1 << c<<” “;
cin>>c;
}
ove si nota che gli interi vengono estratti dall'input (eliminando
i blank e caratteri di tabulazione), ma per consentire un corretto uso
in fase di lettura vengono memorizzati sul file f1 separati da spazio.
Ulteriori tipi di dati
Tipo enumerativo
Un tipo enumerativo è un insieme di costanti intere ciascuna
individuata da un identificatore.
I tipi enumerativi vengono utilizzati per rappresentare un numero limitato
di valori associati ad informazioni numeriche.
ESEMPIO
programma che illustra la dichiarazione e l'uso del tipo di dati enumerativo
#include
<iostream.h>
//
dichiarazione tipo enumerativo
enum
tppersona {studente, docente, impiegato, disoccupato};
main()
{
tppersona p1;
int numpers;
do {
cout << "Inserisci il tipo di persona (studente = 0, docente = 1,
impiegato = 2, disoccupato =3) : ";
cin >> numpers;
} while (numpers < 0 || numpers > 3);
// converti il numero della persona in un enumeratore
switch(numpers)
{
case 0:
p1 = studente;
break;
case 1:
p1 = docente;
break;
case 2:
p1 = impiegato;
break;
case 3:
p1 = disoccupato;
break;
}
cout << "La persona e' ";
switch (p1)
{
case studente:
cout << "studente";
break;
case docente:
cout << "docente";
break;
case impiegato:
cout << "impiegato";
break;
case disoccupato:
cout << "disoccupato";
break;
}
cout << "\n";
}
se non utilizzassi l'istruzione switch finale per stampare p1 ma scrivessi
direttamente:
.....
.....
cout
<< "La persona e' "<<p1;
....
otterrei in output l'intero che è associato a p1 e non la stringa!
timeelapsed% a.out
Inserisci il tipo di persona (studente = 0, docente = 1, impiegato
= 2, disoccupato =3) : 3
La persona e' 3
Consideriamo l'esempio seguente:
union
rec
{
char a;
int b;
};
Nell'esempio di union analizzato, i campi sono due ed hanno
dimensioni differenti l'uno dall'altro, pertanto il compilatore, allocando
la union, riserva una quantità di memoria sufficiente a contenere
il più "ingombrante" dei suoi elementi, e li "sovrappone" a partire
dall'inizio dell'area di memoria occupata dalla union stessa.
Pertanto la union non occupa 24byte (quanto serve per le due variabili,
8+16), bensì solo 16: in quanto a e b sono solo sono
due modi alternativi di accedere al contenuto della union.
L'accesso ai campi di una union segue le stesse regole dell'accesso
ai campi di una struct, cioè mediante l'operatore punto (o come
si vedrà l'operatore "freccia" se si lavora con un puntatore).
E' interessante notare che inizializzando il campo rec.a viene
inizializzato anche il campo rec.b, in quanto condividono la stessa
memoria fisica.
Il tipo union viene spesso utilizzato per realizzare record varianti.
Valori logici
Il C++ non possiede un tipo base per rappresentare i valori logici,
però ogni valore intero può essere interpretato come valore
logico, in base alla seguente convenzione:
Ciò rende possibile eseguire operazioni logiche tramite operatori
logici, ed anche la combinazione fra operazioni logiche e operazioni di
altro tipo.
|| OR
!
NOT
| esempio | risultato | motivo |
| 15 && 2 | 1 | vero and vero = vero |
| 12 || 0 | 1 | vero or falso = vero |
| ! 12 | 0 | not vero = falso |
L’operatore binario && esegue l’AND logico fra gli
operandi:
a && b restituisce vero se entrambi a e b
sono veri, a e b possono essere dati elementari o espressioni
(x>=23)&&(y++<=funz(y))
L’operatore binario || esegue l’OR logico fra gli operandi: a || b restituisce falso se entrambi a e b sono falsi
L’operatore unario ! esegue il NOT logico dell’operando:
!a restituisce vero se a é
falso o viceversa
Operatori sui bit
Il C++ può,
a differenza da altri linguaggi, operare sulle variabili intere a livello
del bit.
Gli operatori che operano sui singoli bit sono:
| Operatore | funzione | esempio | note |
| << | shift a sinistra | k<<4 | equivale a k*16 |
| >> | shift a destra | k>>4 | equivale a k/16 |
| & | and bit a bit | k & 1 | è vera se k è dispari |
| | | or bit a bit | k | 1 | "arrotonda" k al dispari superiore |
| ~ | not bit a bit | ~k | dà il complemento a 1 |
| ^ | xor bit a bit | k ^ 1 | inverte LSB di k |
Es. date due variabili short
unsigned a e b i cui valori sono, in notazione
binaria:
a = 0 1 0 0 1 1 0 1 (77)
b = 0 0 1 1 1 0 1 0 (58)
i risultati delle tre operazioni sono rispettivamente:
a & b = 0 0 0 0 1 0 0 0 (8)
a | b = 0 1 1 1 1 1 1 1 (127)
a ^ b = 0 1 1 1 0 1 1 1 (119)
| esempio | risultato | motivo |
| ~ 12 | -13 | provare per credere... |
void
rappbin(short unsigned num)
{
short
unsigned pot2[8] = { 0,0,0,0,0,0,0,0};
int
i=0;
while
((num!=0)&&(i<8))
{
pot2[i]=(num%2);
i++;
num/=2;
}
for ( i = 7; i > -1; i--) cout << pot2[i];
}
L’operatore binario >> produce lo scorrimento a destra (right-shift)
dei bit dell’operando di sinistra , in quantità pari all’operando
di destra.
In pratica esegue una divisione intera (con divisore uguale a una potenza
di 2);
Es. a >> n equivale a
a / 2n
dalla sinistra entrano cifre binarie 0 se il numero a è positivo,
oppure cifre binarie 1 se il numero a è negativo (a causa della
notazione in complemento a due dei numeri negativi).
L’operatore binario << produce lo scorrimento a sinistra (left-shift)
dei bit dell’operando di sinistra, in quantità pari all’operando
di destra.
In pratica esegue una moltiplicazione per una potenza di 2;
Es. a << n equivale
a a * 2n
dalla destra entrano sempre cifre binarie 0.
#include
<iostream.h>
main()
{
int
n,m;
do {
cout << "Inserisci i dati n e m >=0 \n";
cin >> n>>m;
} while ((n < 0 )||(m<0));
cout<<n<<”
“<<m<<” “<<(n<<m);
}
Gli operatori binari << >>non possono essere applicati ai tipi float, double, long double e void.
ATTENZIONE:
Gli operatori bit a bit & , | ,~ non vanno confusi con gli operatori logici &&, ||, !
Infatti:
gli operatori logici (&&, ||, !)
considerano i loro operandi come un tutt'uno, un operando
è FALSO se vale zero o VERO altrimenti;
il risultato è sempre VERO (1) o
FALSO (0 ).
gli operatori bit a bit (&, |, ~), invece, considerano i loro operandi come un insieme di bit, e applicano l'operazione bit per bit secondo le regole dell'algebra Booleana
| esempio | risultato | motivo |
| 12 && 2 | 1 | vero and vero = vero |
| 12 & 2 | 0 | 1100 & 0010 = 0000 |
| 12 || 0 | 1 | vero or falso = vero |
| 12 | 0 | 12 | 1100 | 0000 = 1100 |
Programma C++ che illustra l'uso degli operatori bit a bit
#include <iostream.h>
//
dichiara i prototipi
void
rappbin(short unsigned);
main()
{
short unsigned A, B, K;
cout
<< "Inserisci un intero : ";
cin >> A;
cout << "Inserisci un altro intero : ";
cin >> B;
// mostra gli input in decimale e in binario
cout << "Rappresentazione binaria di " << A << " :------
";
rappbin(A);
cout << "\n";
cout << " Rappresentazione binaria di " << B << " :------
";
rappbin(B);
cout << "\n";
// prova l'operatore OR bit a bit |
rappbin(A); cout << "\n";
rappbin(B); cout << " |\n";
cout << "------------\n";
rappbin(A | B);
cout << "\n";
cout << A << " OR " << B << " = "
<< (A | B) << "\n";
K=A|B;
cout
<< "**\n";
cout
<< A << " OR " << B << " = "
<< K << "**\n";
// prova l'operatore XOR bit a bit ^
rappbin(A); cout << "\n";
rappbin(B); cout << " ^\n";
cout << "------------\n";
rappbin(A ^ B);
cout << "\n";
cout << A << " XOR " << B << " = "
<< (A ^ B) << "\n";
// prova l'operatore AND bit a bit &
rappbin(A); cout << "\n";
rappbin(B); cout << " &\n";
cout << "------------\n";
rappbin(A & B);
cout << "\n";
cout << A << " AND " << B << " = "
<< (A & B) << "\n";
// prova l'operatore bit a bit di shift (scorrimento) a sinistra <<
rappbin(A); cout << "\n";
cout << " << 2\n";
cout << "------------\n";
rappbin(A << 2);
cout << "\n";
cout << A << " << 2 = "
<< (A << 2) << "\n";
// prova l'operatore bit a bit di shift (scorrimento) a destra >>
rappbin(A);
cout << "\n";
cout << " >> 2\n";
cout << "------------\n";
rappbin(A >> 2);
cout << "\n";
cout << A << " >> 2 = " << (A >> 2) << "\n";
return 0;
}
void
rappbin(short unsigned num)
{
short
unsigned pot2[8] = { 0,0,0,0,0,0,0,0};
int
i=0;
while
((num!=0)&&(i<8))
{
pot2[i]=(num%2);
i++;
num/=2;
}
for ( i = 7; i > -1; i--) cout << pot2[i];
}
timeelapsed% g++ b.c
timeelapsed% a.out
Inserisci un intero : 23
Inserisci un altro intero : 14
Rappresentazione binaria di 23 :------ 00010111
Rappresentazione binaria di 14 :------ 00001110
00010111
00001110 |
------------
00011111
23 OR 14 = 31
**
23 OR 14 = 31**
00010111
00001110 ^
------------
00011001
23 XOR 14 = 25
00010111
00001110 &
------------
00000110
23 AND 14 = 6
00010111
<< 2
------------
01011100
23 << 2 = 92
00010111
>> 2
------------
00000101
23 >> 2 = 5
timeelapsed%
Esempi:
int *p; /* definisco p come puntatore a int */
int* q; /* definisco q come puntatore a int */
la posizione dell' * è irrilevante
Esempio:
int* puntint;
/* puntint è indefinito */

Per indicare che un puntatore non punta a niente, si usa la costante NULL (compatibile con qualsiasi tipo di puntatore):
un puntatore nullo non può essere dereferenziato.
( runtime error)
int x = 5;
puntint = &x; /* ora
puntint punta a x */
....

Per dereferenziare un puntatore si usa l' operatore *
k = *puntint; /* k = 5 */
*puntint = k + 1;
/* x = 6 */

In C++, a differenza di altri linguaggi, il concetto di puntatore è distinto da quello di allocazione dinamica della memoria
int v[20];
sussiste l'identità
v
---------> &v[0]
Infatti la dichiarazione di un array comporta l'allocazione di memoria:
Nel caso della dichiarazione:
int v[20];

Questo fatto spiega le "stranezze" nel passaggio come parametri nelle funzioni C++ degli array .
Infatti, poiché il nome di un array viene interpretato come un puntatore al suo primo elemento, pertanto quando si passa un array, in realtà si passando solo un puntatore al primo elemento dell'array.
Si noti quindi che:
Ma se ciò che viene passato (per valore) è sempre e solo un puntatore all'indirizzo iniziale di v, allora
void
leggiVettore(int dim, int *v)
{
int
i=0;
do
{
printf("Inserire un intero: ");
scanf("%d",&v[i++]);
}
while
(i<dim ) ;
}
main()
{
const
k=5;
int
i, num, vett[k];
printf("Inserire
un vettore di %d interi\n",k);
num
= leggiVettore(k,vett);
for
(i=0; i<num; i++)
printf("v[%d]
= %d\n", i, vett[i]);
}
OSSERVAZIONE
Array e puntatori non sono la stessa cosa....
infatti:
Nota: k celle non significa k byte, ma k*sizeof(T) byte

Analogamente, se p e q sono due puntatori a T, allora:
Ciò evidenzia che ci sono due modi per riferirsi ai singoli elementi di un vettore :
pv
= v + 2; /* punta alla cella contenente 30 */
printf
("%d %d %d %d\n", v[0], v[1], *v, *(v+4) );
printf
("%d %d %d %d\n", pv[0], pv[-1], *pv, *(pv-2) );
}
timeelapsed% g++ b.c
timeelapsed% a.out
10 20 10 50
30 20 30 10
#include <stdio.h>
main
()
{
int
v[5] = {10,20,30,40,50}, i = 3;
printf
("%d %d %d %d\n", v[i], v[1], *v, v[3] );
printf
("%d %d %d %d\n", i[v], 1[v], 0[v], 2[v+1]
);
}
timeelapsed%
a.out
40
20 10 40
40
20 10 40
timeelapsed%
Eppure, a pensarci bene ciò non dovrebbe stupire in quanto:
#include
<iostream.h>
void
leggivet(int n, int* m )
{
int j;
for (j=0; j<n; j++) cin>> (*m++);
}
void stampavet(int n, int* m)
{
int i;
for (i=0; i<n; i++) cout<<(*m++)<<” “;
}
void
ordinavet(int n, int* punt)
{
int i,j;
int
appo;
for
(i = 0; i < (n - 1); i++)
{
for (j = i+1; j < n; j++)
{
if (*(punt + i) > *(punt + j))
{
appo = *(punt + i);
*(punt + i) = *(punt + j);
*(punt + j) = appo;
}
}
}
}
main()
{
const
k=3;
const
h=7;
int
v1[k];
int
v2[h];
leggivet(k,v1);
stampavet(k,v1);
cout<<endl;
ordinavet(k,v1);
stampavet(k,v1);
cout<<endl;
leggivet(h,v2);
stampavet(h,v2);
cout<<endl;
}
#include
<iostream.h>
void
leggimat(int n, int (*m)[3] )
{
int i,j;
for (i=0; i<n; i++)
{
cout<<”riga “<<i<<”\n”;
for (j=0; j<n; j++) cin>> (m[i][j]);
}
}
void stampamat(int n, int (*m)[3] )
{
int i,j;
for (i=0; i<n; i++)
{
cout<<endl;
for (j=0; j<n; j++) {cout<< (m[i][j])<<” “; }
}
}
main()
{
const
k=3;
int
mat[k][k];
cout<<”inserire
una matrice di “<<k<<“ righe “<<”
e “<<k<<” colonne”<<”\n”;
leggimat(k,mat);
stampamat(k,mat);
}
Si noti che c'é una netta distinzione tra le dichiarazioni:
int (*m)[3];
/*dichiarazione di un array di puntatori -posso memorizzare 3 puntatori
a int */
int* m[3];
/* dichiarazione di un puntatore ad un array di 3 interi-
posso memorizzare un puntatore a int
*/
#include <iostream.h>
void leggimat(int n, int (*m)[3] )
{
int i,j;
for (i=0; i<n; i++)
{
cout<<”riga “<<i<<”\n”;
for (j=0; j<n; j++) cin>> (m[i][j]);
}
}
void stampamat(int n, int(*m)[3])
{
int i,j;
for (i=0; i<n; i++)
{
cout<<endl;
for (j=0; j<n; j++) {cout<<
(*(m[i]+j))<<” “; }
}
}
main()
{
const k=3;
int mat[k][k];
int* p;
int j,i;
cout<<”inserire una matrice di “<<k<<“
righe “<<” e “<<k<<”
colonne”<<”\n”;
leggimat(k,mat);
stampamat(k,mat);
cout<<endl;
for (i=0; i<k; i++)
{
p=mat[i];
for (j=0; j<k; j++) {cout<< (*(p+j))<<”
“; }
cout<<endl;
}
}
timeelapsed% g++ b.c
timeelapsed% a.out
inserire una matrice di 3 righe
e 3
colonne
riga 0
1 2 3
riga 1
4 5 6
riga 2
7 8 9
1 2 3
4 5 6
7 8 9
1 2 3
4 5 6
7 8 9
timeelapsed%
In C++ le variabili possono essere allocate:
Memoria stack e memoria heap
L'area
di memoria stack, di cui abbiamo già parlato a proposito delle
funzioni, é quella in cui viene allocato secondo la disciplina LIFO
l' insieme di dati relativi alla funzione chiamata (comprende tutte
le variabili locali e l’indirizzo di rientro nel programma chiamante ),
che viene automaticamente rimosso non appena l’esecuzione della funzione
é terminata.
Sappiamo
anche che, grazie a questo meccanismo, le funzioni possono essere chiamate
ricorsivamente.
Il
“tempo di vita” delle variabili nella memoria stack é legato all’esecuzione
della funzione proprio perché, quando la funzione termina, l’intera
area stack allocata viene rimossa.
L'area di memoria heap, è un’altra area di memoria che il programma può utilizzare. Questa area è soggetta a regole di visibilità e tempo di vita completamente diverse da quelle che governano l’area stack e precisamente:
Allocazione dinamica della memoria tramite la funzione malloc
In C/C++, la funzione malloc costruisce uno o più oggetti nell’area heap e ne restituisce l’indirizzo in una variabile puntatore. La sintassi è:
malloc( <dimensione>*sizeof (<tipo>));
In
caso di errore (memoria non disponibile) restituisce NULL.
I
parametri della funzione sono due e determinano in numero di byte
lo spazio da allocare:
Funzione free
La funzione free dealloca (libera) la memoria dell’area
heap puntata dall’operando.
Poiché non restituisce alcun valore deve essere
usata da sola in un’istruzione.
Esempio:
free(puntvett);
ESEMPIO
#include
<iostream.h>
#include
<stdio.h>
main()
{
const
int c1=5;
int
vett[c1]; /* spazio per c1 interi
*/
int
*puntvett; /* variabile puntatore */
int
k, i;
for
(i=0;i<c1;i++)
vett[i] = 2*i;
/* i primi c1 numeri pari */
for
(i=0;i<c1;i++)
cout<< vett[i]<<" ";
cout<<"\n";
cout<<"Inserire
la dimensione desiderata del vettore\n";
cin>>k;
puntvett
= malloc(k * sizeof(int));
/*
ora posso usare liberamente puntvett come vettore, lunghi k */
for
(i=0;i<k;i++)
puntvett[i] = 2*i; /* i primi k numeri pari */
for
(i=0;i<k;i++)
cout<< puntvett[i]<<" ";
free(puntvett);
}
Operatore
new
In
C++, l’operatore new costruisce uno o più oggetti nell’area heap
e ne restituisce l’indirizzo.
In
caso di errore (memoria non disponibile) restituisce NULL.
Gli
operandi di new (tutti alla sua destra) sono tre, di cui solo il primo
é obbligatorio:
Alloca una variabile di tipo int nell’area heap e usa il suo indirizzo per inizializzare il puntatore punt;
float* punt = new float [100] ; ( alloca 100 oggetti float )
Alloca
100 variabili float nell’area heap e usa l’indirizzo della prima per inizializzare
il puntatore punt;
Operatore
delete
In
C++, l’operatore unario delete dealloca (libera) la memoria dell’area
heap puntata dall’operando.
Poiché
non restituisce alcun valore deve essere usato da solo in un’istruzione.
Esempio:
allocazione:
int* punt = new int;
.....................................
deallocazione:
delete
punt;
Si noti che contrariamente all’apparenza l’operatore delete non cancella la variabile puntatore né altera il suo contenuto: l’effetto é di liberare la memoria puntata rendendola disponibile per ulteriori allocazioni.
Se l’operando punta a un’area in cui sono stati allocati più oggetti, delete va specificato con una coppia di parentesi quadre (senza la dimensione).
Es.:
float*
punt = new float [100] ; ( alloca 100 oggetti float )
delete[]
punt; ( libera tutta la memoria allocata )
L’operatore delete costituisce l’unico mezzo per deallocare memoria heap, che, altrimenti, sopravvive fino alla fine del programma, anche quando non é più raggiungibile.
Esempio:
int*
punt = new int; ( alloca un int nell’area heap)
int
a;
punt
= &a;
( assegna a punt un indirizzo dell’area stack; l’oggetto int dell’area heap non é più raggiungibile )
#include
<iostream.h>
#include
<new.h>
main()
{
const
int c1=5;
int
vett[c1]; /* spazio per c1 interi
*/
int*
puntvett; /* variabile puntatore */
int
k, i;
for
(i=0;i<c1;i++)
vett[i] = 2*i;
/* i primi c1 numeri pari */
for
(i=0;i<c1;i++)
cout<<vett[i];
cout<<"\nInserire
la dimensione desiderata del vettore\n";
cin>>k;
puntvett=new
int[k];
/*
ora posso usare liberamente puntvett come un vettore, lungo k */
for
(i=0;i<k;i++)
puntvett[i] = 2*i; /* i primi k numeri pari */
for
(i=0;i<k;i++)
cout<<" -"<< puntvett[i];
delete[]
puntvett;
}
se durante la creazione dinamica dell'array si desiderava inizializzarlo
a 0 si poteva sostituire l'istruzione:
puntvett=new
int[k];
con
puntvett=new
int[k](0);
Allocazione
dinamica di liste di oggetti
L’allocazione
dinamica della memoria si presta molto bene alla gestione di liste di oggetti,
quando il loro numero non é definito a priori. Queste liste possono
crescere e diminuire di dimensioni dinamicamente in base agli eventi, cioè
all’input-output del programma, e quindi vanno gestite in un modo più
agile ed efficiente che non sia quello di allocare memoria permanente sotto
forma di array (che in tal caso dovrebbe essere molto più grande
di quanto effettivamente necessiti, per disporre di un sufficiente margine
di sicurezza) e spostare continuamente gli elementi dell’array per gestire
gli spazi vuoti creatisi ad ogni nuovo evento.
Con
l’allocazione dinamica della memoria, invece, la memoria impegnata é,
ogni volta, solo quella strettamente necessaria; inoltre, come vedremo,
le operazioni di gestione degli eventi (introduzione o rimozione di un
oggetto nella lista) sono più rapide ed efficienti rispetto al caso
di liste costituite da array.
Strutture concatenate
Una struttura si dice concatenata quando é costituita, oltre che dai suoi normali elementi, anche da uno o due campi aggiuntivi, dichiarati come puntatori alla struttura stessa.
Es.:
struct
lista
{
int val ;
lista* next; } ;
Liste
concatenate
Una
lista concatenata (linked list) é un insieme di
elementi strutturati che comprende oltre ai dati principali , anche uno
o più campi aggiuntivi, dichiarati come puntatori alla struttura
stessa.
In
ogni elemento, il puntatore contiene l’indirizzo di altri oggetti della
lista, creando così un “legame” fra gli oggetti e rendendo la stessa
lista “percorribile”, anche se gli oggetti non sono allocati consecutivamente
in memoria.
Se
la struttura possiede un solo campo puntatore a se stessa, la lista é
detta semplice, se ne possiede due, é detta doppia.
In ogni elemento di una lista semplice, il puntatore alla struttura contiene (di solito) l’indirizzo dell’elemento successivo (in ordine di allocazione), mentre l’ultimo elemento contiene un puntatore NULL.
Deve
anche esistere, separatamente, un puntatore alla struttura che deve contenere
l’indirizzo del primo oggetto della lista (altrimenti la lista non sarebbe
accessibile).
Una
lista semplice é percorribile solo in un verso.
La
definizione di una struttura concatenata é di solito accompagnata
da un certo numero di funzioni, che hanno il compito di gestire le liste,
cioè eseguire le operazioni di inserimento, di eliminazione e di
ricerca di oggetti nelle liste.
#include <iostream.h>
#include
<stdio.h>
#include
<new.h>
struct elem
{
int info;
elem* punt;
};
main()
{
elem*
p;
elem* piniz;
int i;
piniz=NULL;
for(i=0;
i<5; i++)
{
p =
new elem;
(*p).info = i;
(*p).punt = piniz;
piniz= p;
};
p=piniz;
while (p!=NULL)
{cout<<(
(*p).info )<<” “;
p= (*p).punt;
}
}
In questo stesso esempio viene utilizzato un altro modo per riferirsi
alle variabili dinamiche:
#include
<iostream.h>
#include
<stdio.h>
#include
<new.h>
struct
elem
{
int
info;
elem*
punt;
};
main()
{
elem*
p;
elem*
piniz;
int
i;
piniz=NULL;
for(i=0;
i<5; i++)
{
p
= new elem;
p->info = i;
p->punt = piniz;
piniz= p;
};
p=piniz;
while
(p!=NULL)
{cout<<(
p->info )<<” “;
p=
p->punt;
}
}
timeelapsed% g++ b.c
timeelapsed% a.out
4 3 2 1 0 timeelapsed%
Code
e pile
Le
code (queues) e le pile (stacks) sono particolari liste concatenate in
cui l’inserimento e la rimozione degli oggetti obbediscono a precise regole:
Alberi
binari
Gli
alberi binari sono delle particolari liste doppie in cui ogni elemento,
detto nodo, punta a due oggetti successivi, creando così una “ramificazione”
che, partendo dal primo nodo, detto radice, si espande fino ai nodi terminali,
detti foglie. Ogni albero binario necessita di un puntatore “esterno”,
che deve contenere l’indirizzo della radice.
Gli
alberi binari sono usati soprattutto per costruire, in modo rapido ed efficiente,
raccolte ordinate di dati, senza conoscerne a priori il numero. La loro
particolare struttura si presta molto bene ad essere gestita da funzioni
di inserimento e ricerca chiamate ricorsivamente.
L'operatore freccia
viene usato per accedere ad un campo di una variabile struttura tramite
puntatore.
#include <iostream.h>
#include <stdio.h>
#include <string.h>
main()
{
int i;
char s1[]="Prova";
char* s;
s = malloc(strlen(s1)+1);
for(i=0; (s[i]=s1[i])!='\0'; i++);
for(i=0; (s[i]!='\0'); i++) cout<< s[i];cout<<"\n";
free(s);
}
#include
<iostream.h>
main
()
{
char
s1[]= "Hello World!"; //vettore di 13 char
cout<<s1<<endl;
char
s2[]= "Ciao Mondo!"; /* vettore di 12 char */
char
*s;
/* puntatore a char */
/* s1 = s2; VIETATO: ERRORE!! */
s = s1; /* OK, ora puntano allo stesso vettore */
s[1]
= 'a'; /* "Hello" diventa "Hallo" */
cout<<s1<<endl;
/* anche s1 è cambiato! */
}
timeelapsed%
a.out
Hello
World!
Hallo
World!
timeelapsed%
main
()
{
char
s1[]= "Hello World!"; /* vettore di 13 char */
char
*s; /* puntatore a char */
s
= malloc(strlen(s1)+1);
{ int i; for (i=0; s[i]=s1[i]; i++); } /* blocco */
s[1] = 'a'; /* "Hello" diventa "Hallo" */
printf
("%s\n", s1); /* ma s1 NON è cambiato */
printf
("%s\n", s);
}
timeelapsed% a.out
Hello World!
Hallo World!
timeelapsed%
Si noti il modo alquanto criptico per trasferire la stringa
s1 sulla stringa s.
L'algoritmo indica di partire con i=0 e copiare s1[i]
in s[i] fino a quando s1[i]= 0 incrementando ad ogni iterazione i.
In effetti si sicordi che quando all'interno di una istruzione
for es:
for (i=0; s[i]=s1[i]; i++);
di cui si ricorda la sintassi:
for (<inizializzazione>;<condizione>;<incremento>) <istruzione del ciclo>;
nella <condizione> compare solo
una istruzione come ad esempio
s[i]=s1[i];
viene analizzato il valore restituito (ossia il valore he viene
assegnato ad s[i] cioè s1[i] ), se tale valore
è diverso da 0 la condizione è vera, mentre se è uguale
a 0 la condizione è falsa.
Il fatto che la stringa è delimitata da '\0' viene
utilizzato per interrompere il ciclo di copia.
Un codice più chiaro, ma meno efficiente,
sarebbe stato:
#include
<stdio.h>
main
()
{
char
s1[]= "Hello World!"; /* vettore di 13 char */
char
* s;
/* puntatore a char */
s
= malloc((strlen(s1)+1)* sizeof(char));
int
i=0;
while
(s1[i]!=’\0’)
{ s[i]=s1[i];
i++;
}
s[1] = 'a'; /* "Hello" diventa "Hallo" */
printf
("%s\n", s1); /* ma s1 NON è cambiato */
printf
("%s\n", s);
}
main
()
{
char
s1[]= "Hello World!"; /* vettore di 13 char */
char
* s;
/* puntatore a char */
s
= new char [strlen(s1)+1];
int
i=0;
while
(s1[i]!=’\0’)
{ s[i]=s1[i];
i++;
}
s[1]
= 'a'; /* "Hello" diventa "Hallo" */
cout<<
s<<”\n”;
cout<<
s1<<”\n”; /* ma s1 NON è cambiato */
}