Appunti su Programmazione e linguaggio C

a cura del prof. Nunzio Brugaletta

Unità

2

tipi di dati e modificatori di tipo - il costrutto cast - array a una dimensione - stringhe - la scelta multipla: istruzione else if - la scelta multipla: istruzioni switch case e break - array a più dimensioni - array di stringhe

Unità 1 - Unità 2 - Unità 3 - Unità 4 - Unità 5 - Unità 6

Tipi di dati e modificatori di tipo

Negli esempi trattati fino a questo punto tutte le variabili sono state dichiarate di tipo int. È bene però chiarire che esistono anche altri tipi di dichiarazione delle variabili in modo che, le stesse, possano contenere valori di natura diversa. D'altra parte ciò è facilmente intuibile: basta pensare alle limitazioni dettate appunto dal tipo int. In realtà i vari linguaggi di programmazione permettono di definire le variabili in diversi modi; ciò in relazione al tipo di dato da contenere e alle esigenze specifiche del tipo di problemi cui si rivolge il linguaggio stesso.

Nel linguaggio C esistono cinque tipi di dati primari: carattere, intero, reale, doppia precisione e indefinito. Le parole chiavi utilizzate per dichiarare variabili di questi tipi sono: char, int, float, double e void. La dimensione del dato contenibile nella variabile dipende dallo spazio riservato in memoria per ogni tipo di dichiarazione e dalla modalità di conservazione.

Per quanto riguarda il tipo void (tipo indefinito), si tratterà di ciò in un secondo tempo. Per il momento l'attenzione sarà rivolta agli altri tipi.

I tipi di dati primari, eccetto il tipo void, ammettono dei modificatori di tipo. Tali modificatori (signed, unsigned, long, short) sono impiegati per adattare con maggiore precisione i tipi di dati fondamentali alle esigenze del programmatore.

Nella tabella successiva sono elencati le varie combinazioni più comunemente usate con un esempio di utilizzo (per i valori ammessi si fa quì riferimento alla implementazione del C nei PC):

Tipo Dim. in bit Valori ammessi Utilizzo
char 8 da -128 a 127 Numeri piccoli e caratteri ASCII
unsigned char 8 da 0 a 255 Piccoli numeri e il set di caratteri del PC
int 16 da -32.768 a +32.767 Contatori, piccoli numeri, controllo-cicli
unsigned int 16 da 0 a 65.535 Numeri e cicli
long int 32 da -2.147.483.648 a 2.147.483.647 Grandi numeri
float 32 da 3,4x10-38 a 3,4x1038 Notazione Scientifica (7 cifre di precisione)
double 64 da 1,7x10-308 a 1,7x10308 Notazione Scientifica (15 cifre di precisione)

È bene fare qualche osservazione sulla tabella:

Per ogni tipo è prevista dal C una stringa di controllo per l'output formattato della printf (per il tipo int, come si ricorderà, tale stringa è %d): per la conversione del tipo char la stringa %c, per quella di tipo float o double la stringa %f, %l sta per long e %u per unsigned, la stringa %e fa in modo che il valore della variabile mandata in output sia visualizzato con il formato esponenziale.

Nelle assegnazioni è spesso necessario specificare meglio il valore da assegnare:

..

long a;

double b;

char c;

..

a = 12L;

b = 15.0;

c = ‘A’;

Il valore 12 andrebbe pure in una variabile di tipo int, il suffisso L fa in modo che venga espanso in un long.

Nelle assegnazioni ai tipi in virgola mobile è necessario specificare sempre la parte decimale anche quando, come nel caso precedente, non sarabbe indispensabile.

Nelle assegnazioni alle variabili di tipo char il carattere da assegnare deve essere racchiuso fra apici.

Esaminiamo adesso un programma che trasforma un carattere minuscolo nella sua rappresentazione maiuscola. Le righe più significative sono commentate da etichette numeriche cui si farà riferimento poi per alcune osservazioni:

/*

Conversione di un carattere da minuscolo a maiuscolo

Vale per la codifica ASCII

*/

#include <stdio.h>

#define SCARTO 32 /*1*/

