• Non ci sono risultati.

Il linguaggio C dispense del corso di Laboratorio di Algoritmi e Strutture Dati A.A. 2001/2002

N/A
N/A
Protected

Academic year: 2021

Condividi "Il linguaggio C dispense del corso di Laboratorio di Algoritmi e Strutture Dati A.A. 2001/2002"

Copied!
36
0
0

Testo completo

(1)

Il linguaggio C

dispense del corso di

Laboratorio di Algoritmi e Strutture Dati A.A. 2001/2002

(versione molto ma molto draft)

Gianfranco Ciaschetti

1

27 maggio 2002

1Dipartimento di Matematica Pura e Applicata, Universitµa degli Studi di L'Aquila, via Vetoio, Coppito, I-67010 L'Aquila; e-mail: ciasc@univaq.it

(2)

Indice

1 Variabili e Tipi di dato 3

1.1 Tipo intero . . . . 4

1.2 Tipo reale . . . . 5

1.3 Tipo char . . . . 6

2 Espressioni e operatori 7 2.1 Conversione di tipi . . . . 8

3 Istruzioni 9 3.1 Assegnamento . . . . 9

3.2 Assegnamento condizionale . . . . 9

3.3 Cast . . . 10

3.4 Istruzioni di controllo . . . 10

3.4.1 Sequenza . . . 10

3.4.2 Iterazione . . . 11

3.4.3 Selezione . . . 12

3.4.4 Altre istruzioni di controllo . . . 14

4 Puntatori e array 15 4.1 Puntatori . . . 15

4.2 Array . . . 17

4.3 Stringhe . . . 18

5 Funzioni 20 5.1 Passaggio di parametri . . . 22

5.1.1 Passaggio di parametri per valore . . . 22

5.1.2 Passaggio di parametri per indirizzo . . . 22

5.1.3 Passaggio di parametri per riferimento . . . 23

1

(3)

5.2 Struttura di un programma . . . 23

5.3 Visibilitµa . . . 26

5.3.1 Variabili . . . 26

5.3.2 Funzioni . . . 27

6 Tipi di dato aggregati 28 6.1 Tipi enumerati . . . 28

6.2 Strutture . . . 29

6.3 Ulteriori aggregazioni . . . 30

6.3.1 Array multidimensionali . . . 30

6.3.2 Array di puntatori . . . 31

7 Input e Output 33

(4)

Capitolo 1

Variabili e Tipi di dato

Una variabile rappresenta una locazione di memoria in cui µe possibile scrivere dati, in modo che essi siano disponibili ogni volta che ne abbiamo bisogno.

Una variabile µe identi¯cata all'interno di un programma mediante una stringa alfanumerica, che prende il nome di identi¯catore. Il numero di bit che viene riservato per ogni variabile dipende dal tipo della variabile, inteso come il range di valori possibili che la variabile puµo assumere. Una variabile puµo comparire a sinistra o a destra di un'istruzione di assegnamento, a seconda che si voglia leggere o scrivere il suo valore nella memoria.

a = a + 2;

La variabile a viene prima valutata nell'espressione a destra dell'operatore di assegnamento (=), e poi considerata a sinstra come locazione di memoria.

Dopo questa istruzione, il valore della variabile a risulta incrementato di 2.

Il linguaggio C mette a disposizione diversi tipi prede¯niti, oltre a quelli de¯nibili dall'utente:

Interi possono avere diversa dimensione e possono essere considerati con segno (default) o senza segno (parola chiave unsigned). Le parole chi- ave che de¯niscono una variabile intera sono:

char intero su 8 bit short intero su 16 bit

int intero su 16 o 32 bit a seconda della macchina long intero su 32 bit

3

(5)

Caratteri sono de¯niti mediante la parola chiave char che identi¯ca un intero pari alla codi¯ca ASCII del carattere.

Reali sono de¯niti mediante una delle due seguenti parole chiave:

°oat reale in singola precisione su 32 bit double reale in doppia precisione su 64 bit Tipi strutturati array, strutture

Puntatori indirizzi di memoria

In un programma C, ogni variabile utilizzata deve essere dichiarata per mezzo di una dichiarazione di tipo. La dichiarazione di una variabile ha l'e®etto di

² memorizzare l'identi¯catore nella tabella dei nomi

² de¯nire il tipo della variabile

² riservare memoria per la variabile

Una dichiarazione avviene speci¯cando l'identi¯catore di tipo seguito dalla lista di variabili di quel tipo, nel seguente modo:

type var1, var2, ..., varn;

1.1 Tipo intero

Il tipo intero µe un sottoinsieme dell'insieme N, esattamente l'insieme dei numeri interi nell'intervallo

( ¡2

n¡1

) ¥ (2

n¡1

¡ 1)

dove n µe il numero dei bit con cui una variabile intera µe rappresentata. Per una variabile dichiarata come unsigned, il range dei valori possibili µe

0 ¥ (2

n

¡ 1)

La dichiarazione di una variabile di tipo intero µe fatta con le parole chiave

del tipo intnero, come nei seguenti esempi:

(6)

int i,j;

char c;

unsigned long L;

Il C de¯nisce per il tipo intero i seguenti operatori:

+ addizione - sottrazione

* moltiplicazione

/ divisione intera (se a=10 e b=21, b/a=2)

% modulo, resto della divisione intera (se a=10 e b=21, b%a=1) ++ incremento di un'unitµ a

-- decremento di un'unitµ a

> maggiore

< minore

>= maggiore o uguale

<= minore o uguale

== uguale

!= diverso

