Note Ingegneria Informatica · UniTN
Programmazione 1
Lezione
set, giorno?
in corso

Espressioni, operatori e costanti

Indice 6 sezioni
  1. 1Definizioni
  2. 2Concetti
  3. 3Esempi svolti a lezione
  4. 4Esercizi tipo esame
  5. 5Errori tipici
  6. 6Domande

Argomento di Programmazione 1. Fatto a lezione a settembre, seconda parte del deck 2.2: slide 57-76. Prima: Il Linguaggio C (fino all’assegnazione). Dopo: Algebra di Boole.

1Definizioni

Espressione. Una combinazione di costanti, variabili e operatori che produce un valore (slide 57): y * (x + 3) * 2. Anche l’assegnazione è un’espressione, e il suo valore è quello assegnato (visto in Il Linguaggio C).

Albero sintattico. Il modo in cui il compilatore legge un’espressione (slide 58). Le foglie sono variabili e costanti, i nodi interni sono operatori, e ogni operatore si applica ai valori dei suoi figli. Lo decidono le priorità degli operatori, le parentesi e l’associatività. Si valuta dal basso verso l’alto: un nodo si calcola solo quando i suoi figli hanno già un valore.

Per x = y * (x + 3) * 2; i passi sono quattro: prima la parentesi x + 3, poi y * (x + 3), poi il risultato per 2, per ultima l’assegnazione. Con x=2x = 2 e y=5y = 5: 2+3=52 + 3 = 5, 5⋅5=255 \cdot 5 = 25, 25⋅2=5025 \cdot 2 = 50, e xx diventa 5050. La xx a destra vale ancora 22 quando viene letta: si scrive solo al passo 4.

Operatori. Caratteri speciali, o loro combinazioni, che denotano un’operazione (slide 59-60). La slide li elenca tutti insieme; per ora servono questi gruppi:

GruppoOperatoriEsempio
aritmetici+ - * / %7 % 3 vale 1
relazionali< <= > >=x < 10
uguaglianza== e !=x != 0
logici! && ||x > 0 && x < 10
assegnazione=, += -= *= /= %=x += 2
incremento++ --i++
condizionale? :Istruzioni condizionali

Gli altri della lista (^ & | ~ << >> sui bit, -> . :: ->* su struct e classi C++, [] sugli array) arriveranno più avanti.

Precedenza. Quale operatore si applica per primo quando non ci sono parentesi (slide 63). Un gruppo più in alto nella tabella lega più forte.

Associatività. Il verso in cui si raggruppano operatori dello stesso gruppo: 10 - 4 - 3 si legge (10 - 4) - 3, che vale 33 e non 99, perché i sottrattivi associano da sinistra. L’assegnazione associa da destra: a = b = 5 si legge a = (b = 5).

Tabella delle precedenze della slide, dall’alto (lega di più) al basso:

gruppo            operatori                        associatività
postfissi         ( )  [ ]  .  ->                  da sinistra a destra
unari             !  ++  --  *  &  sizeof  -  (tipo)   da destra a sinistra
moltiplicativi    *  /  %                          da sinistra a destra
additivi          +  -                             da sinistra a destra
shift             <<  >>                           da sinistra a destra
relazionali       <  <=  >  >=                     da sinistra a destra
uguaglianza       ==  !=                           da sinistra a destra
AND logico        &&                               da sinistra a destra
OR logico         ||                               da sinistra a destra
condizionale      ? :                              da destra a sinistra
assegnazione      =  +=  -=  *=  /=  %=            da destra a sinistra

Nella riga degli unari * e & sono gli operatori sui puntatori e - è il meno unario (-x), non la moltiplicazione e la sottrazione. La tabella della slide è semplificata: mancano gli operatori sui bit & ^ | (fra uguaglianza e &&) e la virgola (sotto l’assegnazione).

Lazy evaluation (slide 62). Il valore di un’espressione a volte si conosce senza valutare tutto l’albero, e il C in quei casi si ferma. Succede con && e ||, valutati da sinistra:

Serve a scrivere condizioni che proteggono se stesse: in x != 0 && 10 / x > 1 la divisione non avviene mai quando xx è zero. Se B contiene un ++, e non viene valutato, l’incremento non avviene. Si chiama anche valutazione cortocircuitata (short-circuit).

Assegnazioni composte (slide 66, slide 70). Le istruzioni del tipo x = x + a si chiamano autoassegnamenti e sono così frequenti che hanno una forma corta:

Forma compattaForma estesa
x += ax = x + a
x -= ax = x - a
x *= ax = x * a
x /= ax = x / a
x %= ax = x % a
x++x = x + 1
x--x = x - 1

