C++ ... per provare....



Tipi di dati

Il C++ offriva un certo set di tipi di dati fin dalle sue origini. Con l'avvento dello standard che ha portato il linguaggio a chiamarsi, impropriamente, C++11, abbiamo avuto un ampliamento di questo set. Ovviamente, come sottolineano tutti i testi di base, C++ è un linguaggio a oggetti, ciò che permette di estendere la tipologia dei dati trattati costruendocene di nuovi. In questa fase è solo un dettaglio.

In C++ abbiamo due tipi di dati: fondamentali e composti (derivati). In questa sede ci occuperemo dei primi.

Prima ancora però vediamo come definire le variabili nel nostro linguaggio. Un tipo infatti viene applicato ad una variabile, in se stesso non può far nulla. Lo schema per definire le variabili è il seguente:

tipo nomevariabile = valore; oppure, più in stile C++ utilizzando un costruttore
tipo nomevariabile(valore); o ancora, senza inizalizzazione:
tipo nomevariabile;

Se dobbiamoassegnare un tipo a più variabili possiamo anche scrivere:

tipo nomevariabile-1, nomevariabile-2,...., nomevariabile-n;

eventualmente assegnando i valori iniziali.

L'inizializzazione può avvenire anche tramite formule diverse:

int x01 = 3;
int x02 = 2;
int x03 = x01 + (x01 * x02);


Come abbiamo visto è possibile non inizializzare immediatamente le variabili. E' però ottima norma, direi obbligatorio, farlo prima di utilizzarle. I compilatori sono peraltro in grado di segnalare, di solito come warning, l'utilizzo di variabili a cui non è stato assegnato un valore. Loro ve lo dicono se poi insistete a usarle... beh, chi cerca quello che non deve trova quello che non vuole (in particolare trova dei valori casuali). Per la cronaca, io sto usando il compilatore C++ offerto da Microsoft nella suite Visual Studio 2012 e faccio qualche test anche con il compilatore (gcc) a corredo di MinGW. Molti altri linguaggi non permettono che sia definite variabili non inizializzate o alla peggio attribuiscono loro un valore di default (0 per gli interi, "" per le stringhe ecc...). C++ è un po' più lasco su questo aspetto per cui... occhio. Una buona idea è comunque quella di attribuire un valore già all'atto della dichiarazione onde evitare dimenticanze. A questo proposito sempre parlando di C++11 è possibile usare un'altra forma di inizializzazione che fa uso delle parentesi graffe e nel vecchio standard era riservato ad array e strutture. Quindi nel nuovo standard possiamo fare così:

int x = {4}; o anche senza il segno "="
int x{4};

se lasciamo vuota la coppia di parentesi il compilatore attribuisce automaticamente il valore 0. Come spiegato da vari autori il motivo di avere anche questo metodo  è quello di favorire i neofiti del linguaggio, che si troveranno a disposizione un sistema universale di inizializzazione piuttosto che averne diversi in funzione dei tipi. La cosa tutto sommato mi vede d'accordo come approccio, opinione personale. Utilizzare questa modalità dovrebbe aiutare in molti casi a non trovarsi con variabili prive di inizializzazione.

I nomi delle variabili sono ovviamente liberi ma è obbligatorio usare solo lettere (maiuscole e minuscole), numeri e il classico _ (underscore). Da un punto di vista formale è utile usare anche le maiuscole per distinguere eventuali parti significative del nome, così si può usare il già citato underscore. Es:

numero_anni
NomeUtente
nome-via // no, questo non va bene il - non è permesso


e così, via, non voglio essere troppo pedante su questi aspetti che non vanno però sottovalutati. E' importante ricordare che C++ è un linguaggio case sensitve, per cui xyz e Xyz e xYz sono 3 cose diverse. Inoltre è necessario ricordare che:
  • il primo carattere di una variabile non può essere un numero.
  • i nomi che iniziano con due underscore o con underscore + maiuscola sono riservati al compilatore. La cosa pericolosa è che il compilatore non segnala errore, perchè di fatto non lo è, ma il comportamente del programma a quel punto è imprevedibile. Attenzione.
  • i nomi che inizano con singolo underscore sono individuati come identificatori globali
  • non è possibile, ovviamente, usare le parole del linguaggio come identificatori
  • il linguaggio, in se, non pone limiti alla lunghezza del nome di un identificatore ma alcune implementazioni si. Ad esempio C++ di Microsoft considera "solo" i primi 2048 caratteri.
Fatte salve queste regole il compilatore non si interessa ad altro, tanto meno a dispute su quale standard sia meglio adottare. Comunque ci possono essere peculiarità nell'implementazione di ciascun compilatore per cui non è cattiva idea dare un'occhiata al manuale del compilatore stesso.

