I/O del C++
Il C++, come già analizzato, consente, includendo all'interno delle proprie applicazioni le librerie standard di I/O del C, di utilizzare per l'input e l'output le istruzioni scanf e printf, tuttavia mette a disposizione del programmatore anche un nuovo sistema di gestione delle operazioni di I/O più adatto alla programmazione orientata agli oggetti.
Questo sistema, si basa sul concetto di  canale (stream) inteso come mezzo attraverso cui confluiscono le informazioni provenienti o inviate dai diversi dispositivi hardware (le unità esterne).

 In C++ un programma comunica con l'esterno mediante i seguenti:

che rispettivamente rappresentano il canale (stream)  di immissione dati (tastiera), quello di uscita dei risultati (video) e quello in cui vengono riportati eventuali situazioni di errore causate dall’esecuzione di un comando (di solito e’ il video, ma l’utente puo’ scegliere un file diverso)
Questi canali possono essere connessi a dispositivi fisici diversi mantenendo inalterato il proprio comportamento, pertanto un flusso di informazioni, per mezzo delle stesse modalità, può essere mandato in uscita su un video, su una stampante o su un file ed analogamente un flusso di informazioni può provenire adottando le stesse modalità dalla tastiera o da un file presente nella memoria di massa.
I canali standard di I/O sono “collegati” rispettivamente agli:  
 che permettono per mezzo degli: di effettuare le operazioni di I/O.

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;
   }
}
 
 
 


 Input tramite l’operatore di estrazione  >>

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.
 

A causa di ciò non possono essere lette da input tramite cin le stringhe (in quanto si interromperebbe la lettura non appena si incontra un blank)  a ciò si può ovviare  utilizzando la funzione gets già analizzata oppure cin.get ().

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];}
    }
}
 



Manipolatori di formato
I manipolatori di formato possono essere utilizzati previa inclusione dell'header file <iomanip.h>
alcuni sono riportati di seguito:
 
setw(n) pone pari a n l'ampiezza del campo
dec conversione decimale
hex conversione esadecimale
oct conversione ottale
Il manipolatore deve precedere ogni volta il dato a cui è riferito.

#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.

Nella tabella sono riportate alcune delle modalità di accesso ai file più comuni:
 
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.
 



ESEMPIO

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();
 
        }

 
 



ESEMPIO

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
 



Tipo union
 
La dichiarazione di una  union è del tutto analoga a quella di una struttura: l'unica differenza è costituita dalla presenza della parola chiave union in luogo di struct.
La differenza è però enorme a livello concettuale: infatti i campi della union  non occupano locazioni distinte di memoria ma è come se fossero sovrapposti (è riservata solo una quantità di memoria corrispondente al campo più ingombrante).
I campi di una union rappresentano diversi modi di  rappresentare, l'oggetto che la union stessa descrive.

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.
 
  



 
                               OPERATORI LOGICI

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:
 

e inversamente:
   

Ciò rende possibile eseguire operazioni logiche tramite operatori logici, ed anche la combinazione fra operazioni logiche e operazioni di altro tipo.
 



In C++  gli operatori logici sono i seguenti:  
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


Gli operatori binari  &,  |, e  ^ eseguono operazioni logiche bit a bit fra i due operandi, e precisamente:
&    esegue l’AND fra i corrispondenti bit dei due operandi
 |    esegue l’OR inclusivo fra i corrispondenti bit dei due operandi
^    esegue l’OR esclusivo fra i corrispondenti bit dei due operandi

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)
 
 
 
 

L’operatore unario ~ inverte i bit dell’operando.
 
 
 
esempio risultato  motivo
~ 12  -13  provare per credere... 
 
#include <iostream.h>
void rappbin(short unsigned);
main()
{
  short unsigned A;
 cout << "Inserisci un intero : ";
  cin >> A;
// mostra gli input in decimale e in binario
  cout << "Rappresentazione binaria di " << A << " :------ ";
  rappbin(A);
  cout << "\n";
 
  // prova l'operatore bit a bit di negazione
  rappbin(~A );
  cout << "\n";
  cout << “~”<<A << "  = "  << (~A ) << "\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];

}
 
 
 
 

 

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