Attenzione: la parte a destra si calcola tutta prima. x *= y + 1 vuol dire x = x * (y + 1), non x = x * y + 1, perché l’assegnazione ha la precedenza più bassa di tutte.

Incremento e decremento (slide 68). Entrambe le forme cambiano la variabile di uno. Cambia il valore dell’espressione:

EspressioneValore dell’espressioneEffetto su x
x++ (post-incremento)il valore di x primax aumenta di 1
++x (pre-incremento)il valore di x dopox aumenta di 1
x-- (post-decremento)il valore di x primax cala di 1
--x (pre-decremento)il valore di x dopox cala di 1

Da soli, come istruzione (i++; oppure ++i;), fanno la stessa cosa. La differenza si vede solo quando il valore viene usato: y = x++; e y = ++x; lasciano x uguale ma y diverso.

Costante (slide 71). Un valore che non cambia durante il programma. Come le variabili, una costante con nome ha nome, tipo, l-value e r-value. Esempi della slide:

Variabile costante (slide 72).

const tipo identificatore = espressione;

Il valore (r-value) non si può più modificare: un’assegnazione successiva è un errore di compilazione. Il valore va dato subito, nella dichiarazione, perché dopo non si potrà più.

const int kilo = 1024;
const double pi = 3.14159;
const int mille = kilo - 24;

La slide scrive pi = 3.141519: le cifre sono scambiate, π=3,14159…\pi = 3{,}14159\ldots La slide dice anche che l’espressione deve essere calcolabile in fase di compilazione. Per le costanti locali in C non è obbligatorio: const int doppio = 2 * n; con n letto da tastiera compila (provato con gcc). Il vincolo vale per le costanti globali e in C++ per quelle usate come dimensione di un array.

Carattere e stringa (slide 74).

La slide dice che i caratteri vanno “tra apici doppi”: è sbagliato. 'a' e "a" sono oggetti diversi, e in printf("%c", ...) va il primo. La stringa è una sequenza di caratteri trattata come un oggetto unico; come la memorizza il C si vedrà con gli array.

Con printf:

ASCII (slide 75). American Standard Code for Information Interchange, dal 1963. Associa a ogni carattere un numero da 0 a 127, cioè 7 bit. I codici da ricordare:

CarattereCodiceNota
'\0'0NUL, non stampabile
'\n'10a capo
' '32spazio
'0' … '9'48 … 57cifre, consecutive
'A' … 'Z'65 … 90maiuscole, consecutive
'a' … 'z'97 … 122minuscole, consecutive

Dato che un char è un numero, ci si fa aritmetica. Le tre formule che servono sempre:

UTF-8 (slide 76). ASCII copre solo l’inglese. Unicode assegna un numero a ogni carattere di ogni lingua, e UTF-8 è il modo più usato di scriverlo in byte: da 1 a 4 byte per carattere, e i caratteri ASCII restano identici su un byte. Dal 2008 circa è la codifica più usata sul web (grafico della slide). Conseguenza pratica: "è" occupa due byte, non uno.

2Concetti

Valutare un’espressione a mano.

  1. Metti le parentesi implicite seguendo la tabella: prima unari, poi * / %, poi + -, poi i confronti, poi &&, ||, ?:, assegnazione.
  2. A parità di gruppo, raggruppa secondo l’associatività (quasi sempre da sinistra, tranne unari, ?: e assegnazioni).
  3. Calcola dal basso dell’albero. Con due interi, / è divisione intera (tronca verso zero) e % è il resto.
  4. Con && e || fermati appena il risultato è deciso, e ricordati che la parte non valutata non esegue i suoi ++.
  5. Scrivi il valore delle variabili dopo ogni istruzione in una tabella.

Divisione intera e resto. 7 / 2 vale 33, 7 / 2.0 vale 3.53.5: basta un operando double perché la divisione sia decimale. Dal C99 la divisione tronca verso zero: -7 / 2 vale −3-3 e -7 % 2 vale −1-1. Vale sempre (a / b) * b + a % b uguale ad a.

Cifre di un numero. n % 10 è l’ultima cifra, n / 10 toglie l’ultima cifra. Con n=247n = 247: n % 10 è 7, n / 10 % 10 è 4, n / 100 è 2. È il trucco dei tracing con la matricola.

3Esempi svolti a lezione

Albero sintattico (slide 58), verificato:

#include <stdio.h>

int main(void)
{
    int x = 2;
    int y = 5;

    x = y * (x + 3) * 2;
    printf("x = %d\n", x);
    return 0;
}

Output: x = 50.

