Traccia della lezione Lez08: Lucido 3: La sintassi della definizione di variabili di tipo vettoriale è un po' illogica, dato che spezza il nome del tipo: in parte (il tipo degli elementi) prima del nome della variabile e in parte (il loro numero) dopo. Poiché gli elementi di un vettore possono essere di qualsiasi tipo, si possono definire vettori i cui elementi sono di tipi definiti da utente, come pure vettori di vettori e vettori di strutture. Poiché la dimensione è una costante numerica, è bene usare un nome simbolico per chiarirne il significato. Questo vale in particolare per i vettori che potrebbero cambiare di dimensione per applicazioni diverse (ovviamente ricompilando il codice, dato che la costante simbolica è sostituita dal precompilatore, e quindi per il compilatore essa è fissata esplicitamente). Le dichiarazioni di variabili di tipo vettore, come quelle di variabili semplici, stanno nella parte dichiarativa del blocco e terminano con ; perché sono istruzioni (aggiungono elementi alla tabella dei simboli e riservano lo spazio opportuno in memoria). La dimensione dei vettori è sempre una costante. Non sono ammesse espressioni variabili nella dichiarazione dei vettori perché il compilatore non saprebbe quanto spazio riservare. Vedremo poi come realizzare in C89 vettori la cui dimensione varia in funzione dei dati. Il C99 ammette espressioni variabili, perché le traduce automaticamente nel codice necessario a gestirle. Lucido 4: Le parentesi quadre sono uno strano operatore binario: - il primo operando è il vettore - il secondo operando è l'indice L'operatore non è un solo carattere (come +, /, %), né una sola stringa (sizeof, ++, --), ma due caratteri separati, che seguono il primo operando e racchiudono il secondo. La priorità è altissima rispetto agli operatori vicini, ma l'operando indice va pensato come se fosse racchiuso fra parentesi, cioè prima si valuta l'indice e poi si applica l'operatore []. Lucido 5: Dichiarando int V[D+1]; si può rappresentare qualsiasi vettore con estremi s e d tali che 0 <= S <= D. In questo modo si spreca la memoria compresa fra 0 e S, ma oggi la memoria di solito non è la risorsa limitante. Vedremo poi come ottenere vettori che abbiano indici estremi di valore negativo. Gli elementi dei vettori sono l-value, come le variabili, cioe' possono comparire a sinistra (left, da cui "l") di un assegnamento. Gli elementi di un vettore vengono conservati in celle di memoria consecutive, secondo i valori dell'indice: prima V[0], subito dopo V[1], poi V[2], ecc... Eccedere i valori leciti dell'indice significa quindi accedere alle celle successive al vettore, mentre usare indici negativi significa accedere alle celle precedenti. Il seguente codice potrebbe dar luogo a un ciclo infinito: { int a[10], i; for (i = 0; i <= 10; i++) a[i] = 0; } perché il ciclo è errato, in quanto opera sugli elementi del vettore a compresi fra 0 e 10, ma il vettore a ha solo 10 elementi, con indici compresi fra 0 e 9. Il compilatore non rileva l'errore e tratta come elemento a[10] le celle che seguono quelle dell'elemento a[9], cioè quelle dove a[10] dovrebbe stare se esistesse. Nel caso sfortunato in cui la variabile i fosse posta subito dopo l'ultimo elemento del vettore a, che è a[9], cioè dove dovrebbe stare a[10], il ciclo che azzera a[10] in realtà azzererebbe i, che verrebbe viene incrementato dall'istruzione i++ diventando pari a 1. Di conseguenza, i non sarebbe mai > 10 e il ciclo non terminerebbe, ma si ripeterebbe indefinitamente. Tutto questo dipende dalla macchina, dal compilatore e dalle opzioni di compilazione. In generale, a[10] occuperebbe una posizione diversa da i e il problema non si verificherebbe. Ma rimane comunque un errore, dato che il ciclo scrive su una cella esterna al vettore, magari sovrapposta ad altre variabili. A seconda dei casi, si può osservare un errore oppure no. Esercizio: Il codice SOMMA.C riempie un vettore di N elementi, indicizzato da 1 a N, con i numeri da 1 a N, li stampa, poi li somma e poi stampa la somma. Si noti l'utilità fortissima della macro N: se venisse a mancare, dovremmo modificare il codice in 4 punti diversi ogni volta che si volesse modificare N. Esercizio: 1) si modifichi il codice in modo che abbia due costanti simboliche S e D, definisca un vettore capace di contenere i numeri fra S e D, lo riempia con tali numeri in sequenza, ne calcoli la somma e la stampi 2) si modifichi il codice ottenuto dal punto 1 in modo che chieda all'utente due valori s1 e d1 definiti come variabili intere, verifichi che S <= s1 <= d1 <= D e, in caso positivo, esegua la somma solo degli elementi compresi fra s1 e d1 e la stampi a video. Lucido 6: E' possibile definire un tipo di dato vettore di vettore (di vettore...) assegnandogli un nome con l'istruzione typedef. Per esempio, se si vuole poter definire variabili di tipo Matrix, inteso come matrice quadrata di interi con indici di riga e colonna compresi fra 1 e N, si può scrivere: typedef int Matrix[N+1][N+1]; Matrix M; dove Matrix è il nuovo tipo, mentre int[N+1][N+1] è quello vecchio. La sintassi è un po' illogica, come quella per dichiarare le variabili: il nome del tipo vecchio viene spezzato e finisce in parte prima e in parte dopo il nome del tipo nuovo. Il codice PRODUCT.C definisce una riga come vettore di interi, e poi se ne serve per definire una matrice (quadrata) come vettore di righe. Quindi, costruisce una matrice identità I e due matrici quadrate A e B, i cui elementi sono rispettivamente la somma e la differenza degli indici di riga e colonna. Infine, esegue il prodotto delle due matrici e stampa a video le tre matrici. Esercizi: - aggiungere al codice il calcolo del prodotto di ciascuna matrice per l'identità, e la verifica del fatto che il prodotto coincide con la matrice originale. - aggiungere al codice la dichiarazione di un vettore V (di tipo Row) i cui elementi sono l'indice meno N/2 e valutare il prodotto di A per il vettore. Il risultato è un altro vettore U, che va anch'esso dichiarato e poi stampato. Lucido 7: In fondo al codice PRODUCT.C, sfruttando la direttiva di compilazione condizionale #ifdef è possibile azzerare C[1][11] prima di stampare la matrice C. Non cambia nulla, apparentemente in contrasto con quanto detto a proposito dell'accedere alla riga seguente quando si eccede la dimensione. Il motivo è che C[1][11] corrisponde a C[2][0], che non viene usato e stampato, e quindi l'errore non ha effetti. Se però si azzera C[1][12], questo corrisponde a C[2][1], che viene usato e stampato, e quindi l'errore diventa evidente. Nel caso di due o più indici, la matrice viene sempre memorizzata monodimensionalmente, in celle contigue, ordinando secondo gli indici a partire dall'ultimo. Esempio: int A[M][N][P]; A[0][0][0] ... A[0][0][P-1] A[0][1][0] ... A[0][1][P-1] A[0][2][0] ... ... A[0][N-1][P-1] A[1][0][0] ... A[M-1][N-1][P-1] Lucido 8: Il motivo per cui assegnare un vettore a un altro provoca un messaggio di errore è legato alla parentela fra vettori e puntatori, ovvero fra vettori statici e vettori dinamici, che approfondiremo nelle prossime lezioni.