Prima di lasciare questo argomento, visto che le abbiamo citate, ecco l'elenco delle parole riservate nello standard:

asm, auto, bool, break, case, catch, char, class, const, const_cast, continue, default, delete, do, double, dynamic_cast, else, enum, explicit, export, extern, false, float, for, friend, goto, if, inline, int, long, mutable, namespace, new, operator, private, protected, public, register, reinterpret_cast, return, short, signed, sizeof, static, static_cast, struct, switch, template, this, throw, true, try, typedef, typeid, typename, union, unsigned, using, virtual, void, volatile, wchar_t, while


a cui possiamo aggiungere:


and, and_eq, bitand, bitor, compl, not, not_eq, or, or_eq, xor, xor_eq 


anche in questo qualche compilatore potrebbe avocare a suo uso e consumo parole qui non presenti.

Prima di addentrarci nello studio dei numeri segnaliamo che oltre alla variabili C++ permette di definire delle costanti tramite la keyword
const. Un valore costante non può essere modificato nel corso del programma, ovvero non può essere attribuito ad esso un valore diverso da quello inizialmente assegnato. Ovviamente le costanti vengono usati come riferimento interno con cui si attribuisce un "nome" ad un valore significativo, fisso e usato più volte all'interno del nostro codice. Formalmente la definizione è la seguente:

const tipo nomecostante = valore iniziale

quindi ad esempio:


const int prezzo = 1000;


I tipi integrali (interi)

Ricadono in questo insieme un lungo elenco di tipi di dato:

char di solito, nelle varie implementazioni, rappresenta i caratteri ASCII con un range che va da -128 a 127
bool booleano, può assumere i valori true e false
short  
int  
long  
long long  

Per quanto riguarda i char esistono poi alcune altre incarnazioni create per estendere il set di caratteri rappresentabili. In particolare esistono wchar_t, char16_t e char32_t. Il primo garantisce la rappresentabilità di qualunque carattere proprio della macchina su cui il programma è compilato, gli altri due sono usati per le corrispondenti rappresentazioni Unicode. Per quello che riguarda i tipi short, int, long e long long non ho specificato la lunghezza in bytes e quindi il range dei possibili valori ma c'è un motivo. Il fatto è che in C++ la lunghezza di questi tipi è legato a scelte architetturali da parte di chi scrive il compilatore. Lo standard è molto flessibile su questo argomento ed è limitato soltanto dalle seguenti regole:
  • il tipo short sarà definito almeno su 16bits
  • il tipo int sarà almeno ampio quanto il tipo short
  • il tipo long sarà definito su almeno 32 bit e sarà comunque ampio almeno quanto int
  • il tipo long long sarà definito su almeno 64 bits e sarà ampio almeno quanto long
Se avete dei dubbi su quello che il compilatore che state usando vi mette a disposizione ecco un programmino (non è mio ma adattato dal testo C++ Primer di Steven Prata, francamente non me ne è venuto in mente uno più elegante e semplice, comunque ne troverete molti altri simili su Internet) che testa la magnitudo dei vari tipi numerici, può tornare senz'altro utile:

  Esempio 2.1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <climits> // per sistemi un po' datati si usa limits.h
int main()
{
  using namespace std;
  int n_int = INT_MAX;
  short n_short = SHRT_MAX;
  long n_long = LONG_MAX;
  long long n_llong = LLONG_MAX;
  cout << "int: " << sizeof (int) << " bytes." << endl;
  cout << "short: " << sizeof n_short << " bytes." << endl;
  cout << "long: " << sizeof n_long << " bytes." << endl;
  cout << "long long: " << sizeof n_llong << " bytes." << endl;
  cout << endl;
  cout << "Valori massimi:" << endl;
  cout << "int: " << n_int << endl;
  cout << "short: " << n_short << endl;
  cout << "long: " << n_long << endl;
  cout << "long long: " << n_llong << endl << endl;
  cout << "Valore minimo per gli int: " << INT_MIN << endl;
  cout << "Bits per byte = " << CHAR_BIT << endl;
  return 0;
}


In questo esempio viene fatto uso dell'operatore sizeof che restituisce un numero che rappresenta la grandezza dell'operando rispetto al tipo base char. Il risultato di questo operatore in realtà è un tipo (size_t) intero che è contenuto in STDDEF.H risultando indipendente dalla macchina. Vedremo all'opera questo importante operatore anche applicato ad altri elementi. Interessante ed importante è l'uso del header climits, specificato alla riga 2, in quanto esso contiene importanti valori parametrici relativi all'ampiezza in byte del tipo base char ed ai massimi e minimi per ciascun tipo.
Per quanto sia banale bisogna fare attenzione al superamento dei limiti per ciascun tipo per evitare problemi e sorprese. Si veda il seguente esempio e il relativo output:

  Esempio 2.2
  #include <iostream>