Una costante di tipo intero puµo essere espressa in notazione decimale, ottale (se inizia con 0) o esadecimale (se inizia con 0x). Gli operatori di incre- mento e decremento hanno un e®etto diverso a seconda che siano usati in notazione pre¯ssa o post¯ssa. Se, ad esempio, n=3 allora x=n++ ha l'e®etto di incrementare n e assegnare 4 a x, ma x=++n ha l'e®etto di incrementare n e assegnare 3 a x. Questo µe l'unico caso in cui l'operatore di incremento ha precedenza su quello di assegnamento.

1.2 Tipo reale

I numeri reali in C sono approssimati in singola (otto cifre decimali) o doppia precisione (sedici cifre decimali). Il range dei valori reali rappresentabili µe 3:4e

§38

nel primo caso, e 1:7e

§308

nel secondo.

Per il tipo reale sono de¯niti tutti gli operatori de¯niti per il tipo intero,

tranne l'operatore %. In questo caso, l'operatore / restituisce un tipo reale,

quindi il valore e®ettivo (eventualmente troncato alla massima precisione

frazionaria rappresentabile) della divisione. In particolare, basta che uno

dei due operatori sia di tipo reale perch¶e l'operatore / sia considerato come

divisione tra reali.

(7)

1.3 Tipo char

Il tipo carattere µe un intero rappresentabile su 8 bit, e quindi puµo variare da 0 a 2

8

¡ 1 a meno che non sia unsigned. Per il compilatore C, un carattere µe un intero che puµo rappresentare, a seconda della codi¯ca adottata dalla macchina (ASCII nella maggior parte dei casi), un simbolo signi¯cativo. µ E possibile esprimere valori costanti di tipo char indicando un simbolo tra apici, come in 'a', oppure in '?'. Il compilatore C interpreta automaticamente il carattere come l'intero corrispondente nella codi¯ca adottata. Come sarµa discusso nel x7, esistono in C caratteri particolari per la formattazione del testo, come

' ' spazio '\t' tabulazione '\n' newline

Gli altri tipi messi a disposizione dal linguaggio C saranno illustrati a

partire dal x4.

(8)

Capitolo 2

Espressioni e operatori

Le espressioni in C possono essere aritmetiche o logiche. Il linguaggio C non dispone del tipo booleano, ma considera vera una espressione il cui valore µe diverso da 0, e falsa un'espressione nulla. Gli operatori aritmetici disponibili sono quelli de¯niti per i tipi di dato presenti nell'espressione, con le regole di casting descritte in x2.1

Gli operatori logici de¯niti nel linguaggio sono:

& and bit a bit

| or bit a bit

^ xor bit a bit

<< scorrimento a sinistra

>> scorrimento a destra

~ complemento a 1

Vediamo di seguito alcuni esempi di espressioni aritmetiche e logiche:

i=x%y; /* il modulo di x diviso y e' assegnato a i */

i++; /* incremento di i */

a=2*3-4*5 /* il risultato di (2*3)-(4*5) e' assegnato ad a */

c1 && c2 /* c1 and c2 (possono essere intere) */

c >= '0' && c <= '9' /* se c e' un carattere corrispondente a una cifra */

c &= 0 /* azzera tutti i bit di c */

a >> 3 /* risultato dell'operazione a % 7 */

7

(9)

2.1 Conversione di tipi

In un'espressione aritmetica tra diversi tipi, essi sono convertiti tutti al piµ u grande operando presente, in accordo alla seguente regola:

char -> short -> int -> long -> float -> double

In un'istruzione di assegnamento, il valore di uno dei due operandi puµo essere troncato, se si cerca di assegnare un valore di un tipo non rappresentabile nello spazio riservato all'altro, oppure essere riempito senza e®etto da bit nulli. Le istruzioni che seguono illustrano i due casi rispettivamente:

int i;

char i;

float x;

c = i /* vengono scartati i bit in eccesso */

x = i /* i viene convertito in float aggiungendo zeri */

In un'espressione booleana un tipo aritmetico µe considerato vero se ha

valore diverso da 0, e falso altrimenti.

(10)

Capitolo 3 Istruzioni

3.1 Assegnamento

Permette di assegnare a una variabile il risultato di una espressione. Esempi di assegnamento sono i seguenti:

i = 3;

x = w/10+1;

L'operatore di assegnamento = puµo essere preceduto da un operatore binario op, con il seguente signi¯cato:

v op= expr equivale a v = v op expr;

Ad esempio,

i += 3; /* incrementa di 3 il valore di i */

x &= 1; /* mette a 1 tutti i bit di x */

a %= 2; /* mette in a il resto della divisione intera di a con 2 */

3.2 Assegnamento condizionale

In un'espressione del tipo e1 ? e2: e3, si valuta logicamente l'espressione e1. Se µe vera, allora viene valutata l'espressione e2, altrimenti l'espressione e3. Ad esempio, nell'assegnamento seguente

z = (a>b)? a : b;

si assegna a z il valore massimo tra a e b.

9

(11)

3.3 Cast

Un'istruzione di cast impone una conversione esplicita di tipo. La sintassi µe la seguente:

( tipo ) identificatore;

dove tipo µe il nuovo tipo cui vogliamo la variabile appartenga. L'e®etto del cast di una variabile µe lo stesso descritto in x2.

Le istruzioni di cast e assegnamento (semplice o condizionale) possono essere valutate, senza il punto e virgola ¯nale, come espressioni, come negli esempi seguenti:

i*(a+=3)+2;

z = (double)i/x;

L'e®etto del primo esempio µe quello di incrementare di 3 il valore di a, poi moltiplicare il valore di a con i, e in¯ne aggiungere 2. Quello del secondo µe quello di forzare i a double, e poi dividere per x .

3.4 Istruzioni di controllo

De¯niscono le modalitµa con cui si susseguono le operazioni. Le istruzioni di controllo per strutturare un programma C sono di tipo sequenza, selezione, iterazione.

3.4.1 Sequenza

Una sequenza (o blocco) di istruzioni µe un insieme di istruzioni separate da ; e racchiuse tra parentesi gra®e. Ad esempio

