• Non ci sono risultati.

Il C evolve dal BCPL e dal B. E’stato implementato da Dennis Ritchie sul sistema operativo UNIX per il PDP-11.

N/A
N/A
Protected

Academic year: 2022

Condividi "Il C evolve dal BCPL e dal B. E’stato implementato da Dennis Ritchie sul sistema operativo UNIX per il PDP-11."

Copied!
72
0
0

Testo completo

(1)

C C

Introduzione al linguaggio

Ugo de’Liguoro

(2)

Storia e bibliografia

Il C evolve dal BCPL e dal B. E’stato implementato da Dennis Ritchie sul sistema operativo UNIX per il PDP-11.

Nel 1973 Ritchie e Thompson hanno riscritto il nucleo di UNIX in C; da allora il compilatore C è nativo nel sistema UNIX, e tutte le estensioni e le utility del sistema sono scritte in C.

Nel 1983 un comitato ha definito una versione non ambigua del linguaggio tuttoggi in uso: l’ANSI C.

B. W. Kernighan, D. M. Ritchie, The C programming language (trad. it. Linguaggio C, Jackson Libri)

Deitel & Deitel, C Corso completo di programmazione, Apogeo.

(3)

Il C è un linguaggio imperativo:

• strutturato, ma senza gerarchia di funzioni

• tipato, ma con type casting

• consente operazioni a basso livello

Il C ha dunque solo alcune caratteristiche dei linguaggi di alto livello, ma in realtà è piuttosto vicino alla macchina, adatto per programmare anche a livello molto basso,

controllando indirizzi e singoli bit.

(4)

/* Programma per il calcolo del MCD */

#include <stdio.h>

void main(void) {

int a, b;

a = b = 0;

while (((a == 0) && (b == 0)) || (a < 0) || (b < 0)) {

printf("Inserire due interi positivi non entrambi nulli\n");

scanf("%d %d", &a, &b);

}

printf("MCD(%d,%d) = %d\n", a, b, MCD(a,b));

}

int MCD (int a, int b) {

int c;

if (b == 0){c = b; b = a; a = c;}

while (b > 0) {

c = b; b = a % b; a = c;

}

return a;

}

Un esempio di programma in C

(5)

Il file “stdio.h”

Il C non possiede primitive per l’I/O; queste sono definite nel modulo “Standard I/O”; le dichiarazioni dei nomi e dei tipi delle funzioni sono contenute in un file di intestazione (header, .h) che viene importato dal comando per il preprocessore:

#include <stdio.h>

Tra le funzioni definite in stdio.h vi sono:

• printf funzione di stampa (PASCAL: write)

• scanf funzione di lettura (PASCAL: read)

# è il simbolo con cui cominciano i comandi del preprocessore

i comandi del preprocessore non sono terminati da ;

(6)

La funzione “main”

La funzione main è la parte principale del programma: la sua esecuzione coincide con quella del programma.

void main (void)

Indica che la funzione main non ritorna (in questo caso) alcun valore

Indica che la funzione main non ha (in questo caso) argomenti

(7)

La dichiarazione delle variabili;

Le variabili si dichiarano indicando prima il tipo e poi il nome degli identificatori:

int a, b; (PASCAL: var a, b: integer;) Se una variabile viene dichiarata all’interno di un blocco:

{...}

allora è locale (visibile in quel blocco); altrimenti è globale.

Nota: le variabili dichiarate nel blocco che segue il main sono

locali nel main, non globali.

(8)

Le strutture di controllo “while” e “if”

La sintassi del while e dell’if è simile al Pascal, tuttavia con qualche differenza:

while(condizione) istruzione;

if (condizione) istruzione_1;

else

istruzione_2;

while condizione do istruzione ;

if condizione then istruzione_1

else

istruzione_2 ;

C Pascal

(9)

Il comando “return”

Il C non ha la distinzione tra funzioni e procedure: tutti i sottoprogrammi parametrizzati sono funzioni.

Tuttavia tra le funzioni vi sono quelle che ritornano un valore (e che si possono dunque impiegare come espressioni):

return espressione ;

Nota: l’espressione deve avere lo stesso tipo del valore di ritorno della funzione, ad esempio la funzione mcd deve ritornare interi:

int mcd (…)

Le funzioni che non ritornano valori hanno tipo void.

Il comando return interrompe il flusso nella funzione.

(10)

Struttura di un (semplice) programma C

/* commenti: nome del programma, sue funzionalità ecc. */

istruzioni per il preprocessore

dichiarazione di tipi, variabili, costanti tipo di ritorno main (elenco argomenti) {

dichiarazione variabili locali sequenza di istruzioni

}