main(){

char min,mai;

printf("\n Conversione minuscolo-maiuscolo");

printf("\n Introduci un carattere minuscolo");

scanf("%c",&min); /*2*/

if (min>=97 && min<=122) { /*3*/

mai = min-SCARTO; /*4*/

printf("\n Rappresentazione maiuscola %c",mai); /*5*/

printf("\n Codice ASCII %d",mai); /*6*/

}else

printf("\n Carattere non convertibile");

}

Nella riga con etichetta 1 è definita la costante SCARTO. Il valore 32 dipende dal fatto che, nel codice ASCII, tale è la distanza fra le maiuscole e le minuscole (es. ‘A’ ha codice 65, ‘a’ ha codice 97).

Nella riga con etichetta 2 si effettua l’input del carattere da elaborare. Si noti l’uso di %c per indicare che si tratta appunto di un carattere.

Nella riga con etichetta 3 si controlla, utilizzando la sua rappresentazione numerica, se il carattere immesso rientra nei limiti della codifica ASCII delle lettere minuscole. Le lettere minuscole, in ASCII, hanno codice compreso fra 97 (la lettera minuscola a) e 122 (la lettera minuscola z). Il confronto poteva anche essere fatto sulla rappresentazione alfanumerica:

if (min>=’a’ && min<=’z’)

Nella riga con etichetta 4 si effettua in pratica la trasformazione in maiuscolo. Al codice numerico associato al carattere viene sottratto il valore 32. In questo caso è utilizzato il codice numerico del carattere. Si noti che in questo contesto ha senso assegnare ad un char il risultato di una sottrazione (operazione numerica).

Nella riga con etichetta 5 si effettua l’output di mai inteso come carattere (è usato %c), laddove nella riga 6 si effettua l’output del suo codice numerico (è usato %d).

Le direttive di conversione possono avere varianti che specificano l'ampiezza dei campi, il numero di cifre decimali e un indicatore per l'allineamento del margine sinistro.

Un intero posto fra il segno di percentuale e il carattere di conversione indica l'ampiezza minima del campo (vengono aggiunti spazi per assicurare che questa ampiezza sia garantita, se, invece degli spazi si voglio tanti zeri, basta aggiungere uno zero prima dello specificatore di ampiezza). La direttiva %05d, per esempio, usa zeri come caratteri riempitivi in modo da portare la lunghezza totale a cinque caratteri. Per specificare il numero di cifre decimali di un numero in virgola mobile, si scrive, dopo lo specificatore di ampiezza, un punto seguito dal numero di cifre che si vuole siano stampate. La direttiva %10.4f, per esempio, stampa un numero di non meno di 10 cifre con 4 cifre dopo la virgola. Si può inoltre fare in modo che la stampa sia allineata a sinistra (per default lo è a destra) ponendo un segno meno: %-10.2f.

inizio

Il costrutto cast

A volte può interessare effettuare dei cambiamenti al volo per conservare i risultati di determinate operazioni in variabili di tipo diverso principalmente quando si va da un tipo meno capiente ad un tipo più capiente. In tali casi il linguaggio C mette a disposizione del programmatore un costrutto chiamato cast.

Es:

int uno, due;

float tre;

main() {

uno = 1;

due = 2;

tre = uno/due;

printf("%f",tre);

}

Tale programma nonostante le aspettative (dettate dal fatto che la variabile tre è dichiarata float) produrrà un risultato nullo. Infatti la divisione viene effettuata su due valori di tipo int, il risultato viene conservato temporaneamente in una variabile di tipo int e, solo alla fine, conservato in una variabile float. È evidente, a questo punto, che la variabile tre conterrà solo la parte intera (cioè 0).

Affinchè la divisione produca il risultato atteso, è necessario avvisare il C di convertire il risultato intermedio prima della conservazione definitiva nella variabile di destinazione.

Tutto ciò è possibile utilizzando il costrutto in questione che ha la seguente sintassi:

(nome-di-tipo) espressione

Utilizzando questo costrutto, il nostro programma, si scriverebbe così:

int uno, due;

float tre;