Esempi:
 
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 
 



 ESEMPIO

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%
 
 


                 Tipo Puntatore
 
Un puntatore è una variabile destinata a contenere l'indirizzo di un'altra variabile.
Dichiarazione di una variabile puntatore:
<tipoElementoPuntato> * <nomePuntatore> ;

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):

int *puntint = NULL;

un puntatore nullo non può essere dereferenziato.
( runtime error)



Poiché il C++ permette di conoscere l'indirizzo di una variabile tramite l'operatore "estrazione di indirizzo" & possiamo utilizzare tale operatore per inizializzare un puntatore.

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 */
 
 
 

Quindi se puntint è un puntatore*puntint  è l'elemento puntato (denota il valore (intero) puntato).

In C++, a differenza di altri linguaggi, il concetto di puntatore è distinto da quello di allocazione dinamica della memoria



 

                                        ARRAY E PUNTATORI

In C++, fra array e puntatori esiste una stretta relazione.
Ad esempio, se dichiariamo:

int v[20];

sussiste l'identità

v  --------->  &v[0]
 

Infatti la dichiarazione di un array comporta l'allocazione di memoria:

ed inoltre il  puntatore viene dichiarato const e inizializzato con l'indirizzo dell'area puntata (cioè del primo elemento dell'array).

Nel caso della dichiarazione:
int v[20];
 

Quindi: Il C++ ha dunque una visione molto di basso livello di un array, in quanto non lo considera come "entità dotata di sue proprietà", ma solo come "area di memoria" (che inizia a un certo indirizzo).
 

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

int media(int k,int vett[]);
int media(int k, int* vett);
Si noti che l'uso di *vett nell'intestazione della funzione non comporta alcun obbligo circa la notazione da usare nel corpo della funzione - in particolare, è possibile usare  la notazione vett[i] nel codice:

ESEMPIO

#include <stdio.h>

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:

int v[20], w[30];
v = w;   /* NO! */
int v[20], *p;
p = v; /* OK */
int v[20];
int *p;

ARITMETICA DEGLI INDIRIZZI

Se v è un puntatore a variabili di tipo T, e k è un intero, allora:
 

Nota: k celle non significa k byte, ma k*sizeof(T) byte

Analogamente, se p e q sono due puntatori a T, allora:

Si noti che, se q denota un indirizzo successivo a quello di p,   il numero  così ottenuto può essere negativo.
 
 

Ciò evidenzia che ci sono due modi per riferirsi ai singoli elementi di un vettore :

Tali espressioni sono del tutto equivalenti


ESEMPIO

#include <stdio.h>
main ()
{
int v[5] = {10,20,30,40,50};
int *pv = NULL;

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
 


ESEMPIO

L'equivalenza fra v[i] e *(v+i) ha conseguenze inaspettate:

#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:


ESEMPIO

Passaggio di un vettore monodimensionale a una funzione

#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;
}


ESEMPIO

Passaggio di un vettore multidimensionale a una funzione

#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            */

 
 



ESEMPIO
Uso dei puntatori nella gestione di array bidimensionali

#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%
 


Allocazione dinamica di variabili
 

     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: 

p = malloc(5*sizeof(int));
 

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);
}



Allocazione dinamica della memoria tramite l'operatore new

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:

Esempi:
  int*  punt = new int;

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);



Gestione di liste con strutture concatenate
 

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.
 
 




 

Operatori . e ->

L'operatore punto viene usato per accedere ad un campo della variabile struttura  in modo diretto.

L'operatore freccia viene usato per accedere ad un campo di una variabile struttura tramite puntatore.


STRINGHE E PUNTATORI A CARATTERE

 
 
#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);
}
 

 



ESEMPIO

#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%


ESEMPIO

#include <stdio.h>

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);

}
 



 
#include <stdio.h>
#include <iostream.h>
#include <new.h>

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 */
}