• Non ci sono risultati.

4.2 Classificazione dei tipi di dato

4.2.2 Tipi di dati non numerici

La lista dei tipi di dati non numerici è una lista aperta, molto variabile da linguaggio a linguaggio. In C++ esistono alcuni tipi non numerici: bool, char, string, puntatori.

Tipo logico

Il tipo logico ha come dominio l'insieme { false, true } (o sue forme equivalenti) e ha tre operazioni di base {AND, OR, NOT}. In C tali operazioni sono indicate con i simboli && per AND, || per OR e ! per NOT. In C++ esiste il tipo di dato bool avente come costanti false e true, mentre in C non

esiste un tipo di dato logico, dato che false è rappresentato con il numero intero 0 e true da qualsiasi numero intero diverso da 0 (solitamente 1).

Un dato di tipo logico rappresenta il valore di verità di una data affermazione, ovvero se è vera oppure no.

Le operazioni AND, OR e NOT servono a creare affermazioni complesse, costruite a partire da affermazioni di base.

Date due affermazioni P e Q, P && Q è la congiunzione di P e Q ed è vera se sia P che Q sono vere, invece è falsa se almeno una delle due è falsa. Ad esempio 4==3 && 5>2 è falsa in quanto 4==3 è falsa, mentre 4>=3 && 5>2 è vera in quanto sono vere sia 4>=3 che 5>2.

P || Q è la disgiunzione di P e Q ed è vera se almeno una tra P e Q è vera, invece è falsa quando sono entrambe false. Ad esempio 4==3 && 5>2 è vera in quanto 5>2 è vera, mentre 4==3 && 5<2 è falsa dato che sia 4==3 che 5<2 sono false.

Infine ! P è la negazione di P ed è vera solo quando P è falsa, mentre è falsa quando P è vera. Ad esempio ! 4==3 è vera, mentre ! 4>3 è falsa.

Indicando con x il valore di verità di P e con y quello di Q, per calcolare i valori di verità di P && Q, P || Q e ! P si usano le seguenti tabelle di verità:

x y x && y x || y ! x

false false false false true

false true false true true

true false false true false

true true true true false

Come è noto le operazioni && e || sono commutative e associative, && è distributiva rispetto a || e viceversa, vale la legge di De Morgan e della doppia negazione. In formule

● x && y = y && x, x || y = y || x

● x && (y && z) = (x && y) && z, x || (y || z) = (x || y) || z

● x || (y && z) = (x || y) && (x || z), x && (y || z) = (x && y) || (x && z),

● ! (x && y) = !x || !y, !(x || y) = !x && !y

● ! !x = x

Le operazioni && e || hanno un meccanismo particolare di valutazione chiamato corto-circuitazione come vedremo nella sezione 4.X.

Tipo carattere

Il tipo carattere serve a trattare singoli caratteri, di solito, appartenenti all'insieme dei caratteri ASCII o all'insieme Unicode.

In C++ esiste il tipo di dato char per i caratteri ASCII e il tipo wchar per i caratteri Unicode. In questo testo useremo solo i char.

A differenza di quanto avviene in molti linguaggi, in C++ il tipo carattere è in realtà un particolare tipo di dato numerico intero (i char usano 8 bit, i wchar 16 bit) che rappresenta direttamente il codice ASCII/Unicode del carattere.

Quindi il tipo char consente (almeno teoricamente) le stesse operazioni visti per gli int. Le uniche differenze sono che è possibile usare le costanti di tipo char: singoli caratteri racchiusi tra apici, ad

esempio 'A', il cui valore è 65, oppure 'a', il cui valore è 97. Inoltre i char sono trattati come caratteri (e non come numeri) nelle operazioni di ingresso e di uscita.

Illustrazione 1: I caratteri “stampabili” nella parte standard dell'insieme ASCII

Le operazioni di confronto sono fatte in base al codice, il quale rispetta l'ordine alfabetico per le lettere maiuscole e minuscole. È da notare, come riportato dalla tabella che le cifre precedono le lettere maiuscole, che a loro volta precedono quelle minuscole.

Dato che tra ogni lettera maiuscola e la rispettiva lettera minuscolo vi è una distanza fissa di 32 unità, 'A'+32 fa proprio 'a', 'B'+32 fa 'b', ecc..

Invece la conversione tra un numero compreso tra 0 e 9 e la cifra corrispondente si può fare semplicemente aggiungendo '0' al numero: ad esempio 2+'0' fa '2'.

I char sono visti come numeri interi con segno a 8 bit, quindi compresi tra -128 e 127. Esiste però anche il tipo unsigned char.

Tipo stringa

Il tipo di dato stringa, presente ormai in tutti i linguaggi moderni, è un tipo di dato a metà strada tra i tipi elementari e i tipi strutturati. Si tratta di un tipo il cui dominio è A* con A l'insieme dei caratteri (ASCII o Unicode). Le operazioni più diffuse sono la concatenazione tra stringhe, l'estrazione di una parte della stringa e il calcolo della lunghezza.