main() {

uno = 1;

due = 2;

tre = (float) uno/due;

printf("%f",tre);

}

In questo caso il programma fornisce il risultato atteso. Infatti il quoziente viene calcolato come float e quindi, dopo, assegnato alla variabile tre.

In definitiva il costrutto cast forza una espressione a essere di un tipo specifico (nel nostro caso una divisione intera viene forzata a fornire un risultato di tipo virgola mobile).

inizio

Array a una dimensione

L' array (in italiano conosciuto anche come vettore), è la prima struttura di dati trattata in Informatica. È quella più comunemente usata ed è quella che sta a fondamento di quasi tutte le altre.

Un array è un insieme di variabili dello stesso tipo cui è possibile accedere tramite un nome comune e referenziare uno specifico elemento tramite un indice. Possiamo pensare all’array come ad un insieme di cassetti numerati: per poter accedere al contenuto di un cassetto deve essere specificato il numero ad esso associato (l’indice).

Nelle variabili semplici per accedere al valore contenuto in esse è necessario specificare il nome ed, inoltre, una variabile con un valore diverso avrà un nome diverso. Nell’array esiste un nome che però stavolta identifica l’array come struttura (il nome farà riferimento all’insieme dei cassetti dell’esempio precedente: questo concetto sarà chiarito meglio più avanti). I singoli elementi dell’array verranno referenziati specificando la loro posizione relativa all’interno della struttura (l’indice): l’elemento a cui ci si riferisce dipende dal valore assunto dall’indice.

Gli elementi dell'array vengono allocati in posizioni di memoria adiacenti e, per tale motivo, è necessario dichiarare un array prima del suo uso in modo che il compilatore possa riservare in memoria lo spazio necessario per conservarlo.

Il formato generale della dichiarazione di un array a una dimensione è:

tipo nome_variabile[dimensione];

dove tipo è il tipo base dell'array, ovvero il tipo di ogni elemento dell'array; e il numero di elementi dell'array è definito da dimensione.

Il seguente esempio dichiara un array chiamato numero contenente un massimo di 10 elementi interi:

int numero[10];

Poiché il C assegna gli indici agli elementi di un array a partire da 0, gli elementi dell'array di interi appena dichiarato vanno da numero[0] a numero[9]. Quindi, per quanto osservato precedentemente: numero è il nome della struttura, identifica l’insieme degli elementi cioè 10 numeri interi, numero[0], numero[5] individuano il primo ed il sesto numero della struttura. Se si utilizza numero[i], l’elemento a cui ci si riferisce dipende dal valore assunto dalla variabile i che deve essere di tipo int.

Essendo poco abituati a concepire un elemento in posizione zero potrebbe essere opportuno, almeno inizialmente, sistemare gli elementi nell’array a cominciare dalla posizione 1: si sprecherà memoria riservata ad una posizione non utilizzata, ma i programmi potrebbero risultare più leggibili.

È bene ricordare che, seguendo tale convenzione, un array dichiarato contenente un massimo teorico di 10 elementi, può conservare gli stessi dalla posizione 1 alla posizione 9 (di fatto non potrà contenere più di nove elementi).

Il programma di esempio carica il nostro array con numeri provenienti dall'input:

main() {

int numero[10], conta;

for (conta=1;conta<=9;conta++)

scanf("%d",&numero[conta]);

}

Si noti nel programma di esempio l’uso della variabile conta: con una sola istruzione di input ripetuta il programma acquisisce valori da assegnare ai vari elementi dell’array. Il primo input viene effettuato su numero[1] (conta la prima volta vale 1), il secondo viene effettuato su numero[2] (conta vale 2) e così via fino al nono.

Questo tipo di array vengono detti ad una dimensione perchè per individuare un elemento all'interno della struttura, basta specificare un solo indice.

Il linguaggio C non esegue una verifica sui limiti dell'array, pertanto esiste la possibilità di superarli. Se ciò si verifica, per esempio in un assegnamento, si corre il rischio di alterare un'altra variabile o un pezzo di codice del programma. In altri termini si può utilizzare un array di dimensione n oltre i suoi limiti senza che vengano segnalati errori di compilazione o di esecuzione, ma con il risultato che probabilmente sarà il programma a mostrare dei malfunzionamenti.