{

i=2;

j=i;

}

(12)

3.4.2 Iterazione

Un'istruzione iterativa consente di ripetere uno statement (da ora in poi chiameremo statement una singola istruzione o un blocco di istruzioni) un certo numero di volte, dipendente da una condizione logica da valutare in vero o falso. Il C mette a disposizione tre tipi di istruzioni iterative:

while Ha la seguente sintassi:

while ( expr ) statement

Viene valutata l'espressione expr. Se vera, l'esecuzione del programma passa alla prima istruzione dello statement, poi alla seconda, e cosµ³ via.

Alla ¯ne, viene valutata nuovamente l'espressione. Se l'espressione µe falsa, allora l'esecuzione continua immediatamente dopo l'istruzione while. Ad esempio,

while (x >= y) {

x += y;

c++;

}

do-while Ha la seguente sintassi:

do

statement while (expr);

In questo caso, viene prima eseguito lo statement, e poi valutata la condizione di uscita dal ciclo. Come sopra, se la condizione µe ve- ra il ciclo viene ripetuto, altrimenti il controllo passa all'istruzione immediatamente seguente. Ad esempio,

do {

x += y;

c++;

} while (x >= y);

(13)

for La sintassi µe la seguente:

for (expr1; expr2; expr3) statement

ed ha il seguente signi¯cato:

expr1;

while (expr2) {

expr3;

statement }

Un uso frequente dell'istruzione for µe quella di ripetere un numero

¯ssato di volte un dato statement, come in for (i=0; i<n; i++)

a += i;

e corrisponde ad assegnare il valore 0 a i, quindi a valutare se i<n ed eseguire in questo caso l'sitruzione a+=i e l'istruzione i++. Entrambe le istruzioni sono ripetute ¯no a che la condizione i<n µe vera. Si noti che in un'istruzione for tutte e tre le espressioni possono essere nulle.

Se expr1 e expr3 sono nulle, non vengono eseguite le istruzioni di assegnamento corrispondenti, mentre se expr2 µe nulla la condizione µe valutata come falsa (µe possibile utilizzare una costante non nulla per esprimere una condizione sempre vera).

3.4.3 Selezione

Un'istruzione di selezione permette di eseguire uno statement tra diversi pos- sibili in base al veri¯carsi o meno di una data condizione. Il C mette a disposizione due tipi di selezione:

if-else Ha la seguente sintassi:

(14)

if (expr) s1 else

s2

Viene valutata l'espressione expr. Se vera, si esegue lo statement s1, altrimenti lo statement s2. La parte else puµo anche essere omessa.

switch Permette di valutare tra un insieme di alternative possibili. Ha la seguente sintassi:

switch (expr) {

case c1: s1; break;

case c2: s2; break;

...

case cn: sn; break;

default: sdef; break;

}

Viene valutata l'espressione expr e il suo valore viene confrontato, nell'ordine, con le costanti

tt c1, c2, ..., cn. Non appena il confronto ha valore vero, supponiamo la i-esima, viene eseguito lo statement si e il controllo µe passato alla

¯ne dell'istruzione switch. Il valore di default (opzionale) µe utilizzato nel caso in cui nessuno tra

tt c1, c2, ..., cn eguaglia il valore dell'espressione. Un esempio µe il seguente:

while((c=getchar())!= EOF) {

case '0':ncifre++;break;

case '1':ncifre++;break;

...

case '9':ncifre++;break;

case ' ':nspazi++;

case '\t':nspazi++;

case '\n':nspazi++;

default: naltri++; break;

}

(15)

3.4.4 Altre istruzioni di controllo

L'istruzione break permette di uscire da un ciclo all'interno del quale µe inseri- ta. L'istruzione continue inserita in un ciclo provoca l'esecuzione immediata della iterazione successiva. L'istruzione goto permette di saltare in modo in- condizionato a una data istruzione. In C a ogni istruzione µe possibile associare un'etichetta, nel seguente modo: label: istr. Conseguentemente, si puµo saltare direttamente all'istruzione istr nel seguente modo:

goto label;

Nella programmazione, l'uso delle istruzioni goto dovrebbe essere evitato, in

quanto elude la strutturazione di u programma.

(16)

Capitolo 4

Puntatori e array

4.1 Puntatori

Il linguaggio C permette una e±ciente manipolazione degli indirizzi di memo- ria, mettendo a disposizione il tipo puntatore. Un puntatore µe , sostanzial- mente, un'indirizzo di memoria a partire dal quale µe possibile recuperare un dato valore. La dimensione di un puntatore non µe speci¯cata nel linguaggio, ma dipende dalla macchina. Per comprendere il signi¯cato dei puntatori, introduciamo due operatori unari:

* indica il contenuto di una cella di memoria

& indica l'indirizzo di una cella di memoria Ad esempio, con la dichiarazione

int *px, x,z;

speci¯chiamo che px µe una variabile di tipo puntatore a intero, mentre x e z sono interi.

L'istruzione px = &x;

assegna al puntatore px l'indirizzo della variabile x, mentre l'istruzione z = *px;

15

(17)

assegna alla variabile intera z il contenuto della locazione puntata da px.

In generale, quando si dichiara un puntatore, occorre de¯nire il tipo di dato cui esso punta, per istruire il compilatore su quanta porzione di memoria considerare per interpretare il dato a partire dall'indirizzo cui il puntatore si riferisce. Quindi, ad esempio, con le dichiarazioni

char *pc;

long *px;

dichiariamo un puntatore a carattere pc e un puntatore a intero lungo px.

Quando andremo a recuperare i dati memorizzati agli indirizzi corrisponden- ti, ad esempio nelle espressioni *pc e *px, il compilatore saprµa che si sta chiedendo di recuperare il prossimo byte a partire dall'indirizzo pc oppure i prossimi 4 byte a partire dall'indirizzo px.

