Il C++
Non chiederti cosa può fare per te il sistema operativo; chiediti invece cosa puoi fare tu, per il sistema operativo.
In uno dei suoi libri, Bjarne Stroustrup definì il C++:
Un linguaggio di programmazione per svolgere compiti non banali.
Fà che si possa dire la stessa cosa di te.
C++ è un linguaggio di programmazione creato da Bjarne Stroustrup nel 1983, quando lavorava ai Laboratori Bell della AT&T.
Dieci anni prima, il suo collega Dennis Ritchie aveva creato un linguaggio di programmazione chiamato C.
Il C, come ti ho detto, era estremamente efficace se dovevi programmare i computer, ma — così come alcuni dei tuoi confratelli — non gestiva altrettanto bene le entità della vita reale.
Stroustrup, che ai tempi del suo dottorato aveva lavorato con un linguaggio a oggetti chiamato Simula67, pensò che se avesse potuto aggiungere alla velocità di esecuzione del C la possibilità di creare dei nuovi tipi di dato di Simula, avrebbe ottenuto il linguaggio perfetto.
Aveva ragione.
Il nome C++ si riferisce all’operatore ++
, che serve a incrementare di un’unità il valore di una variabile:
#include <iostream>
using namespace std;
int main(int argc, char** argv)
{
int C = 12;
cout << "Valore di C = " << C << endl;
C++;
cout << "Valore di C = " << C << endl;
return 0;
}
Se compili ed eseguti questo codice, otterrai:
> g++ cplusplus-incremento.cpp -o ../out/esempio
> ../out/esempio
Valore di C = 12
Valore di C = 13
C++, infatti, non era un nuovo linguaggio: era un C migliorato.
Tutto il codice e l’esperienza che erano state fatte fino ad allora sul C potevano essere applicate anche al C++.
Parafrasando Neruda, Stroustrsup fece con il C quello che Gesù fece con l’Ebraismo: così come il Nazareno prese una religione spartana, adatta per un popolo in fuga nel deserto e la migliorò, rendendola meno autoritaria, il Danese trasformò un linguaggio pensato per gestire unicamente i computer, in un linguaggio che poteva gestire ogni cosa.
Le principali novità aggiunte al C dal C++ sono: l’astrazione dei dati, la programmazione a oggetti e la generic programming.
Ora ti spiego cosa sono, ma tu non preoccuparti se non capisci: approfondiremo tutti questi concetti in seguito.
I tipi di dato del C sono:
char, int, float, double, array, enum, struct, union
Più che sufficienti per scrivere il kernel di Unix, ma decisamente inadeguati per scrivere un programma che gestisca delle linee telefoniche. Perché un linguaggio di programmazione possa gestire con la stessa facilità un flusso di dati, un utente, una linea telefonica o anche un allevamento di cavalli, è necessario che oltre ai tipi di dato predefiniti, possa gestire anche delle nuove entità definite dal programmatore. Questa è ciò che si chiama data abstraction e il C++ la ottiene per mezzo delle classi. Le classi sono la rappresentazione, all’interno del codice, di un’entità:
class Cavallo {
private:
Sesso _sesso;
string _razza;
public:
const char* getSpecie() const {
return "cavallo";
}
const char getSesso() const {
return (char)_sesso;
}
const char* getRazza() const {
return _razza.c_str();
}
Cavallo() {}
Cavallo(const char* razza, Sesso sesso ) {
_razza = razza;
_sesso = sesso;
}
};
o di un concetto:
class Monta {
private:
Cavallo _maschio;
Cavallo _femmina;
Data _giorno;
public:
Monta(Cavallo& maschio, Cavallo& femmina) {
_maschio = maschio;
_femmina = femmina;
time(&_giorno);
}
};
Grazie alle classi, il programmatore può creare dei nuovi tipi di dato e
utilizzarli all’interno del suo programma nello stesso modo in cui
utilizzerebbe i tipi di dato primitivi del linguaggio.
Ciascuna classe ha degli attributi e dei metodi.
Gli attributi sono dei dati che descrivono le caratteristiche della classe, per esempio, la razza o il sesso di un cavallo.
I metodi sono delle funzioni che definiscono il modo in cui la classe può interagire con gli altri elementi del programma.
Nelle classi dell’esempio gli attributi sono gli elementi che vedi nella sezione private
, mentre i metodi sono le funzioni che vedi nella sezione public
.
La funzione che ha lo stesso nome della classe si chiama costruttore della classe, perché “spiega” al compilatore come debbano essere creati gli oggetti di questa classe.
Le classi, però, sono la ricetta, non sono la pietanza.
Per poter essere utilizzate, le classi devono essere istanziate negli oggetti:
int main()
{
Cavallo stallone("lipizzano", maschio);
Cavallo giumenta("maremmano", femmina);
Monta monta(stallone, giumenta);
cout << monta << endl;
return 0;
}
stallone
, giumenta
e monta
sono tre oggetti.
I primi due sono istanze della classe Cavallo
, il terzo è un’istanza della classe Monta
.
Se aggiungi un po’ di codice alle classi che abbiamo visto prima e compili il programma, otterrai:
% g++ cplusplus-classe.cpp -o ../out/esempio
% ../out/esempio
DATA: Sun Apr 5 10:38:31 2020
MASCHIO: Specie:cavallo, Sesso:m, Razza:lipizzano
FEMMINA: Specie:cavallo, Sesso:f, Razza:maremmano```
Perché un linguaggio di programmazione possa dirsi orientato agli oggetti, però, oltre alle classi deve poter gestire l’ereditarietà e il polimorfismo. L’ereditarietà permette di definire dei nuove classi come estensione di classi esistenti:
class Animale {
private:
string _specie;
string _razza;
Sesso _sesso;
public:
const char* getSpecie() const {
return _specie.c_str();
}
const char getSesso() const {
return (char)_sesso;
}
const char* getRazza() const {
return _razza.c_str();
}
Animale() {}
Animale(const char* specie, const char* razza, const Sesso sesso ) {
_specie = specie;
_razza = razza;
_sesso = sesso;
}
};
class Cavallo : public Animale {
public:
Cavallo() {}
Cavallo(const char* razza, const Sesso sesso )
: Animale("Cavallo", razza, sesso ) {
}
};
Nell’esempio qui sopra, abbiamo prima definito una classe Animale
, che ha tre attributi: la specie
, la razza
e il sesso
; poi abbiamo definito una classe Cavallo
, derivandola dalla classe Animale
.
In questo modo, se oltre ai cavalli il nostro programma dovesse gestire anche altri ungulati, non dovremmo ripetere in ciascuna classe le stesse istruzioni, ma potremmo utilizzare quelle della classe base:
class Asino : public Animale {
public:
Asino() {}
Asino(const char* razza, const Sesso sesso )
: Animale("Asino", razza, sesso ) {
}
};
A questo punto, la tua sagacia dovrebbe averti fatto rilevare un possibile problema (posto che tu sia sveglio, cosa di cui non sono del tutto certo): la classe Monta
può gestire solo oggetti di tipo Cavallo
.
Un linguaggio che non gestisca il polimorfismo ci costringerebbe a scrivere due nuove classi: una per i muli e una per i bardotti:
class MontaMulo {
private:
Asino _maschio;
Cavallo _femmina;
Data _giorno;
public:
MontaMulo(Asino& maschio, Cavallo& femmina) {
_maschio = maschio;
_femmina = femmina;
time(&_giorno);
}
};
class MontaBardotto {
private:
Cavallo _maschio;
Asino _femmina;
Data _giorno;
public:
Monta(const Cavallo& maschio, const Asino& femmina) {
_maschio = maschio;
_femmina = femmina;
time(&_giorno);
}
};
Il problema è che più codice scrivi, più è probabile che farai degli errori e meno facile sarà correggerli.
Al contrario, i programmi con meno righe di codice sono più affidabili e più facili da correggere o da modificare.
Il C++ ci aiuta in questo senso perché permette il polimorfismo, ovvero la capacità di una funzione o di un operatore di svolgere il proprio compito indipendentemente dal tipo di dato che deve gestire.
Se riscriviamo la classe Monta
usando, al posto dei parametri di tipo Cavallo
, dei parametri che hanno il tipo della classe base Animale
:
class Monta {
private:
Animale* _maschio;
Animale* _femmina;
Data _giorno;
public:
Monta(Animale* maschio, Animale* femmina) {
_maschio = maschio;
_femmina = femmina;
time(&_giorno);
}
};
Potremo creare degli oggetti di classe Monta
con qualunque classe derivata:
/**
* @file src/cplusplus-polimorfismo.cpp
* Esempio di polimorfismo delle classi.
*/
#include <iostream>
#include <ctime>
using namespace std;
typedef time_t Data;
typedef enum _sesso {
maschio = 'm',
femmina = 'f'
} Sesso;
class Animale {
private:
string _razza;
Sesso _sesso;
public:
virtual const char* getSpecie() const {
return "";
}
const char getSesso() const {
return (char)_sesso;
}
const char* getRazza() const {
return _razza.c_str();
}
Animale() {}
Animale(const char* razza, const Sesso sesso ) {
_razza = razza;
_sesso = sesso;
}
};
ostream& operator << (ostream& os, const Animale& animale) {
os << "Specie:" << animale.getSpecie() << "\t"
<< "Razza:" << animale.getRazza() << "\t"
<< "Sesso:" << animale.getSesso() << endl;
return os;
}
class Cavallo : public Animale {
public:
const char* getSpecie() const {
return "Cavallo";
}
Cavallo() {}
Cavallo(const char* razza, const Sesso sesso )
: Animale(razza, sesso ) {
}
};
class Asino : public Animale {
public:
const char* getSpecie() const {
return "Asino";
}
Asino() {}
Asino(const char* razza, const Sesso sesso )
: Animale(razza, sesso ) {
}
};
class Monta {
private:
Animale* _maschio;
Animale* _femmina;
Data _giorno;
public:
Monta(Animale* maschio, Animale* femmina) {
_maschio = maschio;
_femmina = femmina;
time(&_giorno);
}
friend ostream& operator << (ostream& os, const Monta& copula) {
os << "DATA: " << asctime(localtime(&copula._giorno))
<< "MASCHIO: " << *copula._maschio
<< "FEMMINA: " << *copula._femmina;
return os;
};
};
int main()
{
Animale* cavallo = new Cavallo("lipizzano", maschio);
Animale* giumenta = new Cavallo("maremmano", femmina);
Animale* asino = new Asino("amiatino", maschio);
Animale* asina = new Asino("sardo", femmina);
Monta puledro(cavallo, giumenta);
Monta ciuco(asino, asina);
Monta mulo(asino, giumenta);
Monta bardotto(asina, cavallo);
cout << "PULEDRO\n" << puledro << endl;
cout << "CIUCO\n" << ciuco << endl;
cout << "MULO\n" << mulo << endl;
cout << "BARDOTTO\n" << bardotto << endl;
return 0;
}
Compilando ed eseguendo il programma, otterrai:
% g++ cplusplus-polimorfismo.cpp -o ../out/esempio
% ../out/esempio
PULEDRO
DATA: Sun Apr 5 12:33:45 2020
MASCHIO: Specie:Cavallo Razza:lipizzano Sesso:m
FEMMINA: Specie:Cavallo Razza:maremmano Sesso:f
CIUCO
DATA: Sun Apr 5 12:33:45 2020
MASCHIO: Specie:Asino Razza:amiatino Sesso:m
FEMMINA: Specie:Asino Razza:sardo Sesso:f
MULO
DATA: Sun Apr 5 12:33:45 2020
MASCHIO: Specie:Asino Razza:amiatino Sesso:m
FEMMINA: Specie:Cavallo Razza:maremmano Sesso:f
BARDOTTO
DATA: Sun Apr 5 12:33:45 2020
MASCHIO: Specie:Asino Razza:sardo Sesso:f
FEMMINA: Specie:Cavallo Razza:lipizzano Sesso:m
Come ti ho detto, però, un buon programmatore non si accontenta di essere riuscito a produrre il risultato atteso, ma si chiede sempre se ci sia un modo più efficiente per ottenerlo. Nel nostro caso, il codice che abbiamo utilizzato per mostrare il risultato degli accoppiamenti:
Monta puledro(cavallo, giumenta);
Monta ciuco(asino, asina);
Monta mulo(asino, giumenta);
Monta bardotto(asina, cavallo);
cout << "PULEDRO\n" << puledro << endl;
cout << "CIUCO\n" << ciuco << endl;
cout << "MULO\n" << mulo << endl;
cout << "BARDOTTO\n" << bardotto << endl;
non è il massimo dell’efficienza, sia perché potremmo sbagliarci ad accoppiare la specie dei genitori con il nome del figlio, sia perché le istruzioni devono essere ripetute per ciascun oggetto.
Per risolvere il primo difetto possiamo aggiungere alla classe Monta
un attributo e un metodo per definire autonomamente che tipo di genìa venga prodotta dalla copula:
string _esito;
void setEsito() {
if(strcmp(_maschio->getSpecie(),"Asino") == 0) {
if(strcmp(_femmina->getSpecie(),"Asino") == 0){
_esito = "asino";
} else {
_esito = "mulo";
}
} else {
if(strcmp(_femmina->getSpecie(),"Cavallo") == 0) {
_esito = "puledro";
} else {
_esito = "bardotto";
}
}
}
ma anche così dovremo comunque riscrivere quattro righe di codice per modificare l’output del programma: un approccio inaccettabile per i sistemi di produzione, dove le entità da gestire possono essere migliaia. Possiamo risolvere questo problema grazie alla generic programming e al modo in cui viene implementata nel C++: le classi template:
template < class T> class list;
La classe list
è una delle classi template del C++ e permette di inserire, rimuovere, spostare, unire, ordinare ed elencare una lista di oggetti di una stessa classe.
La sintassi per creare una lista di oggetti di classe Monta
è:
list<Monta> monte;
Fatto ciò, possiamo aggiungere elementi alla nostra lista con la funzione push_back()
, alla quale passeremo direttamente il costruttore della classe:
monte.push_back(Monta(cavallo, giumenta));
monte.push_back(Monta(asino, asina));
monte.push_back(Monta(asino, giumenta));
monte.push_back(Monta(cavallo, asina));
Questo codice è una forma più efficiente di:
Monta m1(cavallo, giumenta);
Monta m2(asino, asina);
Monta m3(asino, giumenta);
Monta m4(cavallo, asina);
monte.push_back(m1);
monte.push_back(m2);
monte.push_back(m3);
monte.push_back(m4);
Per visualizzare il contenuto della lista, indipendentemente dal numero di elementi, basta l’istruzione:
for (list<Monta>::iterator it=monte.begin(); it!=monte.end(); it++) {
cout << *it << endl;
}
La funzione main()
del nostro programma sarà quindi:
int main()
{
Animale* cavallo = new Cavallo("lipizzano", maschio);
Animale* giumenta = new Cavallo("maremmano", femmina);
Animale* asino = new Asino("amiatino", maschio);
Animale* asina = new Asino("sardo", femmina);
list<Monta> monte;
monte.push_back(Monta(cavallo, giumenta));
monte.push_back(Monta (asino, asina));
monte.push_back(Monta (asino, giumenta));
monte.push_back(Monta (cavallo, asina));
for (list<Monta>::iterator it=monte.begin();
it!=monte.end(); it++) {
cout << *it << endl;
}
return 0;
}
e l’output che otterremo è:
$ g++ cplusplus-template.cpp -o ../out/esempio
% ../out/esempio
DATA: Sun Apr 5 16:19:24 2020
MASCHIO: Specie:Cavallo Razza:lipizzano Sesso:m
FEMMINA: Specie:Cavallo Razza:maremmano Sesso:f
ESITO: puledro
DATA: Sun Apr 5 16:19:24 2020
MASCHIO: Specie:Asino Razza:amiatino Sesso:m
FEMMINA: Specie:Asino Razza:sardo Sesso:f
ESITO: asino
DATA: Sun Apr 5 16:19:24 2020
MASCHIO: Specie:Asino Razza:amiatino Sesso:m
FEMMINA: Specie:Cavallo Razza:maremmano Sesso:f
ESITO: mulo
DATA: Sun Apr 5 16:19:24 2020
MASCHIO: Specie:Cavallo Razza:lipizzano Sesso:m
FEMMINA: Specie:Asino Razza:sardo Sesso:f
ESITO: bardotto
Se per qualche motivo volessimo invertire l’ordine degli elementi nella lista, tutto quello che dovremmo fare è di aggiungere prima del ciclo for
l’istruzione:
monte.reverse();
e l’output che otterremo è:
% g++ cplusplus-template.cpp -o ../out/esempio
% ../out/esempio
DATA: Sun Apr 5 17:08:27 2020
MASCHIO: Specie:Cavallo Razza:lipizzano Sesso:m
FEMMINA: Specie:Asino Razza:sardo Sesso:f
ESITO: bardotto
DATA: Sun Apr 5 17:08:27 2020
MASCHIO: Specie:Asino Razza:amiatino Sesso:m
FEMMINA: Specie:Cavallo Razza:maremmano Sesso:f
ESITO: mulo
DATA: Sun Apr 5 17:08:27 2020
MASCHIO: Specie:Asino Razza:amiatino Sesso:m
FEMMINA: Specie:Asino Razza:sardo Sesso:f
ESITO: asino
DATA: Sun Apr 5 17:08:27 2020
MASCHIO: Specie:Cavallo Razza:lipizzano Sesso:m
FEMMINA: Specie:Cavallo Razza:maremmano Sesso:f
ESITO: puledro
Oltre alle classi template predefinite della Standard Template Library, il C++ permette di definire le proprie classi template, ma di questo parleremo a tempo debito.
Queste caratteristiche, unite alla compatibilità con il codice scritto in C, fecero di C++ il linguaggio object-oriented più utilizzato degli anni ‘90.
L’avvento, alla fine del Secolo, del linguaggio con la “J”, quello che ha bisogno di un sistema di garbage collecion per sopperire alla pochezza dei suoi programmatori, avrebbe dovuto darci un’idea di quello che sarebbe stato il millennio che ci si presentava davanti.
Non a caso, Stroustrsup disse:
I suspect that the root of many of the differences between C/C++ and Java is that AT&T is primarily a user (a consumer) of computers, languages, and tools, whereas Sun is primarily a vendor of such things.