Come caso di applicazione di un array numerico consideriamo il seguente esempio: le temperature rilevate nel corso della giornata in una determinata località sono conservate in un vettore, si richiede di scrivere un programma che, acquisita una determinata temperatura, determini se nella località considerata si è avuta nel corso della giornata tale temperatura e, in caso positivo, in quale rilevazione è registrata tale temperatura.

In termini informatici si tratta di effettuare una scansione sequenziale di un vettore per verificare se tale vettore contiene un determinato valore e, in caso positivo, se ne vuole conoscere la posizione.

Nelle righe più interessanti è presente un commento numerico per le osservazioni successive.

/* Ricerca un elemento in vettore e restituisce la posizione */

#include <stdio.h>

main(){

int temp[8],temprif; /*1*/

int i,posiz;

/* Acquisizione delle temperature rilevate */

for (i=0;i<=7;i++) {

printf("\nRilevazione temperatura n° %d ",i);

scanf("%d",&temp[i]);

}

printf("\nTemperatura da ricercare ");

scanf("%d",&temprif);

/* Scansione del vettore delle temperature */

posiz = -1; /*2*/

for (i=0;i<=7 && posiz==-1;i++) { /*3*/

if (temprif==temp[i])

posiz = i; /*4*/

}

/* Risultati della ricerca */

if (posiz == -1) /*5*/

printf("\nTemperatura non rilevata");

else

printf("\nTemperatura acquisita nella rilevazione %d",posiz);

}

Nella riga 1 si dichiara un array di 8 interi per conservare le rilevazioni delle temperature e una variabile per conservare la temperatura da cercare.

Nella riga 2 si inizializza una variabile che conterrà la posizione nella quale si troverà la temperatura cercata. Il valore –1 dipende dal fatto che le posizioni ammesse vanno dal valore 0 in su e, quindi, per indicare un valore che non indichi una posizione valida occorrerà utilizzare per esempio un numero negativo.

Nella riga 3 inizia il ciclo di scansione dell’array alla ricerca della temperatura di riferimento. Il controllo di fine ciclo si basa su due eventi che devono verificarsi contemporaneamente: elementi non finiti (i<=7) e temperatura non trovata (posiz==-1). Se infatti la temperatura cercata viene ritrovata è inutile continuare ancora ad esaminare gli altri elementi dell’array. Più avanti si esaminerà un modo diverso di uscita anticipata da un ciclo a contatore.

Se la temperatura ricercata è presente nell’array allora, nella riga 4, se ne conserva la posizione nella varabile posiz.

La riga 5 seleziona il caso temperatura non trovata (volere di posiz non modificato) dalla rilevazione ritrovata.

L’algoritmo sviluppato si basa sull’ipotesi che, se c’è, la temperatura ricercata si presenta una sola volta. Per togliere tale limitazione è necessario predisporre un vettore per contenere le posizioni delle varie ricorrenze. Le modifiche da apportare al programma saranno:

inizio

Stringhe

In C uno degli utilizzi degli array ad una dimensione riguarda la conservazione in memoria di stringhe di caratteri: queste in C sono array di tipo carattere la cui fine è segnalata da un carattere null (carattere terminatore), indicato come '\0'.

Il carattere null è il primo codice ASCII corrispondente al valore binario 00000000 e non ha niente a che vedere con il carattere 0 che ha, in ASCII, codice binario 00110000.

char a[10];

dichiara un vettore costituito da un massimo di dieci caratteri e:

char frase[] = "Oggi c'è il sole";

dichiara l'array monodimensionale frase il cui numero di caratteri è determinato dalla quantità di caratteri presenti fra doppi apici più uno (il carattere null che chiude la stringa).

È importante notare la differenza tra le due assegnazioni:

char a = 'r';

char b[] = "r";

Nel primo caso viene assegnato il solo carattere r, nel secondo la stringa r\0. In definitiva: se si vuole fare riferimento ad un solo carattere, questo deve essere racchiuso fra apici singoli; se, invece, si vuole fare riferimento ad una stringa, occorre racchiuderla fra doppi apici.