Esercizio NumCarte (slide 73). Il programma della slide dichiara una costante e una variabile, legge il numero di giocatori e divide le carte. In fondo c’è, commentata, NumCarte = NumCarte-1; //Error. La domanda è perché darebbe errore. Versione compilabile, con il controllo di scanf e una dichiarazione per riga:

#include <stdio.h>

int main(void)
{
    const int NumCarte = 40;
    int NumGiocatori = 4;

    printf("Numero Carte = %d\n", NumCarte);

    printf("Numero Giocatori ?\n");
    if (scanf("%d", &NumGiocatori) != 1 || NumGiocatori <= 0) {
        printf("Input non valido\n");
        return 1;
    }
    printf("Numero Giocatori = %d\n", NumGiocatori);

    printf("Numero Carte/Giocatore = %d\n", NumCarte / NumGiocatori);

    NumGiocatori = NumGiocatori + 1;
    printf("Ora i giocatori sono %d\n", NumGiocatori);
    return 0;
}

Con input 3:

Numero Carte = 40
Numero Giocatori ?
Numero Giocatori = 3
Numero Carte/Giocatore = 13
Ora i giocatori sono 4

40/340 / 3 è una divisione intera: 13, e il resto 1 si perde. NumGiocatori = NumGiocatori + 1 compila perché NumGiocatori è una variabile normale. Aggiungendo NumCarte = NumCarte - 1; gcc si ferma con error: assignment of read-only variable 'NumCarte': NumCarte è const, il suo r-value non si può cambiare. L’errore esce in compilazione, non a run-time.

Stampa di caratteri (slide 74) con l’aritmetica ASCII:

#include <stdio.h>

int main(void)
{
    int a = 5;
    char c = 'a';

    printf("a");
    printf("\n");
    printf("a=%d\n", a);
    printf("%c\n", c);
    printf("%c %d\n", 'A', 'A');
    printf("%c\n", c + 1);
    printf("%d\n", '7' - '0');
    printf("%c\n", 'g' - 'a' + 'A');
    printf("%d\n", 'a' - 'A');
    return 0;
}

Output:

a
a=5
a
A 65
b
7
G
32

%c stampa il carattere che ha quel codice, %d stampa il codice. 'A' con %d dà 65.

Incrementi (slide 68):

#include <stdio.h>

int main(void)
{
    int x = 5;
    int y;
    int z;

    y = x++;
    printf("x = %d, y = %d\n", x, y);
    z = ++x;
    printf("x = %d, z = %d\n", x, z);
    y = x--;
    printf("x = %d, y = %d\n", x, y);
    z = --x;
    printf("x = %d, z = %d\n", x, z);
    return 0;
}
istruzionexyzperché
iniziale5??
y = x++;65?y prende il valore prima
z = ++x;757z prende il valore dopo
y = x--;677valore prima
z = --x;575valore dopo

Output: x = 6, y = 5, x = 7, z = 7, x = 6, y = 7, x = 5, z = 5, una riga ciascuno.

Assegnazioni composte (slide 66):

#include <stdio.h>

int main(void)
{
    int x = 10;
    int y = 3;

    x += y;
    printf("%d\n", x);
    x -= 4;
    printf("%d\n", x);
    x *= y + 1;
    printf("%d\n", x);
    x /= 5;
    printf("%d\n", x);
    x %= 4;
    printf("%d\n", x);
    return 0;
}

Output, una riga per valore: 13, 9, 36, 7, 3. Il terzo è 9⋅(3+1)=369 \cdot (3 + 1) = 36, non 9⋅3+1=289 \cdot 3 + 1 = 28. Il quarto è 36/5=736 / 5 = 7 con divisione intera.

Lazy evaluation (slide 62; la slide rimanda l’esempio a più avanti, questo è mio):

#include <stdio.h>

int main(void)
{
    int x = 0;
    int b = 7;

    if (x != 0 && 10 / x > 1) {
        printf("grande\n");
    } else {
        printf("x nullo o piccolo\n");
    }
    if (x == 0 || b++ > 0) {
        printf("b = %d\n", b);
    }
    return 0;
}

Output: x nullo o piccolo e b = 7. La divisione per zero non avviene, e b++ non viene eseguito perché x == 0 è già vero.

4Esercizi tipo esame

Esercizio 1. Scrivi l’output esatto.

#include <stdio.h>

int main(void)
{
    int m = 247;
    int a = m % 10;
    int b = m / 10 % 10;
    int c = m / 100;
    int x = a++ + --b;

    x += a * b % 5;
    printf("%d %d %d %d\n", a, b, c, x);
    return 0;
}
Soluzione