tipo di ritorno funzione_1 (elenco argomenti) {

dichiarazione variabili locali sequenza di istruzioni

}

….

tipo di ritorno funzione_n (elenco argomenti) { … }

begin

end

blocco

programma principale: main

sottoprogrammi

(11)

Le funzioni

Sintassi per la dichiarazione di una funzione:

tipo_valore_ritornato nome_funzione (lista_argomenti) {

dichirazione variabili esterne ; definizione variabili automatiche ; corpo_della_funzione ;

}

lista_argomenti ha la forma

tipo1 var1 , … , tipon varn

(12)

Funzioni: passaggio di parametri e valori di ritorno

• I paremetri in C sono sempre passati per valore.

• Il passaggio per riferimento si simula passando eseplicitamente l'indirizzo del parametro attuale: quindi il parametro formale sarà di tipo puntatore.

• Non è possibile passare/ritornare parametri di tipo non elementare se non attraverso puntatori (con l'eccezione, per l'ANSI-C, delle struttture).

• Il valore della funzione viene ritornato attraverso l'istruzione return, la cui esecuzione termina, comunque, l'esecuzione del corpo della funzione.

• In C non esiste distinzione tra funzioni e procedure: semplicemente le seconde non contegono l'istruzione return; in tal caso (ANSI-C) il tipo di ritorno

della funzione è void.

(13)

La ricorsione

Il C è un linguaggio ricorsivo che permette definizioni implicite:

int fact (int n)

{ if (n == 0) return 1;

return n * fact(n-1);

};

In caso di mutua ricorsione la dichiarazione delle funzioni chiamate dovrebbe precedere la chiamata (il compilatore accetta comunque la definizione, ma segnala un WARNING). E’ buona pratica dichiarare prima tutte le funzioni mutuamente ricorsive :

void p (int n); void q (int n);

void p (int n)

{ if (n > 0) {...q(n-1);}

}

void q (int n)

{ if (n > 0) {...p(n-1);}

}

(14)

Il preprocessore

Prima della compilazione il codice di un programma C viene trattato da un preprocessore, il quale ha il compito di modificarlo prima della compilazione vera e propria.

L’azione del preprocessore è determinata dall’uso di comandi dei quali i più comuni sono #include e #define.

Esempi:

#define PI = 3.14159

ha l’effetto di sostituire la stringa “PI”con la stringa “3.14159” ovunque nel codice (che la tratterà correttamente come una costante numerica con virgola);

#include “myfile”

causa la copia del contenuto del file myfile nel punto esatto in cui appare l’istruzione #include.

obsoleta: meglio usare const

(15)

Struttura di un programma su più file (1)

Normalmente un programma C si articola in diversi file, i quali concorrono alla formazione del codice di un unico programma in due modi:

1. Per inclusione in fase di preprocessing.

2. Nella fase di linking.

Per favorire la compilazione separata è opportuno suddividere ciascun modulo in due file:

myfile_header.h myfile_code.c

In myfile_header.h saranno contenute le dichiarazioni di tipi, variabili e funzioni che si desidera siano visibili agli altri moduli (ed al modulo che contiene il main): si tratta di una interfaccia.

In myfile_code.c vi saranno le definizioni (quindi il codice) delle funzioni pubbliche e le dichiarazioni di tipi, variabili e funzioni private, ossia conosciute solo all’interno del modulo.

(16)

Struttura di un programma su più file (2)

int numero_di_serie:

int serie(void);

...

#include Serie.h

int serie(void) {...};

...

Serie.c

Serie.h

(17)

Struttura di un programma su più file (3)

Header_generale.h

Header1.h

Modulo1.c Main.c

Problema. Poiché l’inclusione consiste nella copia del codice nel punto in cui si trova il comando #include, le due inclusioni nel file Main.c provocano un messaggio di errore causato dalla ridefinizione degli identificatori nel file Header_generale.h

Header2.h

Modulo2.c

(18)

Struttura di un programma su più file (4)

Per ovviare al problema di possibili ridefinizioni di identificatori causati da inclusioni multiple si può usare il comando del preprocessore #ifindef :

#ifindef identificatore codice

#endif

Il suo effetto è quello di includere il codice racchiuso tra i due comandi

#ifindef e #endif solo se l’identificatore non è stato definito da un comando #define.

Allora la struttura di un file che contenga delle inclusioni multiple dovrebbe essere:

#ifindef HEADER /* id. del file */

#define HEADER

/* codice del file */

#endif

(19)

Tipi di base

char carattere

int intero

float virgola mobile, singola precisione double virgola mobile, doppia precisione void tipo di ritorno di una procedura,

tipo generico di puntatore Modificatori:

unsigned (unsigned int)

short (short int)

long (long int, long double)