Sono disponibili parecchie funzioni specifiche per il trattamento delle stringhe nonostante il linguaggio C non possieda un dato di tipo stringa:

gets e puts

Le funzioni gets e puts sono funzioni specializzate nell'input e output di una stringa. Il programma seguente riceve in input una stringa e, subito dopo, la rimanda in output. Notare che in queste istruzioni si fa riferimento alla stringa nel suo complesso e, quindi, viene utilizzato il nome:

#include <stdio.h>

main() {

char s[80]; /* dichiara la stringa */

gets(s); /* riceve l'input */

puts(s); /* fornisce l'output */

}

Altre funzioni per il trattamento delle stringhe sono disponibili, per usi particolari, a patto che si includa nel programma la linea: #include <string.h>.

strcpy

Questa funzione copia una stringa in un'altra. Può essere utilizzata, per esempio, per assegnare un valore a una stringa. La sua sintassi è:

strcpy(s1,s2);

Si può immaginare che tale funzione equivale, come effetto, all'assegnamento nelle variabili di altro tipo (in altri termini è come se si scrivesse: s1=s2 assegna, cioè, alla stringa s1 il valore contenuto in s2. In realtà, come si vedrà più avanti, tale istruzione è valida ma assume altro significato).

strcat, strlen e strcmp

La funzione:

strcat(s1,s2);

concatena la stringa s2 alla fine della stringa s1:

strcpy(s1,"buon"); /* stringa specificata in s1 */

strcpy(s2,"giorno"); /* lo stesso per s2 */

strcat(s1,s2); /* s1 conterrà "buongiorno" */

La funzione:

strlen(s1);

restituisce un valore numerico che rappresenta la lunghezza della stringa s1.

La funzione:

strcmp(s1,s2);

può essere utilizzata per effettuare comparazioni sul contenuto di due stringhe. In particolare tale funzione:

Per tenere conto mnemonicamente di tali valori, basta pensare al confronto come ad una sottrazione: fra l’altro ciò è non è molto distante da quello che avviene in effetti. Se da s1 si sottrae s2 si avrà un valore positivo nel caso s1>s2, negativo se s1<s2 e nullo se sono uguali

inizio

La scelta multipla: istruzione else-if

Può essere necessario, nel corso di un programma, variare l’elaborazione in seguito a più condizioni. In questi casi il linguaggio C fornisce la struttura:

if(espressione)

istruzione

else if(espressione)

istruzione

..

else

istruzione

In questo caso le espressioni vengono valutate nell’ordine in cui sono scritte: se è vera la prima espressione si esegue l’istruzione specificata e poi si passa all’istruzione successiva alla chiusura della struttura. Se la prima condizione non è vera si passa ad esaminare la seconda e così via. Si tenga presente che:

Al fine di mostrare un esempio di alcune delle istruzioni esposte ultimamente scriviamo un programma che, data una espressione algebrica, calcoli quante parentesi graffe, quadre e tonde sono contenute nella espressione stessa. Le righe più significative sono etichettate per permettere un commento successivo:

/*

Conta i vari tipi di parentesi contenute in una espressione algebrica

(non fa distinzione fra parentesi aperte e chiuse)

*/

#include <stdio.h>

main()

{

char espress[30]; /*1*/

int pargraf,parquad,partond;

int c;

printf("\nEspressione algebrica ");

gets(espress); /*2*/

pargraf=parquad=partond=0;

for(c=0;c<=29 && espress[c]!=‘\0’;c++) { /*3*/

if(espress[c]==‘{’ || espress[c]==‘}’) /*4*/

pargraf++;

else if(espress[c]==‘[’ || espress[c]==‘]’) /*4*/

parquad++;

else if(espress[c]==‘(’ || espress[c]==‘)’) /*4*/

partond++;

}

printf("\nGraffe = %d",pargraf);

printf("\nQuadre = %d",parquad);

printf("\nTonde = %d",partond);

}