In C++ tale tipo di dato si chiama string (o wstring per le stringhe di caratteri Unicode) e sarà descritto nella sezione 9.X. Le costanti di tipo stringa sono delimitate dalle virgolette, ad esempio:

“io sono una stringa”.

Tipo puntatore

Il tipo di dato puntatore serve a manipolare indirizzi di memoria o, più genericamente, riferimenti di variabili o di funzioni.

In C++ tale tipo di dato è dotato anche di alcune operazioni “aritmetiche” e sarà descritto nel capitolo 13. 0 1 2 3 4 5 6 7 8 9 30 33 ! " # $ % & ' 40 ( ) * + , - . / 0 1 50 2 3 4 5 6 7 8 9 : ; 60 < = > ? @ A B C D E 70 F G H I J K L M N O 80 P Q R S T U V W X Y 90 Z [ \ ] ^ _ ` a b c 100 d e f g h i j k l m 110 n o p q r s t u v w 120 x y z { | } ~ 

4.3 Costanti

Le costanti sono valori di un determinato tipo di dato che possono essere scritti direttamente all'interno di un programma. Ovviamente tali valori sono immutabili. Ad esempio scrivendo 5 all'interno di un'istruzione si intende usare il valore intero 5. Come si è visto le costanti devono essere specificate usando opportune regole grammaticali, che dipendono dal linguaggio.

Molti linguaggi consentono di creare costanti con nome, cioè attribuire un nome ad un valore costante ed utilizzare il nome al posto di tale valore.

Ad esempio è possibile definire una costante di nome pi_greco dal valore di 3.141592653589793, in modo che al posto di scrivere ogni volta tutta la sequenza di cifre, si possa semplicemente scrivere pi_greco. La modalità di definizione delle costanti in C++ sarà descritta nel prossimo capitolo.

In altri casi si usano costanti con nome per rendere parametrico un programma allo scopo di facilitare le operazioni di modifica. Ad esempio se in un programma si fa uso molteplice della percentuale dell'IVA conviene definire una costante iva con il valore 0.20, di modo che sia più semplice aggiornare il programma qualora la percentuale cambiasse.

4.4 Variabili

Le variabili sono entità che consentono di memorizzare, di recuperare ed eventualmente modificare un valore di un certo tipo di dato.

Le operazioni fondamentali che possono essere svolte su una variabile V sono l'accesso in lettura e quello in scrittura.

La lettura recupera il valore memorizzato precedentemente in V e lo utilizza in un'espressione.

La scrittura memorizza in V un nuovo valore, perdendo traccia del valore precedentemente memorizzato.

La proprietà fondamentale di una variabile è la persistenza: tutte le letture di una variabile V effettuate tra due operazioni di scrittura recuperano sempre lo stesso valore.

Le variabili sono presenti in tutti i linguaggi imperativi e sono implementate attraverso la memoria centrale o in casi eccezionali tramite i registri: a ciascuna variabile sono assegnate una o più celle di memoria (o registri) e tutte le operazioni di modifica e di accesso sono svolte mediante analoghe operazioni di scrittura e lettura della memoria.

Le caratteristiche principali delle variabili sono 1. nome

2. tipo

3. zona di visibilità 4. modo di allocazione

Il nome deve identificare univocamente la variabile all'interno dell'insieme delle variabili a cui si può accedere dal programma e quindi non possono coesistere variabili con lo stesso nome. Di norma il nome delle variabili è un identificatore la cui grammatica è stata data nella sezione 3.X.

Il tipo indica quali dati possono essere memorizzati nella variabile. Esistono linguaggi tipizzati dinamicamente in cui le variabili non hanno un tipo fissato, ma la maggior parte dei linguaggi compilati sono tipizzati staticamente, per cui ogni variabile è associata ad un tipo di dato e può contenere solo dati di quel tipo.

La zona di visibilità denota la parte di programma in cui una variabile può essere usata. Si distinguono variabili globali, che sono visibili in tutto il programma, e variabili locali, che sono visibili in una parte ristretta del programma, di solito delimitata in qualche modo.

L'allocazione è la modalità con cui viene gestita la parte di memoria associata ad una variabile. Esistono tre modalità principali: statica, automatica e dinamica.

Una variabile allocata staticamente resterà presente in memoria durante tutta l'esecuzione del programma. Si tratta della modalità più consona per le variabili globali, anche se in alcuni linguaggi, tra cui C++, è possibile avere variabili locali statiche.

