_________________________
< *c && printf("%s\n",c); >
-------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
Le espressioni logiche (in linea generale) sono quelle espressioni che contengono una serie di operazioni di confronto e operatori logici. Il risultato di un’espressione logica è un valore booleano, ovvero true o false. Confrontare il valore di due variabili ha senso solo quando si ha a che fare con variabili numeriche: il confronto tra stringhe di caratteri o qualunque altro tipo dato va effettuato mediante opportune funzioni.
Esiste un tipo dati specifico per i risultati di un’espressione logica, il tipo bool che assume valori true o false; questo tipo è presente solo nelle versioni più recenti dello standard C, mentre nelle versioni precedenti è usato un tipo intero al suo posto. Tale possibilità è presente per retrocompatibilità: un valore pari 0 equivale a false e qualunque valore diverso da zero viene considerato true.
| Valore | Significato |
0 | false |
qualunque != 0 | true |
Confronto tra variabili
I tipi che possiamo confrontare sono pertanto: char, int , float, ma anche ogni tipo di puntatore. È possibile confrontare due caratteri perché il tipo char lo si può interpretare come una variabile numerica a 8 bit, ovvero un valore compreso tra 0 e 255 (o se si vuole, -128 a +127). Il suo valore non è altro che il codice ASCII del carattere che rappresenta, per cui confrontare due char è perfettamente lecito e sensato.
Per confrontare due valori numerici si utilizzano gli operatori <, ==, !=, >, <=, >=. Il risultato del confronto è dato dalla seguente tabella:
| Simbolo | Uso | Vero | Falso |
|---|---|---|---|
< | a<b | se a è minore di b | se a è maggioreo uguale a b |
<= | a<=b | se a è minore o uguale a b | se a è maggiore di b |
== | a==b | se a è uguale a b | se a è diverso da b |
!= | a!=b | se a è diverso da b | se a è uguale a b |
> | a>b | se a è maggiore di b | se a è minoreo uguale a b |
>= | a>=b | se a è maggiore o uguale a b | se a è minore di b |
Questi confronti hanno senso quando effettuati tra due tipi numerici, quindi tra int, long, float, double, ma come abbiamo visto anche char e puntatori ; un codice come questo quindi ha perfettamente significato e può essere usato per capire se due variabili puntatore puntano alla stessa area di memoria o meno.
int isEqualAddress(char *p, char *q)
{
return(p==q);
}
E se cercasse di confrontare tipi dati diversi? Ad esempio un intero e un float?
int a=100;
float b=100.0;
if (a==b) printf("Sono uguali\n");
else printf("Non sono uguali\n");
Si tenga presente che i tipi int e float risultano completamente diversi nel modo con cui all’interno delle rispettive aree di memoria vengono codificati i dati[*], quindi anche se apparentemente il valore delle due variabili è lo stesso, nelle rispettive aree di memoria la sequenza di bit usati per rappresentarli è diversa. Ebbene il confronto considera i due valori uguali poiché viene effettuata automaticamente la conversione al tipo dato più ampio, ovvero l’intero viene convertito in virgola mobile prima di effettuare il confronto.
Gli operatori logici
Ai risultati booleani possono poi essere applicati gli operatori logici, che permettono di combinare uno o più valori booleani ottenendo come risultato un valore booleano. Gli operatori logici in C sono &&, || e !, i primi due sono operatori binari mentre il terzo è unario ed in termini più tecnici sono anche detti congiunzione logica, disgiunzione logica e negazione logica. La seguente tabella riporta il significato dei simboli che definiscono gli operatori:
| Operatore | Nome | Significato |
|---|---|---|
&& | AND | Operatore binario di and logico: il risultato è vero se entrambi gli operandi sono veri, è falso in tutti gli altri casi |
|| | OR | Operatore binario di or logico: il risultato è vero se almeno uno degli operandi è vero, è falso in tutti gli altri casi |
! | NOT | Operatore unario di not logico: il risultato è vero se l’operando è falso, è falso se il l’operando è vero |
Questi operatori si possono meglio descrivere attraverso delle tavole di verità:
Congiunzione logica – AND
| A | B | A && B |
|---|---|---|
| true | true | true |
| true | false | false |
| false | true | false |
| false | false | false |
Disgiunzione logica – OR
| A | B | A||B |
|---|---|---|
| true | true | true |
| true | false | true |
| false | true | true |
| false | false | false |
Negazione logica – NOT
| A | !A |
|---|---|
| true | false |
| false | true |
Esempio d’uso degli operatori
Quindi quando si vogliono combinare più istruzioni in una sola condizione gli operatori vanno utilizzati come nell’esempio seguente, in cui viene cercato all’interno di una stringa l’occorrenza di un determinato carattere:
#include <stdio.h>
int FindChar(char *s, char c)
{
char *a = s;
int ret = -1;
while(*s!='\0' && *s!=c) s++;
if(*s!='\0') ret = (int)(s-a);
return ret;
}
void main()
{
char *s="Hello world!";
int index;
index=FindChar(s,'o');
if(index==-1) {
printf("Carattere non trovato\n");
}
else {
printf("Carattere trovato in posizione %d\n",index);
}
}
L’espressione (*s!='\0' && *s!=c) permette di compiere due verifiche: la prima controlla di non aver incontrato un carattere di fine stringa (il carattere null ovvero '\0'), il che significherebbe aver scorso tutta la stringa senza aver trovato nulla; la seconda verifica consiste nel confrontare il carattere puntato con quello cercato. Se si verificano entrambe queste due condizioni si passa a controllare il carattere successivo (con l’operazione di autoincremento s++). Si noti che entrambi i confronti sono “in negativo” ovvero si usa il confronto != ma questa stessa espressione si può riscrivere in maniera equivalente come (!(*s=='\0' || *s==c)): viene negata l’inversa dell’espressione ovvero i confronti si invertono e gli AND vengono sostituiti da OR e viceversa, e ciò per le regole dell’algebra booleana[*]. Ciò comporta che la stessa funzione può essere scritta in maniera equivalente come:
int FindChar(char *s, char c)
{
char *a = s;
int ret = -1;
while(!(*s=='\0' || *s==c)) s++;
if(*s!='\0') ret = (int)(s-a);
return ret;
}
Si noti poi che nell’operazione di autoincremento s++ e la valorizzazione della variabile ret si usino come argomento di operazioni aritmetiche non dei semplici numeri ma dei puntatori: in C ciò è perfettamente valido poiché si applica l’aritmetica dei puntatori[*] .
Precedenza degli operatori
Riguardo espressioni logiche più complesse si tenga presente che si può predeterminare l’ordine con cui gli operatori agiscono mediante le parentesi, anche per permettere una corretta lettura dell’operazione logica che come vedremo presenta alcune peculiarità che complicano la sua interpretazione.
Quando si vuole valutare un’espressione logica composta da più operatori, se non ci sono parentesi a definire l’ordine, si deve immaginare di eseguire prima la negazione, poi la congiunzione e infine la disgiunzione ovvero prima il NOT poi l’AND ed infine l’OR. Operandi con la stessa priorità vengono eseguiti in ordine da sinistra verso destra.
Confrontiamo ad esempio il risultato delle due espressioni logiche (A || B) && C e (A || B && C) mediante un semplice programma di prova
#include <stdio.h>
void main ()
{
int A,B,C;
printf("Exp1=(A || B) && C\n");
printf("Exp2=(A || B && C)\n");
printf("A\tB\tC\t=>\tExp1\tExp2\n");
for (A = 0; A < 2; A++){
for (B = 0; B < 2; B++){
for (C = 0; C < 2; C++){
printf("%d\t%d\t%d\t=>\t%d\t%d\n", A, B, C, (A || B) && C, A || B && C);
}
}
}
}
Otteniamo questo output
Exp1=(A || B) && C
Exp2=(A || B && C)
A B C => Exp1 Exp2
0 0 0 => 0 0
0 0 1 => 0 0
0 1 0 => 0 0
0 1 1 => 1 1
1 0 0 => 0 1
1 0 1 => 1 1
1 1 0 => 0 1
1 1 1 => 1 1
L’ordine di esecuzione è puramente virtuale, dal momento che, se dall’albero logico delle operazioni da compiere si evince che il risultato è già determinato le valutazioni successive non vengono eseguite. Questo comportamento è detto “regola del corto circuito”.
Regola del corto circuito
Nell’eseguire un’espressione logica il compilatore implementa un algoritmo volto ad ottimizzare e minimizzare i calcoli. Se il risultato è già determinato ovvero se, qualunque siano i valori di verità delle successive valutazioni il risultato dell’espressione non cambia, allora queste valutazioni non vengono eseguite. Ad esempio nella seguente espressione (f==0 && g==0), se la variabile f è non nulla allora non viene effettuato il test sulla variabile f, dal momento che qualunque sia il suo valore l’espressione risulterà comunque falsa. Questa sembra una conseguenza di poco conto, mentre potrebbe risultare significativa. Si supponga che f e g non siano semplici variabili, ma delle funzioni, per cui riscriviamo l’espressione come (f()==0 && g()==0), ne consegue che se la funzione f() ritorna un valore pari a zero allora la funzione g() sarà eseguita, altrimenti: questo comportamento può influenzare il flusso del programma. Analogamente nella (f()==0 || g()==0), se la funzione f() ritorna zero, la prima condizione è vera e quindi l’espressione risulterà vera qualunque sia il ritorno della g(), che pertanto non sarà eseguita.
Questo modo di gestire il flusso del programma appesantisce sicuramente la leggibilità del codice e può portare a dei fraintendimenti, pertanto va usato con attenzione laddove è necessario, ma lo si deve conoscere per evitare risultati inaspettati e saper interpretare il significato del codice altrui, poiché si tratta di una simbologia di uso comune tra i programmatori C. Si può trovare talvolta del codice del tipo
char c[]="Muuu!";
*c && printf("%s\n",c);
Questo frammento di codice stampa una stringa di caratteri se non è vuota, ma la modalità con cui compie questa operazione potrebbe sembrare criptico, soprattutto se non si sono ancora approfonditi i puntatori[*] e le stringhe di caratteri[*]. Si tenga conto che zero equivale a falso, pertanto se il carattere è un carattere nullo (il terminatore di stringa) e cioè 0, questo valore viene interpretato come falso; qualunque sia il valore di destra dell’operatore && il risultato risulterà falso, quindi non viene neppure valutato, quindi la funzione printf non viene invocata. Al contrario se la stringa è valorizzata il valore di sinistra sarà vero e pertanto per determinare il risultato dell’espressione algebrica risulterà necessario richiamare la funzione printf. Quanto abbiamo esposto cioè equivale esattamente a questo:
if (c[0] != '\0'){
printf("%s\n",c);
}
Si tratta quindi di un’applicazione della regola del corto circuito che sfrutta la flessibilità della sintassi C creando del codice perfettamente lecito ma che potrebbe destare dei dubbi di interpretazione e che in alcuni contesti è altamente sconsigliato, infatti quando leghiamo tre funzioni con dei connettivi logici, per l’azione delle regole di precedenza e della regola del corto circuito, potremo trovarci di fronte a risultati inaspettati, ad esempio nell’espressione (f() && g() || h()) la funzione f è sempre invocata, ma se ritorna false non esegue la g, ma direttamente la funzione h. Al contrario la (f() || g() && h()) se la f ritorna true non esegue nessuna delle funzioni successive.