Nel x?? abbiamo detto che una dichiarazione di variabile ha l'e®etto di riservare memoria per quella variabile. Ad esempio, con la dichiarazione

short a;

il compilatore riserva 2 byte di memoria per la variabile a nel momento in cui la procedura che contiene la dichiarazione viene attivata. Lo stesso discorso vale per i puntatori: con la dichiarazione di una variabile di tipo punta- tore l'identi¯catore viene inserito nella tabella dei nomi, e un quantitativo di memoria corrispondente alla dimensione di un puntatore (16 o 32 bit a seconda della macchina) viene riservata. Si noti, perµo , che in questo caso solo la memoria per la variabile puntatore viene allocata, e non per il tipo da esso puntato. Ciµo signi¯ca che la scrittura

int *a;

*a=10;

produce un errore di memoria, in quanto si µe cercato di scrivere in una zona

di memoria che non µe stata precedentemente allocata. Cio¶e , si possono

assegnare valori al puntatore a intero a, ma non alla locazione da esso pun-

tata. Per ovviare al problema, si deve in questo caso riservare esplicitamente

memoria per un intero, mediante l'istruzione bf malloc (oppure calloc), nel

seguente modo:

(18)

int *a;

a = (int*)malloc(sizeof(int));

*a=10;

come si puµo osservare, la funzione malloc prende come parametro un numero corrispondente al numero di byte da riservare (in questo caso il formato di un intero) e restituisce un puntatore alla zona di memoria allocata. Il puntatore restituito µe del tipo generico void, quindi siamo costretti a e®ettuare un cast per trasformarlo in un puntatore a intero. Il risultato di questa espressione, ossia un puntatore a intero, viene assegnato quindi alla variabile a.

4.2 Array

Gli array sono un modo per organizzare collezioni di dati omogenei (dello stesso tipo). Una variabile di tipo array µe de¯nita speci¯cando il tipo dei dati contenuti nell'array. Ad esempio, la dichiarazione

int a[10];

dichiara un array di 10 elementi di tipo int. I dati di un'array vengono mem- orizzati in locazioni contigue di memoria, secondo l'ordine de¯nito dall'array.

Si puµo accedere a ognuno degli elementi dell'array speci¯cando un indice che rappresenta l'o®set, a partire dalla locazione in cui µe memorizzato il pri- mo elemento dell'array, in cui si trova l'elemento considerato. Nell'esempio precedente, possiamo leggere il valore di ogni elemento nell'array a mediante a[0]

a[1]

...

a[9]

Il nome di un array corrisponde al puntatore alla locazione in cui µe memo- rizzato il primo elemento dell'array, cio¶e

a = &a[0]

Dunque, la scrittura a[0] equivale a *a, e per ogni indice i, a[i] equivale a

*(a+i). La di®erenza tra array e puntatori µe che un puntatore µe una variabile

di tipo indirizzo di memoria, mentre un array µe una costante e no puµo essere

modi¯cata.

(19)

Un array puµo essere anche dichiarato come puntatore, a patto di allocare memoria per i suoi elementi. Dunque, la dichiarazione dell'esempio puµo essere fatta come

int *a=(int*)malloc(10*sizeof(int));

In questo modo, il nome dell'array µe una variabile, e come tale puµo essere modi¯cata. Se si vuole dichiarare un array come costante senza speci¯care il numero di elementi, si puµo scrivere una dichiarazione del tipo

int a[];

Anche in questo caso occorre allocare esplicitamente memoria prima di uti- lizzare l'array.

4.3 Stringhe

Una stringa µe rappresentata in C come un array di caratteri che termina col carattere tt '

0'. Ad esempio, con la dichiarazione char nome[20];

