Tipi Definiti dall'utente in C++
- In C++, possiamo definire i nostri tipi di dati utilizzando classi.
- Una classe è definita usando la parola chiave
struct
oclass
, seguita dal nome della classe e un corpo racchiuso tra parentesi graffe. - I membri dati di una classe definiscono i dati contenuti negli oggetti di quel tipo.
- Ogni oggetto di una classe ha la propria copia dei membri dati.
- Possiamo fornire inizializzatori in-class per i membri dati.
- Le classi possono essere definite in file header per essere riutilizzate in più file sorgente.
- Le guardie di header prevengono le inclusioni multiple di un file header.
- Le variabili del preprocessore usate nelle guardie di header devono essere uniche in tutto il programma.
Definire Strutture Dati in C++
Al livello più basilare, una struttura dati è un modo per raggruppare insieme elementi di dati correlati e una strategia per usare quei dati.
Prendiamo come esempio una struttura che rappresenta un libro da vendere per un programma che gestisce le vendite di libri.
La struttura dati (ossia il tipo) Libro
raggruppa un ISBN, un conteggio di quante copie di quel libro sono state vendute, e il ricavo associato a quelle vendite. Fornisce anche un insieme di operazioni come la funzione isbn
e gli operatori >>
, <<
, +
e +=
.
In C++ definiamo i nostri tipi di dati definendo una classe. I tipi libreria string, istream e ostream sono tutti definiti come classi. Il supporto C++ per le classi è estensivo, infatti, le successive lezioni saranno largamente dedicate alla descrizione delle caratteristiche relative alle classi. Anche se la classe Libro
è piuttosto semplice, non saremo in grado di definire completamente quella classe finché non impareremo come scrivere i nostri operatori nelle prossime lezioni.
Definire un nuovo tipo
Sebbene non possiamo ancora scrivere la nostra classe Libro
, possiamo scrivere una classe più concreta che raggruppa gli stessi elementi di dati. La nostra strategia per usare questa classe è che gli utenti saranno in grado di accedere agli elementi di dati direttamente e devono implementare le operazioni necessarie da soli.
Poiché la nostra struttura dati non supporta alcuna operazione, chiameremo la nostra versione DatiVendita
per distinguerla da Libro
. Definiremo la nostra classe come segue:
struct DatiVendita {
std::string numero_libro;
unsigned unita_vendute = 0;
double ricavo = 0.0;
};
La nostra classe inizia con la parola chiave struct
, seguita dal nome della classe e un corpo della classe (possibilmente vuoto). Il corpo della classe è circondato da parentesi graffe e forma un nuovo ambito o scope. I nomi definiti dentro la classe devono essere unici all'interno della classe ma possono riutilizzare nomi definiti fuori dalla classe.
La parentesi graffa di chiusura che termina il corpo della classe deve essere seguita da un punto e virgola. Il punto e virgola è necessario perché possiamo definire variabili dopo il corpo della classe:
// Definizione diretta dopo la classe
struct DatiVendita { /* ... */ } accum, trans, *punt_vendite;
// Definizione successiva, modo migliore per definire questi oggetti
struct DatiVendita { /* ... */ };
DatiVendita accum, trans, *punt_vendite;
Il punto e virgola marca la fine della lista (di solito vuota) dei dichiaratori.
Normalmente, è una cattiva idea definire un oggetto come parte di una definizione di classe. Fare ciò oscura il codice combinando le definizioni di due entità diverse, la classe e una variabile, in una singola istruzione.
È un errore comune tra i nuovi programmatori dimenticare il punto e virgola alla fine di una definizione di classe.
Membri Dati della Classe
Il corpo della classe definisce i membri della classe. La nostra classe ha solo membri dati. I membri dati di una classe definiscono i contenuti degli oggetti di quel tipo classe. Ogni oggetto ha la propria copia dei membri dati della classe. Modificare i membri dati di un oggetto non cambia i dati in nessun altro oggetto DatiVendita
.
Definiamo i membri dati nello stesso modo in cui definiamo variabili normali: Specifichiamo un tipo base seguito da una lista di uno o più dichiaratori.
La nostra classe ha tre membri dati: un membro di tipo string
chiamato numero_libro
, un membro unsigned
chiamato unita_vendute
, e un membro di tipo double
chiamato ricavo
. Ogni oggetto DatiVendita
avrà questi tre membri dati.
Con lo standard C++11, possiamo fornire un inizializzatore in-class per un membro dati. Quando creiamo oggetti, gli inizializzatori in-class verranno usati per inizializzare i membri dati. I membri senza un inizializzatore sono inizializzati per default. Quindi, quando definiamo oggetti DatiVendita
, unita_vendute
e ricavo
saranno inizializzati a 0, e numero_libro
sarà inizializzato alla stringa vuota.
Gli inizializzatori in-class sono limitati nella forma che possiamo usare: Devono essere racchiusi dentro parentesi graffe o seguire un segno =
. Non possiamo specificare un inizializzatore in-class dentro parentesi.
Vedremo poi che C++ ha una seconda parola chiave, class
, che può essere usata per definire le nostre strutture dati. Spiegheremo nelle prossime lezioni perché usiamo struct
qui. Fino a quando non trattiamo caratteristiche aggiuntive relative alle classi nelle prossime lezioni, dovresti usare struct
per definire le tue strutture dati.
Usare una Struttura Dati
A differenza di una classe più complessa, la nostra classe DatiVendita
non fornisce alcuna operazione. Gli utenti di DatiVendita
devono scrivere qualsiasi operazione di cui hanno bisogno. Come esempio, scriveremo un programma che somma i ricavi di due transazioni. L'input del nostro programma sarà composto da transazioni come:
0-201-78345-X 3 20.00 0-201-78345-X 2 25.00
Ogni transazione contiene un ISBN, il conteggio di quanti libri sono stati venduti, e il prezzo a cui ogni libro è stato venduto.
Poiché DatiVendita
non fornisce operazioni, dovremo scrivere il nostro codice per fare le operazioni di input, output e addizione. Assumeremo che la nostra classe DatiVendita
sia definita dentro DatiVendita.h
. Vedremo più avanti come definire questo header.
Poiché questo programma sarà più lungo di qualsiasi altro che abbiamo scritto finora, lo spiegheremo in parti separate. Nel complesso, il nostro programma avrà la seguente struttura:
#include <iostream>
#include <string>
#include "DatiVendita.h"
int main()
{
DatiVendita dati1, dati2;
// codice per leggere in dati1 e dati2
// codice per verificare se dati1 e dati2 hanno lo stesso ISBN
// e se sì stampare la somma di dati1 e dati2
}
Come nel nostro programma originale, iniziamo includendo gli header di cui avremo bisogno e definiamo variabili per contenere l'input. Si noti che il nostro programma include l'header string. Abbiamo bisogno di quell'header perché il nostro codice dovrà gestire il membro numero_libro
, che ha tipo string.
Sebbene non descriveremo il tipo libreria string
in dettaglio fino alle prossime lezioni, dobbiamo sapere solo un po' sulle stringhe per definire e usare il nostro membro ISBN. Il tipo string contiene una sequenza di caratteri. Le sue operazioni includono gli operatori >>
, <<
e ==
per leggere, scrivere e confrontare stringhe, rispettivamente. Con questa conoscenza possiamo scrivere il codice per leggere la prima transazione:
// prezzo per libro, usato per calcolare il ricavo totale
double prezzo = 0;
// legge la prima transazione:
// - ISBN
// - numero di libri venduti
// - prezzo per libro
std::cin >> dati1.numero_libro >> dati1.unita_vendute >> prezzo;
// calcola il ricavo totale da prezzo e unita_vendute
dati1.ricavo = dati1.unita_vendute * prezzo;
Le nostre transazioni contengono il prezzo a cui ogni libro è stato venduto ma la nostra struttura dati memorizza il ricavo totale. Leggeremo i dati della transazione in un double chiamato prezzo, da cui calcoleremo il membro ricavo. L'istruzione di input
std::cin >> dati1.numero_libro >> dati1.unita_vendute >> prezzo;
usa l'operatore punto per leggere nei membri numero_libro
e unita_vendute
dell'oggetto chiamato dati1
.
L'ultima istruzione assegna il prodotto di dati1.unita_vendute
e prezzo
nel membro ricavo
di dati1
.
Il nostro programma ripeterà poi lo stesso codice per leggere dati in dati2
:
// legge la seconda transazione
std::cin >> dati2.numero_libro >> dati2.unita_vendute >> prezzo;
dati2.ricavo = dati2.unita_vendute * prezzo;
Il nostro altro compito è verificare che le transazioni siano per lo stesso ISBN. Se sì, stamperemo la loro somma, altrimenti, stamperemo un messaggio di errore:
if (dati1.numero_libro == dati2.numero_libro) {
unsigned conteggio_totale = dati1.unita_vendute + dati2.unita_vendute;
double ricavo_totale = dati1.ricavo + dati2.ricavo;
// stampa: ISBN, totale venduto, ricavo totale, prezzo medio per libro
std::cout << dati1.numero_libro << " " << conteggio_totale
<< " " << ricavo_totale << " ";
if (conteggio_totale != 0)
std::cout << ricavo_totale/conteggio_totale << std::endl;
else
std::cout << "(nessuna vendita)" << std::endl;
return 0; // indica successo
} else {
// le transazioni non erano per lo stesso ISBN
std::cerr << "I dati devono riferirsi allo stesso ISBN"
<< std::endl;
return -1; // indica fallimento
}
Nel primo if
confrontiamo i membri numero_libro
di dati1
e dati2
. Se quei membri sono lo stesso ISBN, eseguiamo il codice dentro le parentesi graffe. Quel codice somma i componenti delle nostre due variabili. Poiché avremo bisogno di stampare il prezzo medio, iniziamo calcolando il totale di unita_vendute
e ricavo
e memorizziamo quelli in conteggio_totale
e ricavo_totale
, rispettivamente. Stampiamo quei valori. Poi verifichiamo che ci siano stati libri venduti e, se sì, stampiamo il prezzo medio calcolato per libro. Se non ci sono state vendite, stampiamo un messaggio che nota quel fatto.
Scrivere Header per le Nostre Classi
Sebbene come vedremo, possiamo definire una classe dentro una funzione, tali classi hanno funzionalità limitate. Di conseguenza, le classi ordinariamente non sono definite dentro funzioni. Quando definiamo una classe fuori da una funzione, può esserci solo una definizione di quella classe in un qualsiasi file sorgente. Inoltre, se usiamo una classe in diversi file diversi, la definizione della classe deve essere la stessa in ogni file.
Per assicurare che la definizione della classe sia la stessa in ogni file, le classi sono di solito definite in file header. Tipicamente, le classi sono memorizzate in header il cui nome deriva dal nome della classe. Per esempio, il tipo libreria string
è definito nell'header string
. Similmente, come abbiamo già visto, definiremo la nostra classe DatiVendita
in un file header chiamato DatiVendita.h
.
Gli header (di solito) contengono entità (come definizioni di classi e variabili const
e constexpr
) che possono essere definite solo una volta in un qualsiasi file dato. Tuttavia, gli header spesso hanno bisogno di usare funzionalità da altri header. Per esempio, poiché la nostra classe DatiVendita
ha un membro string
, DatiVendita.h
deve includere l'header string
. Come abbiamo visto, i programmi che usano DatiVendita
devono anche includere l'header string
per usare il membro numero_libro
. Di conseguenza, i programmi che usano DatiVendita
includeranno l'header string
due volte: una volta direttamente e una volta come effetto collaterale dell'inclusione di DatiVendita.h
. Poiché un header potrebbe essere incluso più di una volta, dobbiamo scrivere i nostri header in un modo che sia sicuro anche se l'header è incluso più volte.
Ricompilazione e Inclusione Multipla
Ogni volta che un header è aggiornato, i file sorgente che usano quell'header devono essere ricompilati per ottenere le nuove o modificate dichiarazioni.
Una Breve Introduzione al Preprocessore
La tecnica più comune per rendere sicuro includere un header più volte si basa sul preprocessore.
Il preprocessore, che il linguaggio C++ eredita dal linguaggio C, è un programma che viene eseguito prima del compilatore e cambia il testo sorgente dei nostri programmi. I nostri programmi già si affidano a una funzionalità del preprocessore, #include
. Quando il preprocessore vede un #include
, sostituisce il #include
con i contenuti dell'header specificato.
I programmi C++ usano anche il preprocessore per definire guardie di inclusione. Le guardie di inclusione si basano su variabili del preprocessore. Le variabili del preprocessore hanno uno di due possibili stati: definito o non definito. La direttiva #define
prende un nome e definisce quel nome come variabile del preprocessore. Ci sono altre due direttive che testano se una data variabile del preprocessore è stata o non è stata definita: #ifdef
è vero se la variabile è stata definita, e #ifndef
è vero se la variabile non è stata definita. Se il test è vero, allora tutto ciò che segue #ifdef
o #ifndef
viene processato fino al #endif
corrispondente. Torneremo su queste direttive in dettaglio più avanti, ma per ora vediamo come possiamo usarle per proteggere i nostri header contro l'inclusione multipla.
Possiamo usare queste funzionalità per proteggerci contro l'inclusione multipla come segue:
#ifndef DATI_VENDITA_H
#define DATI_VENDITA_H
#include <string>
struct DatiVendita {
std::string numero_libro;
unsigned unita_vendute = 0;
double ricavo = 0.0;
};
#endif
La prima volta che DatiVendita.h
è incluso, il test #ifndef
avrà successo. Il preprocessore processerà le righe che seguono #ifndef
fino al #endif
. Di conseguenza, la variabile del preprocessore DATI_VENDITA_H
sarà definita e i contenuti di DatiVendita.h
saranno copiati nel nostro programma. Se includiamo DatiVendita.h
più tardi nello stesso file, la direttiva #ifndef
sarà falsa. Le righe tra essa e la direttiva #endif
saranno ignorate.
Nomi delle variabili del preprocessore e Scope
I nomi delle variabili del preprocessore non rispettano le regole di scoping di C++.
Le variabili del preprocessore, inclusi i nomi delle guardie di header, devono essere uniche in tutto il programma. Tipicamente assicuriamo l'unicità basando il nome della guardia sul nome di una classe nell'header. Per evitare conflitti di nomi con altre entità nei nostri programmi, le variabili del preprocessore sono di solito scritte tutte in maiuscolo.
Guardie di Inclusione
Gli header dovrebbero avere guardie di inclusione, anche se non sono (ancora) inclusi da un altro header. Le guardie di header sono banali da scrivere, e definendole abitualmente non hai bisogno di decidere se sono necessarie.