Nella riga con etichetta 1 si dichiara un vettore di tipo char e di lunghezza massima di 30 caratteri, cioè una stringa di 30 caratteri.

Nella riga con etichetta 2 si acquisisce dall’input la stringa (cioè l’espressione algebrica da elaborare).

Nella riga con etichetta 3 c’è l’inizio di un ciclo a contatore che servirà per la scansione della stringa: i caratteri verranno esaminati uno per volta per verificare se sono parentesi. Nella gets la stringa è vista nel suo complesso, qui si esaminano i singoli elementi dell’array cioè i caratteri che compongono la stringa. Si noti il controllo di fine ciclo: il contatore del ciclo, che qui assume il compito di indice del vettore, non può superare 29 (l’array ha 30 elementi al massimo) ed inoltre il carattere esaminato non può essere null (qualora lo fosse, la stringa sarebbe terminata). Le due condizioni devono essere verificate contemporaneamente: se una delle due non è verificata (superamento dei limiti fissati o fine stringa) l’elaborazione passa all’istruzione immediatamente dopo la chiusura del ciclo: la printf dopo la chiusura della parentesi (qui le parentesi sono state aggiunte per aumentare la leggibilità infatti, contenendo il ciclo una sola istruzione, non sarebbero indispensabili).

Nelle righe etichettate con 4 si esaminano i vari casi possibili (i tre tipi di parentesi). Il carattere esaminato può essere indifferentemente la parentesi aperta o quella chiusa e, a seconda del tipo, viene conteggiato nell’apposito contatore. Se il carattere esaminato non è una parentesi, mancando la clausola else, non viene preso in considerazione.

inizio

La scelta multipla: istruzioni switch-case e break

Esiste nel linguaggio C un'altra istruzione per codificare la scelta multipla. È necessario fare attenzione però alle diversità di comportamento. Sintatticamente la struttura si presenta in questo modo:

switch(espressione)

case valore:

istruzione

case valore:

istruzione

..

default:

istruzione

In pratica in questo caso vengono valutati i diversi valori che può assumere una espressione. Nelle varie istruzioni case si elencano i valori che può assumere espressione e che interessano per l’elaborazione. Valutata l’espressione specificata, se il valore non coincide con alcuno di quelli specificati, viene eseguita l’istruzione compresa nella clausola default. Occorre tenere presente che, sostanzialmente, i vari case esistenti agiscono da etichette: se espressione assume un valore specificato in un case, vengono eseguite le istruzioni a partire da quel punto in poi. Il valore specificato in case, in definitiva, assume funzione di punto di ingresso nella struttura.

Se si vuole delimitare l’istruzione da eseguire a quella specificata nella case che verifica il valore cercato, occorre inserire l’istruzione break che fa proseguire l’elaborazione dall’istruzione successiva alla chiusura della struttura.

Per chiarire meglio il funzionamento della struttura riscriviamo il programma del conteggio delle parentesi di una espressione algebrica facendo uso della nuova struttura.

/*

Conta i vari tipi di parentesi contenute in una espressione algebrica

(non fa distinzione fra parentesi aperte e chiuse)

*/

#include <stdio.h>

main()

{

char espress[30];

int pargraf,parquad,partond;

int c;

printf("\nEspressione algebrica ");

gets(espress);

pargraf=parquad=partond=0;

for(c=0;c<=29 && espress[c]!=‘\0’;c++) {

switch(espress[c]) { /*1*/

case ‘{’:; case ‘}’: /*2*/

pargraf++;

break; /*3*/

case ‘[’:; case ‘]’: /*2*/

parquad++;

break; /*3*/

case ‘(’:; case ‘)’: /*2*/

partond++;

break;

}

}

printf("\nGraffe = %d",pargraf);

printf("\nQuadre = %d",parquad);

printf("\nTonde = %d",partond);

}

Nella riga con etichetta 1 viene specificata l’espressione da valutare: espress[c] cioè il carattere estratto dall’espressione algebrica.

Nelle righe con etichetta 2 si esaminano i casi parentesi aperta o parentesi chiusa. I singoli valori sono seguiti dalla istruzione nulla (il solo carattere ;) e, poiché l’elaborazione continua da quel punto in poi, sia che si tratti di parentesi aperta che di parentesi chiusa si arriva all’aggiornamento del rispettivo contatore.

