INFORMATICA Tipi strutturati
Tipi strutturati I tipi considerati finora hanno la caratteristica comune di non essere strutturati: ogni elemento è una singola entità. Se il programma deve trattare collezioni di dati, anche se sono dello stesso tipo, a ognuno deve essere associato un identificatore. Supponendo di dover gestire le paghe in una ditta di 3000 dipendenti sarebbe necessario definire 3000 variabili diverse, del tipo: operaio1, operaio2, ...., impiegato1, impiegato2, ....., ecc.
Tipi strutturati I linguaggi ad alto livello permettono di ovviare a questo inconveniente con i tipi strutturati, caratterizzati sia dal tipo dei loro componenti che dai legami strutturali tra i componenti stessi, cioè dal metodo di strutturazione. Il linguaggio C, anche in questo caso, si presenta ambivalente: permette di creare dati aggregati, senza peraltro che questi costituiscano dei tipi nell'accezione classica. Infatti l'organizzazione strutturale dei dati e le modalità di accesso ai singoli elementi che costituiscono la struttura non vengono nascoste all'utente, che invece può interagire con esse in piena libertà.
Vettori Il vettore è una collezione di variabili tutte dello stesso tipo (detto appunto tipo base) di lunghezza prefissata. Questa collezione di variabili è individuata da un unico nome, il nome appunto del vettore. Ogni elemento del vettore è detto componente ed è individuato dal nome del vettore seguito da un indice posto tra parentesi quadre. L'indice può essere solo di tipo intero o enumerato e determina la posizione dell'elemento nel vettore.
tipo_componente nome_vettore [numero_componenti]; Vettori L'intervallo dei valori assunti dall'indice determina la dimensione del vettore, che deve essere limitata. Definizione generale di vettore: tipo_componente nome_vettore [numero_componenti]; tipo_componente può essere un qualunque tipo semplice, nome_vettore è il nome da attribuire al vettore, numero_componenti, racchiuso tra parentesi quadre, è il numero di elementi che costituiscono il vettore e pertanto deve essere un intero o un'espressione costante di tipo intero.
Vettori L'indice del vettore può assumere valori compresi tra 0 e numero_componenti – 1. L'indice corrisponde pertanto alla posizione nel vettore dell’elemento a cui è associato, rispetto al primo. Gli elementi del vettore sono memorizzati in celle di memoria contigue (successive)! Vettore a (di 5 elementi): a[0] a[1] a[2] a[3] a[4]
Vettori Esempi di definizioni di vettore: #define NUM_MATERIE 20 char s[8]; double giornate[167]; int voti_ottenuti[NUM_MATERIE]; La variabile s è un vettore di 8 elementi: equivale alle 8 variabili di tipo char: s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7]. La variabile giornate è un vettore di 167 elementi di tipo double il cui indice può variare tra 0 e 166. La variabile voti_ottenuti è un vettore di 20 elementi di tipo intero il cui indice può variare tra 0 e 19.
Inizializzazione di un vettore E’ possibile assegnare un valore iniziale ad un vettore al momento della sua definizione. L’operazione consiste nell’indicare i valori degli elementi del vettore separati tra loro da virgola. Sintassi (per un vettore di N elementi): = {<valore_0>, <valore_1>, ..., ,<valore_N-1>}; Esempio: int lista[4] = { 2, 0, -1, 5 };
Inizializzazione di un vettore NOTA: se vengono specificati meno di N elementi, l’inizializzazione comincia comunque a partire dal primo valore e lascia non assegnati i rimanenti. Esempi: int s[4] = {2, 0, -1}; /* s[0]=2, s[1]=0, s[2]=-1, s[3]=? */ char p[5] = {‘a’, ‘b’, ‘c’}; /* p[0]=‘a’, p[1]=‘b’, p[2]=‘c’, p[3]=?, p[4]=? */ double d[2] = {2.56}; /* d[0]=2.56, d[1]=? */
Vettori e indici L’indice, che definisce la posizione di un elemento di un vettore, DEVE essere rigorosamente un intero! Può ovviamente anche essere un’espressione, più o meno complessa, purché con risultato intero. Esempio: double x, a[30]; /* a vettore di double */ int i, j, k; ............. x = a[2*i+j-k]; /* espressione aritmetica per l’indice */
Vettori e cicli I cicli sono particolarmente utili per “scandire” un vettore. Utilizzo tipico: applicazione iterativa di un’operazione sugli elementi di un vettore. Schema: int data[10], ind; ............. for (ind=0; ind<10; ind++) { elaborazione dell’elemento data[ind] } Ad ogni ciclo è interessato l’elemento individuato dall’indice ind.
è errata anche se vett_x e vett_y sono dello stesso tipo. Vettori Non ci sono operatori che agiscono sul vettore nel suo complesso: non è lecito pertanto l'assegnamento di un vettore ad un altro vettore. Se vett_x e vett_y sono vettori, l'istruzione: vett_x = vett_y; è errata anche se vett_x e vett_y sono dello stesso tipo. Per trasferire un vettore in un altro occorre copiare un elemento per volta.
Vettori Esempio: copia il vettore vett_iniz nel vettore vett_fin #include <stdio.h> #define NUMDATI 5 int vett_iniz[NUMDATI] = {11, -2, -63, 4, 15}; int vett_fin[NUMDATI], indice; main() { for (indice = 0; indice < NUMDATI; indice++) vett_fin[indice] = vett_iniz[indice]; }
Vettori Sugli elementi del vettore agiscono gli operatori previsti per il tipo_componente. Pertanto è lecito scrivere: valor_fin = vett_x[m1] + vett_y[m2]; purché, naturalmente, valor_fin, il vettore vett_x e il vettore vett_y siano dello stesso tipo. Il tempo necessario per accedere a un elemento di un vettore è indipendente dal valore dell'indice: il vettore è pertanto una struttura ad accesso casuale
Esempio Leggere 10 valori da tastiera e memorizzarli in un vettore; quindi calcolarne il minimo ed il massimo. Pseudocodice: Con un indice ind che varia tra 0 e 9: legge un dato e lo salva in vettdati[ind]; Inizializzo la variabile massimo e la variabile minimo col primo elemento del vettore vettdati[0]; Con un indice ind che varia tra 1 e 9: se vettdati[ind] è più grande di massimo: massimo vettdati[ind]; altrimenti se vettdati[ind] è più piccolo di minimo: minimo vettdati[ind]; Visualizza massimo e minimo
Esempio #include <stdio.h> #define NUMDATI 10 main() { main() { int minimo, massimo, ind; int vettdati[NUMDATI]; /* lettura dei dati */ for (ind = 0; ind < NUMDATI; ind++) printf (“\nIntroduci vettdati[%d]: ", ind); scanf ("%d", &vettdati[ind]); }
Esempio /* cerca il massimo e il minimo */ massimo = vettdati[0]; minimo = vettdati[0]; for (ind = 1; ind < NUMDATI; ind++) { if (vettdati[ind] > massimo) massimo = vettdati[ind]; else if (vettdati[ind] < minimo) minimo = vettdati[ind]; } printf (“\nIl massimo è %d e il minimo è %d\n ", massimo, minimo);
Esempio Scrivere un programma che legga un numero decimale positivo minore di 1024 e lo converta nella corrispondente codifica binaria. Analisi Per convertire in binario puro un numero decimale occorre eseguire una sequenza di divisioni per 2 prendendo i resti (0 oppure 1): occorre dunque un vettore per memorizzare questi resti. Poiché i numeri devono essere compresi tra 0 e 1023 sono sufficienti 10 bit: il nostro vettore sarà pertanto lungo 10 elementi e in ogni elemento memorizzeremo una cifra.
Esempio Analisi (continua): I resti ottenuti dalle divisioni per 2 vanno però letti al contrario, conviene pertanto riempire il vettore a partire dall’ultimo elemento. Per eseguire le divisioni per due è intuitivo che conviene servirsi di un ciclo il quale, ad ogni iterazione, calcola un nuovo bit (resto della divisione per 2). for o while? È pressochè indifferente usare un ciclo for o un ciclo while: occorre però che le inizializzazioni delle variabili siano adattate al ciclo prescelto. Se usiamo il for avremo come “dato-guida” del ciclo l’indice del vettore; Se usiamo il while il “dato-guida” sarà il resto delle divisioni per 2.
Esempio (con while) #include <stdio.h> main() { int ind, numero, num; int binario[10]; /* inizializza il vettore risultato con tutti zeri */ for (ind = 0; ind < 10; binario[ind++]=0); /* equivale a : for (ind=0; ind<10; ind++) binario[ind] = 0; */ printf (“\nIntroduci un numero intero positivo minore di 1024: "); scanf ("%d", &numero);
Esempio (con while) if ((numero >= 0) && (numero < 1024)) { num = numero; /* num è il “dato-guida” del ciclo */ ind = 9; while (num != 0) /* finché num è diverso da 0! */ binario[ind] = num % 2; /* calcola un nuovo bit */ num /= 2; /* aggiorna num per il prossimo ciclo */ ind--; /* aggiorna l’indice del vettore */ } printf ("\nConversione del numero %d: ", numero); for (ind=0; ind<10; ind++) /* Visualizza il vettore: */ printf ("%1d",binario[ind]); /* un bit per volta */ else printf (“\nNumero non lecito!”);
Esempio (con for) #include <stdio.h> main() { int ind, numero, num; int binario[10]; /* non è necessario inizializzare il vettore in quanto il ciclo for deve */ /* scrivere comunque tutti gli elementi del vettore */ printf (“\nIntroduci un numero intero positivo minore di 1024: "); scanf ("%d", &numero);
Esempio (con for) if ((numero >= 0) && (numero < 1024)) { num = numero; for (ind = 9; ind >= 0; ind--) /* con un indice che va da 9 a 0 */ binario[ind] = num % 2; /* calcola un nuovo bit */ num /= 2; /* aggiorna num per il prossimo ciclo */ } printf ("\nConversione del numero %d: ", numero); for (ind = 0; ind < 10; ind++) /* Visualizza il vettore: */ printf ("%1d",binario[ind]); /* un bit per volta! */ else printf (“\nNumero non lecito!”);
voti_ottenuti = voti_semestre; Vettori Quando si definisce un vettore il compilatore riserva un’area di memoria sufficiente per contenerlo e associa l'indirizzo iniziale di quell'area al nome simbolico (identificatore) da noi scelto per il vettore. Pertanto il nome vett_dati non è una vera e propria variabile, ma piuttosto un puntatore : in pratica vett_dati è l'indirizzo di memoria del primo elemento del vettore cioè l'indirizzo di vett_dati[0]. Ecco perché è errata l'istruzione: voti_ottenuti = voti_semestre;
Vettori multidimensionali Il concetto di vettore come collezione di elementi consecutivi, può essere esteso immaginando che gli elementi siano a loro volta dei vettori: si ottiene così un vettore multidimensionale o matrice. La definizione di matrice ricalca pienamente quella del vettore: tipo_comp nome [dim1] [dim2].........; tipo_comp può essere un qualunque tipo semplice, dim1, dim2, ecc.; racchiusi tra parentesi quadre, definiscono il numero di elementi di ogni dimensione.
Vettori multidimensionali Esempio: matrice bidimensionale di numeri interi formata da tre righe e 5 colonne: int a[3][5]; a[0][0] a[1][0] a[2][0] a[3][0] a[4][0] a[0][1] a[1][1] a[2][1] a[3][1] a[4][1] a[0][2] a[1][2] a[2][2] a[3][2] a[4][2] a[0] a a[1] a[2]
Vettori multidimensionali Accesso ad un elemento: <nome vettore> [<posizione1>] [<posizione2>]............. Per esempio matrix [10][20][15] individua l'elemento di coordinate rispettivamente 10, 20 e 15 nella matrice a 3 dimensioni matrix. Inizializzazione di un vettore multidimensionale: deve essere effettuata per righe! int vett[3][2] = { {8,1}, /* vett[0] */ {1,9}, /* vett[1] */ {0,3} /* vett[2] */ };
Vettori multidimensionali e cicli Per un vettore a più dimensioni, la scansione va applicata a tutte le dimensioni: in questo caso si devono in genere utilizzare “cicli annidati ”. Esempio: elaborazione degli elementi di un vettore bidimensionale. int vett [3][5]; … for (i = 0; i < 3; i++) { /* per ogni riga */ for (j = 0; j < 5; j++) { /* per ogni colonna */ ... elaborazione su vett[i][j] }
Stringhe
Vettori di caratteri: le stringhe Le variabili di tipo char possono contenere un solo carattere: per trattare sequenze di caratteri come nomi o, più in generale, testi il C prevede le stringhe. Differentemente dagli altri tipi di dato (intero, reale, ecc.) per le stringhe non è sempre possibile fissare a priori le dimensioni: la loro caratteristica peculiare è proprio la lunghezza variabile. Per gestire dati di questo tipo occorrerebbe l'allocazione dinamica della memoria, in modo da riservare tutta e solo la memoria che serve.
Vettori di caratteri: le stringhe Il C prevede per le stringhe un vettore di caratteri, il quale deve quindi avere una lunghezza massima prefissata. All'interno di questo vettore, la lunghezza reale della stringa è determinata dalla presenza di un carattere delimitatore particolare, '\0', detto anche NULL. Come per i tipi base, anche per le stringhe è prevista una notazione particolare per indicarne i valori: la sequenza di caratteri deve essere delimitata da una coppia di doppi apici (").
Stringhe Esempio: const char s[6] = “Ciao!”; Il messaggio è lungo solo 5 caratteri, ma deve essere riservato un carattere in più per il terminatore di stringa, che deve essere sempre presente e viene forzato automaticamente dal linguaggio C. Il vettore può essere definito più lungo, con gli ultimi elementi indefiniti, ma non più corto. NOTA: la stringa vuota non è un vettore “vuoto”! char s[] = “”; ‘C’ ‘i’ ‘a’ ‘o’ ‘!’ ‘\0’ s[0] s[1] s[2] s[3] s[4] s[5] ‘\0’ ‘\0’ s[0] s[0]
#define MESSAGGIO “Ciao!”; Stringhe Per la definizione di una stringa si può anche utilizzare la direttiva define: #define MESSAGGIO “Ciao!”; Attenzione infine a non confondere variabili di tipo carattere con stringhe: per esempio, 'C' rappresenta un unico carattere che è memorizzato in un'unica cella. "C" rappresenta invece una stringa che è memorizzata in due celle consecutive che contengono i caratteri 'C' e '\0'.
Stringhe Un vettore di nomi si realizza mediante una matrice di tipo carattere dove ogni riga (vettore) contiene un nome. Ad esempio, per memorizzare i nomi dei giorni della settimana, si può procedere così: const char giorni[7][10] = {"lunedì", "martedì", "mercoledì", "giovedì", "venerdì", "sabato", "domenica"}; dove la dimensione dei singoli vettori (tutti i vettori!), cioè 10, è determinata sommando 1 alla lunghezza del nome più lungo (mercoledì). Pertanto un vettore di stringhe è in realtà una matrice di caratteri!
Stringhe l u n e d ì \0 giorni[0] m a r t e d ì \0 giorni[1] m e r c o v e d ì \0 giorni[3] v e n e r d ì \0 giorni[4] s a b a t o \0 giorni[5] d o m e n i c a \0 giorni[6]
I/O di stringhe Le stringhe possono comparire come argomento di printf e scanf: per esse si utilizza lo specificatore di formato %s. In particolare la printf, quando trova nel format lo specificatore %s, interpreta i valori contenuti nella variabile corrispondente (che dev’essere un vettore di caratteri!) come caratteri e li visualizza finché non trova un carattere '\0'. Se non è presente il carattere terminatore la printf continua l’output oltre i confini del vettore fino a che non incontra un '\0‘ (ovvero una cella di memoria che contiene 0!). Come per gli altri specificatori, anche in %s si può specificare la lunghezza del campo e gli altri attributi di allineamento.
Esempio Programma per visualizzare i giorni della settimana, uno per riga, allineati a sinistra (flag - ) in un campo di 15 caratteri. #include <stdio.h> const char giorni[7][10] = { "lunedì", "martedì", "mercoledì", "giovedì", "venerdì", "sabato", "domenica"}; main() { int indice; printf ("\nI giorni della settimana sono:\n"); for (indice = 0; indice < 7; indice++) printf ("%-15s\n", giorni[indice]); }
Lettura di stringhe La scanf, quando trova nel format lo specificatore %s, attua un meccanismo di lettura simile a quello usato per i numeri: scarta tutti gli “spazi neutri ” iniziali (spazio, <TAB>, <CR>, ecc.), “legge” i caratteri successivi scrivendoli in locazioni consecutive del vettore indicato e si ferma non appena incontra un altro carattere appartenente alla categoria degli “spazi neutri ”, chiudendo la stringa appena generata nel vettore con il carattere NULL. E’ importante quindi che nella stringa non siano presenti spazi e che il vettore destinazione sia dimensionato opportunamente poiché, come sempre in C, non ci sono controlli sugli indici.
Lettura di stringhe Il fatto che non ci siano controlli sul numero di caratteri introdotti, ad esempio da tastiera, può provocare danni collaterali non trascurabili: infatti la lettura prosegue fino al primo “spazio neutro ” in ogni caso e i caratteri letti vengono memorizzati consecutivamente come se la stringa fosse stata dimensionata in modo corretto anche quando è più corta di quanto sarebbe necessario. I caratteri “in eccesso” e il NULL vengono comunque memorizzati e possono pertanto andare a ricoprire aree di memoria riservate ad altri dati sporcandoli irrimediabilmente. Poiché il nome della stringa è proprio l’idirizzo del vettore di caratteri associato nella scanf non si deve usare il carattere & prima del nome.
Esempio Esempio: programma per leggere i nomi (lunghi al massimo 20 caratteri) e le altezze (in cm) di 10 persone e successivamente visualizzarli incolonnati. #include <stdio.h> #define NUM_NOMI 10 #define L_STRING 21 main() { /* Definizioni */ char nome[NUM_NOMI][L_STRING]; int altezza[NUM_NOMI]; int ind;
Esempio printf (“\nIntroduci il nome e l'altezza di 10 persone:\n"); for (ind = 0; ind < NUM_NOMI; ind++) { printf (“\nPersona N. %2d: ", (ind + 1)); /* indice a partire da 1 */ scanf ("%s%d", nome[ind], &altezza[ind]); } printf("\n Nome Altezza"); printf (“\nPersona N. %4d: %-20s %3d", (ind + 1), nome[ind], altezza[ind]);
Confronto tra stringhe Poiché le stringhe sono vettori, non è lecito assegnare una stringa ad un'altra. Pertanto il frammento di programma che segue è errato: char messag[16]; ..................... messag = "Errore nei dati"; Anche il confronto tra stringhe non può essere effettuato mediante un'unica istruzione, come invece avviene per i singoli caratteri, ma occorre confrontare col criterio opportuno i singoli elementi delle due stringhe.
Confronto tra stringhe: esempio Programma che legge da tastiera due parole (lunghe al più 20 caratteri) e verifica se sono uguali o diverse. #include <stdio.h> #define VERO 1 #define FALSO 0 main() { char parola1[21], parola2[21]; int ind, uguali; printf (“\nIntroduci la prima parola: “); scanf (“%s”, parola1); printf (“\nIntroduci la seconda parola: “); scanf (“%s”, parola2);
Confronto tra stringhe: esempio /* verifica se sono uguali */ uguali = VERO; /* ipotizza che siano uguali */ ind = 0; while (uguali && (ind < 20) && (parola1[ind] != ‘\0’)) { if (parola1[ind] != parola2[ind]) uguali = FALSO; ind++; } if (uguali) printf (“\nLe parole introdotte sono uguali”); else printf (“\nLe parole introdotte sono diverse”);
Confronto tra stringhe Essendo i caratteri interpretati come numeri interi, è lecito confrontarli tra loro per stabilire la precedenza alfabetica mediante una espressione relazionale. 0 < 1 < 2 <.... < A < B < C <.....< Z ..... < a < b < c <....< z Poiché le stringhe sono vettori di caratteri, e quindi composte da elementi di tipo char, è lecito stabilire l’ordine alfabetico di due stringhe confrontandole fra loro carattere per carattere. Esempio: programma che legge da tastiera due parole e stabilisce l’ordine alfabetico.
Confronto tra stringhe: esempio #include <stdio.h> #define VERO 1 #define FALSO 0 main() { char parola1[21], parola2[21]; int ind, finito, prima1; printf ("\nIntroduci la prima parola: "); scanf ("%s", parola1); printf ("\nIntroduci la seconda parola: "); scanf ("%s", parola2); finito = FALSO; /* segnala fine dei confronti! */ ind = 0;
Confronto tra stringhe: esempio while (!finito && (ind < 20)) { if (parola1[ind] == parola2[ind]) /* caratteri uguali: nessuna decisione */ ind++; else if(parola1[ind] < parola2[ind]) prima1 = VERO; /* parola1 precede parola2 */ prima1 = FALSO; /* parola2 precede parola1 */ finito = VERO; /* caratteri diversi: fine dei confronti */ } if (prima1) printf ("\n%s precede %s", parola1, parola2); printf ("\n%s precede %s", parola2, parola1);