MindView Inc.

 

[ Suggerimenti ] [ Soluzioni degli Esercizi] [ Volume 2 ] [ Newsletter Gratuita ]
[
Seminari ] [ Seminari su CD ROM ] [ Consulenza]

Pensare in C++, seconda ed. Volume 1

©2000 by Bruce Eckel

[ Capitolo Precedente ] [ Indice Generale ] [ Indice Analitico ] [ Prossimo Capitolo ]

traduzione italiana e adattamento a cura di Mauro Sorbo

3: Il C nel C++

Poichè il C++ è basato sul C, si deve conoscere la sintassi del C al fine di programmare in C++, come si deve essere ragionevolmente spedito in algebra al fine di affrontare calcoli.

 

Se non si è mai visto prima il C, questo capitolo darà un decente background dello stile del C nel C++. Se si conosce  lo stile del C descritto nella prima edizione di Kernighan & Ritchie (spesso chiamato K&R C), si troveranno nuove e differenti caratteristiche nel C++ in aggiunta al C standard. Se si conosce il C standard, si dovrebbe sfogliare questo capitolo guardando le caratteristiche che sono peculiari del C++. Si noti che ci sono alcune fondamentali caratteristiche del C++  introdotte qui, le quali sono idee base che sono affini alle caratteristiche del C o spesso modifiche ai modi  in cui il C fa le cose. Le caratteristiche più sofisticate del C++ saranno introdotte negli ultimi capitoli.

Questo capitolo è una veloce panoramica dei costrutti del C ed un'introduzione ad alcuni costrutti base del C++, presumendo che si è avuto qualche esperienza di programmazione in un altro linguaggio. Un'introduzione al C si trova nel CD ROM confezionato in fondo al libro, intitolato Thinking in C: Foundations for Java & C++  di Chuck Allison (pubblicata da MindView, Inc., e anche disponibile al sito www.MindView.net). Questo e' un seminario in CD ROM con lo scopo di dare accuratamente tutti i fondamenti del linguaggio C. Si focalizza sulla conoscenza necessaria per poter destreggiarsi nei linguaggi C++ o Java piuttosto che provare a formare esperto del C ( una delle ragioni dell'uso del linguaggio alto come il C++ o il Java è precisamente il fatto che possiamo evitare molti i loro angoli buii). Esso contiene inoltre esercizi e soluzioni guidate. Si tenga a mente ciò perchè va oltre il CD  Thinking in C, il CD non e' una sostituzione di questo capitolo, ma dovrebbe essere usato invece come una preparazione per il libro.

Creare funzioni

Nel vecchio (pre-Standard) C, si poteva chiamare una funzione con qualsiasi numero o tipi di argomento ed il compilatore non avrebbe reclamato. Ogni cosa sembrava giusta fino a quando non  si faceva girare il programma. Si ottenevano risultati misteriosi (o peggio, il programma si bloccava) con nessun accenno ad un perchè. La mancanza di aiuto con il passaggio dell'argomento e gli enigmatici bug risultavano essere probabilmente una ragione per cui il C fu nominato "linguaggio assemblante di alto livello ". I programmatori del C Pre-Standard si adattarono  ad esso.

Il C Standard ed il C++ usano una caratteristica chiamata function prototyping (prototipazione della funzione). Con il function prototyping, si deve usare una descrizione dei tipi di argomenti quando si sta dichiarando e definendo una funzione. Questa descrizione è un "prototipo". Quando la funzione viene chiamata , il compilatore usa il prototipo per assicurarsi che gli opportuni argomenti siano passati e che il valore restituito sia elaborato correttamente. Se il programmatore fa un errore quando chiama la funzione, il compilatore lo trova. 

Essenzialmente, si è appreso della function prototyping ( non è stata chiamata in tal modo) nel capitolo precedente, poichè la forma della dichiarazione di funzione nel C++ richiede opportuni prototipi. In un prototipo di funzione, la lista degli argomenti contiene i tipi degli argomenti che devono essere passati alla funzione e (  a scelta per la dichiarazione) identificatori per gli argomenti. L'ordine ed il tipo di argomenti devono corrispondere nella dichiarazione, definizione e nella chiamata alla funzione. Ecco qui un esempio di un prototipo di funzione in una dichiarazione:

int traduci(float x, float y, float z);

Non si usa la stessa forma quando si dicharano variabili nei prototipi delle funzioni come si fa nelle definizioni ordinarie  di variabili. Ciòè, non si può dire: float x, y, z. Si deve indicare il tipo di ogni argomento. In una dichiazione di funzione, la forma seguente è  anche accettabile:  

int traduci(float, float, float);

Poichè il compilatore non fa altro che controllare i tipi quando la funzione viene chiamata, gli identificatori sono soltanto inclusi per  chiarezza di chi legge il codice.

Nella definizione di funzione, i nomi sono richiesti perchè viene fatto riferimento agli argomenti dentro la funzione:

int traduci(float x, float y, float z) {

  x = y = z;

  // ...

}

 Ne deriva che questa regola si applica solo nel C. Nel C++, un argomento può essere senza nome nella lista degli argomenti della definizione di funzione. Poiché  è senza nome, non lo si può usare nel corpo della funzione naturalmente. Gli argomenti senza nome sono concessi per dare al programmatore un modo per “riservare spazio nella lista degli argomenti”. Chiunque usa la funzione deve chiamare ancora la funzione con i propri argomenti. Comunque, la persona che crea la funzione può dopo usare l’argomento in futuro senza forzare modifiche al codice che chiama la funzione. L’opzione di ignorare un argomento nella lista è anche possibile se si lascia il nome dentro, ma si avrà un irritante messaggio di allarme circa il valore che non è usato ogni volta che si compila la funzione. L’allarme viene eliminato se si rimuove il nome.

Il C ed il C++ hanno altri due modi di dichiarare una lista di argomenti. Se si ha una lista di argomenti vuota, si può dichiararla  come func( ) in C++, la quale dice al compilatore che ci sono esattamente zero argomenti. Si dovrebbe essere consapevoli che ciò significa solamente una  lista di argomenti vuota in C++. In C ciò significa " un indeterminato numero di argomenti" ( il quale è un " buco" nel C poichè disabilita il controllo del tipo in quel caso). Sia in C che C++, la dichiarazione func(void);  intende una lista di argomenti vuota. La parola chiave void intende "nulla" in questo caso (e può anche significare "nessun tipo" nel caso dei puntatori, come si vedrà in seguito in questo capitolo). 

L'altra opzione per le liste di argomenti si usa quando non si sa quanti argomenti o che tipo di argomenti si avranno; ciò è chiamata una lista di argomenti variabile. Questa " lista di argomenti inaccertata" è rappresentata da ellissi (...). Definere una funzione con una lista di argomenti variabile  è significativamente più complicato che definire una funzione regolare. Si può usare una lista di argomenti variabile per una funzione che ha una serie fissata di argomenti se ( per alcune ragioni) si vuole disabilitare il controllo di errore del prototipo di funzione. A causa di ciò, si dovrebbe restringere il nostro uso della lista di controllo variabile  al C ed evitarli nel C++ (nella quale,  come si imparerà, ci sono alternative migliori). L'uso della lista di argomenti variabile  è descritta nella sezione della libreria della nostra guida locale C.

Le funzioni restituiscono valori

Un prototipo di  funzione in C++ deve specificare il tipo del valore di ritorno della funzione ( in C, se si omette il tipo del valore di ritorno esso è per default un int). La specificazione del tipo di ritorno precede il nome della funzione. Per specificare che nessun valore viene restituito, si usa la parola chiave void. Ciò genererà un errore se si prova far ritornare un valore dalla funzione. Ecco qui alcuni  prototipi completi di funzioni  :

int f1(void); // restituisce un  int, non da argomenti

int f2(); // Come f1() in C++ ma non in Standard C!

float f3(float, int, char, double); // Restituisce un float

void f4(void); //  Non da argomenti, non restituisce nulla

Per far restituire un valore da una funzione, si usa la dichiarazione return. return fa ritornare l'esecuzione del programma esattamente dopo la chiamata alla funzione. Se return ha un argomento, quel argomento  diventa un valore di ritorno della funzione. Se la funzione dice che ritornerà un tipo particolare, dopo ogni dichiarazione return deve ritornare quel tipo. Si può avere più di una dichiarazione return in una definizione di funzione:

//: C03:Return.cpp

//: uso del "return"

#include <iostream>

using namespace std;

char cfunc(int i) {

  if(i == 0)

    return 'a';

  if(i == 1)

    return 'g';

  if(i == 5)

    return 'z';

  return 'c';

}

int main() {

  cout << "scrivi un intero: ";

  int val;

  cin >> val;

  cout << cfunc(val) << endl;

} ///:~

In cfunc( ), la prima if che valuta il true esce dalla funzione tramite la dichiarazione return. Notare che la dichiarazione di funzione non è necessaria perché la definizione di funzione appare prima che essa sia usata nel main( ), quindi il compilatore conosce la funzione da quella dichiarazione di funzione.

Utilizzo della libreria di funzioni del C

Tutte le funzioni nella libreria locale di funzioni del C sono disponibili mentre si programma in C++ . Si dovrebbe  guardare attentamente alla libreria di funzioni prima di definire la propria funzione cè una buona possibilità che qualcuno abbia già risolto il problema per noi e probabilmente abbia dato ad esso molti più pensieri e debug.

Tuttavia una parola di prudenza: molti compilatori includono molte funzioni extra che rendono la vita  anche più facile e sono allettanti da usare, ma non sono parte della libreria Standard del C. Se si è certi che non si vorrà mai spostare le applicazioni su un'altra piattaforma (e chi è certo di ciò), si vada avanti e si usino queste funzioni che rendono la vita più facile. Se si vuole che la nostra applicazione sia portabile, si dovrebbe limitare alle funzioni della libreria standard. Se si devono realizzare attività specifiche della piattaforma, si provi ad isolare quel codice in  un punto così che esso possa essere cambiato facilmente quando la si sposta su unaltra piattaforma. In C++ le attività specifiche della piattaforma sono spesso incapsulate in una classe, che è la soluzione ideale.

La formula per usare una libreria di funzioni è la seguente: primo, trovare la funzione nella propria guida di riferimento per la programmazione  ( molte guide elencano nellindice la funzione per categorie e per ordine alfabetico). La descrizione della funzione dovrebbe includere una sezione che dimostri  la sintassi del codice. La cima di questa sezione di solito ha almeno un #include, mostrandoci il file principale contenente  il prototipo della funzione.  Si duplichi questa linea #include nel nostro file, in modo che la funzione è propriamente dichiarata. Adesso si può chiamare una funzione nello stesso modo in cui appare nella sezione della sintassi. Se si fa un errore, il compilatore lo scoprirà comparandolo la nostra chiamata alla funzione con il prototipo della funzione presente in  cima e ci darà informazioni circa il nostro errore.  Il linker cerca la libreria Standard per default, quindi tutto ciò che si ha bisogna fare è: includere il file principale e chiamare la funzione.

Creare le proprie librerie con il librarian.

