Operatori e operandi

 


 

Definizione di operatore e regole generali

 

Un operatore è un token che agisce su una coppia di dati (o su un singolo dato), detti operandi, ottenendo un nuovo dato (risultato dell'operazione).

Ogni operatore è identificato da un particolare simbolo grafico, costituito di solito da un solo carattere (ma talvolta anche da due o più caratteri). Non tutti gli operatori possono applicarsi ad ogni tipo di operando, ma, per ogni operatore esiste un ben definito insieme di tipi di operandi a cui l'operatore è applicabile.

Un operatore è detto binario se agisce su due operandi, unario se agisce su un solo operando. Se l'operatore è binario, i due operandi sono detti left-operand e right-operand.

Un'espressione è una successione di operazioni in cui il risultato di ogni operazione diviene operando per le operazioni successive, fino a giungere ad un unico risultato. L'ordine in cui le operazioni sono eseguite è regolato secondo precisi criteri di precedenza e associatività fra gli operatori.

Es.          a op1 b op2 c       (a, b, c sono operandi, op1 e op2 sono operatori)

  1. op1 ha la precedenza: il risultato di a op1 b diventa left-operand di op2 c

  2. op2 ha la precedenza: il risultato di b op2 c diventa right-operand di a op1

  3. op1 e op2 hanno la stessa precedenza, ma l'associatività procede da sinistra a destra: come nel caso 1.

  4. op1 e op2 hanno la stessa precedenza, ma l'associatività procede da destra a sinistra: come nel caso 2.

Per ottenere che un'operazione venga comunque eseguita con precedenza rispetto alle altre, bisogna racchiudere operatore e operandi fra parentesi tonde.
Es.          a op1 ( b op2 c )       (la seconda operazione viene eseguita per prima)

 


 

Operatore di assegnazione

 

L'operatore binario di assegnazione = copia il contenuto del right-operand (detto nello specifico r-value) nel left-operand (detto l-value).

a = b

b (r-value) può essere una qualunque espressione che restituisce un valore di tipo nativo;

a (l-value) ha un ambito di scelta molto più ristretto (tutti gli l-values possono essere r-values, ma non viceversa); in pratica, salvo poche eccezioni, a deve essere una variabile.

I tipi di a e b devono coincidere, oppure il tipo di b deve essere convertibile implicitamente nel tipo di a.

 


 

Operatori matematici

 

Come in tutti i linguaggi di programmazione, le operazioni matematiche fondamentali (addizione, sottrazione, moltiplicazione, divisione) sono eseguite rispettivamente dai seguenti operatori binari:

+            -            *            /

Se la divisione è fra due numeri interi, il risultato dell'operazione è ancora un numero intero (troncamento).
Es.                      27 / 4           da' come risultato 6 (anziché 6.75).

Il resto di una divisione fra numeri interi si calcola con l'operatore binario %
Es.                      27 % 4           da' come risultato 3

 


 

Operatori a livello del bit

 

Il C++ può, a differenza da altri linguaggi, operare sulle variabili intere a livello del bit.

 

L'operatore binario >> produce lo scorrimento a destra (right-shift) dei bit del left-operand, in quantità pari al right-operand. In pratica esegue una divisione intera (con divisore uguale a una potenza di 2)
Es.                       a >> n             equivale a     a / 2n
Dalla sinistra entrano cifre binarie 0 se il numero a è positivo, oppure cifre binarie 1 se il numero a è negativo (a causa della notazione a complemento a 2 dei numeri negativi).

 

L'operatore binario << produce lo scorrimento a sinistra (left-shift) dei bit del left-operand, in quantità pari al right-operand. In pratica esegue una moltiplicazione per una potenza di 2
Es.                       a << n             equivale a     a * 2n
Dalla destra entrano sempre cifre binarie 0.

 

Gli operatori binari &, |, e ^ eseguono operazioni logiche bit a bit fra i due operandi, e precisamente:

& esegue l'AND fra i corrispondenti bit dei due operandi
| esegue l'OR inclusivo fra i corrispondenti bit dei due operandi
^ esegue l'OR esclusivo (XOR) fra i corrispondenti bit dei due operandi

Es., date due variabili char a e b i cui valori sono, in notazione binaria:
  a = 0 1 0 0 1 1 0 1

(77)

  b = 0 0 1 1 1 0 1 0

(58)

i risultati delle tre operazioni sono rispettivamente:
  a & b = 0 0 0 0 1 0 0 0

(8)

  a | b = 0 1 1 1 1 1 1 1

(127)

  a ^ b = 0 1 1 1 0 1 1 1

(119)

 

L'operatore unario ~ inverte i bit dell'operando, cioè calcola il suo complemento a uno (in pratica, se il numero é signed, lo inverte di segno e sottrae 1).

 


 

Operatori binari in notazione compatta

 

Data l'espressione:

a = a op b

dove op è un'operatore matematico o a livello del bit, b è un'espressione qualsiasi e a è una variabile, le due operazioni possono essere sintetizzate in una tramite l'operatore binario op=

Es.                       MiaVariabile *= 4             equivale a     MiaVariabile = MiaVariabile*4

La notazione compatta è conveniente soprattutto quando il nome delle variabili è lungo!

 


 

Operatori relazionali

 

Gli operatori binari relazionali sono:

>     >=     <     <=     ==     !=

Questi operatori eseguono il confronto fra i valori dei due operandi (che possono essere di qualsiasi tipo nativo) e restituiscono un valore booleano:

Esempi:

 


 

Operatori logici

 

Gli operatori logici sono:

&&          ||          !

Questi operatori agiscono su operandi booleani e restituiscono un valore booleano:

L'operatore binario && esegue l'AND logico fra gli operandi:
         a && b              risultato: true se entrambi a e b sono true; altrimenti: false

L'operatore binario || esegue l'OR logico fra gli operandi:
         a || b                  risultato: false se entrambi a e b sono false; altrimenti: true

L'operatore unario ! esegue il NOT logico dell'operando:
         !
a                       risultato: true se a é false o viceversa

 

Notare la differenza fra gli operatori logici    &&   e   ||   e gli operatori di confronto bit a bit   &   e   |
Es:     5 && 2              

restituisce true in quanto entrambi gli operandi sono true (ogni intero diverso da zero è convertito in true)

5 & 2              

restituisce 0 (e quindi false se convertito in booleano) in quanto i bit corrispondenti sono tutti diversi

 


 

Operatori di incremento e decremento

 

Gli operatori unari di incremento ++ o decremento -- fanno aumentare o diminuire di un'unità il valore dell'operando (che deve essere un l-value di qualunque tipo nativo). Equivalgono alla sintesi di un operatore binario di addizione o sottrazione, in cui il right-operand è 1, con un operatore di assegnazione, in cui il left-operand coincide con il left-operand dell'addizione o sottrazione.

Es.                       MiaVariabile++;             equivale a     MiaVariabile = MiaVariabile+1;

la prima forma è più rapida e compatta (specialmente se il nome della variabile è lungo!) .

L'operatore può seguire (suffisso) o precedere (prefisso) l'operando. Nella forma prefisso l'incremento (o il decremento) viene eseguito prima che la variabile sia utilizzata nell'espressione, nella forma suffisso avviene il contrario.
 

Es:      

int  a,  b,  c=5 ;

a  =   c++;

b  =   ++c;

alla fine di queste operazioni si trovano, nella variabili a, b e c, rispettivamente i valori 5, 7 e 7.
Da questo esempio si capisce anche che la forma incremento (o decremento) conviene non solo perché è più compatta, ma soprattutto perché consente di ridurre il numero di istruzioni.

  [p09]

 


 

Operatore condizionale

 

L'operatore condizionale é l'unico operatore ternario (tre operandi):

condizione ? espressioneA : espressioneB

(dove condizione é un'espressione logica) l'operazione restituisce il valore dell'espressioneA se la condizione e' true o il valore dell'espressioneB se la condizione e' false

Es:       minimo   =   a  <  b  ?  a  :  b  ;

L'operatore condizionale gode della rara proprietà di restituire un ammissibile l-value (non in tutti i compilatori, però!)
Es.          (m  <  n  ?  a  :  b) = c ;
(memorizza il valore di c in a se m é minore di n, altrimenti memorizza il valore di c in b)
in questo caso però a e b non possono essere né espressionicostanti, ma soltanto l-values.

 


 

Conversioni di tipo

 

  Conversioni di tipo implicite

Il C++ esercita un forte controllo sui tipi e da' messaggio di errore quando si tenta di eseguire operazioni fra operandi di tipo non ammesso. Es. l'operatore % richiede che entrambi gli operandi siano interi.

I quattro operatori matematici si applicano a qualsiasi tipo intrinseco, ma i tipi dei due operandi devono essere uguali. Tuttavia, nel caso di due tipi diversi, il compilatore esegue una conversione di tipo implicita su uno dei due operandi, seguendo la regola di adeguare il tipo più semplice a quello più complesso, secondo la seguente gerarchia (in ordine crescente di complessità):

bool -  char -  unsigned char -  short -  unsigned short -  long -  unsigned long -  float -  double -  long double

Es:      

nell'operazione 3.4 / 2 il secondo operando è trasformato in 2.0 e il risultato è correttamente 1.7

Nelle assegnazioni, il tipo del right-operand viene sempre trasformato implicitamente nel tipo del left-operand (con un messaggio warning se la conversione potrebbe implicare loss of data (perdita di dati), trasformando un tipo più complesso in un tipo più semplice).

Es:      

date le variabili    char c   e   double d,    l'assegnazione   c = d   è ammessa, ma genera un messaggio warning in fase di compilazione.

Nelle operazioni fra tipi interi, se il valore ottenuto esce dal range (overflow), l'errore non viene segnalato. La stessa cosa dicasi se l'overflow si verifica a seguito di una conversione di tipo.

Es:      

short  n   =  32767  ;

n++ ;

(l'errore non viene segnalato, ma in n si ritrova il numero -32768)

 

  Conversioni di tipo esplicite (casting)

Quando si vuole ottenere una conversione di tipo che non verrebbe eseguita implicitamente, bisogna usare l'operatore binario di casting (o conversione esplicita), che consiste nell'indicazione del nuovo tipo fra parentesi davanti al nome della variabile da trasformare.

Es. se la variabile n é di tipo int, l'espressione (float)n trasforma il contenuto di n da int in float.

In C++ si può usare anche il formato funzione (function-style casting):

float(n)   é   equivalente   a   (float)n

va detto che il function-style casting non è sempre possibile (per esempio con i puntatori non si può fare).

Tutti i tipi nativi consentono il casting, fermo restando il fatto che, se la variabile da trasformare è operando di una certa operazione, il tipo risultante deve essere fra quelli ammissibili (altrimenti viene generato un errore in compilazione). Per esempio: float(n) % 3 é errato in quanto l'operatore % ammette solo operandi interi.

Vediamo ora un esempio in cui si evidenzia la necessità del casting:
       int m=10, n=4;
float r, a=2.7F;
r = m/n+a;

nell'ultima istruzione la divisione è fra due numeri interi e quindi, essendo i due operandi dello stesso tipo, la conversione implicita non viene eseguita e il risultato della divisione è il numero intero 2; solo successivamente questo numero viene convertito in modo implicito in 2.0 per essere sommato ad a. Se vogliamo che la conversione a float avvenga prima della divisione, e che questa fornisca il risultato esatto (cioè 2.5), dobbiamo convertire esplicitamente almeno uno dei due operandi e quindi riscrivere così la terza istruzione:

r = (float)m/n+a;   (non servono altre parentesi perchè il casting ha la precedenza
  sulla divisione)

Il casting che abbiamo esaminato finora è quello del C (C-style casting). Il C++ ha aggiunto altri quattro operatori di casting, suddividendo le conversioni di tipo in altrettante categorie e riservando un operatore per ciascuna di esse (per fornire al compilatore strumenti di controllo più raffinati). D'altra parte il C-style casting (che li comprende tutti) è ammesso anche in C++, e pertanto non tratteremo in questo corso degli altri operatori di casting, limitandoci a fornirne l'elenco:
                 static_cast<
T>(E)
                 dynamic_cast<T>(E)
                 reinterpret_cast<
T>(E)
                 const_cast<
T>(E)
dove è E un'espressione qualsiasi il cui tipo è convertito nel tipo T.

 


 

Precedenza fra operatori

 

Nella seguente tabella gli operatori sono in ordine di precedenza decrescente (nello stesso blocco di righe hanno uguale precedenza):

[Legenda degli operandi:  id=identificatore;     pid=puntatore a identificatore; expr=espressione;    lv=l-value;     ...=operandi opzionali]

DESCRIZIONE OPERATORE CATEGORIA SIMBOLO E
OPERANDI
ASSOCIATIVITA'
 risoluzione di visibilità
 riferimento globale

binario
unario

id::id
::id

 da sinistra a destra
 ----
 selezione di un membro
 selezione di un membro puntato
 indicizzazione array
 chiamata di funzione
 incremento suffisso
 decremento suffisso
 identificazione di tipo

binario
binario

binario
binario

unario
unario

unario

id.id
pid->id
id[expr]

id(expr)
lv++
lv--
typeid(expr)

 da sinistra a destra
 da sinistra a destra
 da sinistra a destra
 ----
 ----
 ----
 ----
 dimensione di un oggetto
 complemento a 1
 NOT logico
 incremento prefisso
 decremento prefisso
 segno - algebrico
 segno + algebrico
 indirizzo di memoria
 dereferenziazione
 allocazione di memoria
 deallocazione di memoria
 casting (conversione di tipo)

unario
unario

unario
unario

unario
unario

unario
unario
unario
ternario
binario
binario

sizeof(expr)
~ expr

! expr
++lv
--lv
- expr
+ expr
&lv
*pid
new tipo ...
delete ... pid
(tipo)expr

 ----
 da destra a sinistra
 da destra a sinistra
 da destra a sinistra
 da destra a sinistra
 da destra a sinistra
 da destra a sinistra
 da destra a sinistra
 da destra a sinistra
 ----
 ----
 da destra a sinistra
 moltiplicazione
 divisione
 resto di divisione intera

binario
binario

binario

expr * expr
expr / expr

expr % expr

 da sinistra a destra
 da sinistra a destra
 da sinistra a destra
 addizione
 sottrazione

binario
binario

expr + expr
expr - expr

 da sinistra a destra
 da sinistra a destra
 scorrimento a destra
 
scorrimento a sinistra

binario
binario

expr >> expr
expr << expr

 da sinistra a destra
 da sinistra a destra
 minore
 
minore o uguale

 maggiore
 maggiore o uguale

binario
binario

binario
binario

expr < expr
expr <= expr
expr
> expr

expr >= expr

 da sinistra a destra
 da sinistra a destra
 da sinistra a destra
 da sinistra a destra
 uguale
 diverso

binario
binario

expr == expr
expr != expr

 da sinistra a destra
 da sinistra a destra
 AND bit a bit

binario

expr & expr

 da sinistra a destra
 XOR bit a bit

binario

expr ^ expr

 da sinistra a destra
 OR bit a bit

binario

expr | expr

 da sinistra a destra
 AND logico

binario

expr && expr

 da sinistra a destra
 OR logico

binario

expr || expr

 da sinistra a destra
 espressione condizionale

ternario

expr ? expr : expr

 da destra a sinistra
 assegnazione
 moltiplicazione e assegnazione
 divisione e assegnazione
 resto e assegnazione
 addizione e assegnazione
 sottrazione e assegnazione
 scorrimento a destra e assegnazione
 scorrimento a sinistra e assegnazione
 AND bit a bit e assegnazione
 OR bit a bit e assegnazione
 XOR bit a bit e assegnazione

binario
binario
binario
binario
binario
binario
binario
binario
binario
binario
binario

lv = expr
lv *= expr
lv /= expr
lv %= expr
lv += expr
lv -= expr
lv >>= expr
lv <<= expr
lv &= expr
lv |= expr
lv ^= expr

 da destra a sinistra
 da destra a sinistra
 da destra a sinistra
 da destra a sinistra
 da destra a sinistra
 da destra a sinistra
 da destra a sinistra
 da destra a sinistra
 da destra a sinistra
 da destra a sinistra
 da destra a sinistra
 serializzazione delle espressioni

binario

expr , expr

 da sinistra a destra

 


 

Ordine di valutazione

 

Le regole di precedenza e associatività fra gli operatori non garantiscono che l'ordine di valutazione delle sotto-espressioni all'interno di un espressione sia sempre definito. Per esempio, si consideri l'espressione:
         a
= fun1(5) + fun2(3);       (dove fun1 e fun2 sono funzioni)
le regole di precedenza assicurano che prima vengano eseguite le chiamate delle funzioni, poi l'addizione e infine l'assegnazione, ma non è definito quale delle due funzioni venga chiamata per prima.

Un altro caso di ordine di valutazione indefinito si ha fra gli argomenti di chiamata di una funzione:
Es.       funz(expr1,expr2)           valuta
prima expr1 o expr2 ?

 
All'opposto, in molti casi, l'ordine di valutazione è univocamente definito, come per esempio nelle operazioni logiche:

  1.          expr1   &&  expr2
  2.          expr1   ||  expr2

in entrambi i casi expr1 è sempre valutata prima di expr2;  in più, expr2 è  valutata (e quindi eseguita) solo se è necessario. In altre parole:

questo tipo di azione si chiama valutazione cortocircuitata ed è molto utile perché consente di ridurre il numero di istruzioni.

 


 

Torna all'Indice