a = 7, b = 4, c = 2 (cifre di 247). x = a++ + --b: a++ vale 7 e poi a diventa 8; --b porta b a 3 e vale 3. x = 10. x += a * b % 5: * e % stesso gruppo, da sinistra: (8⋅3)%5=24%5=4(8 \cdot 3) \% 5 = 24 \% 5 = 4. x = 14. Output: 8 3 2 14 (verificato).

Esercizio 2. Scrivi l’output esatto.

#include <stdio.h>

int main(void)
{
    int i = 3;
    int j = 2;
    int k;

    k = i++ * 2 + --j;
    j += k-- - i;
    i *= j - 2;
    printf("%d %d %d\n", i, j, k);
    return 0;
}
Soluzione
istruzioneijkconto
iniziale32?
k = i++ * 2 + --j;4173⋅2+13 \cdot 2 + 1
j += k-- - i;446k-- vale 7, 7−4=37 - 4 = 3, 1+31 + 3
i *= j - 2;8464⋅(4−2)4 \cdot (4 - 2)

Output: 8 4 6 (verificato).

Esercizio 3. Disegna l’albero sintattico di a = b + c * d - e / 2 e calcola a con b=1b = 1, c=2c = 2, d=3d = 3, e=8e = 8.

Soluzione
             =
           /   \
          a     -
              /   \
             +     /
            / \   / \
           b   * e   2
              / \
             c   d

* e / prima (più precedenza), poi + e - da sinistra: (b+c⋅d)−e/2=(1+6)−4=3(b + c \cdot d) - e / 2 = (1 + 6) - 4 = 3. Verificato: stampa 3.

Esercizio 4. Valuta ogni espressione, con int dove non c’è il punto decimale.

a) 7 / 2 * 2        b) 7 / 2.0         c) 10 - 4 - 3      d) 100 / 10 / 5
e) 1 + 2 * 3 % 4    f) -7 / 2          g) -7 % 2          h) !5 + 1
i) 3 < 2 < 1        j) 5 > 3 && 2 > 4
Soluzione

a) (7/2)⋅2=3⋅2=6(7/2) \cdot 2 = 3 \cdot 2 = 6. b) 3.53.5. c) (10−4)−3=3(10-4)-3 = 3. d) (100/10)/5=2(100/10)/5 = 2. e) 2⋅3=62 \cdot 3 = 6, 6%4=26 \% 4 = 2, 1+2=31 + 2 = 3. f) −3-3 (tronca verso zero). g) −1-1. h) !5 vale 0 (5 è vero, il NOT dà falso), più 1: 11. i) si legge (3 < 2) < 1, cioè 0 < 1, che vale 1. Non è la catena matematica: gcc con -Wall avvisa proprio per questo. j) 1&&0=01 \&\& 0 = 0. Verificati con un programma (la i scritta con le parentesi esplicite).

Esercizio 5. Scrivi l’output esatto.

#include <stdio.h>

int main(void)
{
    int a = 0;
    int b = 5;
    int r = (a > 0) && (b++ > 0);
    int s = (a == 0) || (b-- > 0);
    int t = (a == 0) && (b++ > 4);

    printf("%d %d %d %d\n", r, s, t, b);
    return 0;
}
Soluzione

r: a > 0 è falso, b++ non si esegue. r = 0, b = 5. s: a == 0 è vero, b-- non si esegue. s = 1, b = 5. t: a == 0 vero, si valuta b++ > 4: confronta 5 con 4 (vero) e poi b diventa 6. t = 1. Output: 0 1 1 6 (verificato).

Esercizio 6. Scrivi l’output esatto.

#include <stdio.h>

int main(void)
{
    char c = 'd';
    int d = c - 'a';
    char e = c + 2;
    char f = 'b' - 'a' + 'A';

    printf("%d %c %c %d\n", d, e, f, e);
    return 0;
}
Soluzione

d = 100−97=3100 - 97 = 3 (la d è la quarta lettera, distanza 3 dalla a). e = 'f'. f = 1+65=661 + 65 = 66, cioè 'B'. L’ultimo %d stampa il codice di 'f', 102. Output: 3 f B 102 (verificato).

Esercizio 7. Quali righe danno errore di compilazione, e perché?

const int max = 10;
int n = 3;
n = max + 1;        (1)
max = n;            (2)
max++;              (3)
const int m;        (4)
Soluzione

(1) va bene: si legge max, si scrive n. (2) e (3) sono errori: max è const, non si può scrivere né assegnare né incrementare. (4) compila, ma m resta senza valore per sempre: una costante va inizializzata nella dichiarazione, dopo non si può più. gcc la accetta in C (in C++ è un errore), quindi è codice sbagliato anche se passa.

5Errori tipici

6Domande