si dichiara una variabile di tipo puntatore a carattere, ossia un array di carat- teri, ossia una stringa che puµo contenere al massimo 19 caratteri (l'ultimo µe riservato per il carattere di terminazione). Non esistono in C operatori de¯niti per un'intera stringa, perµo il linguaggio mette a disposizione delle librerie di funzioni per la loro manipolazione. Alternativamente, bisogna op- erare come per un qualunque array. Se ad esempio, si vuole copiare una stringa in un'altra, occorre scrivere un frammento di codice come

char s[20], t[20];

int i=0;

while (s[i] != '\0') {

t[i]=s[i];

i++;

}

(20)

La libreria standard stdlib mette a disposizione la funzione strcpy per e®ettuare la stessa operazione.

Le stringhe costanti sono espresse racchiuse tra ", come in s = "ciao";

L'assegnamento di una stringa costante a un puntatore a carattere speci¯ca

automaticamente la dimensione della stringa, per cui non µe necessario allocare

memoria corrispondente.

(21)

Capitolo 5 Funzioni

Una funzione µe una procedura che prende in input alcuni parametri e resti- tuisce in output un risultato. Ci sono in C tre momenti diversi nel progetto di una funzione:

² de¯nizione

² dichiarazione

² uso

La de¯nizione di una funzione ha la seguente sintassi:

type idfunct(paramlist) {

STATEMENT }

La prima riga prende il nome di testata della funzione, mentre lo statement tra parentesi gra®e µe il corpo della funzione. Una funzione puµo essere de¯nita in qualunque punto del programma, tranne che nel corpo di un'altra funzione.

Una dichiarazione µe composta solo dalla testata della funzione, seguita da ";".

La dichiarazione di una funzione serve a rendere visibile la funzione (per il suo uso) in frammenti di codice che precedono la de¯nizione.

La testata della funzione speci¯ca con type il tipo del valore restituito dalla funzione, con idfunct il nome della funzione e con paramlist la lista dei parametri che sono passati come argomento. Ogni parametro deve essere speci¯cato con una dichiarazione di tipo. Vediamo di seguito un esempio di de¯nizione di funzione:

20

(22)

void swap(int *p1, int *p2) {

int temp=*p1;

*p1=*p2;

*p2=temp;

}

L'uso di una funzione µe fatto mediante una chiamata di funzione, all'interno di un'espressione. Nell'esempio precedente, possiamo scambiare i valori delle locazioni pa e pb (che supponiamo contenere giµa valori interi) con la chiamata int *pa, *pb;

*pa = 10;

*pb = 20;

swap(pa, pb);

oppure passando gli indirizzi di due variabili intere, come in int x, y;

x = 10;

y = 20;

swap(&x, &y);

Si noti che la funzione swap non restituisce niente (tipo generico void).

In caso contrario, possiamo restituire un valore per mezzo dell'istruzione return, come nell'esempio seguente:

int max(int a, int b);

{

int m = (a > b) ? a : b;

return m;

}

Si puµo chiamare la funzione max per determinare, ad esempio, il massimo in un array di interi

int greater=-MAX_INT;

int a[10];

for (int i=0; i<10; i++)

greater = max(greater, a[i]);

(23)

Una funzione, in generale, serve risolvere un dato problema computazionale, e pertanto µe scritta in termini generici con parametri formali, che vengono di volta in volta istanziati con parametri attuali all'atto della chiamata della funzione. Nell'esempio precedente, i valori dei puntatori pa e pb vengono copiati nelle variabili p1 e p2 e i valori degli interi .

5.1 Passaggio di parametri

Ci sono in C tre modalitµa di passaggio di parametri:

² per valore

² per indirizzo

² per riferimento

In ognuno dei casi, viene eseguita una copia privata e temporanea (cio¶e , che esiste solo ¯no a quando il controllo µe all'interno del corpo della funzione) dei parametri. Tuttavia, il signi¯cato cambia a seconda del tipo passato.

5.1.1 Passaggio di parametri per valore

Non viene modi¯cato il contenuto della locazione in cui sono memorizzati i parametri attuali. Nell'esempio seguente, la variabile a ha valore 10 dopo l'esecuzione della funzione f.

int f(int x) {

x = x+3;

return x;

}

int a=10, b;

b = f(a); /* b vale 13 */

5.1.2 Passaggio di parametri per indirizzo

Se si vuole modi¯care la locazione di un parametro attuale, occorre passare

il puntatore a tale locazione. L'esempio µe quello della funzione swap per lo

scambio di due interi. In questo caso, viene e®ettuata una copia dei puntatori

(24)

formali in quelli attuali, ma l'e®etto di scrivere in *p1 µe quello di scrivere in

*pa, visto che entrambi gli indirizzi si riferiscono dopo la copia alla stessa locazione di memoria.

5.1.3 Passaggio di parametri per riferimento

Uno dei problemi del passaggio per indirizzo µe quello di lavorare con puntatori anzich¶e con tipi elementari. Il passaggio per riferimento permette di passare l'indirizzo di una variabile, pur permettendo di continuare a lavorare come se si avesse a disposizione il tipo elementare della variabile. In sostanza, ha lo stesso e®etto del passaggio per indirizzo, ma µe un modo per sempli¯care la progettazione delle funzioni. Il passaggio per riferimento presuppone che sia posto l'operatore & davanti al parametro formale che si vuole passare in questo modo. La funzione swap potrebbe essere riscritta, in questo caso, come

void swap(int &x, int &y) {

int temp=x;

x=y;

y=temp;

}

e si puµo chiamare la funzione con un'istruzione del tipo int a=10;

int b=20;

swap(a, b);

L'e®etto µe lo stesso del pasaggio per indirizzo, cio¶e i valori delle due variabili vengono scambiati. Si noti che non era possibile de¯nire una funzione che realizza la stessa operazione col passaggio per valore.

5.2 Struttura di un programma

Un programma C µe composto da uno o piµu ¯le con estensione .c o .h. I primi sono chiamati ¯le di implementazione, e i secondi ¯le di intestazione. Nei

¯le di intestazione compaiono le dichiarazioni delle costanti, delle variabili e

delle funzioni che sono utilizzate all'interno dei ¯le di implementazione, e che

(25)

vogliamo rendere visibili agli altri ¯le. A titolo di esempio, supponiamo di scrivere un programma prova. Con il comando make si realizza un progetto dal nome prova che comprende tutti i ¯le di implementazione e intestazione che useremo, e che produrrµa un codice eseguibile prova.exe. Il progetto prova deve contenere un ¯le prova.c piµ u eventuali altri ¯le. Inoltre, nel ¯le prova.c deve comparire una funzione particolare, la funzione main. La prima riga del corpo della funzione main µe il punto di inizio di esecuzione di un programma C. Ad esempio, nel nostro progetto di prova vogliamo che sia presente un ¯le prova.c, e due ¯le functions.h e functions.c nei quali sono poste, rispettivamente, le dichiarazioni e le implementazioni (de¯nizioni) delle funzioni che vogliamo utilizzare all'interno del nostro ¯le principale.

Per poter rendere visibile, ad esempio, la funzione f1 all'interno del ¯le prova.c, supponendo che la funzione µe stata dichiarata nel ¯le functions.h e implementata (de¯nita) nel ¯le functions.c, occorre includere il ¯le di intestazione functions.h all'inizio del ¯le prova.c, nel seguente modo:

/* file prova.c */

#include<functions.h>

void main() {

int a = f1();

...

}

A questo punto, basta produrre i codici oggetto di entrambi i ¯le .c e "linkarli"

assieme, e il gioco µe fatto. Lo stesso discorso vale per le funzioni di libreria messe a disposizione del C. Ad esempio, la funzione di copia di due stringhe strcpy µe de¯nita nella libreria stdlib, per cui la si puµo usare a patto di inserire nel nostro codice la seguente riga:

#include<stdlib.h>

Come prima, per poter utilizzare la funzione strcpy, bisogna "linkare" il codice oggetto ottenuto dalla compilazione della libreria (¯le .lib) con il nostro (¯le .obj), in un unico codice eseguibile. In generale, possiamo dire che per usare delle librerie (o dei ¯le esterni) occorre:

1. includere il ¯le di intestazione della libreria

2. linkare il codice oggetto della libreria a quello del nostro programma

(26)

Molti dei compilatori C commerciali presuppongono giµa il link alle librerie standard, quindi µe lasciato come compito al programmatore solo il punto 1).

L'istruzione #include rientra nel gruppo di istruzioni note come direttive per il compilatore. Infatti, non µe un'istruzione vera e propria, in quanto non manipola dati, ma semplicemente istruisce il compilatore su dove trovare i nomi non dichiarati all'interno del nostro ¯le principale.

Ogni ¯le di implementazione che compone un programma C µe formato da una sequenza di

² direttive per il compilatore

² dichiarazioni di costanti e variabili

² de¯nizioni di funzioni

² commenti

Ogni ¯le di intestazione che compone un programma C µe formato da una sequenza di

² direttive per il compilatore

² dichiarazioni di costanti e variabili

² dichiarazioni di funzioni (de¯nite nel corrispondente ¯le di intestazione)

² commenti

La funzione main puµo prendere due parametri come argomento, int argc e char **argv. Il primo indica il numero di argomenti che vengono associati all'eseguibile quando viene lanciato, e il secondo µe un array di puntatori a carattere che memorizza gli argomenti passati. Ad esempio,

/* file prova.c */

#include <stdio.h>

void main(int argc, char** argv) {

int n = argc;

for (int i=1; i<n; i++)

printf("\n%d-esimo parametro=%s", i, argv[i]);

}

(27)

stampa il valore di tutti i parametri passati al programma prova.exe. Se si invoca, ad esempio

prova pippo pluto minni paperino

l'e®etto dell'esecuzione del programma µe quello di visualizzare sulla consolle le seguenti parole:

pippo pluto minni paperino

I commenti in C si inseriscono tra i caratteri /* e */. Poich¶e un pro- gramma µe compilato per righe da sinistra verso destra, tutta la porzione di testo che si trova tra questi due simboli speciali µe considerato un commento.

I commenti possono essere anche indicati con //, e in questo caso hanno e®etto dal punto in cui iniziano ¯no alla ¯ne della riga.

5.3 Visibilitµ a

5.3.1 Variabili

Le variabili di un programma possono essere dichiarate speci¯cando una stor- age class che ne speci¯ca la visibilitµa all'interno del programma. La sintassi µe la seguente:

storage-class type idvar;

In C sono previste quattro diverse classi di memorizzazione:

automatic la portata (scope) µe la parte di programma compresa tra la sua dichiarazione e la corrispondente parentesi gra®a chiusa. I nomi delle variabili interne (o automatic) sono noti solo al momento di attivazione della procedura in cui sono dichiarate.

extern lo scope di una variabile esterna inizia dal punto di dichiarazione ¯no

alla ¯ne del ¯le sorgente in cui si trova la dichiarazione. Solitamente

viene usata per riferirsi a una variabile de¯nita in un altro ¯le. Una

variabile esterna µe sempre visibile durante l'esecuzione del programma.

(28)

static permette di rendere visibile una variabile in modo permanente dal punto della sua dichiarazione e per tutto il ¯le sorgente in cui si trova.

La variabile non µe visibile all'esterno del ¯le. In pratica, una variabile statica rimane interna come una automatica, ma ha validitµa anche al di fuori della procedura nella quale µe de¯nita. Solitamente, viene usata questa classe per evitare che sulla variabile vengano e®ettuati accessi indesiderati da parte di alcune funzioni.

register Viene usata per variabili che vengono usate molto di frequente.

Questa opzione avverte il compilatore di memorizzare la variabile in un registro della CPU, e non nella memoria RAM, permettendone una lettura piµu veloce. Solo variabili interne possono essere de¯nite come register

Se nessuna delle opzioni viene speci¯cata, si assume di default l'opzione automatica.

La dichiarazione di una variabile puµo comparire ovunque all'interno di un programma, mantenendo le regole di visibilitµa esposte sopra. Questo per- mette di de¯nire variabili esattamente nel punto in cui ne abbiamo bisogno, permettendo evitando in questo modo una piµu e±ciente gestione dei nomi.

5.3.2 Funzioni

I nomi di una funzione, come quelli di variabile, possono essere preceduti da una storage class che ne speci¯ca la visibilitµa . Le funzioni possono essere dichiarate come automatic, static oppure extern. Le regole di visibilitµa sono le stesse che per le variabili, e se nessuna µe speci¯cata si assume di default la classe automatic.

Per quanto detto nel x5.2, le funzioni possono essere rese visibili all'interno di un ¯le anche mediante l'inclusione del ¯le di intestazione in cui sono dichiarate. I questo caso, esse sono visibili dal punto in cui sono incluse

¯no alla ¯ne del ¯le sorgente in cui compare l'inclusione.

(29)

Capitolo 6

Tipi di dato aggregati

Il linguaggio C permette di de¯nire tre tipi di dati aggregati:

array insiemi omogenei di dati enumerati insiemi omogenei di dati strutture insiemi non omogenei di dati

6.1 Tipi enumerati

I tipi enumerati vengono de¯niti mendiante una dichiarazione del tipo enum idtype {c0, c2, ..., cn};

dove c0,...,cn sono degli identi¯catori costanti, che il compilatore considera con numeri ordinari da 0 a n. Un esempio di uso di un tipo enumerato µe il seguente:

enum giorno { lunedi,

martedi, mercoledi, giovedi, venerdi, sabato, domenica };

enum giorno oggi;

28

(30)

Questo frammento di codice de¯nisce il tipo enumerato giorno, e poi la variabile oggi di tipo giorno. La variabile oggi puµo assumere tutti i valori interi da 0 a 6. Si noti che per dichiarare la variabile, occorre speci¯care che si tratta di un tipo enumerato con la parola chiave enum.

6.2 Strutture

Una struttura de¯nisce un insieme aggregato di oggetti di tipo diverso. Ogni oggetto della struttura costituisce un campo della struttura, e deve essere dichiarato speci¯candone il tipo. Una struttura µe dichiarata mediante la parola chiave struct, come nell'esempio seguente:

struct impiegato {

char nome[20];

int matricola;

long salario;

};

La dichiarazione di una struttura de¯nisce un nuovo tipo, per cui µe possibile dichiarare, ad esempio, una variabile di tipo impiegato con

struct impiegato unimpiegato;

Data una variabile di tipo struttura, possiamo riferirci ai suoi campi per mez- zo dell'operatore '.'. Quindi, l'espressione unimpiegato.matricola ha come valore l'intero corrispondente alla matricola dell'impiegato unimpiegato. Se invece dichiariamo una variabile di tipo puntatore a struttura, come in struct impiegato *unimpiegatoptr;

possiamo utilizzare l'operatore freccia -> per indirizzare i suoi campi, come ad esempio in unimpiegatoptr->matricola. COome per i tipi enumerati, per dichiarare una variabile di tipo struttura o puntatore a struttura occorre speci¯care la parola chiave struct. Un modo alternativo µe quello di e®ettuare una dichiarazione di tipo con l'istruzione typedef. La sintassi di questa istruzione µe la seguente:

typedef tipo idtipo;

(31)

dove tipo µe un tipo prede¯nito oppure una de¯nizione di tipo aggregato, e idtipo µe un identi¯catore che verrµa usato come sinonimo per il tipo che abbiamo de¯nito. Ad esempio,

typedef int lunghezza;

typedef struct nodo {

int info;

struct nodo *left;

struct nodo *right;

} node;

viene de¯nito un alias tt lunghezza per il tipo intero, e un alias node per il tipo struct nodo. Gli alias aumentano la legibilitµa e la portabilitµa di un programma. Inoltre, permettono nel caso delle strutture e dei tipi enu- merati, di non ripetere la parola chiave struct o enum ogni qualvolta si voglia dichiarare una nuova variabile di quel tipo. Dopo una de¯nizione con typedef, la dichiarazione

struct nodo* anodeptr;

µe equivalente alla dichiarazione node* anodeptr;

6.3 Ulteriori aggregazioni

E possibile in C de¯nire aggregazioni piµu complesse a partire dalle regole e µ dai costrutti visti ¯nora. In particolare, vediamo in questo paragrafo come realizzare array multidimensionali e array di puntatori.

6.3.1 Array multidimensionali

Ricordando che l'i-esimo elemento di un array monodimensionale µe individ- uato speci¯cando il nome dell'array e l'indice i di o®set, possiamo estendere questo concetto al caso di piµ u dimensioni nel seguente modo (per il mo- mento, supponiamo di voler de¯nire array bidimensionali, ma il discorso µe generalizzabile a qualunque dimensione):

int a[10][5];

(32)

dichiara come automatic una matrice di 10 righe e 5 colonne. Gli indici di riga variano da 0 a 9, mentre gli indici di colonna variano da 0 a 4. Il generico elemento a

ij

della matrice µe indirizzato con l'espressione

int a[i][j];

Un array multidimensionale viene memorizzato per righe. Essendo i nomi di array puntatori al primo elemento dell'array, nella matrice a de¯nita sopra a rappresenta il puntatore alla prima riga (si noti che una riga µe un array monodimensionale), e a+i il puntatore alla (i+1)-esima riga. Conseguente- mente, il tipo di a µe int**. Forniamo a titolo di esempio un frammento di codice che realizza una funzione che stampa tutti gli elementi di una matrice bidimensionale.

void printmatrix(int** a, int m, int n) {

for (int i=0; i<m; i++) for(int j=0; j<n; i++)

printf("\n elemento %d-%d: %d", i, j, a[i][j]);

)

Si puµo notare come, per passare un array (di qualunque numero di dimen- sioni) come parametro, occorre passare il nome dell'array e le sue dimensioni.

6.3.2 Array di puntatori

Abbiamo visto in x6.3.1 che una matrice di interi µe un array (righe) di array (colonne) di interi. Poich¶e in C un array µe anche un puntatore, il discorso µe generalizzabile ad array di puntatori qualunque. In questo modo possiamo collezionare dati complessi, ad esempio di un tipo de¯nito dall'utente. A titolo di esempio, vediamo come realizzare un array di puntatori a strutture, in modo da organizzare in modo omogeneo oggetti di tipo struttura.

int i, n=10;

/* definisci il tipo persona */

struct persona { char nome[20];

char cognome[20];

char sesso;

int eta;

(33)

};

/* definisci un array di puntatori a persona */

struct persona **persone;

/* alloca memoria per l'array di puntatori */

persone = (struct persona**)malloc(n*sizeof(struct persona*));

/* inserisci informazioni nell'array di puntatori */

while(i<n) {

/* alloca memoria per una nuova struttura */

struct persona *p = (struct persona*)malloc(sizeof(struct persona));

/* poni il puntatore alla nuova struttura nell'array */

persone[i] = p;

i++;

}

A seguito di questa dichiarazione, si puµo recuperare, ad esempio, il nome della i-esima persona con

char *p = persone[i]->nome;

Ovviamente, anche le variabili di tipo struttura possono essere collezionate in

un array, ma questo comporta la scrittura sequenziale in memoria delle infor-

mazioni nelle strutture, cosa non richiesta negli array di puntatori. Inoltre,

con i puntatori la manipolazione dell'array risulta piµu e±ciente.

(34)

Capitolo 7

Input e Output

Il linguaggio non dispone di istruzioni di ingresso/uscita. Il compito di in- terfacciare un programma con l'esterno µe delegato a un insieme di funzioni che fanno parte della libreria standard di input/output (stdio). Come ogni altra libreria, per poter usare le sue funzioni dobbiamo includere nel nostro

¯le sorgente il ¯le tt stdio.h (e linkare il suo codice oggetto). Tra le funzioni piµu comunemente usate in questa libreria, abbiamo:

getchar legge un carattere dallo standard input (tastiera) putchar scrive un carattere sullo standard output (terminale) scanf legge una sequenza di caratteri dallo standard input printf scrive una sequenza di caratteri sullo standard output fscanf legge una sequenza di caratteri da un ¯le

fprintf scrive una sequenza di caratteri su un ¯le

La funzione getchar non ha parametri, e restituisce il carattere letto, mentre la funzione putchar prende come parametro il carattere da stampare. Nella lettura tramite getchar, il carattere speciale CTRL+D µe considerato come la costante EOF (¯ne del ¯le) per lo standard input. A titolo di esempio, riportiamo un frammento di codice che legge caratteri da tastiera e li stampa sul terminale, ¯no a che non si digita CTRL+D.

main() {

33

(35)

int c;

while ((c=getchar())!= EOF) putchar(c);

}

La sintassi delle funzioni scanf e printf µe la seguente:

scanf(string, arg1, arg2, ..., argn);

printf(string, arg1, arg2, ..., argn);

La stringa string µe composta da caratteri che vengono stampati su standard output, e da speci¯catori di formato. Gli speci¯catori di formato istruiscono il compilatore su come interpretare la sequenza in input, nel caso di scanf, e in output nel caso di printf. Il numero degli speci¯catori di formato presenti deve essere pari a quello degli argomenti, e la corrispondenza tra essi avviene nell'ordine in cui compaiono. Ad esempio, in

scanf("\n la codifica ASCII del carattere %c e' ", c);

printf("%d", c);

il carattere c viene considerato in lettura come carattere, e in scrittura come un intero (la sua rappresentazione in codice ASCII). Gli speci¯catori di for- mato iniziano con il carattere % e sono seguiti da un carattere di conversione.

I possibili caratteri di conversione sono i seguenti:

'd' intero decimale 'o' intero ottale 'x' intero esadecimale 'h' intero short

'c' carattere 's' stringa 'f' reale °oat

Per la funzione scanf µe inoltre de¯nito il carattere di conversione 'lf' per i reali double, mentre per la printf µe de¯nito in aggiunta a quelli elencati in tabella il carattere di conversione 'e' che permette di stampare un reale,

°oat o double, in notazione esponenziale.

Le funzioni fprintf e fscanf sono del tutto equivalenti alle printf e scanf,

ma hanno un ulteriore parametro che speci¯ca il puntatore a un oggetto di

tipo FILE, dove scrivere o leggere i dati. Quando si vuole che l'input venga

(36)

letto da un ¯le e non da tastiera, oppure che l'output venga reindirizzato su

¯le anzich¶e su terminale, bisogna usare queste due funzioni. L'uso di fprintf e fscanf presuppone che il ¯le sia stato aperto (istruzione fopen) e che il tipo di operazione (lettura o scrittura) sia compatibile con la modalitµa con la quale il ¯le µe stato aperto. La struttura FILE e la funzione fopen sono dichiarate nella stdio.h.

La fopen speci¯ca con un parametro di tipo stringa se il ¯le µe aperto in lettura ("r"), in scrittura ("w"), in scrittura in coda ("a", non cancella il contenuto del ¯le ma appende il nuovo output). Una operazione di apertura di un ¯le µe sempre seguita da un'operazione di chiusura (fclose). Vediamo a titolo di esempio un programma che stampa su ¯le l'e®etto di un ciclo for.

#include <stdio.h>

main(int argc, char*argv[]) {

FILE *fp;

if (argc != 2) /* programma (argv[0]), file output (argv[1]) */

{

printf("errore! numero di parametri non valido");

exit;

}

if ((fp=fopen(argv[1]), "w")!= NULL) {

printf("errore! impossibile aprire il file");

exit;

}

for(int i=0; i<10; i++)

fprintf(fp, "\nindice i alla %d-esima iterazione", i);

}

Riferimenti

Documenti correlati

Scrivere in linguaggio C un programma che implementi le operazioni precedenti indipendentemente dal fatto che la struttura dati di appoggio sia un grafo

L’inizio della coda è rappresentato dalla prima persona della fila (quella la prossima ad essere servita), mentre la fine della coda è rappresentata dall’ultima persona che si

Infine, se la lista è doppiamente puntata, il puntatore prev della testa della lista punta all’elemento in coda

lista.h deve includere la definizione della struttura dati a puntatori (lista semplice non circolare) e i soli prototipi delle funzioni implementate in lista.c. Dunque, per

Un albero binario è una tipo astratto di dato che o è vuoto (cioè ha un insieme vuoto di nodi) o è formato da un nodo A (detto la radice) e da due sottoalberi, che sono a loro

Riversamento dei dati contenuti nell’heap nell’albero binario di ricerca utilizzando una visita lineare dell’array. Stampare i dati contenuti nell’albero binario di

La classe di memoria automatica è relativa a quegli oggetti locali ad un blocco (funzione o programma) che viene liberata non appena si raggiunge la fine di quel blocco. La classe

Scrivere in linguaggio C un programma che implementi le operazioni precedenti indipendentemente dal fatto che la struttura dati di appoggio sia un grafo rappresentato con liste