Si possono mettere le proprie funzioni in una libreria. Ogni ambiente di sviluppo ha un librarian che amministra gruppi di moduli oggetto. Ogni librarian ha i propri comandi, ma lidea generale è questa: se si vuole creare una libreria, bisogna fare un file principale contenente prototipi di funzione per tutte le funzioni nella nostra libreria. Si mette questo file principale da qualche parte nella libreria di ricerca del preprocessore, ciascun nella directory locale (così può essere trovata con #include "header") o in una directory include (così può essere trovata con #include <header>). Poi si prendono tutti i moduli oggetto e  si passano al librarian insieme con un nome per la libreria completata ( la maggior parte dei librarian richiedono una comune estensione, come .lib o .a). Si pone la libreria completata dove risiedono le altre librerie così il linker può trovarle. Quando si usa una propria libreria, si dovrà aggiungere qualcosa alla riga di comando così il linker sa cercare la libreria per la funzione che si chiama. Si devono trovare tutti i dettagli nel proprio manuale locale, poiché variano da sitema a sistema. 

Controllare l'esecuzione

Questa sezione copre le istruzioni per il controllo dell' esecuzione in C++. Si devono conoscere queste dichiarazioni prima di poter leggere e scrivere il codice in C e C++.

Il C++ usa tutte le istruzioni per il controllo dell' esecuzione del C. Esse includono if-else, while, do-while, for ed una selezione di dichiarazioni chiamati switch. Il C++ ammette anche linfame goto, il quale sarà evitato in questo libro.

True e false

Tutte le dichiarazioni condizionali usano la verità o la falsità di una espressione condizionale per determinare la traiettoria di esecuzione. Un esempio di una espressione condizionale è A == B. Questo usa loperatore condizionale == per vedere se la variabile A è equivalente alla variabile B. Lespressione produce un Booleano true o false( queste sono parole chiave solo in C++; nel C una espressione è vera se valuta un valuta che non vale zero ). Altri operatori condizionali sono >, <, >=, etc. Le dichiarazioni condizionali sono più pienamente trattate in seguito in questo capitolo.

if-else

La dichiarazione if-else può esistere in due forme: con o senza l else. Le due forme sono:

if(espressione)

    istruzione

 

oppure

if(espressione)

       istruzione

else

    istruzione

 

La espressione valuta true o false. L' istruzione significa sia una semplice istruzioni terminate da un punto e virgola oppure un' istruzione composta, la quale è un gruppo di semplici istruzioni racchiuse in parentesi. In qualsiasi momento la parola istruzione sia usata, implica sempre che la dichiarazione è semplice o composta. Si noti che questa istruzione può essere un altro if.

//: C03:Ifthen.cpp

// dimostrazione delle condizioni if e if-else

 

#include <iostream>

using namespace std;

int main() {

  int i;

  cout << "scrivi un numero e 'Enter'" << endl;

  cin >> i;

  if(i > 5)

    cout << "è maggiore di 5" << endl;

  else

    if(i < 5)

      cout << "è minore di 5 " << endl;

    else

      cout << "è uguale a 5 " << endl;

  cout << "scrivi un numero e 'Enter'" << endl;

  cin >> i;

  if(i < 10)

    if(i > 5)  // "if" è soltanto un’altra istruzione

      cout << "5 < i < 10" << endl;

    else

      cout << "i <= 5" << endl;

 else // "if(i < 10)"

    cout << "i >= 10" << endl;

} ///:~

E convenzione indentare il corpo delle istruzioni del flusso di controllo così che il lettore  può facilmente determinare dove esso inizia e dove finisce[30].

while

while, do-while e for servono per il controllo del ciclo. Una istruzione è ripetuta fino a quando lespressione di controllo vale  false. La forma del ciclo while è

while(espressione)

    istruzione

Lespressione è valutata solamente una volta allinizio del ciclo e ancora prima di ogni ulteriore iterazione dell' istruzione.

Questo esempio sta nel corpo del ciclo while fino a quando si scrive il numero segreto o si preme control-C.

//: C03:Guess.cpp

// indovina un numero ( dimostra "while")

#include <iostream>

using namespace std;

int main() {

  int secret = 15;

  int guess = 0;

  // "!=" è il condizionale "non-uguale":

 while(guess != secret) { // dichiarazione complessa

    cout << "indovina il numero: ";

    cin >> guess;

  }

  cout << "Hai indovinato !" << endl;

} ///:~

Lespressione condizionale di while non è ristretta ad un semplice test come nellesempio su menzionato; può essere complicato quanto piaccia sempre che produce un  risultato true o false. Si vedra anche il codice dove il ciclo non ha corpo, solamente un semplicemente punto e virgola:

while(/* Do a lot here */)

 ;

In questi casi, il programmatore ha scritto l' espressione condizionale non solo per effettuare il test ma anche per fare il lavoro.

do-while

La forma del do-while  è

do

    istruzione

while(espressione);

 

Il  do-while è differente dal while perchè l'istruzione è eseguita sempre almeno una volta, anche se lespressione vale false in primo luogo. In un nomrale while, se il condizionale è falso nella prima volta l'istruzione non viene mai eseguita.

Se il do-while è usato in Guess.cpp , la variabile guess non ha bisogno di un valore iniziale, poiché è inizializzato dall' istruzione cin prima che viene esaminata:    

//: C03:Guess2.cpp

// il programma guess con do-while

 

#include <iostream>

using namespace std;

int main() {

  int secret = 15;

int guess; // Qui non serve nessuna inizializzazione

  do {

    cout << "Indovina il numero: ";

 cin >> guess; // Avviene l’inizializzazione

 

  }   while(guess != secret);

  cout << "You got it!" << endl;

} ///:~

Per alcune ragioni, la maggior parte dei programmatori tendono a evitare do-while e a lavorare solo con while.

 

for

Un ciclo for effettua un' inizializzazione prima della prima ripetizione. Dopo effettua la prova condizionale e, alla fine di ogni ripetizione, alcune forme di progresso. La forma del ciclo for è:

for(inizializzazione; condizione; passo)

 istruzione

Qualsiasi delle espressioni di inizializzazione,condizione e passo possono essere vuote. Il codice di inizializzazione è eseguito una volta soltanto allinizio. La condizione è eseguita prima di ogni ripetizione ( se vale falso allinizio, l'istruzione non è mai eseguita ). Alla fine di ogni ciclo, viene eseguito il passo.

I cicli for sono di solito usati per il compito di conteggio:

 

//: C03:Charlist.cpp

//Visualizza tutti i caratteri ASCII

// dimostra il "for"

 

#include <iostream>

using namespace std;

int main() {

  for(int i = 0; i < 128; i = i + 1)

 if (i != 26)  // ANSI Terminal Clear screen

      cout << " valore: " << i

           << " carattere: "

           << char(i) // tipo di conversione

           << endl;

} ///:~

Si può notare che la variabile i è definita nel punto in cui è usata, invece che allinizio del blocco denotato dallapertura della parentesi graffa {. Ciò è in contrasto con le procedure di linguaggio tradizionali( incluso C) i quali richiedono che tutte le variabili siano definite allinizio del blocco. Ciò verrà discusso in seguito in questo capitolo.

 

Le parole chiave break e continue

Dentro il corpo di qualsiasi construtto dei cicli while, do-while, o for si può controllare il fluire del ciclo usando break e continue. break fa uscire dal ciclo senza eseguire il resto delle istruzioni nel ciclo. continue fa finire lesecuzione della corrente iterazione e ritorna allinizio del ciclo per iniziare una nuova iterazione.

Come esempio di break e continue, ecco questo programma che è un semplice menu di sistema:

//: C03:Menu.cpp

// dimostrazione di un semplice menu di programma

// l' uso di "break" e "continue"

#include <iostream>

using namespace std;

int main() {

  char c; // per memorizzare il responso

  while(true) {

    cout << "MAIN MENU:" << endl;

    cout << "l: sinistra, r: destra, q: quit -> ";

    cin >> c;

    if(c == 'q')

      break; // fuori dal "while(1)"

     

    if(c == 'l') {

      cout << " MENU A SINISTRA:" << endl;

      cout << "scegli a oppure b: ";

      cin >> c;

      if(c == 'a') {

        cout << "hai scelto 'a'" << endl;

  continue; // ritorna al menu principale

      }

      if(c == 'b') {

        cout << "hai scelto 'b'" << endl;

  continue; // ritorna al menu principale

      }

      else {

        cout << "non hai scelto nè a nè b!"

             << endl;

  continue; // ritorna al menu principale 

      }

    }

    if(c == 'r') {

      cout << " MENU A DESTRA:" << endl;

      cout << "scegli c oppure d: ";

      cin >> c;

      if(c == 'c') {

        cout << "hai scelto 'c'" << endl;

 continue; // ritorna al menu principale  

      }

      if(c == 'd') {

        cout << "hai scelto 'd'" << endl;

 continue; // ritorna al menu principale

      }

      else {

        cout << "non hai scelto nè c nè d!"

             << endl;

  continue; // ritorna al menu principale

      }

    }

    cout << "devi digitare l oppure r oppure q!" << endl;

  }

  cout << "uscita dal menu..." << endl;

} ///:~

Se lutente seleziona q nel menu principale, il break è usato per uscire, altrimenti il programma continua ad eseguire indefinitamente. Dopo ogni selezione del sub-menu, la parola chiave continue è usata per fare un salto indietro allinizio del ciclo while.

L'istruzione while(true) è lequivalente di fai questo ciclo per sempre . La dichiarazione break  ci permette di interrompere questo infinito ciclo while quando lutente scrive una q.

switch

Un' istruzione switch seleziona tra pezzi di codice basati sul valore dellespressione intero. La sua forma è:

switch(selezione) {

    case valore-intero1 : istruzione; break;

    case valore-intero2 : istruzione; break;

    case valore-intero3 : istruzione; break;

    case valore-intero4 : istruzione; break;

    case valore-intero5 : istruzione; break;

    (...)

    default: istruzione;

}

selezione è unespressione  che produce un valore intero. Switch confronta il risultato di selezione con ogni valore intero. Se ne trova uno simile, la corrispondente dichiarazione ( semplice o  composta) viene eseguita. Se non si presenta nessun simile, viene eseguita l'istruzione default.

Si noterà  nelle definizioni summenzionate che ogni case si conclude con un break, il quale sposta lesecuzione alla fine del corpo dello switch ( la chiusura della parentesi che completa lo switch). Questo è il modo convenzionale per costruire una dichiarazione switch, ma il break è facoltativo. Se manca vengono eseguite le istruzione del seguente case fino a che non si incontra il break. Sebbene non si vuole usualmente questo tipo di comportamento, può essere utile per un programmatore esperto.

L'istruzione switch è un modo pulito per attuare una selezione multipla ( es. selezionare differenti percorsi di esecuzione), ma richiede un selettore che valuta un valore intero a tempo di compilazione. Se si vuole usare, ad esempio, un oggetto string come selettore, non funziona in una dichiarazione di switch. Per un selettore string, si deve usare invece una serie di dichiarazioni if e confrontare la string dentro il condizionale.

Lesempio di menu mostrato sotto fornisce un buon particolare esempio di switch:

//: C03:Menu2.cpp

// un menu che usa uno switch

#include <iostream>

using namespace std;

int main() {

  bool quit = false;  // un flag per uscire

  while(quit == false) {

    cout << "Scegli a, b, c oppure q per terminare: ";

    char response;

    cin >> response;

    switch(response) {

      case 'a' : cout << "hai scelto 'a'" << endl;

                 break;

      case 'b' : cout << "hai scelto 'b'" << endl;

                 break;

      case 'c' : cout << "hai scelto 'c'" << endl;

                 break;

      case 'q' : cout << "fine" << endl;

                 quit = true;

                 break;

      default  : cout << "Scegliere a,b,c oppure q!"

                 << endl;

    }

  }

} ///:~

Il flag quit è un bool, abbreviazione di Boolean, che è un tipo che si troverà soltanto in C++. Esso può assumere solo i valori chiave true o false. Selezionare q setta il flag quit su true. La prossima volta che il selettore sarà valutato, quit == false ritorna false così che il corpo del while non è eseguito.

Uso e abuso di goto

La parola chiave goto è supportata in C++, poichè esiste anche in C. L'uso di goto è spesso respinto perchè appartiene uno stile mediocre di programmazione e la maggior parte delle volte è vero. Ogni volta che si usa goto, si guardi il proprio codice per vedere se cè un altro modo per scriverlo. In rare occasioni, si può scoprire che goto può risolvere un problema che non può essere risolto in altro modo, ma tuttavia, lo si consideri con cura. Ecco qui un esempio che ne fa forse un possibile candidato:

//: C03:gotoKeyword.cpp

// L’infame goto è supportato nel C++

#include <iostream>

using namespace std;

int main() {

  long val = 0;

  for(int i = 1; i < 1000; i++) {

    for(int j = 1; j < 100; j += 10) {

      val = i * j;

      if(val > 47000)

        goto bottom;

//Break andrebbe messo solamente esternamente al ‘for’

    }

  }

  bottom: // un'etichetta

  cout << val << endl;

} ///:~

Lalternativa sarebbe settare un Booleano che è testato nella parte più esterna del ciclo for, e dopo fare un break allinterno del ciclo. Comunque, se si hanno svariati livelli di for o while questo potrebbe essere scomodo.

Ricorsione

La ricorsione è uninteressante e qualche volta utile tecnica di programmazione con cui si può chiamare la funzione in cui si è. Naturalmente se questo è tutto quello che si fa, si continuerà a chiamare la funzione dove si è fino a  quando non si esaurisce la memoria, quindi ci deve essere qualche modo di raggiungere il livello più basso della chiamata ricorsiva. Nel esempio seguente, questo raggiungimento del livello più basso è compiuto dicendo semplicente che la ricorsione andrà soltano fino a che il cat supera Z: :[31]

//: C03:CatsInHats.cpp

// semplice dimostrazione di ricorsione

#include <iostream>

using namespace std;

void removeHat(char cat) {

  for(char c = 'A'; c < cat; c++)

    cout << "  ";

  if(cat <= 'Z') {

    cout << "cat " << cat << endl;

  removeHat(cat + 1); // chiamata ricorsiva

  } else

    cout << "VOOM!!!" << endl;

}

int main() {

  removeHat('A');

} ///:~

In removeHat( ), si può vedere che finchè cat  è minore di ‘Z, removeHat( ) verrà chiamata dentro removeHat( ), effettuando così la ricorsione. Ogni volta che removeHat( ) è chiamata, il proprio argomento è l'unico più grande del corrente cat  quindi largomento continua ad incrementare.

La ricorsione è spesso usata quando si valuta qualche tipo di problema complesso, poichè non si è ristretti ad una particolare taglia per la soluzione la funzione può richiamarsi fino a che non si è raggiunta la fine del problema.

Introduzione agli operatori

Si può pensare che gli operatori  come tipi speciali di funzioni ( si apprenderà che il sovraccaricamento degli operatori del C++ tratta gli operatori precisamente in quel modo). Un operatore prende uno o più argomenti e produce un nuovo valore. Gli argomenti hanno differente forma rispetto alle ordinarie chiamate di funzioni, ma leffetto è lo stesso.

Dalla propria precedente esperienza di programmazione, si dovrebbe aver abbastanza confidenza con gli operatori che sono stati usati. I concetti di addizione (+), sottrazione ed il segno meno (-), moltiplicazione (*), divisione (/), e assegnamento(=) hanno tutti essenzialmente lo stesso significato in ogni linguaggio di programmazione. Lintera serie di operatori è elencata in seguito in questo capitolo.

Precedenza

Loperatore precedenza definisce lordine nel quale è valutata unespressione quando sono presenti svariati operatori. Il C ed il C++ hanno specifiche regole per determinare lordine di valutazione. Il più facile da ricordare è che la moltiplicazione e la divisione vengono prima delladdizione e della sottrazione. Dopo ciò, se unespressione non ci è trasparente probabilmente non lo sarà per tutti quelli che leggono il codice, quindi si dovrebbero usare le parentesi per rendere esplicito l'ordine di valutazione. Per esempio:

A = X + Y - 2/2 + Z;

ha un differente significato dalla stessa dichiarazione con un gruppo particolare di parentesi:

A = X + (Y - 2)/(2 + Z);

(si provi a valutare il risultato con X = 1, Y = 2, e Z = 3).

Auto incremento e decremento

Il C, e di conseguenza il C++, è pieno di scorciatoie. Le scorciatoie possono rendere il codice molto più facile da scrivere e qualche volta molto più diffice da leggere. Forse i creatori del linguaggio C pensarono che sarebbe stato più facile capire un pezzo difficile di codice se i nostri occhi non avevano da esaminare una larga area di testo.

Una della migliori scorciatoie sono gli operatori auto-incremento e auto-decremento. Si usano spesso per cambiare le variabili del ciclo, le quali controllano il numero di volte che un ciclo viene eseguito.

Loperatore auto-decremento è -- e significa decrementa di una unità . Loperatore auto-incremento è ++ e significa incrementa di una unità . Se A è un int, ad esempio, lespressione ++A è equivalente a (A = A + 1). Gli operatori auto-incremento e auto-decremento producono il valore della variabile come risultato. Se loperatore appare prima della variabile (es. ++A), loperazione è prima eseguita ed il valore risultante viene prodotto. Se loperatore appare dopo la variabile (es. A++), il valore è prodotto corrente e loperazione è eseguita successivamente. Ad esempio :

//: C03:AutoIncrement.cpp

// mostra l’uso degli operatori auto-incremento

//e auto-decremento

#include <iostream>

using namespace std;

int main() {

  int i = 0;

  int j = 0;

cout << ++i << endl; // Pre-incremento

  cout << j++ << endl; // Post-incremento

  cout << --i << endl; // Pre-decremento

  cout << j-- << endl; // Post decremento

} ///:~

  cout << ++i << endl; // Pre-increment

  cout << j++ << endl; // Post-increment

  cout << --i << endl; // Pre-decrement

  cout << j-- << endl; // Post decrement

} ///:~

Se ci si è chiesto perchè il nome C++, ora si capisce cosa significa: un passo oltre il C .

Introduzione ai tipi di dato

I Data types ( tipi di dato) definiscono il modo in cui si usa lo spazio ( memoria) nei programmi che si scrivono. Specificando un tipo di dato, si dice al compilatore come creare un particolare pezzo di memoria e anche come manipularla.

I tipi di dato possono essere predefiniti o astratti. Un tipo di dato predefinito è uno che il compilatore intrinsecamente conosce, uno che è fissato nel compilatore. I tipi di dato predefiniti sono quasi identici in C e C++. Un tipo di dato definito da un utente è uno che noi o un altro programmatore crea come una classe. Queste sono comunemente detti tipi di dato astratti. Il compilatore sa come gestire tipi predefiniti quandoparte; esso impara come gestire i tipi di dato astratti leggendo i file principali contenenti le dichiarazioni di classi ( si imparerà ciò nel prossimo capitolo).

Tipi base predefiniti

La specificazione dello standard C per i tipi predefiniti ( che il C++ eredita) non dice quanti bit ogni  tipo predefinito deve contenere. Invece, esso stipula che il minimo ed il massimo valore che il tipo predefinito deve   avere. Quando una macchina è basata su binari, questo valore massimo può essere direttamente tradotto in un numero minimo di bit necessari a rappresentare quel valore. Tuttavia, se una macchina usa, ad esempio, il codice binario decimale (BCD) per rappresentare numeri, allora lammontare dello spazio richiesto nella macchina per ottenere il numero massimo per ogni tipo di data sarà differente. I valori di minimo e massimo che possono essere immagazzinati in vari tipi di data sono definiti nei file del sistema principale limits.h e float.h (in C++ si scriverà invece generalmente #include <climits> e <cfloat>).

Il C ed il C++ hanno quattro data tipi base predefiniti, descritti qui per le macchine basate sul sistema binario. Una char memorizza caratteri ed usa un minimo di 8 bit( 1 byte ), sebbene può essere più grande. Un int immagazzina un numero intero ed usa un minimo di due byte. I tipi float e double memorizzano numeri a virgola mobile,  di solito nel formato virgola mobile IEEE. Float è per singola precisione virgola-mobile e double per virgola mobile doppia precisione .

Come menzionato in precedenza, si possono definire variabili in qualsiasi parte in uno scope e le si possono definire ed inizzializzare allo stesso tempo. Ecco qui come definire variabili usano i quattro tipi di dato base:

 

//: C03:Basic.cpp

//Definizione dei quattro tipi base di dato

//in C e C++

int main() {

//definizione senza inizializzazione 

  char protein;

  int carbohydrates;

  float fiber;

  double fat;

 //definizione ed inizializzazione simultanea

  char pizza = 'A', pop = 'Z';

  int dongdings = 100, twinkles = 150,

    heehos = 200;

  float chocolate = 3.14159;

//Notazione esponenziale:

  double fudge_ripple = 6e-4;

} ///:~

La prima parte del programma definisce variabili dei quattro tipi di dato base senza inizializzarli. Se non si inizializza una variabile, lo standard dice che essi sono indefiniti ( di solito, significa che contiene spazzatura). La seconda parte del programma definisce e inizializza variabili allo stesso tempo ( è sempre meglio, se possibile, procurare un valore di inizializzazione al punto di definizione). Si noti che luso della notazione esponenziale è nella costante 6e-4.

bool, true, & false

Prima che bool diventasse parte dello standard C++, ognuno tendeva ad usare differenti tecniche per simulare un Booleano. Si ottenevano problemi di portabilità  e potevano introdurre subdoli errori.

Il tipo bool dello standard C++ può avere due stati espressi dalle costanti true ( che è convertita all'intero 1) e quella false (che è convertita all'intero zero). Tutti e tre i nomi sono parole chiave. In più, alcuni elementi del linguaggio sono stati adattati.

 

Elemento

Uso con bool

&& || !

prende argomenti bool  e produce risultati bool.

< > <= >= == !=

produce risultati bool.

if, for,  while, do

Espressione convezionale  che converte ai valori bool.

? :

Primo operando converte al  valore bool.

Poichè ci sono molti codici esistenti che usano un int per rappresentare un flag, il compilatore implicitamente convertirà un int in un bool ( il valore non zero produrrà true mentre quello zero produrrà false). Idealmente, il compilatore ci darà un allarme come suggerimento per correggere la situazione.

Un idioma che è classificato come: stile povero di programmazione è luso del ++ per settare la bandiera a true. Ciò è ancora permesso, ma deprecato, cioè in futuro potrebbe non esserlo. Il problema è che se si sta facendo una conversione di tipo implicita da bool a int, incrementando il valore (forse oltre la portata del valori normali di bool zero e uno) , e dopo implicitamente lo si riconverte ancora.

I puntatori (i quali verranno introdotti in seguito in questo capitolo) saranno automaticamente convertiti in bool quando sarà necessario.

Specificatori

Gli specificatori modificano i significati dei tipi basi incorporati e li espandono ad una serie abbastanza più larga. Ci sono quattro specificatori: long, short, signed, e unsigned.

Long e short modificano i valori di massimo e minimo che il tipo di dato gestirà. Un semplice int deve essere come minimo la grandezza di uno short. La gerarchia della grandezza per un tipo intero è: short int, int, long int. Tutte le grandezze potrebbero essere le stesse, sempre che soddisfano le richieste dei valori di minimo e  massimo. In una macchina con una word di 64-bit, per esempio, tutti i tipi di dato potrebbero essere a 64 bit.

La gerarchia di grandezza per i numeri virgola-mobile è : float, double, e long double. long float non è un tipo legale. Non ci sono numeri in virgola mobile short.

Gli specificatori signed e unsigned dicono al compilatore come usare il bit segno con i tipi interi ed i caratteri (i numeri in virgola mobile contengono un segno) un numero unsigned non tiene conto del segno e dunque ha un bit extra disponibile, quindi può salvare numeri positivi grandi due volte i numeri positivi che possono essere salvati in un numero signed. signed è per default ed è solo necessario con char; char può essere o non essere per default signed. Specificando signed char, si utilizza il bit di segno.

Il seguente esempio mostra come la grandezza del tipo di dato in bytes usando loperatore sizeof, introdotto più avanti in questo capitolo:

//: C03:Specify.cpp

// Dimostra l’uso dei specificatori

#include <iostream>

using namespace std;

int main() {

  char c;

  unsigned char cu;

  int i;

  unsigned int iu;

  short int is;

 short iis; // come uno short int

  unsigned short int isu;

  unsigned short iisu;

  long int il;

  long iil;  // come long int

  unsigned long int ilu;

  unsigned long iilu;

  float f;

  double d;

  long double ld;

  cout

    << "\n char= " << sizeof(c)

    << "\n unsigned char = " << sizeof(cu)

    << "\n int = " << sizeof(i)

    << "\n unsigned int = " << sizeof(iu)

    << "\n short = " << sizeof(is)

    << "\n unsigned short = " << sizeof(isu)

    << "\n long = " << sizeof(il)

    << "\n unsigned long = " << sizeof(ilu)

    << "\n float = " << sizeof(f)

    << "\n double = " << sizeof(d)

    << "\n long double = " << sizeof(ld)

    << endl;

} ///:~

il risultato che si ottiene facendo girare il programma probabilmente sarà differente da macchina/sistema operativo/ compilatore, poichè ( come menzionato precedentemente) lunica cosa che deve essere consistente è che ogni differente tipo memorizza i valori minimi e massimi specificati nello standard.

Quando si modifica un int con short o long, la parola chiave int è facoltativa, come mostrato sopra.

Introduzione ai puntatori

Ogni volta che si fa girare un programma, viene prima caricato ( tipicamente dal disco) nella memoria del computer. Quindi, tutti gli elementi del nostro programma sono allocati da qualche parte nella memoria. La memoria è tipicamente disposta come una serie sequenziale di locazioni di memoria; ci riferiamo usualmente a queste locazioni come bytes di 8 bit ma realmente la grandezza di ogni spazio dipende dallarchitettura della particolare macchina e di solito è chiamata grandezza della word della macchina. Ogni spazio può essere unicamente distinto dagli altri spazi dal proprio indirizzo. Per i propositi di questa discussione, si dirà solamente che tutte queste macchine usano byte che hanno indirizzi sequenziali che cominciano da zero e proseguono in su fino alla fine della memoria che si ha nel proprio computer.        

Poichè il proprio programma vive in memoria mentre sta è eseguito, ogni elemento del programma ha un indirizzo. Supponiamo di iniziare con un semplice programma:

 

//: C03:YourPets1.cpp

#include <iostream>

using namespace std;

int cane, gatto, uccello, peshe;

void f(int animale) {

  cout << "numero id animale: " << animale<< endl;

}

int main() {

  int i, j, k;

} ///:~

Ciascuno degli elementi in questo programma ha una locazione in memoria quando il programma viene eseguito. Anche la funzione occupa memoria. Come si vedrà, larea di memoria dove lelemento è posto dipende dal modo in cui si definisce esso da e quale elemento è.

Cè un operatore in C e C++ che ci dirà lindirizzo di un elemento. Questo è loperatore &. Tutto quello che si fa è precedere il nome dellidentificatore con & ed esso produrrà lindirizzo di quell' identificatore. YourPets1.cpp può essere modificato per scrivere gli indirizzi di tutti questi elementi:

 

//: C03:YourPets2.cpp

#include <iostream>

using namespace std;

int cane, gatto, uccello, pesce;

void f(int animale) {

  cout << "numero id animale: " << animale<< endl;

}

int main() {

  int i, j, k;

  cout << "f(): " << (long)&f << endl;

  cout << "cane: " << (long)&cane<< endl;

  cout << "gatto: " << (long)&gatto<< endl;

  cout << "uccello: " << (long)&uccello<< endl;

  cout << "pesce: " << (long)&pesce<< endl;

  cout << "i: " << (long)&i << endl;

  cout << "j: " << (long)&j << endl;

  cout << "k: " << (long)&k << endl;

} ///:~

(long) è un cast. Significa: Non trattare questo come se fosse un tipo normale, invece trattalo come un long. Il cast non è essenziale, ma se non vi fosse, gli indirizzi sarebbero scritti  invece in esadecimale, così assegnare un long rende le cose un po più leggibili.

Il risultato di questo programma varierà dipendendo al tuo computer, OS, e tutte le sorti di altri fattori, ma darà sempre alcuni interessanti comprensioni. Per un singolo eseguibile sul nostro computer, il risultato assomiglia a questo:

The results of this program will vary depending on your computer, OS, and all sorts of other factors, but it will always give you some interesting insights. For a single run on my computer, the results looked like this:

f(): 4198736

dog: 4323632

cat: 4323636

bird: 4323640

fish: 4323644

i: 6684160

j: 6684156

k: 6684152

si può vedere come le variabili che sono definite dentro il  main( ) sono in differenti aree delle variabili definite fuori dal main( ), si capirà il perchè qunado si imparerà di più il linguaggio. f() appare nella sua area; il codice è tipicamente separato dal dato in memoria.

Unaltra cosa interessante da notare è che le variabili definite una dopo laltra sono poste in memoria in modo contiguo. Esse sono separate da un numero di byte che sono richiesti dai loro tipi di dato. Qui, lunico tipo di dato usato è int, e cat è posto 4 byte da dog, e bird 4 byte da cat, ecc. Così apparirebbe che, in questa macchina, un int è lungo 4 byte.

Unaltra come questo interessante esperimento mostrante come la memoria è mappato, cosa si può fare con un indirizzo? La cosa principale importante da fare è salvarlo dentro unaltra variabile per un uso in seguito. C e C++ hanno un tipo speciale di variabile che detiene un indirizzo. Questa variabile è chiamata puntatore.

Other than this interesting experiment showing how memory is mapped out, what can you do with an address? The most important thing you can do is store it inside another variable for later use. C and C++ have a special type of variable that holds an address. This variable is called a pointer.

Loperatore che definisce un puntatore è lo stesso usato per la moltiplicazione *. Il compilatore sa che non è una moltiplicazione dal contesto in cui è usato, come si vedrà.

Quando si definisce un puntatore, si deve specificare il tipo di variabile a cui punta. Si inizia dando un nome di un tipo, dopo invece di immediatamente dare un identificatore per la variabile, si dice aspetta, è un puntatore inserendo una stella tra il tipo e lidentificatore. Così il puntatore verso un int somiglia come questo:

When you define a pointer, you must specify the type of variable it points to. You start out by giving the type name, then instead of immediately giving an identifier for the variable, you say Wait, its a pointer by inserting a star between the type and the identifier. So a pointer to an int looks like this:

int* ip; // ip punta ad una variabile int

int* ip; // ip points to an int variable

Lassociazione di * con un tipo appare sensata a si legge facilmente, ma può realmente essere un po ingannevole. La nostra inclinazione potrebbe essere dire puntatore int come se fosse un singolo discreto tipo. Comunque, con un int o un altro tipo data base, è possibile dire:

The association of the * with the type looks sensible and reads easily, but it can actually be a bit deceiving. Your inclination might be to say intpointer as if it is a single discrete type. However, with an int or other basic data type, its possible to say:

int a, b, c;

mentre con un puntatore, ci piacerebbe dire:

whereas with a pointer, youd like to say:

int* ipa, ipb, ipc;

la sintassi del C ( e in eredità, quella del C++) non permette espressioni così sensate. Nella definizione sopracitata, solo ipa è un puntatore, ma ipb e ipc sono ordinari int( si può dire che * lega più saldamente all identificatore). Conseguentemente, il miglior risultato può essere realizzato usando solo una definizione per riga; ancora si ottiene una sintassi sensata senza confusione:

C syntax (and by inheritance, C++ syntax) does not allow such sensible expressions. In the definitions above, only ipa is a pointer, but ipb and ipc are ordinary ints (you can say that * binds more tightly to the identifier). Consequently, the best results can be achieved by using only one definition per line; you still get the sensible syntax without the confusion:

int* ipa;

int* ipb;

int* ipc;

poichè una linea direttiva per la programmazione in C++ è che si dovrebbe sempre inizializzare una variabile al punto di definizione, questa forma effettivamente funziona meglio. Ad esempio, le variabili soprammenzionate non sono inizializzate ad alcun particolar valore; contengono spazzatura. E molto meglio dire qualcosa come:

Since a general guideline for C++ programming is that you should always initialize a variable at the point of definition, this form actually works better. For example, the variables above are not initialized to any particular value; they hold garbage. Its much better to say something like:

int a = 47;

int* ipa = &a;

ora entrambi a e ipa sono stati inizializzati, e ipa detiene lindirizzo di a.

Una volta che si ha inizializzato un puntatore, la cosa principale che si può fare è usarlo per modificare il valore a cui punta. Accedere ad una variabile attraverso un puntatore, si deferenzia il puntatore usando lo stesso operatore per definirlo, come questo:

Once you have an initialized pointer, the most basic thing you can do with it is to use it to modify the value it points to. To access a variable through a pointer, you dereference the pointer using the same operator that you used to define it, like this:

*ipa = 100;

ora a contiene il valore 100 invece di 47.

Queste sono le basi per i puntatori: si può detenere un indirizzo, e lo si  può usare per modificare la variabile originale. Ma la domanda rimane ancora: perché non voler modificare una variabile usando unaltra variabile come una delega?

These are the basics of pointers: you can hold an address, and you can use that address to modify the original variable. But the question still remains: why do you want to modify one variable using another variable as a proxy?

Per questo introduttorio punto di vista, possiamo mettere la risposta in due larghe categorie:

1.   cambiare gli oggetti di fuori dal di dentro di una funzione. Questo è forse la uso base principale dei puntatori, e sarà esaminato qui.

2.   per realizzare molti altri intelligenti tecniche di programmazione, le quali ci impareremo un po alla volta nel resto del libro.

For this introductory view of pointers, we can put the answer into two broad categories:

1.   To change outside objects from within a function. This is perhaps the most basic use of pointers, and it will be examined here.

2.   To achieve many other clever programming techniques, which youll learn about in portions of the rest of the book.

Modificare l’oggetto esterno

Normalmente, quando si passa un argomento ad una funzione, una copia di quel argomento viene fatta dentro la funzione. Questo è detto pass-by-value (passaggio per valore). Si può vedere leffetto del passaggio per valore nel seguente programma:

//: C03:PassByValue.cpp

#include <iostream>

using namespace std;

void f(int a) {

  cout << "a = " << a << endl;

  a = 5;

  cout << "a = " << a << endl;

}

int main() {

  int x = 47;

  cout << "x = " << x << endl;

  f(x);

  cout << "x = " << x << endl;

} ///:~

In f(), a è una variabile locale, quindi esiste solo per la durata della chiamata di funzione a f(). Poiché esso è un argomento di funzione , il valore di a è inizializzato dagli argomenti che sono passati quando la funzione è chiamata, in main() largomento è x, il quale ha un valore di 47, quindi questo valore è copiato in a quando f() è chiamata.

 

Quando si esegue questo programma si vedrà:

x = 47

a = 47

a = 5

x = 47

Inizialmente, naturalmente, x vale 47. Quando f() viene chiamata, viene creato spazio temporaneo per tenere la variabile a per la durata della chiamata alla funzione ed a è inizializzata copiando il valore di x, che si verifica stampandola. Naturalmente, si può cambiare il valore di a e mostrare che questa è cambiata. Ma quando f() è termina, lo spazio temporaneo che è stato creato per a scompare e vediamo che lunica connessione esistita tra a e x è avvienuta quando il valore di x è stato copiato in a.

Quando si è dentro f(), x è un outside object ( oggetto esterno, una mia termonologia ), e cambiare la variabile locale non influena loggetto esterno, naturalmente, poiché essi sono due separate locazioni di memoria. Ma cosa accade se si vuole modificare loggetto esterno? Ecco dove i puntatori tornano utili. In un certo senso, un puntatore è uno pseudonimo per unaltra variabile. Quindi se noi passiamo un puntatore in una funzione invece di un ordinario valore, stiamo realmente passando uno pseudonimo delloggetto esterno, permettendo alla funzione di modificare loggetto esterno:

//: C03:PassAddress.cpp

#include <iostream>

using namespace std;

void f(int* p) {

  cout << "p = " << p << endl;

  cout << "*p = " << *p << endl;

  *p = 5;

  cout << "p = " << p << endl;

}

int main() {

  int x = 47;

  cout << "x = " << x << endl;

  cout << "&x = " << &x << endl;

  f(&x);

  cout << "x = " << x << endl;

} ///:~

ora f() prende un puntatore come argomento e lo dereferenzia durante lassegnamento e ciò causa la modifica delloggetto esterno x.Il risultato è:

x = 47

&x = 0065FE00

p = 0065FE00

*p = 47

p = 0065FE00

x = 5

si noti che il valore contenuto in p è lo stesso dellindirizzo di x il puntatore p punta veramente a x. Se ciò non è convincente abbastanza, quando p è dereferenziato per assegnare il valore 5, si vede che il valore di x è ora pure cambiato a 5.

Quindi, passare un puntatore in una funzione permetterà alla funzione di modificare loggeto esterno. In seguito si farà un grande uso dei puntatori, ma questo è l'uso più comune e semplice.

Introduzione ai riferimenti in C++

Approssimativamente i puntatori funzionano allo stesso modo in C e in C++, ma il C++ ha un modo aggiuntivo per passare un indirizzo in una funzione. Ciò è il passaggio per riferimento ed esiste in vari altri linguaggi di programmazione quindi non è una invenzione del C++.

Come prima impressione sui riferimenti si può pensare che non sono necessari e che si potrebbe scrivere tutti i propri programmi senza i riferimenti. In generale, ciò è vero, con leccezione di pochi importanti posti in cui ne apprenderemo in seguito nel libro. Si apprenderà anche di più sui riferimenti in seguito, ma lidea base è la stessa della dimostrazione delluso dei puntatori sopracitato: si può passare lindirizzo un argomento usando un riferimento. La differenza tra i puntatori e i riferimenti è che chiamare una funzione che accetta i riferimenti è più pulito, sintatticamente, che chiamare una funzione che accetta puntator i( ed è questa differenza sintattica che rende essenziali i riferimenti in certe situazioni). Se PassAddress.cpp è modificato per usare i riferimenti, si può vedere la differenza nella chiamata alla funzione nel main( ):

//: C03:PassReference.cpp

#include <iostream>

using namespace std;

void f(int& r) {

  cout << "r = " << r << endl;

  cout << "&r = " << &r << endl;

  r = 5;

  cout << "r = " << r << endl;

}

int main() {

  int x = 47;

  cout << "x = " << x << endl;

  cout << "&x = " << &x << endl;

 f(x); // Assomiglia ad un passaggio di valore

            // è in realtà passata per riferimento

  cout << "x = " << x << endl;

} ///:~

nella lista di argomenti lista di f(), invece di dire int* per passare un puntatore, si dice int&  per passare un riferimento. Dentro f(), se di dice solamente r( il quale produrrebbe lindirizzo se r fosse un puntatore) si ottiene il valore della variabile a cui r si riferisce. Se si assegna r, si assegna realmente la variabile a cui r si riferisce. Infatti,  lunico modo per ottenere lindirizzo che è stato tenuto in r è con loperatore &.        

In main( ), si può vedere leffetto chiave  del riferimento nella sintassi della chiamata a f(), la quale è proprio f(x). Anche se assomiglia ad un ordinario passaggio di valore, leffetto del riferimento è che realmente prende lindirizzo e lo passa dentro, piuttosto che farne una copia del valore. Loutput è :

x = 47

&x = 0065FE00

r = 47

&r = 0065FE00

r = 5

x = 5

Quindi si può vedere che il passaggio per riferimento permette ad una funzione di modificare loggetto esterno, proprio come si fa passando un puntatore ( si può  anche osservare che il riferimento oscura il fatto che un indirizzo sta per essere passato; ciò sarà esaminato in seguito nel libro ). Dunque, per questa semplice introduzione si può ritenere che i riferimenti sono soltanto un modo differente sintatticamente ( qualche volta detto zucchero sintattico ) per ottenere la stessa cosa che il puntatore fa: permette alle funzioni di cambiare gli oggetti esterni.

I puntatori ed  i riferimenti come modificatori

Si è visto che i tipi data base char, int, float, e double, insieme agli specificatori  signed, unsigned, short, e long, i quali possono essere usati con i tipi data base quasi in ogni combinazione. Ora si sono aggiunti i puntatori ed i riferimenti che sono ortogonali ai tipi data base e agli specificatori, quindi le possibili combinazioni si sono triplicate:

//: C03:AllDefinitions.cpp

// Tutte le possibili combinazioni dei tipi di data base,

//specificatori, puntatori e riferimenti

#include <iostream>

using namespace std;

void f1(char c, int i, float f, double d);

void f2(short int si, long int li, long double ld);

void f3(unsigned char uc, unsigned int ui,

  unsigned short int usi, unsigned long int uli);

void f4(char* cp, int* ip, float* fp, double* dp);

void f5(short int* sip, long int* lip,

  long double* ldp);

void f6(unsigned char* ucp, unsigned int* uip,

  unsigned short int* usip,

  unsigned long int* ulip);

void f7(char& cr, int& ir, float& fr, double& dr);

void f8(short int& sir, long int& lir,

  long double& ldr);

void f9(unsigned char& ucr, unsigned int& uir,

  unsigned short int& usir,

  unsigned long int& ulir);

int main() {} ///:~

I puntatori e i riferimenti funzionano anche quando si passano oggetti dentro e fuori le funzioni; si apprenderà ciò in seguito nell’ ultimo capitolo.

Cè solo un altro tipo che funziona con i puntatori: void. Se si dichiara che il puntatore è un void*, significa che qualsiasi tipo di indirizzo può essere assegnato al puntatore ( laddove se si ha un int*, si può assegnare solo lindirizzo di una variabile int a quel puntatore). Ad esempio:

//: C03:VoidPointer.cpp

int main() {

  void* vp;

  char c;

  int i;

  float f;

  double d;

 //L’indirizzo di QUALSIASI tipo può essere 

//assegnato ad un puntatore void

  vp = &c;

  vp = &i;

  vp = &f;

  vp = &d;

} ///:~

ogni volta che si assegna ad un void*, si perde ogni informazione sul tipo. Ciò significa che prima che si usa il puntatore,  lo si può castare nel tipo corretto:

 

//: C03:CastFromVoidPointer.cpp

int main() {

  int i = 99;

  void* vp = &i;

// Non si può dereferenziare un puntatore void:

  // *vp = 3; // errore a Compile-time

  // Si deve fare un cast ad un int prima di dereferenziare:

  *((int*)vp) = 3;

} ///:~

il tipo (int*)vp prende il void*e dice al compilatore di trattarlo come un int*, così può essere dereferenziato con successo. Si può osservare che questa sintassi è brutta, ed è, ma il peggio è che il void* introduce un buco nel sistema del tipo di linguaggio. Cioè, permette, o anche  incoraggia, il trattamento di un tipo come un altro tipo. Nellesempio di sopra, si è trattato un int come un int assegnando vp ad un int*, ma non cè nulla che dice che noi non possiamo assegnarlo ad un char* o double*,che modificherebbe un differente ammontare di memoria che è stata allocata per lint, possibilmente interrompendo un programma. In generale, i puntatori void dovrebbero essere evitati, e usati solo in rari e speciali casi, i quali saremo pronti a considere significativamente in seguito  nel libro.

Non si può avere un riferimento void, per ragioni che saranno spiegate nel capitolo 11.

Scoping ( Visibilità )

Le regole di scope ci dicono dove una variabile è valida, dove viene creata e dove viene distrutta( es. va fuori dallo scope). Lo scope della variabile si estende dal punto dove è definita fino alla prima parentesi chiusa che confronta la più vicina parentesi aperta prima che la variabile fu definita. Cioè, uno scope è definito dalla propria serie di parentesi più vicine. Per illustrarle:

//: C03:Scope.cpp

// quante variabili sono esaminate

// How variables are scoped

int main() {

  int scp1;

// scp1 visibile qui

 

  {

    // scp1 visibile qui ancora

    //.....

    int scp2;

   // scp2 visibile qui

    //.....

    {

      // scp1 & scp2 visibili ancora qui

      //..

      int scp3;

      //scp1 ,scp2 & scp3 visibili qui

      // ...

    }

// <-- scp3 distrutto qui

// scp3 non disponibile qui

// scp1 & scp2 visibili ancora qui

// ...

} // <-- scp2 distrutto qui

// spc2 & scp3 non disponibili qui

// ..

} // <-- scp1 distrutto qui

///:~

 

lesempio di sopra mostra quando le variabili sono visibili  e quando non sono disponibili (ciòè, quando vanno fuori dallo scope). Una variabile può essere usata solo quando è dentro il proprio scope. Gli scope possono essere nidificati. Nidificare significa che si può accedere ad una variabile in uno scope che  racchiude lo scope in cui si è. Nellesempio di sopra, la variabile scp1 è disponibile dentro tutti gli altri scope, mentre scp3 è disponibile solo  nello scope più interno.

Definire le variabili al volo

Come visto poco fa in questo capitolo, cè una significativa differenza tra C e C++ quando si definiscono le variabili. Entrambi i linguaggi richiedono che le variabili siano definite prima di essere usate, ma il C ( e molti altri linguaggi tradizionali procedurali) obbligano a definire tutte le variabili allinizio dello scope, cosicchè quando il compilatore crea un blocco può allocare spazio per queste variabili.

Mentre si legge il codice C, un blocco di definizioni di variabili è di solito la prima cosa da vedere quando si entra in uno scope. Dichiarare tutte le variabili allinizio del blocco richiede che il programmatore scriva in un particolare modo a causa del compimento di dettagli del linguaggio. La maggior parte della gente non sa che tutte le variabili sono state usate prima  che loro scrivono il codice, quindi devono cominciare a saltare indietro allinizio del blocco per inserire nuove variabili, che è scomodo e causa errori. Queste definizioni di variabili  di solito non significano molto per il lettore e realmente tendono a confondere perché appaiono fuori dal contesto nelle quali sono usati.

 

C++ (non il  C) ci permette di definire variabili da qualsiasi pari in uno scope, quindi si può definire una variabile giusto prima di usarla.In più, si può inizializzarla una variabile al punto in cui la si definisce, che impedisce di compiere varie classi di errori. Definire variabili in questo modo rende il codice più facile da scrivere e riduce gli errori che si ottengono dal dover saltare avanti e dietro in uno scope. Rende il codice più facile da capire perché si vede una variabile definita nel contesto del proprio uso. Questo è molto importante quando si sta definendo e inizializzando una variabile allo stesso tempo si può vedere il significato dellinizializzazione del valore dal modo in cui la variabile è usata.

Si possono anche definire variabili dentro le espressioni di controllo di cicli for e while, dentro la condizione di una istruzione if, e dentro l' istruzione di selezione di uno switch. Ecco qui un esempio mostrante le definizioni di variabili al volo:

  

//: C03:OnTheFly.cpp

// definizione di variabili al volo

#include <iostream>

using namespace std;

int main() {

  //..

  {

// Inizia un nuovo scope

    int q = 0; // il C richiede una definizione qui

  

    //..

// definizione nel punto di uso:

    for ( int i = 0; i < 100; i++) {

     q++; // q proviene da un grande scope

  // definizione alla fine dello scope:

     

      int p = 12;

    }

int p = 1;   // un p differente

  } // fine scope contenente q & p

   

  cout << "Digita caratteri:" << endl;

  while ( char c = cin.get() != 'q') {

    cout << c << " non era lui!" << endl;

    if ( char x = c == 'a' || c == 'b')

      cout << "hai digitato a oppure b" << endl;

    else

      cout << "hai digitato" << x << endl;

  }

  cout << "Digita A, B, oppure C" << endl;

  switch ( int i = cin.get()) {

    case 'A': cout << "Snap" << endl; break ;

    case 'B': cout << "Crackle" << endl; break ;

    case 'C': cout << "Pop" << endl; break ;

    default : cout << "Non A, B o C!" << endl;

  }

} ///:~

nello scopo più interno, p è definito proprio prima che lo scope finisca, quindi è realmente un gesto inutile ( ma mostra che si puo' definire una variabile da qualche parte). Il p nello scope esterno è nella stessa situazione.

La definizione di i nell' espressione di controllo del ciclo for è un esempio di come si possa definire una variabile esattamente nel punto in cui si ha bisogno ( lo si puo' fare solo col C++). Lo scope di i è lo scope dell' espressione controllata del ciclo for , quindi si puo' girare intorno e riusare i per il prossimo ciclo for . Questo idioma è semplice e comunemente usato in C++; i è il classico nome per un contatore di ciclo e non si devono inventarne di nuovi.

 

Benchè l' esempio mostra anche variabili definite senza le dichiarazioni while , if, e switch, questo tipo di definizione è abbastanza meno comune di queste nelle espressioni for , forse perchè la sintassi è molto vincolata. Ad esempio, non si possono usare parentesi. Cioè, non si puo' dire:

while (( char c = cin.get()) != 'q')

L' aggiunta di parentesi extra sembrerebbe una cosa innocente e utile da fare, e poichè non si puo' usarli, il risultato non è quello che potrebbe piacere. Il problema si presenta perchè' !=' ha una precedenza maggiore rispetto a' =' , quindi char c finisce contenendo un bool convertito in char . Quando è stampato, su molti terminali si vedrà  una faccia sorridente come carattere.

In generale, si puo' considerare l' abilità  del definire variabili senza le dichiarazioni while , if, e switch per completezza, ma l' unico posto dove si userà  questo tipo di definizione di variabile è dentro un ciclo for ( dove lo si userà  abbastanza spesso).

Specificare un' allocazione di memoria

Quando si crea una variabile, si hanno diversi modi per specificare il tempo di vita di una variabile, come la memoria viene allocata per quella variabile e come la variabile è trattata dal compilatore.

Le variabili globali

Le variabili globali sono definite fuori da tutti i corpi   delle funzioni e sono disponibili in tutte le parti del programma ( anche codificati in altri file). Le variabili globali sono semplici da esaminare è sempre disponibili ( es. il tempo di vita di una variabile globale finisce quando il programma termina). Se l' esistenza della variabile globale in un file è dichiarata usando la parola chiave extern in un altro file, il dato è disponibile per l' uso dal secondo file. Ecco qui un esempio di uso di variabili globali:

//: C03:Global.cpp

//{L} Global2

// dimostrazioni delle variabili globali

#include <iostream>

using namespace std;

int globale;

void func();

int main() {

  globale = 12;

  cout << globale << endl;

 

func(); // Modifica globale

 

  cout << globale << endl;

} ///:~

ecco qui un file che accede a globale come un extern :

//: C03:Global2.cpp {O}

// accesso alle variabili globali esterne

extern int globale;  

// ( il linker risolve il riferimento )

void func() {

  globale = 47;

} ///:~

la memoria per la variabile globale è creata dalla definizione di Global.cpp e quella stessa variabile è acceduta dal codice in Global2.cpp. Poichè il codice in Global2.cpp è compilato separatamente dal codice nel Global.cpp, il compilatore deve essere informato che la variabile esiste altrove dalla dichiarazione.

extern int globale;

quando si esegue il programma, si vedrà  che la chiamata a func() veramente influisce sulla singola globale istanza di globale .

In Global.cpp si puo' vedere uno speciale commento ( i quali sono un mio progetto):

//{L} Global2

cio' significa che per creare il programma finale, il file oggetto col nome Global2 deve essere linkato ( non c' è nessuna estenzione perchè i nomi di estenzioni dei file oggetto differiscono da un sistema all' altro). In Global2.cpp la prima riga ha un altro speciale commento in una serie di caratteri {O}, il quale dice: " non provare a creare un eseguibile da questo file, esso va compilato in modo che possa essere linkato a qualche altro eseguibile". Il programma ExtractCode.cpp nel Volume 2 di questo libro ( scaricabile dal sito www.BruceEckel.com ) legge questa serie di caratteri e crea l'appropriato makefile e ogni cosa è compilata correttamente ( si apprenderanno i makefile alla fine di questo capitolo).

Variabili locali

Le variabili locali vivono dentro uno scope, sono " locali " alla funzione. Esse spesso sono chiamate variabili automatiche perchè vengono automaticamente create quando si entra nello scope e automaticamente muiono quando lo scope finisce. La parola chiave auto la rende esplicita, ma   le variabili locali   per default sono' auto e quindi non è mai necessario dichiarare qualcosa come un auto .

Variabili register

Una variabile register è un tipo di variabile locale. La parola chiave register dice al compilatore: " rendi l'accesso a questa variabile il più veloce possibile " . Incrementare   la velocità  di accesso è un compito che dipende dall'implementazione, ma come, il nome suggerisce, è spesso fatto piazzando la variabile in un registro. Non c' è garanzia che la variabile sarà  posta in un registro o anche che la velocità  di accesso sarà  incrementata. Esso è un suggerimento al compilatore.

Ci sono delle restrizioni per l' uso delle variabili register . Non si puo' prendere o calcolare l' indirizzo della variabile register . Una variabile register puo' essere dichiarata solo dentro un blocco ( non si hanno variabili globali o static register ). Si puo', comunque, usare un register come un argomento formale in una funzione ( es. nella lista degli argomenti ).

In generale, le ottimizzazioni del compilatore saranno migliori delle nostre, quindi è meglio evitare la parola chiave register .

static

La parola chiave static ha diversi significati distinti. Normalmente, le variabili definite locali nella funzione scompaiono alla fine dello scope della funzione. Quando si chiama una funzione di novo, la memoria per le variabili è creata di nuovo e i valori sono rinizializzati. Se si vuole che un valore sia esistente in tutta la vita del programma, si puo' definire un variabile locale della funzione per essere static e darle un valore iniziale. L' inizializzazione è prodotta solo la prima volta che la funzione è chiamata ed il dato conserva il proprio valore tra le chiamate alla funzione. In questo modo, una funzione puo' " ricordarsi " alcuni pezzi di informazioni tra le chiamate alla funzione.

Si puo' restare forse   meravigliati del perchè una variabile globale invece non è usata. La bellezza di una variabile static è che non è disponibile fuori lo scope della funzione, quindi non puo' essere inavvertitamente cambiata. Essa localizza l' errore.

Ecco qui un esempio di uso di variabili static :

//: C03:Static.cpp

// usare una variabile statica in una funzione

#include <iostream>

using namespace std;

void func() {

  static int i = 0;

  cout << "i = " << ++i << endl;

}

int main() {

  for ( int x = 0; x < 10; x++)

    func();

} ///:~

ogni volta che func() vienechiamata nel ciclo for , esso stampa un valore differente. Se la parola chiave static non è usata, il valore stampato sarà  sempre ' 1' .

Il secondo significato di static è riferito al primo nel senso " non disponibile fuori un certo scope " . Quando static è applicato ad un nome di funzione o ad una variabile che è fuori da tutte le funzioni, significa " questo nome non è disponibile fuori da questo file. " Il nome di funzione o la variabile è locale al file, si dice che ha un file scope. Come dimostrazione, compilare e collegare i seguenti due file causerà  un errore di collegamento:

//: C03:FileStatic.cpp

// dimostrazione dello scope del file. Compilando e

// linkando questo file con FileStatic2.cpp

// si avrà  un errore dal linker

// Scope di file significa che è solo disponibile in questo file:

static int fs;

int main() {

  fs = 1;

} ///:~

anche se la variabile fs esiste come un extern nel seguente file, il linker non la troverà  perchè è stata dichiarata   static in FileStatic.cpp .

//: C03:FileStatic2.cpp {O}

provando a riferisi fs

// Trying to reference fs

extern int fs;

void func() {

  fs = 100;

} ///:~

un specificatore static puo' essere usato anche dentro una class . Questa spiegazione sarà  rimandata fino a quando non si imparerà  a creare classi, più avanti nel libro.

extern

la parola chiave extern è già  stata descritta e dimostrata in breve. Essa dice al compilatore che una variabile o una funzione esiste, anche se il compilatore non l' ha ancora vista nel file corrente che sta per essere compilato. Questa variabile o funzione puo' essere definita in un altro file o più avanti nel file corrente. Come esempio dell' ultimo:

//: C03:Forward.cpp

// Forward function & dichiarazione di dati

// Forward function & data declarations

#include <iostream>

using namespace std;

// questo non è realmente esterno, ma bisogna dire al compilatore

//che esiste da qualche parte:

extern int i;

extern void func();

int main() {

  i = 0;

  func();

}

int i; // definizione di dato

void func() {

  i++;

  cout << i;

} ///:~

quando il compilatore incontra la dichiarazione  ' extern int i' , sa che la definizione per i deve esistere da qualche parte come una variabile locale. Quando il compilatore raggiunge la definizione di i , nessun altra definizione è visibile, quindi esso sa che ha trovato la stessa i dichiarata poco fa nel file. Se si avesse definito i come static , si sarebbe detto al compilatore che la i   è definita globalmente ( passando per extern ), ma che ha anche un file scope( passando per static ), quindi il compilatore genererà  un errore.

Linkage

Per capire il comportamento dei programmi in C e C++, si ha bisogno di conoscere il linkage (il concatenamento ). In un programma eseguibile, un identificatore è rappresentato dallo spazio in memoria che detiene una variabile o un corpo di funzione compilato. Il collegamento descrive questa memoria come se fosse vista dal linker. Ci sono due tipi di collegamento: internal linkage e external linkage.

Concatenamento interno ( internal linkage )   significa che la memoria   è creata per rappresentare l' identificatore solo per il file che sta per essere compilato, gli altri file possono usare gli stessi nomi di identificatori con il concatenamento interno, o per la variabile globale, e nessun controversia sarà  trovata dal linker, viene creata memoria per ogni identificatore. Il concatenamento interno è specificato dalla parola chiave static in C e C++.

concatenamento esterno ( external linkage) significa che un singolo pezzo di memoria è creata per rappresentare l' identificatore per tutti i file che stanno per essere compilati. La memoria è creata una volta ed il linker deve risolvere tutti gli altri riferimenti a quella memoria. Le variabili globali ed i nomi di funzione hanno un collegamento esterno. Queste sono accessibili dagli altri file dichiarandole con la parola chiave extern . Le variabili definite fuori da tutte le funzioni ( con l' eccezione di const in C++) e le definizioni di funzione sono per default concatenamento esterno. Si puo' specificamente obbligarli ad avere un concatenamento interno usando la parola chiave static . Si puo' esplicitamente   dichiarare che un identificatore ha un concatenamento esterno definendolo con la parola chiave extern. Definire una variabile o una funzione con extern   non è necessario in C, ma lo è qualche volta per const in C++.

Le variabili automatiche (locali) esistono solo temporaneamente, nello stack, mentre una funzione sta per essere chiamata. Il linker non conosce le variabili automatiche e quindi queste non hanno linkage .

Le costanti

Nel vecchio   (pre-Standard)   C, se si voleva una costante, si doveva usare il preprocessore:

#define PI 3.14159

dovunque si usava PI , il valore 3.14159 veniva sostituito dal preprocessore( si puo' ancora usare questo metodo in C ed in C++).

quando si usa il preprocessore per creare costanti, si piazza il controllo di queste costanti, fuori dalla portata del compilatore. Nessun controllo del tipo è prodotto   sul nome di PI e non si puo' prendere l' indirizzo di PI (così non si puo' passare il puntatore o un riferimento a PI ). PI non puo' essere una variabile di   un tipo definito dall' utente. Il significato di PI dura   dal punto in cui è definito fino alla fine del file; il preprocessore non riconosce lo scoping.

Il C++ introduce il concetto di costanti con nome che sono proprio come le variabili, eccetto che il loro valore non puo' essere cambiato. Il modificatore const dice al compilatore che il nome rappresenta una costante. Ogni tipo di dato, predefinito o definito dall' utente, puo' essere definito come const . Se si definisce qualcosa come const e dopo si cerca di modificarlo, il compilatore genererà  un errore.

Si deve specificare il tipo di const :

const int x = 10;

nello standard C e C++, si puo' usare una costante con nome in un lista di argomenti, anche se l' argomento è un puntatore o un riferimento (es. si puo' prendere l' indirizzo di un const ) un const ha uno scope, proprio come una variabile locale, quindi si puo' " nascondere " unconst dentro una funzione ed essere sicuri che il nome non influenzerà  il resto del programma.

Il const fu preso dal C++ e incorporato nello standard C, sebbene abbastanza diffentemente. In C, il compilatore tratta un const proprio come una variabile che ha una speciale etichetta che dice " non cambiarmi " . Quando si definisce una const in C, il compilatore crea memoria per essa, così se si definisce più di una const con lo stesso nome in due differenti file ( o si pone la definizione in un file principale ), il linker genererà  un messaggio di errore circa la controversia. L' uso inteso del const in C è abbastanza differente di quello inC++ ( in breve, è migliore in C++) .

Valori costanti

In C++, una const deve sempre avere un valore di inizializzazione ( in C ciò non è vero). I valori costanti per i tipi predefiniti come decimali, ottali, esadecimali, o numeri in virgola mobile (tristemente, i numeri binari non sono considerati importanti), o come caratteri.

In assenza di ogni altro indizio, il compilatore assume un valore costante come numero decimale. Il numero 47, 0, e 1101 sono tutti trattati come numeri decimali.

Un valore costante con   un primo numero 0 è trattato come un numero ottale (base 8). I numeri a base 8 possono contenere solo digitazioni 0-7; il compilatore richiama le digitazioni oltre il 7 come errore. Un leggittimo numero ottale è 017 (15 in base 10).

Un valore costante con   un primo numero 0x è trattato come un numero esadecimale (base 16). I numeri in base 16 contengono digitazioni 0-9 e a-f o A-F. Un legittimo numero esadecimale è 0x1fe (510 in base 10).

I numeri in virgola mobile possono contenere punti decimali ed potenze esponenziali (rappresentata da e, che significa " 10 alla potenza di " ): il punto decimale e la e sono opzionali. Se si assegna una costante da una variabile in virgola mobile, il compilatore prenderà  il valore costante e lo convertirà  in un numero in virgola mobile (questo processo è una forma di ciò che è chiamato conversione di tipo implicito). Comunque è una buona idea usare il punto deciamale o una e per ricordare al lettore che si sta usando un numero in virgola mobile; anche alcuni vecchi compilatori hanno bisogno del suggerimento.

Leggittimi valori costanti in virgola mobile sono: 1e4, 1.0001, 47.0, 0.0, e -1.159e-77. Si puo' aggiungere il suffisso per forzare il tipo di numero in virgola mobile. f o F forza un float , L o l forza un long double ; altrimenti il numero sarà  un double .

Le costanti carattere sono caratteri racchiuse tra singole apicette come:  ' A' ,' 0' ,'' . Si noti che c' è una grossa differenza tra il carattere' 0' (ASCII 96) e il valore 0. i caratteri speciali sono rappresentati con il " backslash escape " :' \n' ( nuova riga),' \t' (tab),' \\' (backslash),' \r' (carriage return),' \"' (doppia apicetta),' \'' (singola apicetta), etc. si puo' snche esprimere le costanti char in ottale:' \17' o in esadecimale:' \xff' .

volatile

laddove il qualificatore const dice al compilatore che " questo non cambia mai " ( il quale permette al compilatore di produrre una ottimizzazione extra ), il qualificatore volatile dice al compilatore " non si sa mai quando questo cambierà  " , e previene il compilatore dal produrre qualsiasi ottimizzazione basata sulla stabilità  di quella variabile. Si usi questa parola chiave quando si leggono valori fuori dal controllo del proprio codice, come un registro in un pezzo di hardware per comunicazioni. Una variabile volatile è sempre letta  ogni volta che è richiesta, anche se è già stata letta la riga prima.

Un caso speciale di un pò di memoria che è " fuori dal controllo del nostro codice " è un programma multithreaded. Se si sta osservando un particolare flag che è modificato da un' altra thread o processo, quel flag dovrebbe essere volatile così il compilatore non crea la assunzione che puo' ottimizzare letture multiple del flag.

Si noti che volatile puo' non avere effetto quando un compilatore non è ottimizzato, ma puo' prevenire bug critici quando si inizia ad ottimizzare il codice(che accade quando il compilatore comincerà  a guardare le letture ridondanti).

Le parole chiave const e volatile saranno illustrate nel capitolo in seguito.

Gli operatori e il loro uso

Questa sezione illutstra tutti gli operatori del C e del C++.

Tutti gli operatori producono un valore dai loro operandi. Questo valore è prodotto senza modificare gli operandi, eccetto con gli operatori assegnamento, incremento e decremento. Modificare un operando è chiamato un side effect(effetto secondario). Il più comune uso per gli operatori che modificano i loro operandi è generare il side effect, ma si dovrebbe tenere a mente che il valore prodotto è disponibile solo per il nostro uso proprio come nell' operatore senza gli effetti secondari .

L'assegnamento

L' assegnamento è prodotto con l' operatore =. Esso significa " prendi la parte di destra ( spesso chiamata rvalue ) e copiala dentro la parte di sinistra (spesso chiamato lvalue ). " Un rvalue è una qualsiasi costante, variabile o una espressione che produce un valore, maun lvalue deve essere una distinta, variabile chiamata( cioè, deve essere uno spazio fisico nella quale salvare il dato). Ad esempio, si puo' assegnare un valore costante alla variabile   ( A = 4; ), ma non si puo' assegnare qualcosa al valore costante- non puo' essere un lvalue ( non si puo' dire   4 = A; ).

Gli operatori matematici

Gli operatori basici matematici sono gli stessi di quelli disponibili nella maggior parte dei linguaggi di programmazione: addizione ( + ), sottrazione ( - ), divisione ( / ), moltiplicazione ( * ), e modulo ( % ; ciò produce il resto delle divisioni con interi). Le divisioni con interi troncano il risultato ( non arrotonda ). L' operatore modulo non puo' essere usato con un numero a virgola mobile.

Il C ed il C++ usano anche una notazione stenografica per produrre una operazione ed un assegnamento allo stesso tempo. Questo è denotato da un operatore seguito da un segno uguale, ed è consistente con tutti gli operatori nel linguaggio ( ogni volta che ha senso). Ad esempio, aggiungere 4 alla variabile x e assegnare x al risultato, si dice: x += 4; .

Questo esempio mostra l' uso degli operatori matematici:

//: C03:Mathops.cpp

// operatori matematici

#include <iostream>

using namespace std;

// una macro per visualizzare una stringa ed un valore.

#define PRINT(STR, VAR) \

  cout << STR " = " << VAR << endl

int main() {

  int i, j, k;

float u, v, w;   //   si applica anche ai double

  cout << "inserisci un intero: " ;

  cin >> j;

  cout << "inserisci un altro intero: " ;

  cin >> k;

  PRINT( "j" ,j);   PRINT( "k" ,k);

  i = j + k; PRINT( "j + k" ,i);

  i = j - k; PRINT( "j - k" ,i);

  i = k / j; PRINT( "k / j" ,i);

  i = k * j; PRINT( "k * j" ,i);

  i = k % j; PRINT( "k % j" ,i);

  // il seguente funziona solo con gli interi:

  j %= k; PRINT( "j %= k" , j);

  cout << "Inserisci un numero a virgola mobile: " ;

  cin >> v;

  cout << "Inserisci un altro numero a virgola mobile:" ;

  cin >> w;

  PRINT( "v" ,v); PRINT( "w" ,w);

  u = v + w; PRINT( "v + w" , u);

  u = v - w; PRINT( "v - w" , u);

  u = v * w; PRINT( "v * w" , u);

  u = v / w; PRINT( "v / w" , u);

    // il seguente funziona con int,char

// e anche i double:

 

  PRINT( "u" , u); PRINT( "v" , v);

  u += v; PRINT( "u += v" , u);

  u -= v; PRINT( "u -= v" , u);

  u *= v; PRINT( "u *= v" , u);

  u /= v; PRINT( "u /= v" , u);

} ///:~

gli rvalue di tutti gli assegnamenti possono, naturalmente, essere ben più complessi.

 

Introduzione alle macro del preprocessore

 

Si noti che l' uso della macro PRINT(   ) per salvare il testo ( ed i suoi errori!). Le macro del preprocessore sono tradizionalmente chiamati con tutte lettere maiuscole quindi risaltano, si apprenderà  in seguito chele macro possono velocemente diventare pericolose ( e possono essere anche molto utili).

Gli argomenti nella lista nella parentesi successiva al nome della macro sono sostituiti in tutto il codice che segue la parentesi chiusa. Il preprocessore rimuove il nome PRINT e sostituisce il codice   dovunque la macro è chiamata, quindi il compilatore non puo' generare nessun messaggio di errore usando il nome della macro, e non esegue alcun tipo di controllo del tipo sugli argomenti ( l' ultimo puo' essere giovevole, come mostrato nei comandi di debug alla fine del capitolo).

Gli operatori relazionali

Gli operatori relazionali stabilicono una relazione tra il valori degli operandi. Essi producono un booleano ( specificato con la parola chiave bool in C++) true se la relazione è vera e false se la relazione è falsa. Gli operatori relazionali sono: più piccolo di (<), più grande di (>),   più piccolo o uguale di (<=), più grande o uguale di (>=), equivalente (= =), e diverso da (!=). Essi possono essere usati con tutti i tipi di dato predefiniti in C e C++. In C++ possono dare ad essi speciali definizioni per i tipi data definiti dagli utenti( li si apprenderà  nel capitolo12, il quale riguarda l' operatore sovraccaricato).

Gli operatori logici

Gli operatori logici and ( && ) e or ( || ) producono un true o false basato sulla relazione logica dei suoi argomenti. Si ricordi che in C e in C++, una dichiarazione è vera se essa ha il valore non-zero, è falsa se ha un valore di zero. Se si stampa un bool , tipicamente si vedrà  un ' 1' per true e ' 0' per false .

 

Questo esempio usa gli operatori relazionali e logici:

 

//: C03:Boolean.cpp

// Operatori relazionali e logici.

 

#include <iostream>

using namespace \std;

int main() {

  int i,j;

  cout << "Inserisci un intero: " ;

  cin >> i;

  cout << "Inserisci un altro intero: " ;

  cin >> j;

  cout << "i > j is " << (i > j) << endl;

  cout << "i < j is " << (i < j) << endl;

  cout << "i >= j is " << (i >= j) << endl;

  cout << "i <= j is " << (i <= j) << endl;

  cout << "i == j is " << (i == j) << endl;

  cout << "i != j is " << (i != j) << endl;

  cout << "i && j is " << (i && j) << endl;

  cout << "i || j is " << (i || j) << endl;

  cout << " (i < 10) && (j < 10) is "

       << ((i < 10) && (j < 10))   << endl;

} ///:~

si puo' rimpiazzare la definizione per int con float o double nel programma sopracitato. Bisogna stare attenti, comunque, che il confronto di un numero a virgola mobile con il valore zero è preciso; un numero che è di frazione differente da un altro numero è ancora " non-uguale " . Un numero a virgola mobile che il più piccolo bit sopra zero è ancora vero.

operatori su bit (Bitwise)

Gli operatori su bit permettono di manipolare bit individuali in un numero ( poichè i valori a virgola mobile usano un formato interno, gli operatori bit a bit funzionano solo con tipi integrali : char , int e long ). Gli operatori su bit effettuano l' algebra Booleana nei corrispondenti bit negli argomenti per produrre il risultato.

L' operatore bit a bit and (&) produce un uno nel bit dell' output se entrambi i bit dell' input sono alzati; altrimenti esso produce zero. Il bitwise or oppure l' operatore ( | ) produce   un uno nel bit dell' output   se uno dei due bit in input è uno e produce uno zero se entrambi i bit in input sono zero. Il bitwise exclusive or, o xor ( ^ ) produce di primo ordine nel bit dell' output se il primo o l' altro bit del input, ma non entrambi, è alzato. Il bitwise not ( ~,   chiamato anche l'operatore complemento ad uno) è un operatore unario, prende solo un argomento ( tutti gli altri operatori bit a bit sono operatori binari ). I bitwise non producono l' opposto del bit di input, un uno se il bit del input è zero, uno zero se il bit del input è uno.

Gli operatori su bit possono essere combinati col segno = per unire l' operazione e l' assegnamento: &= , |=, e ^= sono tutte operazioni leggittimw ( poichè ~ è un operatore unario che non puo' essere combinato col segno = ).

operatori Shift

Anche gli operatori shift manipolano bit. L' operatore left-shift (<<) shifta a sinistra l' operando a sinistra dell' operatore del numero di bit specificati dopo l' operatore. L' operatore right-shift ( >> ) shifta l' operando a sinistra dell' operatore a destra del numero di bit specificati dopo l' operatore. Se il valore dopo   l' operatore shift è più grande del numero di bit nell' operatore di sinistra, il risultato è indefinito. Se l' operatore di sinistra è senza segno, il right shift è un logico shift quindi il bit alto sarà  riempito con zero. Se l' operatore di sinistra ha segno, il right shift puo' o non essere un logico shift ( cioè, il comportamento è indefinito).

Shift puo' essere combinato con un segno uguale ( <<= e >>= ).   Il lvalue è rimpiazzato dal lvalue shiftato di rvalue.

Quello che segue è un esempio che dimostra l' uso di tutti gli operatori riguardanti i bit. Per primo, ecco una funzione di scopo generale che stampa un byte in formato binario, creato separatamente così che puo' essere facilmente riusato. L'header file dichiara la funzione:

 

//: C03:printBinary.h

// visualizzare un byte in binario

void printBinary( const unsigned char val);

///:~

ecco qui un'implementazione della funzione:

//: C03:printBinary.cpp {O}

#include <iostream>

void printBinary( const unsigned char val) {

  for ( int i = 7; i >= 0; i--)

    if (val & (1 << i))

      std::cout << "1" ;

    else

      std::cout << "0" ;

} ///:~

la funzione   printBinary(   ) prende un singolo byte e lo visualizza bit per bit. L' espressione

(1 << i)

produce un uno in ogni successiva posizione del bit; in binario: 00000001, 00000010, etc. se questo bit è   un bitwise and con val e il valore non è 0, significa che c' era uno in quella posizione in val .

Finalmente, la funzione usata nell' esempio che mostra gli operatori che manipola il bit:

//: C03:Bitwise.cpp

//{L} printBinary

// dimostrazione della manipolazione dei bit

#include "printBinary.h"

#include <iostream>

using namespace std;

// una macro per scrivere meno:

#define PR(STR, EXPR) \

  cout << STR; printBinary(EXPR); cout << endl;  

int main() {

  unsigned int getval;

  unsigned char a, b;

cout << "digita un numero compreso tra 0 e 255: " ;

  cin >> getval; a = getval;

  PR( "a in binario: " , a);

  cout << "digita un numero compreso tra 0 e 255: " ;

  cin >> getval; b = getval;

  PR( "b in binario: " , a);

  PR( "a | b = " , a | b);

  PR( "a & b = " , a & b);

  PR( "a ^ b = " , a ^ b);

  PR( "~a = " , ~a);

  PR( "~b = " , ~b);

// un interessante pattern di bit :

  unsigned char c = 0x5A;

  PR( "c in binario: " , c);

  a |= c;

  PR( "a |= c; a = " , a);

  b &= c;

  PR( "b &= c; b = " , b);

  b ^= a;

  PR( "b ^= a; b = " , b);

} ///:~

ancora una volta, una macro del preprocessore è utilizzato per scrivere di meno. Essa stampa la stringa della nostra scelta, poi la rappresentazione binaria di un' espressione, dopo un newline.

In main( ) le variabili sono unsigned . Questo perchè, in generale, non si vuole il segno quando si sta lavorando con i byte. Un int deve essere usato invece di una char per getval perchè la dichiarazione " cin >> " alla prima digitazione sarà  trattata altrimenti come carattere. Assegnando getval ad a e b , il valore è convertito in un singolo byte ( troncandolo).

Il << ed il >> esegue lo shift dei bit, ma quando i bit vengono shiftati alla fine del numero, questi bit sono persi ( è comune dire che loro cadono dentro il mitico bit bucket,   un posto dove i bit scartati terminano, presumibilmente così possono essere riusati ...). Quando si manipolano i bit si puo' anche   produrre la rotazione ( rotation) , che significa che i bit che arrivano alla fine ricompaiono all' altro capo, come se stessero ruotando in giro. Anche se la maggior parte dei processori dei computer forniscono un comando di rotazione a livello di macchina ( lo si vedrà  nel linguaggio assembler per quel processore ), non c' è un supporto diretto per la "rotazione" in C o in C++. Presumibilmente i creatori del C si sentirono giusificati nel tralasciare la " ruotazione" ( aspirando, come essi dissero, ad linguaggio minimale ) perchè si puo' costruire il proprio comando di rotazione. Ad esempio, ecco qui funzioni per eseguire rotazioni a destra e a sinistra:

//: C03:Rotation.cpp {O}

// produce rotazioni a destra e a sinistra

unsigned char rol( unsigned char val) {

int highbit;

  if (val & 0x80) // 0x80 è il bit alto solo

    highbit = 1;

  else

    highbit = 0;

// left shift( l'ultimo bit diventa 0) :

  val <<= 1;

// ruota il bit alto all'ultimo:

  val |= highbit;

  return val;

}

unsigned char ror( unsigned char val) {

  int lowbit;

  if (val & 1) // controlla il bit basso

    lowbit = 1;

  else

    lowbit = 0;

 

  val >>= 1; //spostamento a destra di una posizione

  // ruota il bit basso all'inizio:

  val |= (lowbit << 7);

  return val;

} ///:~

si provi ad usare queste funzioni in Bitwise.cpp . Si noti che le definizioni ( o almeno le dichiarazioni) di rol() e ror() devono essere viste dal compilatore in Bitwise.cpp . prima che le funzioni vengano usate.

Le funzioni bit a bit sono estremamente efficienti da usare perchè si traducono direttamente in dichiarazioni del linguaggio assembler. Qualche volta una singola dichiarazione in C o C++ genererà  una singola riga del codice assembler.

Gli operatori unari

Non ci sono solo operatori che prendono un singolo argomento. Il suo compagno, il not logico (!), prenderà  il valore true e produrrà  un valore false . L' unario meno (-) e l' unario più (+) sono gli stessi operatori del binario meno e più; il compilatore capisce a quale uso è diretto dal modo in cui si scrive l' espressione. Per esempio, l'istruzione

x = -a;

ha un significato ovvio. Il compilatore puo' capire:

x = a * -b;

ma il lettore puo' confondersi, quindi è più sicuro dire:

x = a * (-b);

L' unario meno produce il negativo del valore. L' unario più   fornisce ad una simmetria con l' unario meno, sebbene non fa realmente niente.

Gli operatori decremento e incremento ( ++ e   -- ) sono stati introdotti poco fa in questo capitolo. Queste sono gli unici operatori oltre quelli che riguardanti l' assegnamento che hanno effetti secondari. Questi operatori incrementano o decrementano la variabile di una unità , possono avere differenti significati secondo il tipo di dato, questo è   vero specialmente con i puntatori.

Gli ultimi operatori unari sono l' indirizzo di (&), dereferenza ( * and -> ), e gli operatori di cast in C e in C++, e new e delete in C++. L' indirizzo e la dereferenza sono usati con i puntatori, descritti in questo capitolo. Il castingè descritto in seguito in questo capitolo, e new e delete sono introdotti nel capitolo 4.

 

L'operatore ternario

Il ternario   if-else è unusuale perchè ha tre operandi. Esso è veramente un operatore perchè produce un valore, dissimile dall' ordinaria dichiarazione if-else . Consiste in tre espressioni: se la prima espressione ( seguita da un ?) vale il true , l' espressione successiva a ? è valutata ed il risultato diventa il valore prodotto dall' operatore. Se la prima espressione è false , la terza (successiva a : ) è eseguita ed il suo risultato diventa il valore prodotto dall' operatore.

L' operatore condizionale puo' essere usato per i suoi effetti secondari o per il valore che produce. Ecco qui un frammento di codice che li dimostra entrambi:

a = --b ? b : (b = -99);

qui, il condizionale produce un rvalue. a è assegnato al valore di b se il risultato del decremento di b non è zero. Se b diventa zero, a e b sono entrambi assegnati a -99. b è sempre decrementato, ma è assegnato a -99 solo se il decremento fa diventare b 0. Una dichiarazione simile puo' essere usata senza la " a = " solo per i suoi effetti secondari :

--b ? b : (b = -99);

qui la seconda b è superflua, poichè il valore prodotto dall' operatore è inutilizzato. Una espressione è richiesta tra ? e :. In questo caso, l' espressione puo' essere semplicemente una costante che puo' rendere l' esecuzione del codice   un po' più veloce.

 

L'operatore virgola

La virgola non è limitata a separare i nomi delle variabili in definizioni multiple, tipo:

int i, j, k;

naturalmente, esso è usato anche nelle liste degli argomenti di funzione. Comunque esso puo' essere usata per separare espressioni, in questo caso produce solo il valore dell' ultima espressione. Tutto il resto dell' espressione nella lista separata dalla virgola è valutata solo per i loro effetti secondari. Questo esempio incrementa una lista di variabili e usa   l' ultimo come rvalue:

//: C03:CommaOperator.cpp

#include <iostream>

using namespace std;

int main() {

  int a = 0, b = 1, c = 2, d = 3, e = 4;

  a = (b++, c++, d++, e++);

  cout << "a = " << a << endl;

// le parentesi sono cruciali qui. Senza di

// loro, la dichiarazione valuterà :

  (a = b++), c++, d++, e++;

  cout << "a = " << a << endl;

} ///:~

in generale, è meglio evitare l' uso della virgola come nient'altro che un separatore, poichè non si è abituati a verderla come un operatore.

 

Tranelli comuni quando si usano gli operatori

Come illustrato sopra, uno dei tranelli quando si usano gli operatori è provare ad evitare di usare le parentesi quando si è incerti su come una espressione verrà valutata  (si consulti il   proprio manuale locale C per l' ordine della valutazione dell' espressione ).

Ecco un altro errore estremamente comune   che assomiglia a questo:

 

//: C03:Pitfall.cpp

// errori di operatore

 

int main() {

  int a = 1, b = 1;

  while (a = b) {

    // ....

  }

} ///:~

la dichiarazione a = b varra   sempre vero quando b non è  zero. La variabile a è assegnata al valore di b ed il valore di b è anche prodotto dall' operatore =. In generale, si usa l' operatore equivalenza = = dentro una dichiarazione condizionale, non nell' assegnamento. Questo confonde molti programmatori ( comunque, alcuni compilatori richiameranno all' attenzione il problema, che è di aiuto).

Un problema affine è usare il bitwise and o or invece delle loro controparti logiche. I bitwise and o or usano uno dei caratteri ( & o| ), mentre il logico and o or ne usano due ( && e || ). Esattamente come con = e = =, è facile scrivere un carattere invece di due. Un utile stratagemma mnemonico è osservare che " i bit sono più piccoli, quindi non hanno bisogno di molti caratteri nei loro operatori " .

Casting degli operatori

La parola cast è usata nel senso di " fondere in uno stampo" . Il compilatore cambierà  automaticamente un tipo di dato in un altro se ciò è sensato. Per esempio, se si assegna un valore intero ad una variabile in virgola mobile, il compilatore chiamerà  segretamente una funzione ( o più probabilmente, inserirà  un codice) per convertire un int in un float . Il casting permette di fare questo tipo di conversione esplicita, o di forzarla quando non accadrebbe normalmente.

Per eseguire un cast, mettere il tipo di dato desiderato ( includendo tutti i modificatori) dentro le parentesi a sinistra del valore. Questo valore puo' essere una variabile, una costante, un valore prodotto da una espressione o il valor di ritorno di una funzione. Ecco qui un esempio:

//: C03:SimpleCast.cpp

int main() {

  int b = 200;

  unsigned long a = ( unsigned long int )b;

} ///:~

il casting è potente, ma puo' causare mal di testa perchè in alcune situazioni forza il compilatore a trattare il dato come se fosse ( ad esempio) più grande di quanto realmente è, quindi esso occuperà  più spazio in memoria, questo puo' calpestare altri dati. Ciò usualmente accade quando si fa il casting di puntatori, non quando si fanno semplici cast come quello mostrato sopra.

Il C++ ha una sintassi di cast addizionale, la quale segue la sintassi della chiamata a funzione. Questa sintassi mette le parentesi intorno all' argomento, come una chiamata a funzione, piuttosto che intorno al tipo di dato :

//: C03:FunctionCallCast.cpp

int main() {

  float a = float (200);

// questo è l'equivalente di :  

  float b = ( float )200;

} ///:~

naturalmente nel caso di sopra non si avrebbe bisogno di un cast, si potrebbe solo dire 200 . f oppure 200.0f ( in effetti, ciò è tipicamente cosa il compilatore farà  nella espressione di sopra). I cast sono usualmente usati con le variabili, piuttosto che con le costanti.

Cast espliciti in C++

I cambi dovrebbero essere usati con cautela, poichè quello che realmente si sta facendo è dire al compilatore " dimentica il controllo del tipo , trattalo invece come qualche altro tipo. " . Cioè, si sta introducendo un buco nel sistem del tipo del C++ ed impedendo al compilatore di dire che si sta facendo qualcosa di sbagliato con un tipo. Cosa peggiore, il compilatore crede a noi implicitamente e non esegue alcun altro controllo per controllare gli errori. Una volta iniziato il casting , ci apriremo a tutti i tipi di problemi. Infatti, qualsiasi programma che usa molti cast dovrebbe essere visto con sospetto, non importa quante volte viene detto che "deve" essere fatto in quel modo. In generale, i cast dovrebbero essere pochi e isolati alla soluzione di specifici problemi.

Una volta capito ciò e viene presentato un programma bacato, la prima tendenza potrebbe essere quella di cercare i cast come colpevoli, ma come si localizzano i cast dello stile C? Ci sono semplici nomi dentro le parentesi e se si inizia a cercare tali cose si scoprirà  che è spesso difficile distinguerli dal resto del codice.

Lo standard C++ include un' esplicita sintassi di cast che puo' essere usata per riporre completamente il vechio stile di cast in C (naturalmente, lo stile   del cast in C non puo' essere bandito senza   un codice di interruzione, ma gli inventori del compilatore potrebbero facilmente richiamare l' attenzione sul vecchio stile di cast). L' esplicita sintassi del cast in C è tale che si puo' facimente trovarlo, come si puo' vedere dai loro nomi :

static_cast Per i cast " ben funzionanti" e " ragionevolmente ben funzionanti" ,   incluse le cose che si possono fare senza un cast( come   una conversione automatica di tipo).  
const_cast Per cast con const e/o volatile .
reinterpret_cast

casting in un altro significato completamente differente . Il punto chiave è che si avrà bisogno di fare un cast all' originale per usarlo con sicurezza. Il tipo a cui si fa il casting è tipicamente usato solo rigirare i bit o per altri misteriosi propositi. Questo è il più pericoloso tra i cast.

dynamic_cast Per un downcasting sicuro (si veda il Capitolo15).

 


i primi tre espliciti cast saranno descritti più esaustivamente nelle sezioni seguenti, mentre l'ultimo puo' essere dimostrato solo quando si sarà  appreso di più, nel capitolo 15.

 

static_cast

Uno static_cast (cambio static) è usato per tutte le conversioni che sono ben definite. Queste includono conversioni " sicure " che il compilatore permetterebbe a noi di programmare senza un cast e  convesioni meno sicure che sono   tuttavia ben definite. I tipi di conversione   inclusi in static_cast includono tipiche convesioni senza cast , conversioni con narrowing(perdita di informazioni ), forzando una conversione da un void* , conversioni del tipo implicite, ed una navigazione statica di   gerarchie di classi ( poichè non si sono viste ancora le classi e l' ereditarietà , quest' ultimo argomento sarà  rimandato fino al capitolo 15 ) :

 

//: C03:static_cast.cpp

void func( int ) {}

int main() {

  int i = 0x7fff; // valore della posizione max = 32767

  long l;

  float f;

// (1) tipica conversione priva di cast:

  l = i;

  f = i;

// funziona anche:   

  l = static_cast < long >(i);

  f = static_cast < float >(i);

//(2)   conversione con narrowing:  

  i = l; // può perdere digit

  i = f; // può perdere informazioni

// dice "capisco," elimina i warnings :

  i = static_cast < int >(l);

  i = static_cast < int >(f);

  char c = static_cast < char >(i);

  // (3) forza una conversione dal void* :

 

  void * vp = &i;

// un vecchio modo produce una conversione pericolosa :  

  float * fp = ( float *)vp;

// il nuovo modo   è ugualmente pericoloso :  

  fp = static_cast < float *>(vp);

 

// (4) conversione implicita del tipo normalmente

// eseguita dal compilatore :  

  double d = 0.0;

int x = d; // conversione del tipo automatica

  x = static_cast < int >(d); // più esplicita

  func(d); // conversione del tipo automatica

  func( static_cast < int >(d)); // più esplicita

} ///:~

nella sezione (1), si vedono i tipi di conversioni che utilizzati programmando in C, con o senza il cast. Promuovere da un int a un long o float non è un problema perchè l' ultimo puo' sempre contenere ogni valore che un int puo' contenere. Sebbene non è necessario, si puo' usare lostatic_cast( cambio static) per mettere in rilievo queste promozioni.

L' altro modo di riconversione è mostrato in (2). Qui, si puo' perdere dati perchè   un int non è grande come un long o un float ; non conterrebbe numeri della stessa grandezza. Perciò queste sono chiamate   conversioni con narrowing. Il compilatore le eseguirà  lo stesso, ma dimenticherà  spesso di darci un warning. Si puo' eliminare questo warning ed indicare che realmente si intendeva usare un cast.

Assegnare da un void* non è permesso senza un cast in C++ (non come in C), come visto in (3). Questo è pericoloso e richiede che i programmatori sappiano ciò che stanno facendo. Lo static_cast, almeno, è più facile da localizzare che il vecchio cast standard quando si stanno cercando i bachi.

La sezione (4) del programma mostra i tipi di conversione di tipo implicito che sono normalmente eseguiti automaticamente dal compilatore. Questi sono automatici e non richiedono il casting, ma ancora lo  static_cast mette in rilievo l' azione nel caso che si voglia rendere chiaro cosa succede o cercala in seguito.

const_cast

Se si vuole convertire da una const a una non const o da una volatile a una non volatile , si usa const_cast . Questo è l' unica conversione permessa con const_cast; se ogni altra conversione è utilizzara, essa deve essere fatto usando una espressione separata o si avrà  un errore al tempo di compilazione.

//: C03:const_cast.cpp

int main() {

  const int i = 0;

  int * j = ( int *)&i; //   forma deprecata

  j   = const_cast < int *>(&i); // Preferita

  // non si puo' fare un cast simultaneo addizionale :

//! long* l = const_cast<long*>(&i); // Errore

  volatile int k = 0;

  int * u = const_cast < int *>(&k);

} ///:~

se si prende l' indirizzo di un oggetto const , si produce un puntatore ad un const , e ciò non puo' essere assegnato ad un puntatore di una non const senza un cast. Il vecchio stile di cast eseguirà  ciò, ma il   const_cast e l' unico appropriato da usare. Lo stesso vale per volatile .

reinterpret_cast

Questo è il meno sicuro dei meccanismi di casting, e l' unico che produce con più probabilità  i bachi. Un reinterpret_cast pretende che un oggetto è un pattern di bit che puo' essere trattato ( per alcuni propositi oscuri) come se fosse un tipo interamente differente. Questa è la manipolazione a basso livello dei bit per cui il C è noto. Virtualmente si avrà  sempre bisogno di   reinterpret_cast per tornare al tipo originale ( o altrimenti si tratta la variabile come il tipo originale ) prima di fare qualsiasi cosa con esso.

//: C03:reinterpret_cast.cpp

#include <iostream>

using namespace std;

const int sz = 100;

 

struct X { int a[sz]; };

 

void print(X* x) {

  for ( int i = 0; i < sz; i++)

    cout << x->a[i] << ' ';

  cout << endl << "--------------------" << endl;

}

 

int main() {

  X x;

  print(&x);

  int * xp = reinterpret_cast < int *>(&x);

  for ( int * i = xp; i < xp + sz; i++)

    *i = 0;

// non si puo' usare xp come un   X* a questo punto

// a meno che non si fa il cast indietro:

  print( reinterpret_cast <X*>(xp));

// in questo esempio, si puo' anche solo usare

// l'identificatore originale:  

  print(&x);

} ///:~

in questo semplice esempio, struct X contiene un array di int , ma quando se ne crea uno nello stack come in X x , il valore di ogni int è spazzatura ( ciò è mostrato usando la funzione print( ) per visualizzare i contenuti di struct ). Per inizializzarli, l' indirizzo di X è preso e cambiato in un puntatore int , il quale è poi agisce lungo l' array per settare ogni   int a zero. Si noti come il più alto designato ad arrivare ad i è calcolato " aggiungendo " sz a xp ; il compilatore sa che si vuole realmente le locazioni di sz più grandi di xp e esso produce il corretto puntatore aritmetico per noi.

In this simple example, struct X just contains an array of int , but when you create one on the stack as in X x , the values of each of the int s are garbage (this is shown using the print(   ) function to display the contents of the struct ). To initialize them, the address of the X is taken and cast to an int pointer, which is then walked through the array to set each int to zero. Notice how the upper bound for i is calculated by " adding " sz to xp ; the compiler knows that you actually want sz pointer locations greater than xp and it does the correct pointer arithmetic for you.

L' idea del reinterpret_cast è che quando lo si usa, quello che si ottiene non ha così relazione che non puo' essere usato per il proposito originale del tipo a emno che non si lo si cambi indietro. Qui, si vede il cambio in X* nella chiamata a stampare, ma naturalmente poichè si ha ancora l' originale identificatore si puo' anche usarlo. Ma l' xp è solo utile come un   int* , il quale è realmente una " rinterpretazione " dell' originale X .

The idea of reinterpret_cast is that when you use it, what you get is so foreign that it cannot be used for the type' s original purpose unless you cast it back. Here, we see the cast back to an X* in the call to print, but of course since you still have the original identifier you can also use that. But the xp is only useful as an int* , which is truly a " reinterpretation " of the original X .

Un reinterpret_cast spesso indica sconsigliato e non trasportabile per la programmazione, ma è disponibile quando si decide di dover usarla.  

A reinterpret_cast often indicates inadvisable and/or nonportable programming, but it' s available when you decide you have to use it.

sizeof - un operatore a se

sizeof - an operator by itself

l' operatore sizeof sta da solo perchè soddisfa un bisogno unusuale. Sizeof ci da l' informazione circa l' ammontare della memoria allocata per gli oggetti data. Cme descritto poco fa in questo capitolo, sizeof dice a noi   il numero di   byte usati da ogni particolare variabile. Esso puo' anche dare la grandezza di un tipo data ( con nessun nome di variabile ):

  The sizeof operator stands alone because it satisfies an unusual need. sizeof gives you information about the amount of memory allocated for data items. As described earlier in this chapter, sizeof tells you the number of bytes used by any particular variable. It can also give the size of a data type (with no variable name):

//: C03:sizeof.cpp

#include <iostream>

using namespace std;

int main() {

  cout << "sizeof(double) = " << sizeof ( double );

  cout << ", sizeof(char) = " << sizeof ( char );

} ///:~

per definizione, il sizeof di ogni tipo di char ( signed , unsigned o pieno) è sempre uno, anche se la memoria utilizzata per un char non è realmente un byte. Per tutti gli altri tipi, il risultato è la grandezza in byte.

Si noti che sizeof   è un operatore, non una funzione. Se lo si applica ad un tipo, deve essere usato con le parentesi nella forma mostrata sopra, ma se si applica ad una variabile si puo' usarla senza parentesi :  

//: C03:sizeofOperator.cpp

int main() {

  int x;

  int i = sizeof x;

} ///:~

sizeof   puo' anche darci la grandezza per i tipi di dato definiti dagli utenti. Ciò è usato in seguito nel libro.

 

La parola chiave asm

Questo è un meccanismo di fuga che ci permette di scrivere il codice assembler per il nostro hardware in un programma C++. Spesso si può far riferimento a variabili in C++ senza un codice assembly, che significa che si puo' comunicare facilmente con il proprio codice in C++ e limitare il codice assembly al necessario per l' efficienza o per l' uso di istruzioni speciali del processore. L' esatta sintassi che si deve usare quando si scrive in linguaggio assembly è compilatore-dipendente e puo' essere scoperto nella propria documentazione del compilatore.

operatori espliciti

Queste sono parole chiave per gli operatori logici o su bit. I programmatori non americani senza caratteri della tastiera come & , | , ^ , e così via, sono obbligati a usare l' orribile trigraphs (trigrafo) , il quale non solo è noiso da scrivere, ma oscura quando lo si legge. In C++ si rimedia con le parole chiave addizionali :

Parola chiave Significato
and && (and logico)
or || (or logico)
not ! ( NOT logico)
not_eq != (logico non-equivalente)
Bitand & (bitwise and )
And_eq &= (bitwise and -assegnamento)
Bitor | (bitwise or )
Or_eq

|= (bitwise or-assegnamento)

Xor

^ (bitwise esclusivo-or)

xor_eq ^= (bitwise esclusivo-or-assegnamento)
Compl ~ (complemento)

 

 

 

 

Or

 

Not

 

not_eq

 

Bitand

& (bitwise and )

And_eq

&= (bitwise and -assegnamento)

Bitor

| (bitwise or )

Or_eq

|= (bitwise or-assegnamento)

Xor

^ (bitwise esclusivo-or)

xor_eq

^= (bitwise esclusivo-or-assegnamento)

Compl

 

 

Se il proprio compilatore è conforme allo Standard C++, supporta  queste parole chiave.

 

Creazione di tipo composto

I tipi di dato fondamentali e le loro variazioni sono essenziali, ma piuttosto primitive. Il C ed il C++ forniscono di strumenti che ci permettono di comporre tipi di dato più sofisticati   dai tipi di dato fondamentali. Come si vedrà , la più importante di queste è struct , che è la base per class in C++. Comunque, il modo più semplice per creare tipi più sofisticati è semplicemente dare uno pseudonimo ad un altro attraverso typedef .

Pseudonimi con typedef

Questa parola chiave promette più di quanto offre: typedef suggerisce " definizione di tipo " quando " pseudonimo" probabilmente sarebbe stata una descrizione più accurata , poichè questo è ciò che realmente fa. La sintassi è:

Typedef esistente-tipo-descrizione dello pseudonimo

si usa spesso   typedef quando il tipo di dato diventa leggermente complicato, proprio per scrivere di meno. Ecco un typedef comunemente usato :

typedef unsigned long ulong;

ora se si dice ulong il compilatore sa che si intende unsigned long . Si puo' pensare che   ciò potrebbe essere compiuto più facilmente usando la sostituzione del preprocessore, ma ci sono situazioni chiave nelle quali il compilatore deve essere consapevole che si sta trattando un nome come se fosse un tipo, quindi typedef è essenziale.

 

Un posto dove typedef   torna utile è per i tipi di puntatori. Come precedentemente menzionato, se si dice :

int * x, y;

ciò realmente produce un int* il quale è x e un int ( non un int*   ) il quale è y. Cioè, il ' *'   si riferisce a destra non a sinistra. Comunque se si usa un typedef :

typedef int * IntPtr;

IntPtr x, y;

Dopo entrambi x e y sono tipi di int* .

Si puo' dire che è più esplicito, perciò più leggibile, evitare typedef per i tipi primitivi, ed effettivamente i programmi diventano rapidamente difficili da leggere quando vengono usati molti typedef . Tuttavia, i typedef diventano specialmente importanti in C quando sono usati con struct .

Combinare le variabili con struct

Una struct è un modo per raccogliere un gruppo di variabili in una struttura. Una volta creata una struct , dopo si possono fare molti esempi di questo " nuovo " tipo di variabile che si è inventati. Ad esempio:

//: C03:SimpleStruct.cpp

struct Structure1 {

  char c;

  int i;

  float f;

  double d;

};

 

int main() {

  struct Structure1 s1, s2;

  s1.c = 'a'; // Selezionare un elemento usando un '.'

  s1.i = 1;

  s1.f = 3.14;

  s1.d = 0.00093;

  s2.c = 'a';

  s2.i = 1;

  s2.f = 3.14;

  s2.d = 0.00093;

} ///:~

la dichiarazione struct deve terminare con un punto e virgola. In main( ) , le due istanze di Structure1 sono creati : s1 e s2. ognuna di queste ha la loro propria versione separata di c, i , f , e d . Quindi s1 e s2 rappresenta   un gruppo di variabili completamente indipendenti. Per selezionare uno degli elementi senza s1 o s2 , si usa un ' .' , sintassi che si è vista nel capitolo precedente quando si usano gli oggetti class in C++ , poichè class evolve dalle struct , questa è da dove quella sintassi è venuta fuori.

Una cosa che si noterà  è l' incapacità  di uso di Structure1 ( a cui è rivolto, questo è solamente richiesto dal C, non in C++). In C non si puo' dire solo    Structure1 quando si sta definendo le variabili, si deve dire struct Structure1 . Questo è dove typedef diventa specialmente pratico in C:

//: C03:SimpleStruct2.cpp

// usare typedef con struct

typedef struct {

  char c;

  int i;

  float f;

  double d;

} Structure2;

 

int main() {

  Structure2 s1, s2;

  s1.c = 'a';

  s1.i = 1;

  s1.f = 3.14;

  s1.d = 0.00093;

  s2.c = 'a';

  s2.i = 1;

  s2.f = 3.14;

  s2.d = 0.00093;

} ///:~

usando typedef in questo modo, si puo' fingere ( in C; si provi a rimuovere il typedef per il C++) che Structure2 sia un tipo predefinito, come int o float , quando si definiscono s1 e s2 ( ma si noti che esso ha solo la caratteristica del dato non il comportamento, che è quello che otteniamo con gli oggetti veri in C++). Si noterà  che l' identificatore struct è stata persa via all' inizio, perchè lo scopo è creare il typedef . Comunque, ci sono delle volte in cui quando si puo' aver bisogno di riferirsi alla struct durante la sua definizione. In questi casi, si puo' realmente ripetere il nome del struct come il nome del struct e come typedef :

//: C03:SelfReferential.cpp

// permettere ad una struct di riferirsi a se stessa

 

typedef struct SelfReferential {

  int i;

  SelfReferential* sr; // un giramento di testa ancora?

} SelfReferential;

 

int main() {

  SelfReferential sr1, sr2;

  sr1.sr = &sr2;

  sr2.sr = &sr1;

  sr1.i = 47;

  sr2.i = 1024;

} ///:~

se si osserva questo per un pò, si vedrà  che   sr1 e sr2 si puntano l'un l'altro ed ognuno ha un pezzo di dato.

Realmente, il nome struct non deve essere lo stesso del nome di typedef , ma è usualmente fatto in questo modo perchè tende a rendere le cose più semplici.

Puntatori e struct

Nell' esempio di sopra, tutte le struct sono manipolate come oggetti. Tuttavia, come qualsiasi pezzo di memoria, si puo' prendere l' indirizzo di un oggetto struct ( come visto sopra in    SelfReferential.cpp ). Per scegliere gli elementi di un particolare oggetto struct, si usa un ' .' , come visto sopra. Tuttavia, se si ha un puntatore ad un oggetto struct , si deve scegliere un elemento di quell' oggetto usando un differente operatore : il ' ->' . Ecco qui un esempio :

//: C03:SimpleStruct3.cpp

//usare i puntatori alle struct

typedef struct Structure3 {

  char c;

  int i;

  float f;

  double d;

} Structure3;

 

int main() {

  Structure3 s1, s2;

  Structure3* sp = &s1;

  sp->c = 'a';

  sp->i = 1;

  sp->f = 3.14;

  sp->d = 0.00093;

  sp = &s2; // punta ad un differente oggetto struct

  sp->c = 'a';

  sp->i = 1;

  sp->f = 3.14;

  sp->d = 0.00093;

} ///:~

in main( ) , il puntatore alla struct sp sta inizialmente puntando a s1 ed i membri di s1 sono inizializzati selezionandoli con ' ->' ( si puo' usare lo stesso operatore per leggere questi membri ). Ma poi sp punta a s2 e queste variabili sono inizializzate allo stesso modo. Quindi si puo' vedere che un altro beneficio dei puntatori è che   possono essere dinamicamente deviati a puntare a oggetti differenti; questo provoca più flessibilità  nel proprio programma, come si apprenderà .

Per adesso, questo è tutto ciò che di cui si ha bisogno sulle struct , ma ci sentiremo maggiormente a nostro agio con loro ( e specialmente i loro più potenti successori, le classi) più in avanti nel libro.

Chiarificare i programmi con enum

Un tipo data numerato è un modo di assegnare i nomi ai numeri, quindi dando più significato a chiunque legge il codice. La parola chiave enum (dal C ) automaticamente enumera qualsiasi lista di identificatori che si danno assegnando i valori di 0,1,2. ecc.. Si possono dichiarare le variabili enum ( le quali sono sempre rappresentate come valori inteir ). La dichiarazione di un enum assomiglia ad una dichiarazione di struct .

Un tipo di dato enumerato è utile quando si vuole tenere conto di alcuni tipi di caratteristiche :

  

//: C03:Enum.cpp

// tenere traccia delle forme

enum ShapeType {

  circle,

  square,

  rectangle

};

// deve finire con un punto e virgola come una struct

 

int main() {

  ShapeType shape = circle;

// attività qui....

// adesso facciamo qualcosa basata su quale forma è :

  switch (shape) {

case circle:   /* cosa circolare */ break ;

    case square:   /* cosa quadrata */ break ;

    case rectangle:   /* cosa rettangolare */ break

  }

} ///:~

shape è una variabile di tipo dato enumerato ShapeType ed il suo valore è confrontato col valore nella enumerazione. Poichè shape è realmente solo un int , comunque, puo' essere qualsiasi valore che si può assegnare ad un int ( incluso un numero negativo ). Si puo' anche confrontare una variabile int col valore nella enumerazione.

Si deve essere consapevoli che l'esempio precedente il tipo muta ad essere un modo problematico al programma. C++ ha un modo migliore per codificare questa sorta di cosa,

 

la spiegazione del quale deve essere rimandata in seguito nel libro.

Se non piace il modo in cui il compilatore assegna valori, si può fare da sè:

enum ShapeType {

  circle = 10, square = 20, rectangle = 50

};

se si dà il valore ad alcuni nomi e non ad altri, il compilatore userà il prossimo valore intero. Ad esempio,

enum snap { crackle = 25, pop };

il compilatore da a pop il valore 26.

Si può vedere come sia più leggibile il codice quando si usa un tipo di dato enumerato. Comunque, in una certa misura ciò è ancora un tentativo ( in C ) per compiere le cose che noi facciamo con class in C++, quindi si vedrà poco enum in C++.

Controllo del tipo  per le enumerazioni

Le enumerazioni sono abbastanza primitive, semplicemente associano  valori interi con nomi, ma essi non provvedono ad un controllo del tipo. In C++ il concetto del tipo è fondamentale e ciò è vero con le enumerazioni. Quando si crea un' enumerazione con nome, effettivamente si crea un nuovo tipo proprio come si fa con una classe: il nome della nostra enumerazione diventa una parola riservata per la durata dellunità di traduzione.

In più, cè un  controllo di tipo per le enumerazioni in C++ più preciso del C. Si noterà ciò in particolare se si hanno esempi di enumerazione color chiamati a. In C si può dire a++, ma in C++ non si può. Cioè poichè incrementare una enumerazione  è produrre due conversione di tipo, uno di loro legale in C++ e laltro illegale. Primo, il valore dell enumerazione è  implicitamente cambiata da un color ad un int, dopo il valore è incrementato, poi l' int è cambiato in un color. In C++ ciò non è permesso, perché color è un tipo distinto e non equivalente ad int. Ciò ha senso, perché come si fa a sapere se lincremento di blue sarà nella lista dei colori ? Se si vuole incrementare un color, esso dovrebbe essere una classe ( con unoperazione di incremento ) e non un enum, poiché la classe può essereabbastanza sicura. Ogni volta che si scrive codice che assume una conversione implicita in un tipo enum, il compilatore richiamerà allattenzione su questa pericolosa attività.

Le union (descritte in seguito ) hanno controlli sul tipo addizionali simili in C++.

 

Risparmiare la memoria con union

Qualche volta un programma gestirà differenti tipi di dato usando la stessa variabile. In questa situazione, si hanno due scelte : si può creare una struct contenente tutte i possibili tipi che potrebbero servire o si può usare un union. Un union accatasta tutti i tipi di dato in un singolo spazio, ne segue che la dimensione della union è data dal loggetto più grande che si è messo in essa. Si usi union per risparmiare la memoria.

Ogni volta che si piazza un valore in  una union, il valore comincia sempre allo stesso posto allinizio dellunion, ma usa solo lo spazio necessario. Quindi si crea una super variabile capace di detenere ogni variabile in  union. Tutte gli indirizzi delle variabili di union sono gli stessi (in una class o struct, gli indirizzi sono differenti ).

Ecco un semplice uso di union. Si provi a rimuovere vari elementi e si veda che effetto esso ha sulla grandezza dellunion. Si noti che non ha senso dichiarare più di un esempio di un singolo tipo data in un union ( tranne quando si sta usando un nome differente).

//: C03:Union.cpp

// la dimensione ed il semplice uso di una unione

#include <iostream>

using namespace std;

union Packed { // dichiarazione simile a class

  char i;

  short j;

  int k;

  long l;

  float f;

  double d; 

 

// l’unione avrà la dimensione di un

//double, poiché quello è l’elemento più grande

};  // il punto e virgola termina una union, come una struct

int main() {

  cout << "sizeof(Packed) = "

       << sizeof(Packed) << endl;

  Packed x;

  x.i = 'c';

  cout << x.i << endl;

  x.d = 3.14159;

  cout << x.d << endl;

} ///:~

il compilatore esegue la giusta assegnazione secondo il membro union che si è scelto.

Ua volta si esegue un'assegnazione, il compilatore non si preoccupa di ciò che si fa con lunion. Nellesempio sopracitato, si potrebbe assegnare un valore in virgola mobile a x:

x.f = 2.222;

e dopo mandarlo alloutput come se fosse un int :

cout << x.i;

ciò produrrebbe spazzatura.

Gli array

I vettori sono un tipo composto perchè permettono di racchiudere molte variabili insieme, una dopo laltra, sotto un singolo nome identificatore. Se si dice :

int a[10];

si crea memoria per 10 variabili int accatastate una su laltra, ma senza un unico nome identificatore per ogni variabile. Invece, esse sono tutte raggruppate sotto il nome a.

Per accedere ad una di questi elementi del vettore (array elements ), si usa la stessa sintassi di con parentesi quadre che si usa per definire un array:

a[5] = 47;

comunque, si deve ricordare che anche se la grandezza di a è 10, si scelgono gli elementi del vettore cominciando da 0 ( questo è chiamato qualche volta zero indexing), quindi si possono selezionare solo elementi del vettore 0-9 :

//: C03:Arrays.cpp

#include <iostream>

using namespace std;

int main() {

  int a[10];

  for(int i = 0; i < 10; i++) {

    a[i] = i * 10;

    cout << "a[" << i << "] = " << a[i] << endl;

  }

} ///:~

l'accesso ad un array è estremamente veloce. Tuttavia, se lindice supera la fine di un array, non cè nessuna rete di sicurezza si andrà su altre variabili. Laltro  svantaggio è che si deve definire la grandezza di un array al tempo di compilazione; se si vuole cambiare la grandezza al tempo di esecuzione non si può usare la sintassi sopracitata ( il C ha un modo per creare un array dinamicamente, ma è significamente il più disordinato ). Il vector del C++, introdotto nel capitolo precedente, produce un array simile ad un oggetto che automaticamente cambia da sè le dimensioni, quindi è solitamente  la migliore soluzione  se non si conosce la dimensione dell' array al tempo di compilazione. 

Si possono programmare array di ogni tipo, anche di struct :

//: C03:StructArray.cpp

// un array di struct

typedef struct {

  int i, j, k;

} ThreeDpoint;

int main() {

  ThreeDpoint p[10];

  for(int i = 0; i < 10; i++) {

    p[i].i = i + 1;

    p[i].j = i + 2;

    p[i].k = i + 3;

  }

} ///:~

si noti come lidentificatore i di struct sia indipendente da i del ciclo for.

Per vedere che ogni elemento di un array è vicino al prossimo, si possono stampare gli indirizzi :

//: C03:ArrayAddresses.cpp

#include <iostream>

using namespace std;

 

int main() {

  int a[10];

  cout << "sizeof(int) = "<< sizeof(int) << endl;

  for(int i = 0; i < 10; i++)

    cout << "&a[" << i << "] = "

         << (long)&a[i] << endl;

} ///:~

quando si fa girare questo programma, si vedrà che ogni elemento è distante int da quello precedente. Cioè, loro sono ammassati luno su laltro.

Puntatori ed array

Lidentificatore di un array è diverso da quello per le variabili ordinarie. Per una cosa, un identificatore di array non è un lvalue; non si può assegnare ad esso. E realmente solo un gancio nella sintassi con le parentesi quadre, quando si da' un nome di un array, senza parentesi quadre, quello che si ottiene è lindirizzo iniziale di un array :

//: C03:ArrayIdentifier.cpp

#include <iostream>

using namespace std;

int main() {

  int a[10];

  cout << "a = " << a << endl;

  cout << "&a[0] =" << &a[0] << endl;

} ///:~

 

quando si fa partire il programma si vedrà che i due indirizzi ( i quali saranno stampati in esadecimale, poichè non cè il cast a long) sono gli stessi.

Quindi un modo per vedere lidentificatore dellarray è come un puntatore allinizio dellarray di sola lettura. E sebbene non possiamo cambiare un identificatore dellarray per puntare da qualche altra parte, ma possiamo creare un altro puntatore e usarlo per muoverci nellarray. Infatti, la sintassi con le parentesi quadre funziona anche con puntatori regolari :

//: C03:PointersAndBrackets.cpp

int main() {

  int a[10];

  int* ip = a;

  for(int i = 0; i < 10; i++)

    ip[i] = i * 10;

} ///:~

il fatto di dare un nome ad un array produce che il suo iniziale indirizzo tende ad essere abbastanza importante quando si vuole passare un array ad una funzione. Se si dichiara un array come un argomento della funzione, quello che realmente si dichiarerà è un puntatore. Quindi nellesempio seguente, func1( ) e func2( ) effettivamente hanno le stesse liste di argomento :

//: C03:ArrayArguments.cpp

#include <iostream>

#include <string>

using namespace std;

void func1(int a[], int size) {

  for(int i = 0; i < size; i++)

    a[i] = i * i - i;

}

void func2(int* a, int size) {

  for(int i = 0; i < size; i++)

    a[i] = i * i + i;

}

void print(int a[], string name, int size) {

  for(int i = 0; i < size; i++)

    cout << name << "[" << i << "] = "

         << a[i] << endl;

}

int main() {

  int a[5], b[5];

// probabilmente valori spazzatura :

 

  print(a, "a", 5);

  print(b, "b", 5);

// inizializzazione degli array : 

  func1(a, 5);

  func1(b, 5);

  print(a, "a", 5);

  print(b, "b", 5);

// si noti che gli array sono sempre modificati : 

  func2(a, 5);

  func2(b, 5);

  print(a, "a", 5);

  print(b, "b", 5);

} ///:~

 

anche se func1( ) e func2( ) dichiarano i loro argomenti differentemente, luso è lo stesso dentro la funzione. Ci sono alcuni altri problemi che questo esempio rivela : gli array non possono essere passati per valore[32], cioè, non si ottiene mai automaticamente una copia locale di un array che si passa in una funzione. Quindi, quando si modifica un array, si sta modificando sempre loggetto esterno. Questo può essere un po confusionario allinizio, se ci si aspetta un passaggio per valore fornito di argomenti ordinari.

Si noti che  print( ) usa la sintassi con le parentesi quadre per gli argomenti dellarray. Anche se la sintassi del puntatore e la sintassi con le parentesi quadre sono effettivamente le stesse quando si passano gli array come argomenti, la sintassi con le parentesi quadre lo rende più chiaro al lettore che si intende questo argomento essere un array.

Si noti anche che largomento size è passato in ogni caso. Passare solamente lindirizzo di un array non è un'informazione sufficiente; si deve sempre essere in grado di sapere quanto grande è un array dentro la nostra funzione, così non si corre alla fine dellarray per saperlo.

Gli array possono essere di ogni tipo, incluso array di puntatori. Infatti, quando si vuole passare argomenti della riga di comando nel proprio programma, il C ed il C++ hanno una speciale lista  di argomenti per main( ), che assomiglia a questa:

int main(int argc, char* argv[]) { // ...

 

il primo argomento è il numero di elementi nellarray, il quale è il secondo argomento. Il secondo argomento è sempre un array di  char*, perché gli argomenti sono passati da una riga di comando come array di caratteri ( e si ricordi, un array può essere passato solo come puntatore ). Ogni cluster di caratteri  delimitati da  spazi bianchi nella riga di comando è trasformata in un argomento dellarray separato. Il seguente programma stampa tutti gli argomenti della riga di comando procedendo lungo larray :

//: C03:CommandLineArgs.cpp

#include <iostream>

using namespace std;

int main(int argc, char* argv[]) {

  cout << "argc = " << argc << endl;

  for(int i = 0; i < argc; i++)

    cout << "argv[" << i << "] = "

         << argv[i] << endl;

} ///:~

 

si noterà che argv[0] è il path ed il nome stesso del programma. Questo permette al programma di scoprire informazioni su di esso. Esso aggiunge anche allarray gli argomenti del programma, quindiun comune errore quando si va a prendere gli argomentti della riga di comando è afferrare argv[0] quando si vuole argv[1].

Non si è obbligati ad usare argc e argv come identificatori nel main( ), questi identificatori sono solo convenzioni ( ma confonderanno la gente se non li si usano). Ancora, cè un modo alternativo per dichiarare argv:

int main(int argc, char** argv) { // ...

 

entrambi le forme sono equivalenti, ma io trovo che la versione usata in questo libro è la più intuitiva quando si legge il codice, poiché essa dice, direttamente , questo è un array di puntatori di caratteri.

Tutto quello che si ottiene dalla riga di comando sono array di carattere; se si vuole trattare un argomento come un altro tipo, si è responsabili della conversione dentro il proprio programma. Per facilitare la conversione in numeri, ci sono alcune funzioni utili nella libreria dello Standard C, dichiarata in <cstdlib>. Le più semplici da usare sono atoi( ), atol( ), e atof( )     per convertire un array di carattere ASCII in un  int, long, ed un valore in virgola mobile double, rispettivamente. Ecco qui un esempio che usa atoi( ) ( le altre due funzioni sono chiamate allo stesso modo) :

//: C03:ArgsToInts.cpp

// convertire gli argomenti della riga di comando in int

#include <iostream>

#include <cstdlib>

using namespace std;

int main(int argc, char* argv[]) {

  for(int i = 1; i < argc; i++)

    cout << atoi(argv[i]) << endl;

} ///:~

in questo programma, si può mettere qualsiasi numero di argomenti nella riga di comando. Si noterà che il ciclo for comincia al valore 1 per saltare il nome del programma in argv[0]. Ancora, se si pone un numero in virgola mobile contenente la virgola nella riga di comando, atoi( )  prende solo le digitazioni fino alla virgola. Se si mettono non-numeri nella riga di comando questi ritornano dietro da  atoi( ) come zero.  

 

Esplorare il formato in virgola mobile

La funzione printBinary( ) introdotta prima in questo capitolo è pratica per fare ricerche dentro la struttura interna di vari tipi di dato. La più interessante tra queste è il formato in virgola mobile che permette al C e al C++ di salvare numeri rappresentati valori molto grandi e molto piccoli in un limitato ammontare di spazio. Sebbene i dettagli non possono essere completamente esposti qui, i bit dentro i float e i double sono divisi dentro tre regioni: lesponente, la mantissa ed il bit di segno;quindi esso memorizza i valori che usano una notazione scientifica. Il programma seguente permette di giocare qua e là stampando i pattern binari di vari numeri in virgola mobile in modo che si possa dedurre lo schema usato nel formato in virgola mobile del propriocompilatore ( di solito questo è lo standard IEEE per i numeri in virgola mobile, ma il proprio compilatore potrebbe non seguirlo) : 

//: C03:FloatingAsBinary.cpp

//{L} printBinary

//{T} 3.14159

#include "printBinary.h"

#include <cstdlib>

#include <iostream>

using namespace std;

int main(int argc, char* argv[]) {

  if(argc != 2) {

   cout << "si deve fornire un numero" << endl;

    exit(1);

  }

  double d = atof(argv[1]);

  unsigned char* cp =

    reinterpret_cast<unsigned char*>(&d);

  for(int i = sizeof(double)-1; i >= 0 ; i -= 2) {

    printBinary(cp[i-1]);

    printBinary(cp[i]);

  }

} ///:~

primo, il programma garantisce che si è dato un argomento controllando il valore di argc, il quale vale due se cè un singolo argomento ( vale uno se non ci sono argomenti, poiché il nome del programma è sempre il primo elemento di argv). Se fallisce, un messaggio vienestampato e la funzione exit( ) della  libreria dello standard C viene chiamata per terminare il programma.

Il programma prende largomento dalla riga di comando e converte i caratteri in double usando atof( ). Dopo il double è trattato come un array di byte prendendo lindirizzo e cambiandolo in un  unsigned char*. Ognuno di questi byte è passato a printBinary( ) per la visualizzazione.

Questo esempio è stato impostato per stampare i byte in un ordine tale che il bit del segno appare per primo sulla mia macchina. Il proprio potrebbe essere differente, quindi si potrebbe ri-arrangiare il modo in cui le cose vengono stampate. Si dovrebbe essere consapevoli che i formati in virgola mobile sono non facili da comprendere; ad esempio, lesponente e la mantissa non sono generalmente arrangiati sul byte, ma invece un numero di bit è riservato per ognuno e sono immagazzinati nella minor memoria possibile. Per vedere veramente cosa succede, si avrebbe bisogno di trovare la grandezza di ogni parte del numero ( i bit di segno sono sempre un bit, ma gli esponenti e le mantisse sono di differenti grandezze ) e stampare i bit in ogni parte separatamente.

Il puntatore aritmetico

Se tutto quello che si potesse fare con un puntatore che punta ad un array è trattarlo come se fosse uno pseudonimo dellarray, i puntatori ad array non sarebbero molto interessanti. Tuttavia, i puntatori sono molto più flessibili, poiché essi possono essere modificati per puntare altrove ( ma si ricordi, lidentificatore dellarray non può essere modificato per puntare da qualche altra parte).

I puntatori aritmetici si riferiscono allapplicazione di alcuni degli operatori aritmetici ai puntatori. La ragione per cui il puntatore aritmetico è un soggetto separato da un ordinario aritmetico è che i puntatori devono conformarsi a speciali costrizioni in modo da funzionare correttamente Ad esempio, un comune operatore che si usa con i puntatori è ++, il quale siginifica aggiungi uno al puntatore . Quello che questo realmente significa è che il puntatore è cambiato per puntareal prossimo valore , qualunque cosa significhi. Ecco un esempio :

//: C03:PointerIncrement.cpp

#include <iostream>

using namespace std;

int main() {

  int i[10];

  double d[10];

  int* ip = i;

  double* dp = d;

  cout << "ip = " << (long)ip << endl;

  ip++;

  cout << "ip = " << (long)ip << endl;

  cout << "dp = " << (long)dp << endl;

  dp++;

  cout << "dp = " << (long)dp << endl;

} ///:~

per una esecuzione sulla mia macchina, loutput è :

ip = 6684124

ip = 6684128

dp = 6684044

dp = 6684052

 

quello che è interessante qui è che anche se loperazione ++ appare essere la stessa operazione per gli entrambi   int*  e double*, si può vedere che il puntatore era cambiato solo di 4 byte per int*  ma 8 byte per double*. Non allo stesso tempo, queste sono le grandezze int e double sulla mia macchina. E questoè il trucco del puntatore aritmetico : il compilatore calcola il corretto ammontare per cambiare il puntatore così esso punta al prossimo elemento nellarray( il puntatore aritmetico è solo significativo dentro gli array). Ciò funziona anche con array di struct :

//: C03:PointerIncrement2.cpp

#include <iostream>

using namespace std;

typedef struct {

  char c;

  short s;

  int i;

  long l;

  float f;

  double d;

  long double ld;

} Primitives;

int main() {

  Primitives p[10];

  Primitives* pp = p;

  cout << "sizeof(Primitives) = "

       << sizeof(Primitives) << endl;

  cout << "pp = " << (long)pp << endl;

  pp++;

  cout << "pp = " << (long)pp << endl;

} ///:~

 

Loutput per una esecuzione sulla mia macchina era:

sizeof(Primitives) = 40

pp = 6683764

pp = 6683804

 

così si può vedere che il compilatore fa ancora la giusta cosa per puntatori a struct ( e class e union ).

Il puntatore aritmetico funziona anche con loperatore --, +, e -, ma gli ultimi due operatori sono limitati : non si possono addizionare due puntatori e se si sottraggono i puntatori il risultato è il numero di elementi tra i due puntatori. Comunque, si può aggiungere o sottrarre un valore intero e un puntatore. Ecco un esempio per dimostrare luso dei puntatori aritmetici :

//: C03:PointerArithmetic.cpp

#include <iostream>

using namespace std;

#define P(EX) cout << #EX << ": " << EX << endl;

int main() {

  int a[10];

  for(int i = 0; i < 10; i++)

    a[i] = i; // Dare i valori indice

  int* ip = a;

  P(*ip);

  P(*++ip);

  P(*(ip + 5));

  int* ip2 = ip + 5;

  P(*ip2);

  P(*(ip2 - 4));

  P(*--ip2);

P(ip2 - ip); // Produce un numero di elementi

} ///:~

 

esso comincia con un altra macro, ma questa usa una caratteristica del preprocessore chiamata stringizing ( implementata con il segno # prima di una espressione ) che prende qualsiasi espressione e la cambia in un array di caratteri. Ciò è abbastanza utile, poiché permette allespressione di essere stampata, seguita da due punti, seguita dal valore dellespressione. In main( ) si può vedere lutile stenografia che è stata prodotta.

Sebbene le versioni di prefisso e di suffisso di ++ e - sono valide con i puntatori, solo le versioni di prefisso sono usati in questo esempio perchè sono applicate prima che i puntatori siano dereferenziati nellespressione di sopra, così ci permettono di vedere gli effetti delle operazioni. Si noti che solo i valori interi erano stati aggiunti o sottratti; se due puntatori fossero combinati il compilatore non lavrebbe permesso.

Ecco loutput per il programma di sopra :

*ip: 0

*++ip: 1

*(ip + 5): 6

*ip2: 6

*(ip2 - 4): 2

*--ip2: 5

in tutti i casi, i puntatori arimetici risultano nel puntatore che è adattato a puntare al posto giusto , basato sulla grandezza degli elementi a cui sta puntando.

Se il puntatore aritmetico sembra un pò opprimente allinizio, non ci si deve preoccupare. La maggior parte delle volte che si avrà bisogno di creare array ed indici dentro essi con [ ], di solito si avrà bisogno al massimo di ++ e --. Il puntatore aritmetico è generalmente riservato per programmi più intelligenti e complessi, e molti dei contenitori nella libreria standard C++ nascondono la mggioranza di questi dettagli intelligenti quindi non ci si deve preoccupare di essi.

 

Suggerimenti per debugging

 

In uno ambiente ideale, si ha un eccellente debugger disponibile che rende facilmente trasparente il comportamento del proprio programma così si possonopresto scoprire gli errori. Tuttavia, molti debugger hanno punti deboli, e questi  richiederanno di includere frammenti di codice nel proprio programma per aiutarci a capire cosa succede. In più, può capitare di sviluppare in un ambiente ( come un sistema embedded, dove io ho speso i miei anni formativi ) che non ha debugger disponibili e forse un feedback molto limitato (es. un display LED ad una riga ). In questi casi si diventa creativi nei modi in cui si scoprono e si visualizzano informazioni sullevoluzione del proprio programma. Questa sezione suggerisce alcune tecniche per fare ciò.

I flag di debugging

Se si cabla  il codice di debugging nel programma, si può incorrere in alcuni problemi. Si inizia ad ottenere troppe informazioni, che rendono difficile isolare i bug. Quando si pensa di aver trovato il bug si inizia a strappare via il codice di debugging, per capire poi che c'è bisogno di rimetterlo di nuovo. Si possono risolvere questi problemi con due tipi di flag : i flag di debug del preprocessore e i flag di debug a runtime.

I flag di debugging del preprocessore

Usando le #define per defiire uno o più flag di debugging ( preferibilmente in un header file ) si può provare a testare un flag usando una dichiarazione #ifdef ed includere il codice di debugging condizionalmente. Quando si pensa che il nostro debugging è finito, si può semplicemente usare #undef  per il/i flag ed il codice automaticamente sarà rimosso ( e si ridurrà la grandezza e il tempo di esecuzione del proprio eseguibile).

E meglio decidere sui nomi per le flag di debug prima che si inizi a costruire il proprio progetto così i nomi saranno consistenti. I flag del preprocessore sono tradizionalmente distinguibili dalle variabili scrivendoli con sole lettere maiuscole. Una comune nome di flag è semplicemente DEBUG (ma sia faccia attenzione a non usare NDEBUG, il quale è riservato in C). La sequenza di istruzioni potrebbe essere:

#define DEBUG // Probabilmente in un header file

//...

#ifdef DEBUG // controllare per vedere se il flag è definito

/* qui il codice di debugging */

#endif // DEBUG

La maggior parte delle implementazioni del C e C++ permetteranno di utilizzare #define e #undef dalla riga di comando del compilatore, quindi si può ricompilare il codice ed inserire linformazione di debugging con un singolo comando ( preferibilmente con il makefile, uno strumento che sarà descritto tra breve). Si controlli la propria documentazione per dettagli.

 

Le flag di debugging a tempo di esecuzione

In alcune situazioni è più conveniente accendere o spegnere i flag del debug durante lesecuzione del programma, settandoli quando il programma inizia usando la riga di comando. I programmi grandi sono noiosi da ricompilare solo per inserire il codice di debug.

Per accendere e spegnere il codice di debug dinamicamente, creare le flag bool :

//: C03:DynamicDebugFlags.cpp

#include <iostream>

#include <string>

using namespace std;

// i flag di debug non sono necessariamente globali :

bool debug = false;

int main(int argc, char* argv[]) {

  for(int i = 0; i < argc; i++)

    if(string(argv[i]) == "--debug=on")

      debug = true;

  bool go = true;

  while(go) {

    if(debug) {

     

 // codice di debug qui

      cout << "Debugger è on!" << endl;

    } else {

      cout << "Debugger è off." << endl;

    } 

    cout << "Stato debugger [on/off/quit]: ";

    string reply;

    cin >> reply;

   if(reply == "on") debug = true; // accendere

    if(reply == "off") debug = false; // spegnere

    if(reply == "quit") break; // Fuori dal 'while'

  }

} ///:~

questo programma permette di accendere e spegnere i flag di debugging fino a che si scrive quit (esci) per dire che se si vuole uscire. Si noti che esso richiede che siano scritte dentro le parole intere, non solo lettere ( si può accorciarle ad una lettera se lo si desidera ). Ancora, largomento della riga di comando può opzionalmente essere usata per accendere il debugging allinizializzazione questo argomento può apparire in ogni posto della riga di comando, poiché  il codice di inizializzazione in main( ) guarda  tutti gli argomenti. La dimostrazione è abbastanza semplice :

string(argv[i])

 

questo prende larray di carattere  argv[i] e crea una string, la quale dopo può essere facilmente confrontata  con la parte di destra di ==. Il programma di sopra cerca lintera stringha --debug=on . Così si può anche cercare --debug= e dopo vedere cosa c'è dopo, per fornire più opzioni. Il Volume 2 (disponibile da www.BruceEckel.com) dedica un capitolo alla classe string dello Standard C++.

Sebbene un flag di debugging è una delle poche aree dove  ha molto senso usare una variabile globale, non cè niente che dice che esso deve essere in quel modo. Si noti che la variabile è scritta in lettere  minuscole  per ricordare al lettore che non è un flag del preprocessore.

Convertire le variabili e le espressioni in stringhe

Turning variables and expressions into strings

Quando si scrive il codice di debugging è noioso scrivere le espressioni di stampa consistenti in array di caratteri contenenti il nome della variabile seguita dalla variabile. Fortunatamente, lo Standard C include loperatore stringize #, usato inizialmente in questo capitolo. Quando si pone una # prima di un argomento in una macro del preprocessore, il preprocessore cambia quel argomento in un array di caratteri. Questo, combinato con il fatto che gli array di caratteri con nessun intervento di punteggiatura, permette di rendere una macro più utile al fine di stampare i valori di variabili durante il debugging : 

#define PR(x) cout << #x " = " << x << "\n";

 

se si stampa la variabile a chiamando la macro PR(a), si avrà lo stesso effetto del codice :

cout << "a = " << a << "\n";

questo stesso processo funziona con intere espressioni. Il programma seguente usa una macro per creare una stenografia che stampa lespressione stringized e dopo valuta lespressione e ne stampa il risultato :

//: C03:StringizingExpressions.cpp

#include <iostream>

using namespace std;

#define P(A) cout << #A << ": " << (A) << endl;

int main() {

  int a = 1, b = 2, c = 3;

  P(a); P(b); P(c);

  P(a + b);

  P((c - a)/b);

} ///:~

 

si può vedere come una tecnica come questa diventa presto indispensabile, specialmente se si non hanno debugger ( o si devono usare ambienti di sviluppomultipli): si può anche inserire un #ifdef  per definire P(A) come nulla quando si vuole rimuovere il debugging.

La macro assert( ) del C

Nello standard file principale <cassert> si troverà assert( ), il quale è una utile macro di debugging. Quando si usa assert( ), si dà un argomento che è una espressione che si asserisce essere vera . Il preprocessore genera il codice che proverà lasserzione. Se lasserzione non è vera, il programma si fermerà dopo aver prodotto un messaggio di errore evidenziano quale asserzione è fallita. Ecco qui un esempio:

//: C03:Assert.cpp

// uso della macro di debugging assert()

#include <cassert>  // Contiene la macro

using namespace std;

int main() {

  int i = 100;

assert(i != 100); // Fallisce

} ///:~

il comando originato nello standard C, è così disponibile nel header file assert.h.

Quando è finito il debugging, si può rimuovere il codice generato dalla macro ponendo la riga :

 

#define NDEBUG

 

nel programma prima dellinclusione di <cassert>, o definendo NDEBUG nella riga di comando del compilatore. NDEBUG è un flag usato in <cassert> per cambiare il modo in cui il codice è generato dalle macro.

In seguito in questo libro, si vedranno alcune alternative più sofisticate di assert( ).

Indirizzi della funzione

Una volta che una funzione viene compilata e caricata nel computer per essere eseguita, esso occupa un pezzo di memoria. Questa memoria, e quindi la funzione, ha un indirizzo.

Il C non è mai stato un linguaggio per sbarrare lentrata dove gli altri avevano paura di leggere. Si possono usare gli indirizzi della funzione con i puntatori proprio come si possono usare gli indirizzi delle variabili. La dichiarazione e luso dei puntatori a funzione appaiono un pò opachi inizialmente, ma seguono il formato del resto del linguaggio.

Definire un puntatore a funzione

Per definire un puntatore ad una funzione che non ha nessun argomento e nessun valore di ritorno, si scrive:

void (*funcPtr)();

 

quando si cerca una definizione complessa come questa, il modo migliore per attaccarla è iniziare nel mezzo e trovare l'uscita. “Cominciando dal mezzo significa iniziare dal nome della variabile, che è funcPtr . Trovare l'uscita significa cercare a destra per loggetto più vicino ( nessuno in questo caso; la parentesi destra ci ferma bruscamente ), poi guardare a sinistra ( un puntatore denotato con lasterisco), poi guardare a destra( una lista di argomenti vuota indicante una funzione che non prende argomenti), poi guardare a sinistra ( void, che indica la funzione che non ha valore di ritorno). Questo movimento a destra e a sinistra funziona con la maggio parte delle dichiarazioni.

Per esaminare, cominciare in mezzo (funcPtr è un  ...), si vada a destra (niente la - ci si fermerà dalla parentesi destra), andare a sinistra e trovare "*" ( punta a ), andare a destra e trovare lista di argomenti vuota ( la funzione che non prende argomenti …”), andare a sinistra e trovare il void (funcPtr è un puntatore ad una funzione che non prende argomenti e restituisce un void ).

Ci si può chiedere perché *funcPtr richiede le parentesi. Se non le si usava, il compilatore avrebbe visto :

void *funcPtr();

 

si potrebbe dichiarare una funzione  ( che restituisce un  void*) piuttosto che definire una variabile. Il compilatore esegue lo stesso processo che facciamo noi quando capisce quale dichiarazione o definizione è supposta essere. Esso ha bisogno di queste parentesi per sbatterci contro così esso torna a  sinistra  e trova la   *, invece di continuare a destra e trovare una lista di argomenti vuota.

Definizioni e dichiarazioni complesse

Come digressione, una volta che si capito come funzionano le sintassi di dichiarazione in C e C++ si possono creare oggetti molto più complicati. Ad esempio :

//: C03:ComplicatedDefinitions.cpp

// definizioni complicate

/* 1. */     void * (*(*fp1)(int))[10];

/* 2. */     float (*(*fp2)(int,int,float))(int);

/* 3. */     typedef double (*(*(*fp3)())[10])();

             fp3 a;

/* 4. */     int (*(*f4())[10])();

int main() {} ///:~

si cammini attraverso ognuno e si usi la linea direttiva destra-sinistra. Il numero 1 dice fp1 è un puntatore ad una funzione che prende un argomento intero e restituisce un puntatore ad un array di 10 puntatori void .

Il numero 2 dice fp2 è un puntatore ad una funzione che prende tre argomenti (int, int, e float) e restituisce un puntatore a funzione che prende un argomento intero e restituisce un float .

Se si stanno creando molte definizioni complicate, si potrebbe voler usare un typedef. Il numero 3 mostra come un typedef risparmia di scrivere ndo la descrizione complicata ogni volta. Esso dice fp3 è un puntatore alla funzione che non prende argomenti e restituisce un puntatore ad un array di 10 puntatori a funzione che non prendono argomenti  e restituisce double. Esso dopo dice a è uno di questi tipi fp3. Typedef è generalmente utile per costruire descrizioni complicate da quelle semplici.

Il numero 4 è una dichiarazione di funzione invece di una definizione di variabile. Esso dice f4 è una funzione che restituisce un puntatore ad un array di 10  puntatori a funzione che restituisce interi. 

Raramente si avrà bisogno di dichiarazioni  e definizioni complicate come queste. Comunque, se con lesercizio  non si sarà disturbati da espressione complicate che si potrebbero incontrare nella vita reale.

Usare un puntatore a funzione

Una volta definito un puntatore a funzione, si deve assegnarlo ad un indirizzo di una funzione prima di poterlo usare. Proprio come l'indirizzo di un array arr[10] è prodotto dal nome dellarray senza le parentesi (arr), lindirizzo di una funzione func() è prodotto dal nome della funzione senza largomento lista (func). Si può anche usare una sintassi più esplicita &func(). Per chiamare una funzione, si dereferenzia il puntatore allo stesso modo in cui lo si è dichiarato ( si ricordi che il C ed il C++ provano sempre a rendere laspetto delle definizioni identico al modo in cui sono usate ). Il seguente esempio mostra come un puntatore ad una funzione è definita ed usata :

//: C03:PointerToFunction.cpp

// definire e usare un puntatore ad una funzione

#include <iostream>

using namespace std;

void func() {

 cout << "func() chiamata..." << endl;

}

int main() {

 void (*fp)();  // definire un puntatore a funzione

  fp = func;  // Inizializzarlo

  (*fp)();    // dereferenziare chiama la funzione

  void (*fp2)() = func;  // Definire ed inizializzare

  (*fp2)();

} ///:~

dopo che il puntatore alla funzione fp è chiamato, è assegnato allindirizzo della funzione func() usando fp = func (si noti che la lista degli argomenti manca nel nome della funzione ). Il secondo caso mostra simultaneamente la definizione e linizializzazione.

 

Array di puntatori a funzioni

Uno dei più interessanti costrutti che si possono creare è un array di puntatori a funzioni. Per selezionare una funzione, si indicizza larray e si dereferenzia il puntatore. Questo supporta il concetto di codice a tabella (table-driven code); invece di usare i condizionali e le dichiarazioni del caso, si selezionano le funzioni da eseguire basata su un variabile di stato ( o una combinazione di variabili di stato). Questo tipo di approccio può essere utile se spesso si aggiungono o si eliminano funzioni dalla tabella ( o se si vuole di creare o cambiare così una tabella dinamicamente).

Il seguente esempio crea alcune funzioni finte usando una macro del preprocessore, che crea un array di puntatori a queste funzioni che usano linizializzazione aggregata automatica. Come si può vedere, è facile aggiungere o rimuovere funzioni dalla tabella ( e così, funzionalmente dal programma) cambiando poco del codice :

//: C03:FunctionTable.cpp

// usare un array di puntatori a funzioni

#include <iostream>

using namespace std;

// una macro per definire funzioni dummy :

#define DF(N) void N() { \

 

 cout << "funzione " #N " chiamata..." << endl; }

DF(a); DF(b); DF(c); DF(d); DF(e); DF(f); DF(g);

void (*func_table[])() = { a, b, c, d, e, f, g };

int main() {

  while(1) {

  cout << "premi un tasto da 'a' fino a 'g' "

      "oppure q per uscire" << endl;

    char c, cr;

    cin.get(c); cin.get(cr); // un secondo per CR

  

    if ( c == 'q' )

           break; // ... fuori dal while(1)

    if ( c < 'a' || c > 'g' )

      continue;

    (*func_table[c - 'a'])();

  }

} ///:~

a questo punto, si potrebbe immaginare come questa tecnica possa essere utile quando si crea qualche tipo di interprete o programmi per processare liste.

Make: gestire compilazioni separate

Quando si usa una compilazione separata ( spezzando il codice in un numero di unità di traduzione), si ha bisogno in qualche modo di compilare automaticamente ogni file e dire al linker di costruire tutti i pezzi- insieme con le librerie appropriate ed il codice di inizio in un file eseguibile. La maggior parte dei compilatori permettono di fare ciò con una singola instruzione della riga di comando. Per il compilatore C++ GNU, ad esempio, si potrebbe scrivere :

g++ SourceFile1.cpp SourceFile2.cpp

 

il problema con questo approccio è che il compilatore compilerà per primo ogni file individuale, nonostante che quel file ha bisogno di essere ricostruito o no. Con molti file in un progetto, può diventare proibitivo ricompilare tutto se è cambiato solo un singolo file.

La soluzione a questo problema, sviluppato in Unix disponibile dovunque in alcune forme, è un programma chiamato make. Lutilità di make è che amministra tutti i file individuali in un progetto seguendo le istruzioni in un file testo chiamato makefile. Quando si editano alcuni dei file nel progetto e si scrive make, il programma make segue le linee direttive nel makefile per confrontare le data nei file del codice sorgente con le date dei file del target corrispondente, e se un file del codice sorgente è più recente del suo file target, make invoca il compilatore sul file del codice sorgente. Make ricompila solo i file del codice sorgente che erano cambiati, ed ogni altro file di codice sorgente che sono modificati. Usando make, non si deve ricompilare tutti i file nel proprio progetto ogni volta che si fa un cambiamento, ne si deve controllare che ogni cosa siacostruita propriamente. Il makefile contiene tutti i comandi per mettere il proprio progetto insieme. Imparare ad usare make farà risparmieremolto tempo e frustazioni. Si scoprirà anche che make è un tipico modo in cui si installa un nuovo software sulla macchina Linux/Unix ( sebbene questi makefile tendono ad essere molto più complicati da quelli presenti in questo libro, e spesso automaticamente si genererà un makefile per la propria particolare macchina come parte del processo di istallazione ).

Poichè make è disponibile in qualche forma virtualmente per tutti i compilatori C++ ( anche se non lo è, si possono usare i make liberamente disponibili con qualsiasi compilatore), sarà lo strumento usato dappertutto in questo libro. Comunque, i venditori del compilatore hanno anche creato il loro tool per costruire progetti. Questi tool richiedono quali file sono nel proprio progetto e determinano tutte le relazioni tra gli stessi. Questi tool usano qualcosa di simile ad un makefile, generalmente chiamato file progetto (project file ) , ma lambiente di programmazione mantiene questo file quindinon cè da preoccuparsi di esso. La configurazione e luso dei file progetto variano da un ambiente di sviluppo allaltro, quindi si deve trovare una documentazione appropriata su come usarli ( sebbene questi tool sono di solito così semplici da usare che si può apprendere giocandoci qua e la la mia favorita forma di studio).

I makefile usati in questo libro dovrebbero funzionare anche se si sta usando un tool specifico.

Make activities

Quando si scrive make ( o qualsiasi nome abbia il proprio programma make  ), il programma make cerca nella corrente directory un file chiamato makefile, il quale si è creato se questo è il proprio progetto. Questo file elenca le dipendenze tra i file del codice sorgente. make guarda le date dei file. Se un file dipendente ha una data più vecchia del file da cui dipende, make esegue la regola data dopo la dipendenza.

Tutti i commenti nel makefile iniziano con # e continuano fino alla fine della riga.

Come semplice esempio, il makefile per un programma chiamato hello potrebbe contenere:

# un commento

hello.exe: hello.cpp

        mycompiler hello.cpp

ciò dice che hello.exe(il target)  dipende da hello.cpp  . Quando hello.cpp ha una data più nuova di hello.exe , make esegue la regola mycompiler hello.cpp . Potrebbero esserci dipendenze e regole multiple. Molti programmi make richiedono che tutte le regole comincino con una etichetta. Oltre ciò, lo spazio bianco è generalmente ignorato quindi si può formattare per la leggibilità.

Le regole non sono ristrette ad essere chiamate al compilatore; si può chiamare qualsiasi programma dentro make. Creando gruppi di insiemi di regole-dipendenze interdipendenti, si possono modificare i file del proprio codice sorgente, scrivere make ed essere certi che tutti i file interessati saranno ricostruiti correttamente.

 

Le macro

Un makefile contiene le macro ( si noti che queste sono completamente differenti dalle macro del processore C/C++). Le macro permettono il rimpiazzo delle stringhe. I makefile in questo libro usano una macro per chiamare il compilatore del C++. Ad esempio :

CPP = mycompiler

hello.exe: hello.cpp

        $(CPP) hello.cpp

= è usato per identificare CPP come una macro, e $ e le parentesi espandono la macro. In questo caso, lespansione significa che la chiamata della macro  $(CPP) sarà sostituita con la stringa mycompiler . Con la macro sopracitata, se si vuole cambiare in una differente chiamata al compilatore cpp, si cambia solo la macro in :

CPP = cpp

Si possono aggiungere alla macro le flag del compilatore, o usare le macro per aggiungere i flag del compilatore.

 

Le regole di suffisso

Diventa noioso dire a make di invocare il compilatore per ogni singolo file cpp del proprio progetto, quando si sa che è lo stesso progetto ogni volta. Poiché make è progettato per far risparmiare tempo, esso ha anche un modo per abbreviare le azioni, sempre che esse dipendano dai suffissi del nome del file. Queste abbreviazioni sono chiamate regole del suffisso. Una regola del suffisso è un modo per insegnare al make come convertire un file con un tipo di estensione (.cpp ad esempio) in un file con un altro tipo di estensione  (.obj o .exe). Una volta che si insegna a make le regole per produrre un tipo di file da un altro, tutto quello che si deve fare è dire a make quali sono le dipendenze tra i file. Quando make trova un file con una data più recente di un file da cui dipende, esso usa una regola per creare un nuovo file.

La  regola di suffisso dice a make che non ha bisogno di esplicitare le regole per costruire ogni cosa, ma invece può capire come costruire le cose basate sulla estensione del file. In questo caso esso dice per costruire un file che termina in exe da uno che termina con cpp, invoca il seguente comando“. Ecco qui a cosa assomiglia lesempio sopracitato :

CPP = mycompiler

.SUFFIXES: .exe .cpp

.cpp.exe:

        $(CPP) $<

La direttiva .SUFFIXES dice a make che esso dovrebbe stare attento a qualsiasi delle seguenti estensioni del nome del  file perché essi hanno un significato speciale per questo particolare makefile. In seguito si vede la regola di suffisso .cpp.exe, che dice qui è come si converte qualsiasi file con una estensione cpp come in uno con un estensione exe ( quando il file cpp è più recente di quello exe). Come prima, la macro $(CPP)  è usata, ma poi si vede qualcosa di nuovo. $<. Poichè inizia con una $ , esso è una macro, ma questa è una delle macro speciali predefinite di make. La $< può essere usata solo nelle regole di suffisso, e significa qualunque prerequisito innesca la regola ( qualche volta chiamata dipendente) ,che in questo caso traduce il file cpp che ha bisogno di essere compilato.

Una volta che le regole di suffisso sono state impostate, si può semplicemente dire, ad esempio, make Union.exe,  e la regola di suffisso sarà immessa, anche se non c‘è nessuna menzione di Union da qualsiasi parte nel makefile.

Target di Default

Dopo le macro e le regole di suffisso, make cerca il primo target in un file, e lo costruisce, a meno che si specifica differentemente. Quindi il seguente makefile :

CPP = mycompiler

.SUFFIXES: .exe .cpp

.cpp.exe:

        $(CPP) $<

target1.exe:

target2.exe:

se si scrive solo "make", allora target1.exe sarà costruito ( usando la regola del suffisso di default ) perché questo è il primo target che make incontra. Per costruire target2.exe si dovrebbe dire esplicitamente make target2.exe. Ciò diventa noioso, così normalmente si crea un targetfinto di default che dipende da tutto il resto dei target, come questo :

CPP = mycompiler

.SUFFIXES: .exe .cpp

.cpp.exe:

        $(CPP) $<

all: target1.exe target2.exe

qui, all non esiste e non cè alcun file chiamatoall, quindi ogni volta che si scrive make, il programma vedeall come primo target nella lista ( e quindi il target di default), dopo esso vede che all non esiste quindi deve controllare tutte le dipendenze. Quindi esso guarda al target1.exe e (usando la regola di suffisso) guarda sia che (1) target1.exe esiste e se (2) target1.cpp è più recente di target1.exe, e così esegue la regola di suffisso ( se si fornisce una regola esplicita per un obiettivo particolare, quella regola è invece usata). Dopo esso passa al prossimo file nella lista dei target di default ( tipicamente chiamata all per convenzione, ma si può chiamarla con qualunque nome) si può costruire ogni eseguibile del proprio progetto semplicemente scrivendo make. In più , si possono   avere altre liste dei target non di default che fanno altre cose - ad esempio, si può settarla così scrivendo make debug che ricostruisce tutti i file che hanno allinterno il debugging.

I makefile di questo libro

Usando il programma ExtractCode.cpp del Volume 2 di questo libro, tutti i codici elencati in questo libro sono automaticamente estratti da lla versione di testo ASCII di questo libro e posti in subdirectory secondo i loro capitoli. In più, ExtractCode.cpp crea diversi makefile in ogni subdirectory ( con differenti nomi) così ci si può semplicemente spostare dentro questa subdirectory e scrivere make -f mycompiler.makefile ( sostituendo  mycompiler’ con il nome del proprio compilatore, la flag -f dice usa quello che segue come un makefile). Infine, ExtractCode.cpp crea un makefile master nella directory principale dove i file del libro sono stati espansi, e questo makefile ha origine in ogni subdirectory chiama make con lappropriato makefile. Questo modo in cui si può compilare tutti i codici invocando un singolo comando make, e il processo si fermerà ogni volta che il nostro compilatore è incapace di gestire un file particolare ( si noti che un compilatore conforme al C++ dovrebbe essere capace di compilatore tutti i file in questo libro). Poiché gli esempi di make variano da sistema a sistema, solo le caratteristiche base e più comuni sono usate nei makefile generati.

 

Un esempio di makefile

Come menzionato, lo strumento di estrazione del codice ExtractCode.cpp automaticamente genera i makefile per ogni capitolo. A causa di ciò, i makefile per ogni capitolo non saranno posti nel libro ( tutti i makefile sono confezionati con il codice sorgente, il quale si può scaricare da www.BruceEckel.com ). Comunque, è utile vedere un esempio di un makefile. Quello che segue è una versione ridotta di uno che era automaticamente generato per questo capitolo dallo strumento di estrazione del libro. Si troverà più di un makefile in ogni sotto-directory (essi hanno nomi differenti; si invoca un nome specifico con make-f). Questo è per il C++ GNU :

CPP = g++

OFLAG = -o

.SUFFIXES : .o .cpp .c

.cpp.o :

  $(CPP) $(CPPFLAGS) -c $<

.c.o :

  $(CPP) $(CPPFLAGS) -c $<

all: \

  Return \

  Declare \

  Ifthen \

  Guess \

  Guess2

# il resto dei file per questo capitolo non sono mostrati

Return: Return.o

  $(CPP) $(OFLAG)Return Return.o

Declare: Declare.o

  $(CPP) $(OFLAG)Declare Declare.o

Ifthen: Ifthen.o

  $(CPP) $(OFLAG)Ifthen Ifthen.o

Guess: Guess.o

  $(CPP) $(OFLAG)Guess Guess.o

Guess2: Guess2.o

  $(CPP) $(OFLAG)Guess2 Guess2.o

Return.o: Return.cpp

Declare.o: Declare.cpp

Ifthen.o: Ifthen.cpp

Guess.o: Guess.cpp

Guess2.o: Guess2.cpp

La macro CPP è settata al nome del compilatore. Per usare un differente compilatore, si può editare il makefile o cambiare il valore della macro nella riga di comando, come segue :

make CPP=cpp

si noti, comunque che ExtractCode.cpp ha uno schema automatico per costruire automaticamente i makefile per i compilatori addizionali.

La seconda macro OFLAG è un flag che è usato per indicare il nome del file di output. Sebbene molti compilatori assumono automaticamente  come file di output quello che ha lo stesso nome di base del file in input, per altri questo non vale( come i compilatori in Linux/Unix, che per default crea un file chiamato a.out).

Si può vedere che ci sono due regole di suffisso qui, uno per i file cpp e uno per i file .c ( nel caso qualsiasi codice sorgente di C abbia bisogno di essere compilato). Il target di default è all ed ogni riga per questo target è continuato usando il segno \, fino a Guess2, il quale  è lultimo nella lista e quindi non ha il segno\. Ci sono molti più file in questo capitolo, ma solo questi sono mostrati qui per ragione di brevità.

Le regole di suffisso si prendono cura di creare i file oggetti ( con una estensione .o) dai file cpp, ma in generale si ha bisogno di specificare esplicitamente le regole per creare leseguibile, perché normalmente leseguibile è creato dal collegamento di molti file oggetti ed il make non può indovinare quali questi sono. Ancora, in questo caso (Linux/Unix) non cè nessuna estensione standard per gli eseguibili quindiuna regola di suffisso non funziona per queste semplici situazioni. Dunque, si vedono tutte le regole per la costruzione delleseguibile esplicitamente specificate.

Questo makefile prende la via assolutamente più sicura di usare le caratteristiche minime di make ; esso usa i concettibase di make e i target e le dipendenze, come pure le macro. Questo modo virtualmente assicura il funzionamento come quanti più programmi make possibili. Esso tende a produrre un makefile più grande, ma ciò non è un male poiché è automaticamente generato da ExtractCode.cpp.

Ci sono molte caratteristiche di make che questo libro non userà, come pure le più nuove e più intelligenti versioni e variazioni di make con le scorciatoie avanzate che possono far risparmiare molto tempo. La propria documentazione locale forse descrive le ulteriori del proprio particolare make, e si può imparare di più sul make da  Managing Projects with Make di Oram e Talbott (OReilly, 1993). Ancora, se il proprio fornitore di compilatore non fornisce un make o usa un make non standard, si può trovare il make di GNU per virtualmente ogni piattaforma esistente cercando su Internet gli archivi di  GNU ( dei quali ce ne sono molti).  

 

Sommario

Questo capitolo è un viaggio abbastanza intenso attraverso tutti le fondamentali caratteristiche della sintassi del C++, la maggior parte dei quali sono ereditate dal C e sono in comune con esso( e risultano il vanto della retro compatibilità del C++ con il C ). Sebbene alcune caratteristiche del C++ sono state qui introdotte, questo viaggio è per primo inteso per persone pratiche nella programmazione, e semplicemente che hanno bisogno di un' introduzione alla sintassi base del C e del C++. Se si è già un programmatore C, forse si è anche visto una o due cose sul C qui che non erano familiari, oltre alle caratteristiche del C++ che erano per la maggior parte nuove. Comunque, se questo capitolo sembra ancora un po opprimente, si dovrebbe andare al corso su CD ROM Thinking in C: Foundations for C++ and Java ( il quale contiene letture, esercizi, e soluzioni guidate ), che è allegato al libro, e anche disponibile al sito www.BruceEckel.com

Esercizi

Soluzioni degli esercizi scelti possono essere trovati nel documento elettronico The Thinking in C++ Annotated Solution Guide, disponibile per una piccola retta da www.BruceEckel.com

 

  1. creare un file principale ( con un estensione di ‘.h’). In questo file, dichiarare un gruppo di funzioni variando la lista di argomenti e  i valori di  ritorno fra i seguenti :  void, char, int, e float. Ora creare un file .cpp che include il nostro file header e creare definizioni per tutte queste funzioni. Ogni definizione dovrebbe semplicemente stampare il nome della funzione, la lista degli argomenti, il tipo di ritorno così si sa che è stata chiamata. Creare un secondo file .cpp che include il file header e definisce int main( ), contenente chiamate a tutte le nostre funzioni. Compilare ed eseguire il programma.
  2. scrivere un programma che usa due cicli for nidificati e loperatore modulo(%) per stampare i numeri primi (i numeri interi che non sono divisibili per alcun numero eccetto loro stessi e 1 ).
  3. scrivere un programma che usa un ciclo while per leggere parole dal input standard (cin) in una string. Questo è un ciclo while  infinito, il quale si interrompe ( ed esce) dal programma usando l'istruzione break. Per ogni parola che è letta, la si valuti prima usando una sequenza di dichiarazioni if per mappare un valore intero alla parola, e dopo usare un'istruzione switch che usa quel valore intero come suo selettore ( questa sequenza di eventi non è un buon stile di programmazione; è usata per esercitarsi sul flusso di controllo ). Dentro ogni case, si stampi qualcosa di significativo. Si deve decidere quali sono le paroleinteressanti e qual è il loro significato. Si deve anche decidere quale parola segnalerà la fine del programma. Testare il programma ridirezionando un file nello standard input del programma ( se si vuole scrivere di meno, questo file può essere il nostro file sorgente del programma).
  4. Modificare Menu.cpp per usare switch invece di if.
  5. scrivere un programma che valuta due espressioni nella sezione chiamata “precedenza”
  6. modificare  YourPets2.cpp così che esso usi i vari tipi di dato (char, int, float, double e le loro varianti). Far girare il programma e creare una mappa del risultante schema della memoria. Se si ha accesso a più di un tipo di macchina, sistema operativo, o compilatore, provare questo esperimento con quante più variazioni si riescono a fare.
  7. creare due funzioni, una che prende una string* ed una che prende una string&. Ognuna di queste dovrebbe modificare loggetto esterno string in un unico modo. In main( ), si crei e si inizializzi un oggetto string, lo si stampi , e dopo lo si passi ad ognuna delle due funzioni, stampare il risultato.
  8. scrivere un programma che usa tutti i trigrafici per vedere se il proprio compilatore li supporta.
  9. compilare e far girare Static.cpp. rimuovere la parola chiave static dal codice, compilare e farlo partire ancora spiegando cosa succede.
  10. si provi a compilare e collegare  FileStatic.cpp con FileStatic2.cpp. Cosa significa il messaggio dellerrore risultante ?
  11. modificare Boolean.cpp così che esso funzioni con double invece che con gli int.
  12. modificare Boolean.cpp e Bitwise.cpp in modo da usare operatori espliciti ( se il proprio compilatore è conforme allo standard C++ li supporterà)
  13. modificare Bitwise.cpp  per usare le funzioni da Rotation.cpp. assicurarsi che venga visualizzato il risultato in un modo tale che sia chiaro cosa succede durante le rotazioni.
  14. modificare Ifthen.cpp per usare loperatore  ternario if-else (?:)
  15. creare una struct che ha due oggetti string e int. Usare typedef per il nome della struct. Creare un esempio di struct, inizializzare tutti e tre i valori nel proprio esempio, e stamparli. Prendere l’indirizzo del proprio indirizzo e assegnarlo ad un puntatore al proprio tipo struct. Cambiare i tre valori nel proprio esempio e stamparli, usando con tutti il puntatore.
  16. creare un programma che usa una enumerazione di colori. Creare una variabile di questo tipo enum e stampare tutti i numeri che corrispondono con i nomi del colore, usando un ciclo for.
  17. sperimentare con Union.cpp rimuovìendo vari elementi union per vedere gli effetti nella grandezza del risultante union. Provare ad assegnare ad un elemento ( quindi un tipo ) di union e stampare via via un differente elemento ( quindi un differente tipo ) per vedere cosa accade.
  18. creare un programma che definisce due array int, uno dopo l’altro. Indicizzare la fine del primo array dentro il secondo, e fare un assegnamento. Stampare il secondo arrray per vedere i cambiamenti causati da questo. Ora si provi a definire una variabile char tra la definizione del primo array e la seconda, e ripetere l’esperimento. Si può volere creare un array stampando una funzione per semplificare il proprio codice.
  19. modificare ArrayAddresses.cpp per lavorare con i tipi data char, long int, float, e double.
  20. applicare la tecnica in ArrayAddresses.cpp per stampare la grandezza della struct e lindirizzo degli elementi dellarray in StructArray.cpp.
  21. creare un array di oggetti string e assegnare una stringa ad ogni elemento. Stampare l’array usando un ciclo for.
  22. creare due nuovi programmi cominciando da ArgsToInts.cpp in modo che essi usino rispettivamente atol( ) e atof( ).
  23. modificare PointerIncrement2.cpp  in modo che esso usi union invece di una struct.
  24. modificare PointerArithmetic.cpp per lavorare con long e long double.
  25. definire una variabile float. Prendere il suo indirizzo, cambiarlo in un  unsigned char, e assegnarlo ad un puntatore di unsigned char. Usando questo puntatore e [ ], elencare nella variabile float e usare la funzione printBinary( ) definita in questo capitolo per stampare una mappa di float ( si vada da 0 a sizeof(float)).  Cambiare il valore di float e vedere se si può capire cosa succede ( un float contiene un dato codificato).
  26. definire un array di int. Prendere lindirizzo iniziale di quell' array ed usare  static_cast per convertirlo in un void*. Scrivere una funzione che prende un void*, un numero (indicando il numero di byte ), ed un valore (indicando il valore per il quale ogni byte dovrebbe essere settato ) come argomento. La funzione dovrebbe settare ogni byte in un specificato intervallo ad un valore specificato. Provare la funzione su un nostro array di int.
  27. creare un array const di double un array volatile di double. Elencare ogni array edusare const_cast per cambiare ogni elemento in un non-const e non-volatile, rispettivamente, ed assegnare un valore ad ogni elemento.
  28. creare una funzione che prende un puntatore ad un array di double e il valore indicante la grandezza di quel array. La funzione dovrebbe stampare ogni elemento in un array. Adesso si crei un array di double e inizializzare ogni elemento a zero, dopo usare la nostra funzione per stampare l’array. Dopo usare reinterpret_cast per cambiare l’indirizzo iniziale del nostro array in un unsigned char*, e settare ogni byte di array a 1 (suggerimento :si avrà bisogno di usaresizeof per calcolare il numero di byte in un double ). Ora si usi il nostra funzione array stampato per stampare il risultato. Perché non pensare che ogni elemento è settato al valore 1.0 ?
  29. (impegnativo) modificare FloatingAsBinary.cpp così che esso stampi ogni parte del double come un gruppo separato di bit. Si dovranno rimpiazzare le chiamate a printBinary( ) con il proprio codice specializzato ( il quale si può derivare da printBinary( )) per fare ciò, e si dovrà vedere e capire il formato in virgola mobile con lordinamento del byte del proprio compilatore ( questa e la parte impegnativa ).
  30. creare un makefile che non solo compila YourPets1.cpp e YourPets2.cpp ( per il proprio compilatore ) ma anche esegue entrambi i programmi come parte del target di default. Assicurarsi di usare le regole di suffisso.
  31. modificare StringizingExpressions.cpp in modo che P(A)  è usata condizionatamente a   #ifdef per permettere al codice di debugging di essere automaticamente eliminato settando una flag della riga di comando. Si avrà bisogno di consultare la documentazione del proprio compilatore per vedere come definire e non definire i valori del preprocessore nella riga di comando del compilatore.
  32. definire una funzione che prende un argomento double e restituisce un int, creare e inizializzare un puntatore a questa funzione, e chiamare la funzione attraverso il puntatore.
  33. dichiarare un puntatore ad una funzione che prende un argomento int e restituisce un puntatore ad una funzione che prende un argomento char e restituisce un float.
  34. modificare FunctionTable.cpp così che ogni funzione restituisca una string ( invece di stampare un messaggio ) e questo valore sia stampato dentro il main( ).
  35. creare un makefile per uno dei precedenti esercizi ( a propria scelta) che permette di scrivere un make per la produzione del programma, e make debug per la costruzione di un programma includente l’informazione di debugging. 

[30] si noti che tutte le convenzioni sembrano terminare dopo l'accordo che qualche tipo di indentazione prende posto. La contesa  tra stili di formattazione è infinita. Vedere l’appendice A per la descrizione dello stile del codice di questo libro.

[31] grazie a Kris C. Matson per aver suggerito questo esercizio sullargomento

[32] a meno che si prenda l’approccio veramente severo che “in C/C++ tutti gli argomento passati sono per valore, e il ‘valore’ di un array è quello che è prodotto da un identificatore di array: è un indirizzo.” Ciò può essere visto come vero dal punto di vista del linguaggio assemblatore, ma io non credo che aiuti quando si prova a lavorare con concetti di alto livello. L’addizione dei riferimenti in C++ rende gli argomenti “  tutti i passaggi per valore” più confusionari, al punto dove io credo è più di aiuto pensare in termini di “ passaggio di valore” contro “ passaggio di indirizzi “.

[ Capitolo Precedente ] [ Indice Generale ] [ Indice Analitico ] [ Prossimo Capitolo ]

Ultima Modifica: 28/04/2003