Una variabile X ad allocazione automatica è creata quando la parte di programma in cui X è visibile inizia ad essere eseguita ed è eliminata dalla memoria quando tale parte finisce di essere eseguita. La creazione e la sua eliminazione sono svolte automaticamente dall'ambiente di esecuzione. Si tratta della modalità più adatta a gestire le variabili locali, in quanto permette di avere in memoria solo le variabili a cui si può accedere eliminando quelle che invece non sono più accedibili. Daremo maggiori dettagli nel capitolo 11.

Infine nell'allocazione dinamica è il programmatore attraverso delle opportune istruzioni a creare ed eventualmente ad eliminare le variabili dalla memoria tramite delle opportune istruzioni. Comunque in alcuni linguaggi moderni, ad esempio in Java, la eliminazione delle variabili allocate dinamicamente è svolta dall'ambiente di esecuzione, mediante un meccanismo chiamato garbage

collection. L'allocazione dinamica richiede l'uso dei puntatori (o dei riferimenti) e la sua

implementazione in C++ sarà descritta nel capitolo 13.

In molti linguaggi le proprietà delle variabili sono stabilite in un'opportuna istruzione di

dichiarazione. In tali istruzioni il programmatore elenca tutte le variabili utilizzate, nel programma

o in una sua parte, indicandone il nome, il tipo ed altre caratteristiche. Le restanti caratteristiche sono desunte dal traduttore mediante il contesto in cui si trova la dichiarazione. Ad esempio la zona di visibilità in C++, in Pascal o in linguaggi simili, è limitata al blocco in cui si trova la dichiarazione.

La dichiarazione delle variabili fa sì che il traduttore sappia quali sono le variabili presenti nel programma, riuscendo sia ad evitare errori di accesso a variabili inesistenti, sia a poter attribuire correttamente il tipo di ogni espressione. La sintassi per dichiarare variabili in C++ sarà fornita nel prossimo capitolo.

4.5 Espressioni

Le espressioni sono entità sintattiche che denotano valori. Il valore di un'espressione è calcolato mediante un processo di valutazione (che corrisponderà ad istruzioni effettivamente eseguite dalla macchina), a differenza di quanto avviene per le costanti (il cui valore è fisso) e per le variabili (il cui valore è memorizzato).

La grammatica che genera le espressioni è la seguente:

espressione ::= costante | variabile | “(“ espressione “)” |

opUn espressione | espressione opBin espressione | funzione “(“ espressione { “,” espressione } “)” opUn ::= “-” | “!” | “*” | ...

opBin ::= “+” | “-” | “*” | “/” | “%” | “&&” | “||” | “<” | ... funzione ::= “labs” | “sqrt” | “pow” | “log” | “exp” | ...

Gli operatori si distinguono per

il numero di argomenti: quelli unari, con un argomento, tipo il – per cambiare di segno, e quelli binari, con due argomenti, come il +, sono quelli più frequenti

la posizione rispetto agli operandi: quelli unari possono essere prefissi (precedono l'argomento) o postfissi (lo seguono), invece quelli binari sono di solito infissi (si trovano in mezzo ai due operandi)

• la precedenza: si dice che un operatore binario ⊗ ha precedenza su un operatore binario

se l'espressione x ⊕ y ⊗ z è interpretata come x ⊕ (y ⊗ z). Ad esempio il * ha precedenza sul +. In modo analogo un operatore unario † ha precedenza su un operatore binario ⊕ se l'espressione †x ⊕ y è interpretata come (†x) ⊕ y.

• L'associatività: si dice che un operatore binario ⊕ è associativo a sinistra se l'espressione x ⊕ y ⊕ z è interpretata come (x ⊕ y) ⊕ z. Analogo è il concetto di operatore associativo a

destra.

La grammatica sopra elencata è ambigua e per ottenere un unico albero di derivazione per ciascuna espressione sintatticamente corretta si definiscono dei vincoli di precedenza. Ecco la tabella delle precedenza per i principali operatori presenti in C++ ed utilizzati in queste dispense, disposti in ordine decrescente di precedenza

1. operatori unari - ! * 2. * / % 3. + - 4. < > <= >= 5. != == 6. && 7. ||

Dall'albero di derivazione dell'espressione eliminando i simboli non terminali è possibile ricavare un albero che rappresenta in maniera univoca l'espressione stabilendo l'ordine di applicazione delle operazioni e delle funzioni. Tale albero è essenziale per capire il meccanismo di valutazione delle espressioni (sezione 4.7).

Ogni nodo interno dell'albero contiene un'operazione ed è collegato ai suoi operandi. La radice dell'albero corrisponde all'operazione da svolgere per ultima, mentre le foglie corrispondono agli operandi più interni (costanti e variabili).

Ad esempio l'albero che rappresenta l'espressione y*z-3 è

Le parentesi alterano la normale precedenza tra le operazioni: una sottoespressione tra parentesi sarà valutata per prima. Ad esempio l'albero che rappresenta y*(z-3) è

y

z

3

-

Infatti l'operazione da svolgere per ultima è il *.

Documenti correlati