Traccia della lezione Lez04 Lucido 2: Come ogni operatore, l'assegnamento ha la sua priorità e associatività: - la priorità è la minima fra gli operatori visti sinora (minore di quelli logici) - l'associatività è da destra a sinistra, contrariamente a quasi tutti gli operatori visti sinora L'effetto collaterale (side effect) ha luogo DOPO aver valutato l'espressione. Questo è importante quando l'espressione a destra di un assegnamento contiene la variabile a sinistra. Ad esempio, si può scrivere: l1 = l1 + 5; In questo caso, prima si valuta l'espressione, poi si assegna il valore. Quindi nel valutare l'espressione (l1+5) la variabile l1 assume il valore vecchio, mentre alla fine assume il valore nuovo. Lucido 3: Alcuni compilatori segnalano con un avvertimento (warning) l'uso di un assegnamento in un contesto più adatto per un'uguaglianza (per esempio, in una condizione o in un ciclo). Ma nessuno lo segnala come errore, perché per il C non è un errore. E' lo stesso motivo per cui si può scrivere: b = i < j < k; che però non significa (come parrebbe) che b è vero se j è compreso fra i e k e falso altrimenti, bensì significa: b = ((i < j) < k); dato che gli operatori relazionali sono associativi a sinistra. Per convenzione, un'espressione logica vale 1 se è vera, 0 se falsa. Quindi: - quando i < j, b vale true per k > 1 e false per k <= 1 - quando i >= j, b vale true per k > 0 e false per k <= 0 (per es., i = 4, j = 3, k = 2) Ovviamente questo è ben diverso dal significato apparente dell'espressione, quindi non va usato. Si scriverà: b = (i < j) && (j < k); Lucido 4: Anche l'assegnamento composto è associativo da destra a sinistra: i += j += 4; significa i = (i + (j = (j + 4))); Lucido 5: Il file PREPOST.C illustra la differenza tra le due istruzioni riportate nella tabella. Illustra anche un'altra serie di esempi: int l1, l2; l1 = 5; l2 = 30 / ++l1; Si incrementa l1 (6), si divide 30 per l1 (5) e si assegna ad l2: l1 vale 6, l2 vale 5. int l1, l2; l1 = 5; l2 = 30 / l1++; Si esegue l'incremento di l1 (6), ma si rimanda l'assegnamento a l1, poi si divide 30 per l1 (5) e si assegna il risultato ad l2. Infine si assegna il valore incrementato a l1: l1 vale 6, l2 vale 6. int l1, l2; l1 = 5; l2 = -l1++; Si esegue l'incremento di l1 (6), ma si rimanda l'assegnamento a l1, poi si esegue il '-' unario (-5), e si assegna il risultato ad l2. Infine si assegna il valore incrementato a l1: l1 vale 6, l2 vale -5. int l1, l2; l1 = 5; l2 = -++l1; Si incrementa l1 (6), si esegue il '-' unario (-6), e si assegna il risultato ad l2: l1 vale 6, l2 vale -6. Infine il codice contiene un esempio che non viene compilato grazie a una direttiva di compilazione condizionale. int l1, l2; l1 = 5; l2 = ++-l1; Questa espressione è scorretta: si esegue il '-' unario (-5), dopo di che il risultato è un valore, e non l'indirizzo di una cella, per cui non si può applicare l'operatore di incremento. Questo brano di codice è disattivato dalla direttiva #ifdef. Per sottoporlo al compilatore e osservare l'errore che ne deriva, basta togliere il commento alla direttiva #define ESEMPIOAGGIUNTIVO Lucido 6: La tabella delle priorità nel lucido fonde alcuni livelli. In realtà, gli operatori postfissi hanno priorità più alta, di quelli prefissi. Questo sembra contraddire quanto detto prima, ma la contraddizione è apparente: gli operatori postfissi vengono valutati per primi, ma il loro effetto collaterale viene applicato per ultimo. La mia esperienza personale, per quel che vale, è che in realtà l'effetto collaterale non viene applicato rigorosamente per ultimo e non sempre segue le regole che si trovano sui libri. Quindi compilatori diversi possono dare risultati diversi, compilare e dar luogo a errori in esecuzione, o infine non compilare. Istruzioni come i = 30/i++; portano a risultati diversi secondo l'ordine di esecuzione fra incremento e assegnamento. Lucido 7: Il file PRIORITY.C esegue questi esercizi sulla priorità int a, b, c, d; a = 1; b = 2; c = 3; d = 4; d = a * b / c; d = ((a * b) / c) d vale 0 (è una divisione fra numeri interi!) int a, b, c, d; a = 1; b = 2; c = 3; d = 4; d = ++a * b - c; d = (((++a) * b) - c) d vale 1 int a, b, c, d, e; a = 1; b = 2; c = 3; d = 4; e = 5; e = 5 + c * d / e; e = (5 + ((c * d) / e)) e vale 7 int a, b, c, d, e; a = 1; b = 2; c = 3; d = 4; e = 5; e = 30 / e++ + 29 % c; e = ((30 / (e++)) + (29 % c)) L'incremento e++ non viene eseguito subito: e resta 5 30 / e++ fa quindi 6 29 % c fa 2 La somma da' 8, che viene assegnato ad e, poi si applica l'incremento e vale 9 a = b += c++ - d + --e / -f a = b += (c++) - d + --e / -f a = b += (c++) - d + (--e) / (-f) a = b += (c++) - d + ((--e) / (-f)) a = b += ((c++) - d) + ((--e) / (-f)) a = b += (((c++) - d) + ((--e) / (-f))) a = (b += (((c++) - d) + ((--e) / (-f)))) (a = (b += (((c++) - d) + ((--e) / (-f))))) Quindi, con a = b = c = d = e = 2, f = 1 l'espressione (a = b += c++ - d + --e / -f) comporta queste operazioni: c++ viene valutato subito (3), ma l'assegnamento a c del risultato viene rimandato all'ultimo momento (l'incremento è postfisso) (a = (b += (((2++) - 2) + ((--2) / (-1))))) e viene decrementato a 1 immediatamente (il decremento è prefisso) (a = (b += ((2 - 2) + (1 / (-1))))) si calcola l'opposto di f (- unario) (a = (b += ((2 - 2) + (1 / -1)))) il nuovo e viene diviso per -f (a = (b += ((2 - 2) - 1))) al vecchio c viene sottratto d (a = (b += (0 - 1))) alla differenza di c e d si somma il rapporto fra nuovo e e -f (a = (b += -1)) b viene incrementato del risultato, cioè cala di 1 (a = b) a diventa uguale al nuovo b Infine, c viene incrementato. Lucido 8: Il file CONFUSION.C illustra la differenza tra le espressioni b = (a == 4) e b = (a = 4) introdotta nel lucido 3 e un paio di interessanti problemi privi di soluzione. Il primo è che l'ordine con cui si valutano gli operandi di un operatore non è predefinito dallo standard. Se gli operandi sono espressioni prive di effetti collaterali, questo non ha effetti sul risultato. Se invece hanno effetti collaterali, ne ha molta. Per esempio, supponendo che sia a = 2 e b = 2: (b = a + 2) - (a = 1) può avere diversi valori in C: - se si esegue prima b = a + 2 e poi a = 1, b assume valore 4, a assume valore 1 e la differenza vale 3. - se si esegue prima a = 1 e poi b = a + 2, a assume valore 1, b assume valore 3 e la differenza vale 2. Il secondo problema è che, quando si valuta un'espressione logica, spesso non è necessario conoscere il valore di tutti gli operandi per conoscere il valore dell'espressione. Per esempio, (a && b) è false se anche uno solo dei due operandi è false. Questo significa che a volte è inutile valutare b. Lo standard C consente che in questi casi si eviti parte della valutazione. Si chiama "valutazione cortocircuitata" ed è fatta per efficienza. Tuttavia, lo standard non specifica l'ordine con cui si devono valutare le espressioni. Il risultato è che, se alcuni operandi sono espressioni composte, dotate di effetti collaterali, non si può sapere se verranno valutati tutti, e quindi se gli effetti collaterali avranno tutti luogo. Ad esempio, c = (a = 1) || (b = 2); è sicuramente true, ma - se valuto entrambe le espressioni, al termine a vale 1 e b vale 2 - se valuto solo la prima, a = 1 e b ha ancora il valore precedente - se valuto solo la seconda, b = 2 e a ha ancora il valore precedente La conclusione è molto semplice: 1) meglio non combinare mai nella stessa espressione due operatori che hanno effetti collaterali (salvo gli assegnamenti iterati, come a = b = c = 1, in cui l'ordine è chiaro) 2) non usare mai operatori con effetti collaterali in situazioni in cui vale la valutazione cortocircuitata 3) quando un'espressione è complessa, spezzarla in sottoespressioni più semplici, assegnare i loro valori a variabili ausiliarie e combinarli fra loro in una sequenza di istruzioni. Lucido 9: Siccome un'espressione può anche essere vuota, il semplice ; è un'istruzione, che viene detta "istruzione vuota". Si possono seminare ; in giro per il codice senza indurre errori in compilazione, perché sono istruzioni lecite. Se però sono messi in posizioni particolari, possono indurre errori difficili da interpretare. Anche i prototipi di funzioni sono istruzioni, e sono le uniche istruzioni che non stanno all'interno di un blocco. Che istruzioni sono? Indicano al compilatore di inserire nella tabella dei simboli il nome della funzione stessa, con l'indicazione dei suoi dati e risultati. Lucido 10: Aggiungere un ';' a una macro è uno degli errori più insidiosi, perché a volte il compilatore genera messaggi indecifrabili che attribuiscono l'errore a punti del codice anche lontani dalla macro stessa. Altre volte, invece, non si ha nessun effetto dannoso. Comunque, se si ricorda che cosa succede durante la compilazione non si può fare confusione: una macro è completamente diversa da un'istruzione (la macro è gestita dal precompilatore e non fa che sostituire testo a testo, un'istruzione è gestita dal compilatore ed eseguita dal processore e produce modifiche in aree di memoria quando viene eseguita).