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 e : , , , e diventa . La a destra vale ancora 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:
| Gruppo | Operatori | Esempio |
|---|---|---|
| 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 e non , 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:
- in
A && B, seAè falso il risultato è falso qualunque siaB, quindiBnon viene valutato; - in
A || B, seAè vero il risultato è vero, eBnon viene valutato.
Serve a scrivere condizioni che proteggono se stesse: in x != 0 && 10 / x > 1 la divisione non avviene mai quando è 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 compatta | Forma estesa |
|---|---|
x += a | x = x + a |
x -= a | x = x - a |
x *= a | x = x * a |
x /= a | x = x / a |
x %= a | x = 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:
| Espressione | Valore dell’espressione | Effetto su x |
|---|---|---|
x++ (post-incremento) | il valore di x prima | x aumenta di 1 |
++x (pre-incremento) | il valore di x dopo | x aumenta di 1 |
x-- (post-decremento) | il valore di x prima | x cala di 1 |
--x (pre-decremento) | il valore di x dopo | x 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:
- numeriche:
5,2.4,0xFF(esadecimale, vale 255),3E10(),-4.5E-9; - stringhe:
"marco","\tstudenti info\n"(\tè una tabulazione,\nun a capo); - predefinite:
NULL, il puntatore nullo, definito instdio.he in altri header standard; - booleane:
trueefalse. In C11 non esistono da sole: servono#include <stdbool.h>(dal C99) oppure il C23, dove sono diventate parole chiave. Senza, gcc con-std=c11dà errore “‘true’ undeclared”. Il C tradizionale usa gli interi:0è falso, qualunque altro valore è vero, e gli operatori relazionali e logici restituiscono1o0.
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, 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).
- Un carattere si scrive fra apici singoli:
'a'è il carattere a, di tipochar, e vale il suo codice ASCII (97). - Una stringa si scrive fra apici doppi:
"a"è una stringa di un carattere,"alfa"una di quattro. - Senza apici,
aealfasono identificatori di variabili.
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:
printf("a")stampa il carattere a;printf("a=%d", a)stampaa=seguito dal valore della variabilea;printf(a)conaintero non compila: il primo argomento diprintfdeve essere una stringa (gcc: “makes pointer from integer without a cast”). Lo pseudo-C delle slide lo scrive, il C vero no.
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:
| Carattere | Codice | Nota |
|---|---|---|
'\0' | 0 | NUL, non stampabile |
'\n' | 10 | a capo |
' ' | 32 | spazio |
'0' … '9' | 48 … 57 | cifre, consecutive |
'A' … 'Z' | 65 … 90 | maiuscole, consecutive |
'a' … 'z' | 97 … 122 | minuscole, consecutive |
Dato che un char è un numero, ci si fa aritmetica. Le tre formule che servono sempre:
- valore di una cifra:
c - '0'(da'7'si ottiene 7); - minuscola in maiuscola:
c - 'a' + 'A'; - distanza fra minuscole e maiuscole:
'a' - 'A'vale 32.
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.
- Metti le parentesi implicite seguendo la tabella: prima unari, poi
* / %, poi+ -, poi i confronti, poi&&,||,?:, assegnazione. - A parità di gruppo, raggruppa secondo l’associatività (quasi sempre da sinistra, tranne unari,
?:e assegnazioni). - Calcola dal basso dell’albero. Con due interi,
/è divisione intera (tronca verso zero) e%è il resto. - Con
&&e||fermati appena il risultato è deciso, e ricordati che la parte non valutata non esegue i suoi++. - Scrivi il valore delle variabili dopo ogni istruzione in una tabella.
Divisione intera e resto. 7 / 2 vale , 7 / 2.0 vale : basta un operando double perché la divisione sia decimale. Dal C99 la divisione tronca verso zero: -7 / 2 vale e -7 % 2 vale . 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 % 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
è 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;
}
| istruzione | x | y | z | perché |
|---|---|---|---|---|
| iniziale | 5 | ? | ? | |
y = x++; | 6 | 5 | ? | y prende il valore prima |
z = ++x; | 7 | 5 | 7 | z prende il valore dopo |
y = x--; | 6 | 7 | 7 | valore prima |
z = --x; | 5 | 7 | 5 | valore 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 è , non . Il quarto è 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: . 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
| istruzione | i | j | k | conto |
|---|---|---|---|---|
| iniziale | 3 | 2 | ? | |
k = i++ * 2 + --j; | 4 | 1 | 7 | |
j += k-- - i; | 4 | 4 | 6 | k-- vale 7, , |
i *= j - 2; | 8 | 4 | 6 |
Output: 8 4 6 (verificato).
Esercizio 3. Disegna l’albero sintattico di a = b + c * d - e / 2 e calcola a con , , , .
Soluzione
=
/ \
a -
/ \
+ /
/ \ / \
b * e 2
/ \
c d
* e / prima (più precedenza), poi + e - da sinistra: . 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) . b) . c) . d) .
e) , , . f) (tronca verso zero). g) .
h) !5 vale 0 (5 è vero, il NOT dà falso), più 1: .
i) si legge (3 < 2) < 1, cioè 0 < 1, che vale 1. Non è la catena matematica: gcc con -Wall avvisa proprio per questo.
j) .
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 = (la d è la quarta lettera, distanza 3 dalla a). e = 'f'. f = , 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
- Credere che
x *= y + 1siax = x * y + 1. La destra si calcola tutta prima. - Scrivere
i = i++;oa[i] = i++;: la stessa variabile modificata due volte senza un punto di sequenza è comportamento indefinito. gcc con-Wallavvisa (-Wsequence-point). - Confondere
'a'(carattere, apici singoli) con"a"(stringa, apici doppi), come fa la slide 74. - Scrivere
7 / 2aspettandosi 3.5: fra interi la divisione è intera. - Leggere
3 < x < 10come in matematica: vale sempre 1, perché3 < xdà 0 o 1, che è sempre minore di 10. Si scrive3 < x && x < 10. - Usare
trueefalsesenza#include <stdbool.h>in C11. - Contare su un
++scritto nella parte destra di&&o||: con la lazy evaluation può non essere eseguito. - Usare = al posto di == in un confronto:
if (x = 0)assegna 0 e la condizione è sempre falsa. gcc con-Wallavvisa.
6Domande
-
Cos’è l’albero sintattico di un’espressione e cosa ne determina la forma?
-
In che ordine si valuta
x = y * (x + 3) * 2;? -
Che differenza c’è fra precedenza e associatività?
-
Quanto vale
10 - 4 - 3e perché? -
Cos’è la lazy evaluation e con quali operatori interviene?
-
A cosa equivale
x *= y + 1? -
Che differenza c’è fra
x++e++x? -
Con
xche vale 5, quanto valgonoxeydopoy = x++;? E dopoy = ++x;partendo da 5? -
Cosa succede se si assegna un nuovo valore a una variabile dichiarata
const? -
Quanto valgono
7 / 2,7 % 2e7 / 2.0? -
Che differenza c’è fra
'a'e"a"? -
Come si ottiene il valore numerico di un carattere cifra
c? -
Quanto vale
'a' - 'A'e a cosa serve? -
Cosa sono ASCII e UTF-8?
-
In C11 si possono usare
trueefalsesenza include? -
Con
ache vale 0 ebche vale 5, quanto valebdopo aver valutato(a > 0) && (b++ > 0)?