Nelle righe con etichetta 3 si blocca l’esecuzione del programma altrimenti, per esempio, una parentesi graffa oltre che come graffa verrebbe conteggiata anche come quadra e tonda, una parentesi quadra verrebbe conteggiata anche come tonda. Si noti che, anche se sono presenti due istruzioni, non vengono utilizzate parentesi per delimitare il blocco: il funzionamento della switch-case prevede infatti la continuazione dell’elaborazione con l’istruzione successiva. L’ultima istruzione break è inserita solo per coerenza con gli altri casi. Inoltre se in seguito si dovesse aggiungere una istruzione default, il programma continuerebbe a dare risultati coerenti senza necessità di interventi se non nella parte da inserire.

inizio

Array a più dimensioni

Un array a più dimensioni è un insieme di elementi dello stesso tipo, ognuno referenziabile da un insieme di indici: tanti quante sono le dimensioni dell’array stesso. Se si ha un array bidimensionale, per esempio, occorrono due indici per identificare un elemento della struttura.

Il formato generale della dichiarazione di un array a più dimensioni è:

tipo nome_variabile[dimensione] [dimensione] [dimensione]..

Gli array bidimensionali (matrici), che sono quelli più comunemente utilizzati, possono essere pensati come un insieme di elementi posizionati in una tabella: il primo indice identifica la riga, il secondo la colonna. Un elemento viene identificato specificando la riga e la colonna in cui si trova.

Come esempio di uso di un array a due dimensioni scriviamo un programma che costruisce e poi visualizza la tavola pitagorica. Le righe più significative sono, al solito, corredate da etichette numeriche che serviranno per i commenti successivi:

/* Stampa su video la tavola pitagorica */

#include <stdio.h>

main(){

int tavpit[11][11]; /*1*/

int i,j;

/* Costruisce la tavola */

for (i=1;i<=10;i++){ /*2*/

for (j=1;j<=10;j++){ /*3*/

tavpit[i][j]=i*j; /*4*/

}

}

/* Manda in output la tavola */

for (i=1;i<=10;i++) {

for (j=1;j<=10;j++)

printf("%3d ",tavpit[i][j]); /*5*/

printf("\n"); /*6*/

}

}

Nella riga con etichetta 1 si dichiara una matrice di 11 righe ed 11 colonne.

Nella riga con etichetta 2 si inizia il ciclo per la scansione delle righe della tabella (l’indice i viene utilizzato per identificare la riga esaminata della tabella). L’indice viene inizializzato ad 1 non avendo interesse a visualizzare la riga formata da tutti valori nulli.

Nella riga con etichetta 3 si inizia un nuovo ciclo, interno al primo, per la scansione delle colonne. L’uso delle parentesi mette in evidenza il funzionamento di questo doppio ciclo: si esamina la prima riga (i=1) e quindi le varie colonne di quella riga (per i=1 l’indice J varia da 1 a 10) . Si passa poi alla riga successiva e così via.

Nella riga con etichetta 4 si assegna il valore all’elemento della tabella che sta nella riga i e nella colonna j: nel nostro caso sarà equivalente al prodotto fra i due indici.

Nella riga con etichetta 5 viene mandato in output l’elemento generico. La stringa di formattazione dell’output consente l’allineamento e l’incolonnamento dei numeri della tabella.

La printf della riga 6 manda in output semplicemente un newline che consente di passare a capo quando si tratta di stampare su video una nuova riga della tabella. La printf è infatti posizionata alla fine del ciclo controllato da j (quello che stampa le colonne di una riga), quindi dopo la stampa di tutte le colonne di una riga, e prima della chiusura del ciclo controllato da i (quello che stampa le righe) quindi prima di passare alla prossima riga.

inizio

Array di stringhe

Una applicazione interessante delle matrici è l’array di stringhe. La stringa è un array di caratteri e quindi potremmo dire che un array di stringhe è un array di array di caratteri. Per chiarire la gestione di questo tipo di array esaminiamo un programma che si occupa di cercare delle parole in un vocabolario e ci dica, per ognuna, se è contenuta nel vocabolario stesso