#include <climits>
int main()
{
  using namespace std;
  short n_short = SHRT_MAX;
  cout << n_short << endl;
  n_short = n_short + 1;
  cout << n_short;
  return 0;
}

Il risultato come detto può lasciare un po' confusi:

32767
-32768


Ma, se ci pensate, e soprattutto ragionate sulla rappresentazione binaria dei numeri non è una sorpresa considerando che siamo nel'ambito dei 16 bit che formano il tipo short. Tuttavia se questo e altri effetti collaterali vengono ignorati potreste avere delle amare sorprese. Ovviamente ci sono dei meccanismi per evitare che possano accadere situazioni simili, io consiglio di abituarsi a tecniche non propriamente native per C++ come il design by contract.
Da considerare sono poi tutti quei tipi numerici che ricadono nella grande famiglia degli unsigned, ovvero senza segno. Il primo effetto della mancanza del segno è, in pratica, il raddoppio del massimo valore esprimibile (che resta nel campo positivo, un unsigned non può valere meno di 0); ad esempio un unsigned short va da 0 a 65535 (il che comunque in linea di principio non risolve il problema dell'overflow che si ripresenta al raggiungimento del limite massimo del tipo unsigned usato, oltre a togliervi accesso ai numeri negativi). Perchè vi siano tutte queste tipologie di numeri interi è evidente: se siete sicuri che il range numerico che utilizzerete ricadrà sicuramente nell'ambito dei short è inutile usare un int o un long: sprechereste inutilmente dello spazio in memoria (se questo non è un problema usando un singolo numero deve essere invece ben ponderato se ad esempio vi capitasse di dover gestire array con milioni di dati numerici) e appesantireste inutilmente il programma. Va detto che alcuni non pensano che questo sia un vantaggio sufficiente a compensare i potenziali pericoli; ad esempio l'ottimo linguaggio Fantom prevede solo interi a 64 bit tagliando la testa al toro, o meglio al problema. Questo approccio però pesa dal punto di vista delle prestazioni. Comunque come detto se volete avere il range dei valori degli unsigned basta in pratica raddoppiare il valore massimo positivo della controparte "signed" ed aggiungere 1 (quindi per esempio i già citati short hanno una controparte unsigned che va da 0 a 32767 * 2 + 1 = 65535). Per quanto la riguarda la scelta del tipo numerico da usare la cosa più semplice è ricorrere prevalentemente agli int (i longo spesso hanno la medesima ampiezza). Per i csi per i quali prevedete che gli int possano non bastare usate i long long.

In C++ userete prevalentemente la notazione decimale classica ma è permessa anche quella ottale e quella esadecimale. La prima è caratterizzata da uno 0 messo davanti alle cifre mentre gli esadecimali hanno il prefisso 0x o 0X. Ad esempio:

145 - decimale
0145 - ottale
0x145 - esadecimale.

Di default tuttavia cout espone il suo output in formato decimale ed è necessario usare forme "speciali" del suo uso per avere output in altri formati, in particolare si usano dei modificatori come vediamo nel seguente esempio:

  Esempio 2.3
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
int main()
{
  using namespace std;
  int x1 = 32;
  cout << x1 << endl;
  cout << hex;
  cout << x1 << endl;
  cout << oct;
  cout << x1 << endl;
  cout << dec;
  cout << x1;
}

La riga 7 propone un modificatore affinchè cout stampi il valore contenuto in x1 nel formato esadecimale, alla riga 9 troviamo il modificatore per il formato ottale mentre la riga 11 rimette a posto il nostro cout per farlo lavorare di nuovo in formato decimale. Oppure, per mostrare chiaramente cosa stiamo espnendo come output si può anche inserire l'ulteriore manipolatore showbase. Ad esempio modificando la riga 8 come segue:

cout << showbase << x1 << endl;

l'output di quella riga risulterebbe:

0x20

Non è difficile trovare l'elenco completo dei manipolatori su vari siti, anche il nostro endl lo è.

La ricchezza di nozioni degli interi non finisce qui. C++ dispone anche di ulteriori specifiche per la determinazione dei tipi da attribuire a costanti numeriche. Cosa si intende? Lo vediamo subito:

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

Il punto è quel valore "10". Qual è il suo tipo, ovvero, come è memorizzato? Di norma C++ memorizza i valori numerici costanti come int, a meno che il valore non sia fuori range oppure a meno che non vengano apposte ulteriori specifiche sotto forma di suffisso legato al numero. I suffissi ammessi sono:

l o L per indicare l'appartenenza al tipo long
u o U specificano che il tipo è unsigned int
ul o UL (o anche Ul e uL e anche invertendo la u e la l) stanno per unsigned long
ll o LL stanno ad indicare long
ull o ULL o uLL o Ull (solo così) li useremo per gli unsigned long long

di norma, stante la somiglianza tra il numero "uno" e la lettera "elle" minuscola (1 / l) specialmente se si usano certi font, si consiglia di usare la L maiuscola.
Teniamo infine presente che computers o più in generale dispositivi diversi possono rappresentare lo stesso numero in maniera diversa a seconda della diversa impostazione del sistema (16 -32 bit ad esempio).

Altrettanto importante è il tipo char che, come evidente dal nome, è adatto alla memorizzazione di valori lettera, intesi anche come cifre singole. Il tipo char è definito tipicamente su 8 sufficienti alla rappresentazione di 128 caratteri (la base del classico sistema ASCII). Ovviamente non è possibile soddisfare tutte le esigenze alfabetiche con un range di soli 128 caratteri per cui sono stati creati tipi "allargati", già citati in precedenza. La gestione di base non è differente da quanto già visto:

  Esempio 2.4
1
2
3
4
5
6
7
8
9
#include <iostream>
int main()
{
  using namespace std;
  char ch01;
  cout << "Inserisci un carattere: ";
  cin >> ch01;
  cout << "Hai scritto: " << ch01;
}

Come vi faranno notare su tutti i testi di C++ quando digitate un carattere interviene il nostro fido cin che si occupa di convertire il carattere inserito nel suo corrispondente valore ASCII. In memoria è registrato il valore numerico corrispondente. Di contro quando richiamate il carattere per esporlo a video è cout ad intervenire e convertire il valore numerico nel carattere corrispondente nella solita tabella ASCII. Sono quindi cin e cout a fare il lavoro nell'ombra. Evidentemente questi due operatori sono guidati dal tipo di variabile che trattano. Un problema banale che a volte si presenta è quello di ricavare il codice ASCII di un certo carattere e quello di rappresentare il carattere identificato da un certo numero. Il seguente programma fa tutte queste cose:

  Esempio 2.5
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
int main()
{
using namespace std;
int x01;
char ch01 = 'R';
x01 = ch01;
cout << x01 << endl;
cout << ch01 << endl;
int x02 = 82;
char ch02 = x02;
cout << ch02;
}

La riga 6 è interessante in quanto ci propone la rappresentazione di un singolo carattere che, come evidente, viene semplicemente posto tra due singoli apici.

Anche C++ accetta le famose sequenza di escape, ovvero quelle concatenazioni che permettono di inserire in una stringa dei caratteri che, altrimenti, non sarebbero rappresentabili in modo facile, si pensi al semplice "a capo". La seguente tabella rappresenta le sequenze che possiamo usare:

Sequenza Significato
\n a capo
\t tabulazione orizzontale
\v tabulazione verticale
\b backspace, un carattere indietro
\r ritorno del carrello
\a beep di allarme
\\ backslash
\? ?
\' singolo apice '
\" doppio apice "

Per vedere all'opera queste sequenze l'esempio seguente:

  Esempio 2.6
1
2
3
4
5
6
#include <iostream>
int main()
{
using namespace std;
cout << "Ciao, \namico";
}

La riga 5 mette pratica in mostra quanto detto. Ovviamente potete fare qualche ulteriore test.
Gli altri tipi carattere sono wchar_t, char16_t, char32_t., i primi due definiti su 16 bits l'ultimo, come traspare dal nome, su 32. Il primo (abbreviazione di wide character) tuttavia è soggetto a implementazioni che possono cambiare ed è pensato per la gestione dell'intero set di caratteri della macchina su cui gira ed è consigliato per avere portabilità. Di solito comunque coincide con uno degli  altri due i quali invece vengono usati per rappresentare i caratteri Unicode 16 / 32. Vedremo altrove il loro uso più approfondito. Va comunque sottolineato che char16_t e char32_t sono caratteristici dell'ultima versione del linguaggio. e la loro nascita è dovuta alla possibilità generalizzata di usare Unicode.

L'ultimo tipo di cui ci occupiamo in questo paragrafo è il booleano caratterizzato dalla keyword bool. Il classico tipo che può assumere i due valori true e false. Tuttavia 1 e 0 sono puri valori accettabili corrispondendo rispettivamente a true e false. Anzi, ad una variabile booleana è possibile attribuire un valore numerico diverso. Solo 0 corrisponde a false, qualsiasi altro valore, anche negativo, a true. Questo comportamento in molti altri linguaggi è stato eliminato e i boolean hanno valore true / false e nessun altro. Probabilmente è un approccio migliore ma il C++ è nato con quella impostazione un po' ambigua e se la tiene (ci saranno dei motivi, chiaramente).