(20)

Dichiarazione delle variabili

tipo elenco_variabili ;

int i, j, l;

short int si;

unsigned int ui;

double bilancio, profitto, perdita;

Dichiarazioni e inizializzazioni:

char car = 'a';

int primo = 0;

float importo = 1230000.0;

definizione del formato delle dichiarazioni delle variabili

La virgola (punto decimale) si aggiunge per indicare che la costante 1230000 deve essere memorizzata in virgola mobile.

(21)

Classi di memorizzazione

Le dichiarazioni delle variabili e delle funzioni le classifica secondo due dimensioni ortogonali:

• Visibilità

• globali: sono visibili a tutte le funzioni del programma;

• locali: visibili solo all’interno del blocco di dichiarazione.

• Persistenza

• statiche: restano allocate per tutta la durata dell’esecuzione del programma;

• automatiche: restano allocate solo durante l’esecuzione

del blocco in cui sono dichiarate.

(22)

Tre classi di memorizzazione

statiche automatiche globali ✔

locali ✔ ✔

Variabili static locali: sono variabili automatiche che, tuttavia, mantengono il loro valore (e dunque continuano ad esistere) attraverso successive chiamate della funzione in cui si trovano.

int serie(void) {

static int num_serie = 100;

/* iniz. che viene eseguita una sola volta */

return(num_serie++);

}

(23)

La struttura della memoria

Il funzionamento delle variabili statiche/automatiche e della parte dinamica della memoria è

illustrato dalla ripartizione della memoria in tre aree

fondamentali: statica, dinamica

ed automatica (pila dei record di

attivazione).

(24)

Variabili esterne

Un programma C si articola usualmente in più file. Ciò consente di introdurre una gerarchia di visibilità tra variabili. Essa si basa sulla definizione di varibili esterne:

Variabili extern: una dichirazione extern di una variabile in un file (nell'es. File 2) dice al compilatore che la variabile in questione è stata definita altrove (File 1), e ne estende la visibilità alle funzioni definite in quel file.

File 1 File 2

int x, y;

char car;

void

main(void) {…}

extern int x, y;

extern char car;

int funz(void) {…}

(25)

Costanti

Nome delle costanti Tipo

'a' '\n' '\0' char

1 123 - 234 int

35000L -34L long int

10000U 987U unsigned

123.23F 4.34e-3F float 123.23 12312333 double

1001.2L long double

•\0 = fine stringa, e \n = fine riga, sono singoli caratteri;

•L abbrevia long, U unsigned, F float,

•e separa la mantissa dall’esponente

(26)

Definizione di identificatori per le costanti

Con comandi per il preprocessore (cioè come alias):

#define nome della costante valore della costante

#define MAXLINEE 100 Usando il modificatore const:

const tipo nome_costante = valore ; Esempi:

const double e = 2.71828182845905;

const char tm[] = “type missmatch”;

(27)

Espressioni e assegnazioni

Le espressioni sono definite dalla grammatica:

espressione ::= variabile | costante | operatore(lista_espressioni) Gli operatori aritmetici binari si scrivono in notazione infissa, parentesizzati secondo le usuali convenzioni di precedenza. Ad esempio:

x + 2 5 * (y - 1) x % y (PASCAL x mod y) … Le assegnazioni hanno la forma:

variabile = espresione ;

Un' assegnazione in C è un'espressione, il cui valore è il valore assegnato alla variabile a sinstra del simbolo `=´

if ((n = strlen(s)) > 10) ... equivale al Pascal n:= streln(s); if n > 10 then ...

Ciò permette di fare assegnazioni multiple simultanee:

x = y = z = 0;

(28)

I booleani

Il C non ha il tipo dei booleani. Al loro posto si usano gli interi:

0 (false) qualunque intero > 0 (true) onde è preferibile introdurre le costanti:

#define TRUE 1 ovvero const int true = 1;

#define FALSE 0 const int false = 0;

Il metodo migliore per introdurre i booleani usa l’istruzione typedef

(29)

Espressioni booleane

Le espressioni booleane si formano utilizzando:

• Simboli relazionali: > >= < <=

• Simboli di eguaglianza e diseguaglianza: == !=

• Connettivi: && (and) || (or) ! (not) Esempi:

x > 3 (y == 1) || (y > 0) !(even(n))

(even() deve essere definita) Attenzione:

if (n == m)

equivale al Pascal

if n = m then ...

if (n = m)

equivale al Pascal

n:= m; if n > 0 then ...

(30)

Conversione automatica del tipo

Implicite nelle espressioni (promotion)

Quando in un'espressione sono utilizzati tipi differenti il compilatore li converte tuti nello stesso tipo, scegliendo quello che occupa più memoria.

char c; int i; float f; double d;

tot = (c / i) + (f * d) - (f + i);

int double float double

double

Nota: poiché un argomento di funzione è un'espressione, le conversioni di tipo avvengono anche con il passaggio di parametri.

(31)

Conversione esplicita del tipo

Negli assegnamenti

int i; char c;

i = c;

/* un carattere e´ identificato col suo codice ASCII */

c = i; /* il valore di c e' inalterato */

Nota: la conversione float ⇒ int provoca troncatura;

la conversione double ⇒ float provoca arrotondamento.

Nelle espressioni (cast)

(tipo) espressione

int n;

printf("la radice di %d risulta %f",n,sqrt((double) n));

(32)

Iterazione con while

while (condizione) comando ;

Esecuzione: la condizione viene valutata; se il valore ottenuto è true allora viene eseguito il comando e l’istruzione while viene eseguita nuovamente;

altrimenti il controllo passa all’istruzione successiva.

Esempio:

int a, b, c; /* pre: a, b interi positivi */

while (b > 0) { c = b;

b = a % b;

a = c;

}

return a; /* post: a e’ l’MCD tra a e b */

(33)

Iterazione con do while

do

istruzione

while (condizione);

equivale al Pascal repeat

istruzione

until not condizione;

Si osservi che la condizione in

PASCAL è negata: questo perché until esce se la condizione è vera, mentre do while esce se la condizione è falsa

(34)

Incrementi, decrementi, operazioni riflessive

Incremento di due specie:

preincremento ++i

postincremento i++ significano i:= i+1 in entrambi ì casi ma:

x = ++y equivale a y = y + 1; x = y;

x = y++ equivale a x = y; y = y + 1;

Esistono anche i decrementi in questa forma:

--i ovvero i-- equivalenti a i = i - 1;

Operazioni riflessive:

i += 7 significa i := i +7 utile in contesti del tipo:

numero_addetti += 7; invece di

numero_addetti = numero_addetti + 7;

(35)

Il ciclo for

for (inizializzazioni; condizione; operazioni) corpo;

La semantica di for è spiegata dalla seguente formulazione equivalente con il while:

inizializzazioni ;

while (condizione) { corpo ;

operazioni ; }

Non essendovi restrizioni sulle condizioni e sulle operazioni che vengono eseguite al termine di ciascun ciclo, il for ed il while sono in C

equivalenti (a differenza del PASCAL).

(36)

Esempio: fibonacciano

/* Fibonacciano:

fib(0) = 0 fib(1) = 1

fib(n+2) = fib(n) + fib(n+1)

*/

int fib (int n)

/* pre: n intero positivo */

{

int a = 0, b = 1, i;

if (n == 0) return 0;

for(i = 1; i < n; i++) /* a = fib(i-1), b = fib(i) */

{

b = a+b;

a = b-a;

}

return b;

}

(37)

Esempio: somma di 10 interi

/* legge 10 numeri e ne stampa la somma */

void main (void) {

int i, num, somma;

for (i = 0, somma = 0; i < 10; i++) { scanf("%d", &num);

somma += num;

}

printf("la somma vale: %d \n", somma);

}

(38)

Esempio: inversione di una stringa

void inversione (char s[])

/* post: inverte la stringa s sul posto */

{

int c, i, j;

for (i = 0, j = strlen(s) - 1; i < j; i++, j--) {

c = s[i];

s[i] = s[j];

s[j] = c;

} }

Nota. i = 0, j = strlen(s) - 1 sono due comandi che

vengono eseguiti in sequenza; analogamente i++, j--.

(39)

If annidati

Attenzione: else si riferisce all'ultimo if che ne è privo

if (n > 0)

for (i = 0; i < n; i++)

if (s + i > 0) { … }

else … /* errore: else non si riferisce all'if esterno */

Correzione:

if (n > 0)

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

if (s + i > 0) { … }

}

else ….

Annidamento if (condizione)

istruzione_1;

else if (condizione) istruzione_2;

else istruzione_3 Forma generale

if (condizione) istruzione ; if (condizione)

istruzione_1;

else

istruzione_2;

(40)

Selezione con switch (Pascal case)

switch c { case 'a':

case 'b':

case 'c': abc_num = abc_num + 1;

break;

case 'd': d_num = d_num +1;

break;

default: altri_num = altri_num +1;

}

Il comando break serve ad impedire l’esecuzione

sequenziale dei test successivi (tra i quali default avrebbe

sicuramente successo).

(41)

Puntatori

Un puntatore è una variabile il cui campo di valori è costituito da indirizzi.

Dichiarazione di un puntatore:

tipo *nome_variabile;

Esempio: int *p; /* p e' un puntatore ad interi */

Operatori su puntatori:

Operatore Semantica Esempio

& var ritorna l'indirizzo di var p = &n;

* var si riferisce al valore nella locazione puntata da var

n = *p;

La costante NULL è un valore comune per i puntatori di qualunque tipo e denota convenzionalmente una locazione indefinita (PASCAL nil).

(42)

Esempio

/* esempio dell’uso degli operatori sui puntatori

*/

#include <stdio.h>

void main(void) {

int x;

int *p1, *p2;

p1 = &x;

p2 = p1;

printf("%p", p2);

/* stampa il valore esadecimale dell'ind. di x */

}

(43)

Condivisione (sharing)

I puntatori possono condividere l’area di memoria cui puntano:

int *p, *q; int n = 44; p = q = &n;

44 n

p q

Ogni modifica del valore di n che avvenga per assegnazione su *p si

riflette su n e su *q. Questa caratteristica viene sfruttata per ottonere effetti collaterali sui valori dei parametri attauli, passandoli cioè per indirizzo.

(44)

Esempio: passaggio di parametri

Poiché in C i parametri delle funzioni sono sempre passati per valore, si impiegano i puntatori per ottenere effetti collaterali sui parametri attuali (passati alla chiamata):

void scambia (int *p, int *q) {

int temp;

temp = *p;

*p = *q;

*q = temp;

}

che, nel contesto di chiamata, si deve usare con l'operatore &:

int n, m; ...

scambia (&n, &m);

(45)

Vettori

In C i vettori sono puntatori il cui valore è costante (cioè un indirizzo

fissato); la loro dichiarazione è però anche una definizione, nel senso che il compilatore alloca la memoria per le componenti del vettore:

tipo_elementi nome_vettore [dimensione];

L’accesso in lettura scrittura ha la forma

nome_vettore [indice];

Esempi:

int v[10];

v[3] = 7; printf(“%d”, v[0]);

Nota: in C gli indici variano tra 0 e la dimensione del vett. - 1.

(46)

Esempio: dichiarazione e inizializzazione

int giorni_mese[12];

/* vettore di 12 interi */

La definizione può inizializzare gli elementi del vettore (quando siano un numero ragionevole):

int giorni_mese[12] =

{31,28,31,30,31,30,31,31,30,31,30,31};

Poiché in C il range di un array di n elementi è da 0 a n-1 abbiamo:

giorni_mese[3] sono i giorni del quarto mese

(47)

Esempio: massimo in un vettore

int maxvett (int n, int v[])

/* pre: v è un vettore di n > 0 interi positivi post: ritorna il massimo in v[0..n-1],

*/

{

int max = 0, i;

for (i = 0; i < n; i++)

if (max < v[i]) max = v[i];

return max;

}

(48)

Un tipico errore sui vettori

Un vettore non può essere copiato su un altro, di egual tipo e dimensione, con un’unica operazione di assegnamento:

int v[3] = {0,1,2};

int w[3];

w = v; /* errore */

la copia deve essere effettuata componente per componente:

for (i = 0; i < 3; i++) w[i] = v[i];

(49)

Funzioni con vettori quali argomenti

Un array non può essere passato per valore, ma sempre attraverso un puntatore, e dunque per indirizzo (riferimento). Vi sono tre modi per ottenere questo risultato:

void stampa(int num[10]);

/* stampa i dieci elementi dell'array num */

/* num ha una dimesnione fissata */

{

int i;

for (i = 0; i < 10; i++) printf("%d", num[i]);

}

void stampa(int num[]);

/* stampa i dieci elementi dell'array num */

/* num non ha una dimensione fissata */

{

int i;

for (i = 0; i < 10; i++) printf("%d", num[i]);

}

È preferibile aggiungere un parametro che spcifichi la

lunghezza del vettore

(50)

Aritmetica dei puntatori:

Sui puntatori sono definite operazioni di

• incremento e decremento: int *p; p = &n; p++;

/* p punta a &n + sizeof(int) */

• somma e sottrazione di un intero:

int n, m, *p; p = &n; p = p + m;

/* p punta a &n + m * sizeof(int) */

• differenze tra puntatori:

int n, a, b, *p, *q; p = &a, q = &b; n = p - q;

/* n è il numero degli interi allocabili tra gli indirizzi di a e di b */

• confronto tra puntatori:

int n, m, *p; p = &n; q = &m;

if (p < q) … /* eseguito se l’indirizzo di n è minore di quello di m */

(51)

Esempi

Si può puntare ad un singolo elemento di un array:

int a[10]; int *p; p = &a[2];

Tuttavia il nome di un array è una costante di tipo puntatore, il cui valore è l'indirizzo del primo elemento dell'array (offset 0). Dunque quanto segue è lecito:

p = a; che equivale a p = &a[0];

E' anche possibile accedere all'i+1-esimo elemento di un array usando un puntatore al primo e l'offset i: se p == a allora

n = *(p + i); che equivale a

n = a[i]; che equivale a

n = *(a + i); (!!!) Funziona perché

indirizzo di a[i] = indirizzo di a[0] + (i × sizeof(tipo el. di a)) e perché

valore di p + i = indirizzo di p + (i × sizeof(tipo di *p))

dimensione in byte

(52)

Stringhe

Una stringa è un vettore di caratteri non vuoto, il cui ultimo elemento è il terminatore `\0’: quindi una stringa di lunghezza n occupa n+1 byte.

“abc” = [`a’,`b’,`c’,`\0’]

In particolare “” = [`\0’] è la stringa vuota.

Il tipo delle stringhe è char*, ossia il tipo dei puntatori a char; le seguenti dichiarazioni sono corrette, ma hanno significato diverso:

char* s; s è una variabile e può puntare ad una stringa char s[]; s è un parametro di stringa nelle funzioni char s[80]; s è una stringa di 80 caratteri indefiniti char* s = “ciao”; s è la stringa “ciao”

(53)

Esempi: lunghezza e copia di una stringa

int lunghezza (char s[]) {

int i = 0;

while (s[i] != ‘\0’) ++i;

return i;

}

void copia(char s[], char t[]) {

int i = 0;

while ((s[i] = t[i]) != '\0') ++i;

}

Nota: queste ed altre funzioni sono nella libreria string, dove

sono chiamate strlen e strcpy rispettivamente.

(54)

Le strutture (record)

Un struttura è un insieme finito di valori, possibilmente di tipo differente, raccolti sotto un'unico nome. Ciascun valore è associato ad un campo etichettato da un nome-campo, ed è accessibile attraverso di esso.

struct nome_struttura { tipo nome_campo;

tipo nome_campo;

} variabili_struttura;

L’istruzione struct definisce un tipo, che può essere impiegato nella dichiarazione di varabili, purché la parola chiave struct sia ripetuta:

struct point {

double x; double y;

};

struct point p;

facoltativo

(55)

Strutture: accesso

Una struttura può essere:

• copiata: struct point p, struct point q;… p = q;

• indirizzata mediante l’uso dell’operatore &:

struct point *r; r = &p;

• acceduta attraverso i suoi membri:

p.x, r->x

• passata per parametro e ritornata come valore da una funzione:

struct point move(struct point p, double dx, double dy)

A differenza dei vettori le strutture possono essere copiate con una sola assegnazione

(56)

Esempio

struct point {

double x; double y;

} p, q;

struct point move (struct point p, double dx, double dy) {

p.x += dx; p.y += dy;

return p;

}

void showpoint (struct point p) {

printf("(%f,%f)\n", p.x, p.y);

}

void main (void) {

p.x = 1.0; p.y = 2.0;

showpoint(p);

q = move(p,1.5,3.1);

showpoint(p); showpoint(q);

}

p resta invariato, mentre q viene assegnato al valore di move, che è un punto

ottenuto traslando p di dx, dy

(57)

Dichiarazioni e definizioni

Dichiarazione: consiste nell’associare un tipo ad un identificatore.

Esempi: int n; int fact(int n);

Definizione: consiste nell’associare un valore ad un identificatore.

Esempi: n = 12; int fact(int n) {…}

Dichiarazioni e definizioni spesso coincidono (e.g. la definizione di fact è anche una dichiarazione).

La distinzione tra dichiarazioni e definizioni è importante per capire l’uso della memoria. Nel caso dei puntatori, ad esempio, una dichiarazione ha l’effetto di riservare uno spazio in memoria che possa contenere un indirizzo; una

definizione invece può (quando non vi sia sharing) comportare un’allocazione, riservando spazio in memoria, il cui indirizzo iniziale diventa il valore del

puntatore.

(58)

Gestione dinamica della memoria

Sia data la dichiarazione: T* p; (T sia un tipo)

Per definire *p (cioe’ associargli un valore) si può procedere in tre modi:

1. p = q; /* supponendo che *q sia definito */

2. p = &x; /* se x e’ definita */

3. p = (T*) malloc(sizeof(T));

/* allocando un oggetto di tipo T */

Allocazione: è il processo con cui una porzione della memoria dinamica viene riservata per ospitare dati di una certa dimensione; la dimensione, espressa in byte, dipende dal tipo ed è calcolata dalla funzione sizeof.

La funzione malloc ritorna l’indirizzo del primo byte dell’area allocata: tale indirizzo deve essere qualificato mediante un tipo, per cui si impiega il

casting (T*).

(59)

Allocatori

E’ opportuno dedicare a ciascun tipo di struttura dati utilizzato dinamicamente una funzione per l’allocazione:

char* StrAlloc(int len) {

char *p;

p = (char*) malloc((len+1)*sizeof(char));

return p;

}

Questa funzione alloca una stringa di lunghezza len: deve perciò allocare len+1 caratteri, per tener conto del terminatore \0.

Se l’allocazione non va a buon fine (memoria insufficiente) malloc e quindi StrAlloc, ritorna NULL: pertanto l’uso corretto di un allocatore è il seguente:

if ((s = StrAlloc(n)) != NULL) … ;

else StrError(); /* avendo definito tale fun. */

(60)

Deallocazione

Per deallocare ciò che è stato allocato da malloc si usa la funzione di libreria:

void free (void *p)

Il puntatore void *p è generico (polimorfo); l’informazione sulla dimensione del blocco puntato da p è stata memorizzata da malloc

nell’intestazione del blocco (header), che è a sua volta parte della free list gestita da malloc; grazie ad essa la funzione free può calcolare le

dimensioni del blocco da deallocare.

(61)

Tipi definiti dall’utente

Il C consente all’utente di definire propri tipi mediante il costrutto typedef (PASCAL type):

typedef definizione_del_tipo nome_del_tipo ;

I tipi così dichiarati possono essere impiegati esattamente come i tipi di base:

typedef unsigned int Length;

Length len, maxlen;

Length* lengths[]; /* vettore di puntatori a Length */

Un simile esempio è la definizione del tipo String come puntatore a char:

typedef char* String;

(62)

Tipi enumerati

Un tipico impiego della definizione di tipi è il caso dei tipi enumerati, ossia che ammettono un numero finito di valori ordinati linearmente:

typedef enum {lun,mar,mer,gio,ven,sab,dom} Giorni;

In realtà il compilatore attribuisce ai valori di un tipio enumerato un numero intero progressivo a partire da 0. Sfruttando questa particolarità possiamo introdurre il tipo dei booleani:

typedef enum {False, True} Boolean;

Questa dichiarazione attribuisce a False il valore 0 e a True il valore 1: di conseguenza un’istruzione come

if (True) ...

avrà il comportamento atteso.

(63)

Tipi e strutture

Sebbene il costrutto struct definisca dei tipi, l’uso del typedf (introdotto nell’ANSI C) consente una scrittura più perspicua del codice. Quindi la

dichiarazione:

typedef struct { double x;

double y;

} Point;

è preferibile alla dichiarazione:

struct point { double x;

double y;

};

Si rammenti che in quest’ultimo caso la dichiarzione di una variabile avrebbe avuto la forma:

struct point p;

(64)

Tipi unione

L’insieme dei valori di un tipo unione è esattamente l’unione dell’insieme dei valori delle componenti. Tecnicamente una unione è implementata da una sovrapposizione di formati la cui dimensione è il massimo delle dimensioni:

union nome_tipo { tipo1 flag1; … tipon flagn; };

L’informazione sul tipo del valore attuale deve essere mantenuta in una variabile distinta; il flag serve esclusivamente per l’accesso in lettura/scrittura.

typedef union {int ival; char cval;} U_int_char;

typedef enum {Ival, Cval} U_int_char_flag;

U_int_char u;

U_int_char_flag uf;

if (uf == Ival) u.ival = 0; else u.cval = `0’;

La sintassi per accedere alle componenti di una unione è la stessa usata per le strutture (punto e freccia).

(65)

Tipi ricorsivi

Un tipo è ricorsivo quando l’insieme dei sui valori sia definito induttivamente; ad esempio un albero binario con etichette intere ha valori nell’insieme:

EmptyTree ∈ BinTree,

n ∈ Int ∧ tl, tr ∈ BinTree ⇒ ConsTree(n,tl,tr) ∈ BinTree.

La sua definizione in C richiede l’uso delle stutture e dei puntatori:

typedef struct tnode *BinTree;

typedef struct tnode { int Info;

BinTree left;

BinTree right;

} Tnode;

const BinTree EmptyTree = NULL;

(66)

Allocazione di tipi ricorsivi

Nel definire l’allocatore per gli alberi binari bisogna fare attenzione che ciò che si alloca è la struttura di tipo Node, non il puntatore:

BinTree BinTreeAlloc (void)

{ BinTree bt;

bt = (BinTree) malloc(sizeof(BinTree));

/* errore */

return bt;

}

La versione corretta è:

BinTree BinTreeAlloc (void)

{ BinTree bt;

bt = (BinTree) malloc(sizeof(Tnode));

return bt;

}

In generale si deve scrivere:

variabile_puntatore = (T*) malloc (sizeof(T));

Nel caso dell’esempio, BinTree è infatti definito come Tnode*.

numero di byte per un indirizzo di memoria

numero di byte per una struttura che rappresenta un nodo dell’albero

(67)

I/O da terminale (1)

I/O formattato. Si basa sulle seguenti funzioni, definite in stdio

int printf(char stringa_di_controllo, espressioni_argomenti) stampa il valore delle espressioni argomenti utilizzando la stringa di controllo quale maschera per la formattazione. Retsituisce il numero dei caratteri scritti, oppure un numero negativo in caso di errore.

int scanf(char stringa_di_controllo, puntatori_argomenti) legge stringhe da tastiera, considerando spazi, tabultori e ENTER quali separatori; effettua la conversione secondo la stringa di controllo, e ne memorizza i valori agli indirizzi che sono valore dei puntatori argomenti.

La funzione ritorna il numero degli argomenti cui è stato assegnato un valore corretto; ritorna EOF (ossia -1) in caso d'errore.

(68)

I/O da terminale (2)

La stringa di controllo di printf e di scanf contiene, oltre a caratteri che vengono semplicemente riprodotti (solo per printf), caratteri detti specificatori, preceduti dal carattere %. I principali sono:

d numero decimale o numero ottale

x numero esadecimale

f numero in virgola mobile

e numero in mantissa ed esponente c carattere

s stringa

(69)

Esempio

#include <stdio.h>

void main(void) {

char str[80];

int i;

printf("inserisci una stringa ed un intero: ");

scanf("%s %d", str, &i);

if (i < strlen(str))

printf("%c risulta %d esimo carattere di %s\n", str[i], i, str);

}

(70)

I/O da file (1)

Si basa su due concetti:

Flusso: dispositivo logico, cratterizzato da una sequenza di informazioni bufferizzate; è di due tipi:

testo = sequenza di caratteri, binario = sequenza di byte.

File: dispositivo fisico, quale un file su disco, un terminale, una stampante ecc.

La libreria stdio definisce un tipo FILE, che consente le operazioni di apertura, lettura, scrittura e chiusura su di un file, mediante un puntatore:

FILE *fp; /* fp e' un puntatore a file */

(71)

I/O da file (2)

Ritorna un puntatore a FILE (inizio del flusso) ovvero NULL in caso di errore.

int fclose(FILE *fp);

chiude il file puntato da fp, effettuando il flushing del buffer (ossia ricopiando sul dispositivo fisico quanto fosse rimasto sul buffer).

int feof(FILE *fp);

ritorna TRUE (ossia 1) se fp punta alla fine del file, FALSE (0) altrimenti.

Inoltre sono disponibili funzioni I/O verso file di tipo formattato:

int fprintf(FILE *fp, const char *stringa_di_controllo, elenco_espressioni);

int fscanf(FILE *fp, const char *stringa_di_controllo, elenco_puntatori);

"r" Apre file di testo in lettura

"w" Apre file di testo in scrittura

"a" Apre file di testo in scrittura in coda

FILE *fopen(const char *nome_file, const char *modo);

associa un file ad un flusso, fissandone la modalità (elenco parziale):

(72)

Argomenti non trattati

In questo corso non sono stati trattati i seguenti aspetti del linguaggio C (per cui si rinvia, ad esempio, al manuale di Kernighan e Ritchie):

• primitive di basso livello

• puntatori generici

• passaggio di funzioni per puntatore

• argomenti del main nelle funzioni di linea

• il C nel sistema UNIX

Riferimenti

Documenti correlati

Deve sempre essere verificata una delle seguenti condizioni: entrambi gli operandi sono di tipo aritmetico, nel qual caso l’operando di destra viene convertito

[r]

– una variabile di tipo char può avere o meno il segno, ma in ogni caso i caratteri stampabili sono

Scrivere un metodo che, dati un array di interi a (contenente almeno due elementi), un intero v ed un intero k (k&gt;0), restituisce true se in a esistono almeno k coppie di interi,

• mette il flag di stato del nick a connesso e crea una FIFO, se non esiste già, il cui nome corrisponde al nick specificato nel messaggio, questa FIFO

» se il processo che termina ha figli in esecuzione, il processo init adotta i figli dopo la terminazione del padre (nella process structure di ogni figlio al pid del processo

Una volta definito e identificato un nuovo tipo ogni variabile può essere dichiarata di quel tipo come di ogni altro tipo già esistente:.

Una volta definito e identificato un nuovo tipo ogni variabile può essere dichiarata di quel tipo come di ogni altro tipo già esistente:..