/* Cerca parole in un vocabolario */#include <stdio.h>

#include <string.h> /*1*/

main(){

char vocab[10][20], parcerc[20]; /*2*/

int i,trovata;

/* Acquisisce parole da inserire nel vocabolario */

for (i=0;i<=9;i++) {

printf("\nParola %d ",i);

gets(vocab[i]); /*3*/

}

/* Acquisisce la parola da cercare */

printf("\n\nParola da cercare (Invio per finire) ");

gets(parcerc);

while (strcmp(parcerc,"")) { /*4*/

/* Cerca la parola */

trovata=0; /*5*/

for (i=0;i<=9;i++) {

if (!strcmp(parcerc,vocab[i])) { /*6*/

trovata=1; /*7*/

break; /*8*/

}

}

if (trovata) /*9*/

printf("\nParola trovata");

else

printf("\nParola non trovata");

/* Prossima parola da cercare */

printf("\n\nParola da cercare (Invio per finire) ");

gets(parcerc);

}

}

La dichiarazione della riga 1 permette l’utilizzo all’interno del programma delle funzioni per il trattamento delle stringhe.

Nella riga 2 viene dichiarato l’array di caratteri vocab[10][20]. Il nostro vocabolario conterrà quindi un massimo di 10 parole ognuna composta di un massimo di 20 caratteri. La prima parola è contenuta in vocab[0] e con vocab[2][7], qualora dovesse interessare, si individuerebbe l’ottavo carattere della terza parola del vocabolario. Ogni riga della tabella conterrà una parola e in ogni colonna ci sono i caratteri che compongono le parole. L’array parcerc[20] servirà per contenere la parola da cercare.

Nella riga 3 si acquisiscono le parole da inserire nel vocabolario: la prima volta l’istruzione si leggerà gets(vocab[0]), quindi gets(vocab[1]) e infine gets(vocab[9]) in relazione ai diversi valori assunti dal contatore i. L’uso di un solo indice dipende dal fatto che, con la gets, si acquisisce una stringa e quindi nel nostro caso una intera riga della matrice. Se avessimo voluto acquisire le stringhe un carattere per volta, si sarebbero dovuti gestire due cicli uno annidato nell’altro, così come nell’esempio della tavola pitagorica, contenenti l’istruzione scanf("%c",&vocab[i][j]). Alla fine avremmo dovuto aggiungere il carattere null.

Nella riga 4 si gestisce la condizione di uscita dal ciclo. La stringa ricevuta dall’input viene comparata con la stringa vuota (due doppi apici consecutivi): se alla richiesta di input della stringa da cercare si risponde premendo solamente il tasto Invio, viene acquisita una stringa contenente solo il carattere terminatore null. Ciò rende falsa la condizione e quindi il ciclo ha termine (strcmp in questo caso restituisce 0).

La variabile trovata che viene azzerata nella riga 5 registrerà l’eventuale ritrovamento della stringa all’interno del vocabolario. Il valore assegnato non ha il senso di un valore numerico a tutti gli effetti ma il senso di un valore logico (0 = falso, un valore non nullo=vero). Una variabile usata in questo modo viene chiamata solitamente variabile logica, switch o flag.

Nella 6 si confronta la parola da cercare via via con le parole contenute nel vocabolario. L’equivalenza delle due stringhe, quella da cercare e la parola del vocabolario esaminata, rende vera la condizione. In questo caso, riga 7, si rende vera la variabile trovata assegnandole un valore non nullo e con la break della riga 8 si forza l’uscita dal ciclo poiché è inutile continuare a confrontare la parola cercata con il resto delle parole del vocabolario: la ricerca viene sospesa.

Nella riga 9 si esegue un test sul flag trovata: se è vero (cioè è stata eseguita l’istruzione della riga 7) la parola è stata trovata, se è falso (è rimasto inalterato il valore assegnato nella istruzione della riga 5) la ricerca ha avuto esito negativo.

 

Inizio