Docsity
Docsity

Prepara i tuoi esami
Prepara i tuoi esami

Studia grazie alle numerose risorse presenti su Docsity


Ottieni i punti per scaricare
Ottieni i punti per scaricare

Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium


Guide e consigli
Guide e consigli


I puntatori (Parte 2), Dispense di Fondamenti di informatica

Continuazione della spiegazione sui puntatori nel linguaggio C

Tipologia: Dispense

2018/2019

Caricato il 08/03/2019

aaron.
aaron. 🇮🇹

8 documenti

1 / 34

Toggle sidebar

Questa pagina non è visibile nell’anteprima

Non perderti parti importanti!

bg1
Dispense del corso di
Fondamenti di Informatica 1
Il linguaggio C - Puntatori 2
Aggiornamento del 24/10/2018
pf3
pf4
pf5
pf8
pf9
pfa
pfd
pfe
pff
pf12
pf13
pf14
pf15
pf16
pf17
pf18
pf19
pf1a
pf1b
pf1c
pf1d
pf1e
pf1f
pf20
pf21
pf22

Anteprima parziale del testo

Scarica I puntatori (Parte 2) e più Dispense in PDF di Fondamenti di informatica solo su Docsity!

Dispense del corso di

Fondamenti di Informatica 1

Il linguaggio C - Puntatori 2

Aggiornamento del 24/10/

Definire nuovi tipi di dato

  • In C è possibile definire variabili con tipi di dato composti di diverse parti, ad esempio: unsigned char *p;
  • La variabile p è un puntatore a intero a 8 bit senza segno.
  • A volte la cosa può risultare prolissa e poco chiara, perché si dice il tipo, ma perdiamo il significato che volevamo dare a quella cosa. Per esempio, p potrebbe essere un puntatore ad una variabile che contiene il numero di anni di una persona.
  • In C è possibile definire un nuovo nome per aggiungere semantica ai tipi di dato. La sintassi è analoga a quella di definizione delle variabili: typedef < tipo-di-dato > < nome-del-nuovo-tipo >;
  • Ad esempio potremmo definire un nuovo tipo «anni» che diventi un sinonimo di unsigned char: typedef unsigned char anni ;
  • La definizione precedente diventa allora: anni *p;

La standard library del C

  • Il C, oltre a tutti i suoi operatori, mette a disposizione una collezione di funzioni, macro e definizioni di tipi che permettono di semplificare il lavoro del programmatore che non deve reimplementare da solo funzioni basilari come quelle matematiche, quelle per l’input/output, la manipolazione di stringhe, ecc.
  • Tutto ciò che viene messo a disposizione dalla standard library del C è accessibile attraverso l’inclusione di opportuni file .h nei file del proprio programma: #include <...>
  • In questo caso utilizzeremo < e > in quanto i file da includere non si troveranno nella cartella corrente ma tra le librerie del compilatore.
  • Al momento la libreria standard mette a disposizione 29 file .h
  • Di seguito vedremo alcuni dei più importanti

La libreria stdint.h

  • Contiene le definizioni dei tipi interi a larghezza fissa, signed e unsigned e i rispettivi valori minimi e massimi.
  • I tipi in essa contenuti esprimono chiaramente la dimensione in bit di una variabile e la presenza del segno, ad esempio: uint N _t a; dichiara un intero senza segno grande esattamente N bit. int N _t a; dichiara un intero con segno grande esattamente N bit.
  • Vengono definite anche le macro che vengono sostituite dal preprocessore con i valori massimi e minimi per ogni tipo: INT N _MIN → il minimo valore rappresentabile con N bit con segno UINT N _MAX → il massimo valore rappresentabile con N bit senza segno
  • I valori di N per cui sono stati definiti i tipi e le relative macro sono 8, 16, 32 e 64.

La libreria stdlib.h

  • stdlib.h contiene anche la definizione di un tipo di dato particolare chiamato size_t
  • size_t è un tipo di dato intero senza segno utilizzato per rappresentare le dimensioni di un oggetto nell’implementazione che si sta usando. Essendo « implementation defined » la sua dimensione in memoria può variare ma è di almeno 16 bit. È il tipo che viene comunemente utilizzato quando si deve dichiarare una variabile che contiene la dimensione di un oggetto.
  • Con implementation defined si intende che lo standard specifica solamente «cosa» fa una funzione o un tipo, lasciando il compito di decidere «come» farlo a chi realizzerà il compilatore per una specifica architettura.
  • In Visual Studio quando si compila a 32 bit (default) size_t è un unsigned int, quando si compila a 64 bit size_t è un unsigned long long.

La libreria stdbool.h

  • Il C dalla sua revisione C99 supporta il tipo fondamentale booleano _Bool, una variabile di questo tipo è in grado di memorizzare valori interi 0 e 1. In una variabile di tipo _Bool viene memorizzato il valore 0 quando le si assegna uno 0, altrimenti quando le si assegna qualsiasi altro valore nella variabile viene memorizzato un 1.
  • La libreria stdbool.h definisce principalmente tre macro:
    • bool
    • true
    • false
  • Esse vengono espanse rispettivamente a:
    • _Bool
    • 1
    • 0
  • Con esse è possibile dichiarare e utilizzare variabili di tipo booleano come se fosse un tipo fondamentale, ad esempio scrivendo: bool var; bool var = true;

L’operatore sizeof

  • L’operatore sizeof ritorna la dimensione in byte di un tipo o di un’espressione in una variabile di tipo size_t: - sizeof( type ) - sizeof expression
  • Ad esempio: size_t res; float a = 3.2f, b = 2.1f; char c; res = sizeof a; // res = 4 res = sizeof c; // res = 1 res = sizeof 'c'; // res = 4 res = sizeof(255 + 10); // res = 4 res = sizeof(a * b); // res = 4 res = sizeof(3.4 + 21); // res = 8 res = sizeof(&res); // res = 4 (compilando a 32 bit) res = sizeof(char); // res = 1 res = sizeof(short); // res = 2 res = sizeof(int); // res = 4 res = sizeof(double); // res = 8
  • ATTENZIONE: l’operatore sizeof ha precedenza più alta rispetto agli operatori aritmetici, quindi: res = sizeof 2u + 3u; // res = 7!!

Accesso in memoria con i puntatori

  • In ADE8 abbiamo introdotto i puntatori per consentire ai programmi di accedere a più dati consecutivi in memoria.
  • Per ora invece i puntatori sono stati usati unicamente per l’accesso indiretto a singole variabili, con lo scopo di modificare da una funzione una variabile esterna.
  • Quello che dobbiamo fare è ora utilizzare operazioni aritmetiche sui puntatori (incremento e decremento) e vedere come definire più variabili in memoria a indirizzi consecutivi.
  • A differenza di quanto visto in ADE8 però le variabili non sono tutte grandi uguali e quindi «andare all’elemento successivo» non è necessariamente «incrementare di 1 il puntatore».

Aritmetica dei puntatori

  • In generale, date le variabili: < tipo > a = 3, *pa = &a;
  • Sono valide le seguenti espressioni: pa + < int-expr > pa - < int-expr >
  • Le quali, rispettivamente, aggiungono o sottraggono implicitamente al valore del puntatore il valore di: * < int-expr >
  • Il tipo di queste espressioni sarà sempre < tipo >*, quindi potranno essere assegnate solamente a variabili puntatore a tipo!

Aritmetica dei puntatori

  • Esiste anche un altro tipo di espressione valida, la sottrazione tra due puntatori dello stesso tipo: < tipo > *pa, *pb; ptrdiff_t n = pa – pb;
  • Il risultato è memorizzato in una variabile di tipo ptrdiff_t, un tipo intero con segno definito nella standard library del C che serve proprio a rappresentare il risultato di differenze tra puntatori.
  • In questo caso il valore memorizzato in n sarà: (pa – pb) /
  • Questo serve per ottenere la «distanza» tra due puntatori in numero di elementi e non in byte.

Puntatori a void

extern void* malloc(size_t size);

  • La malloc ritorna un puntatore a void … cosa significa?
  • Un puntatore a void indica un puntatore senza un tipo associato, ovvero un indirizzo a cui manca l’informazione sul tipo di dato a cui si sta puntando.
  • Questo è un puntatore speciale, nel senso che non è direttamente utilizzabile: - non si può dereferenziare (quanti byte è grande il dato puntato?) - l’aritmetica dei puntatori non è ammessa su di esso (di quanto incremento?)
  • Come posso fare quindi a utilizzarlo per accedere alla memoria appena allocata?
  • Prima di poter essere utilizzato deve essere convertito in un puntatore a un tipo specifico.
  • In C, un puntatore a void può essere assegnato a qualsiasi puntatore e, viceversa, qualsiasi puntatore può essere assegnato ad una variabile di tipo void*.

Puntatori a void

  • La conversione avviene implicitamente quando un puntatore a void viene assegnato a un puntatore a un altro tipo, ad esempio: void *vp = malloc(12); int *ip = vp;
  • Si potrà quindi scrivere direttamente: int *ip = malloc(12);
  • La conversione può anche essere resa esplicita attraverso un cast al tipo che dobbiamo ottenere: int ip = (int) malloc(12); anche se questo non è mai necessario. Non scrivetelo.
  • La malloc() utilizza void* come tipo di ritorno perché deve essere in grado di allocare memoria per qualsiasi tipo di dato, l’unica cosa che deve sapere per allocare memoria è il numero di byte necessari per contenere un certo tipo di dato.

Chiedere memoria al Sistema Operativo

  • La memoria che viene allocata in questo modo deve essere liberata quando non serve più. Questo viene fatto chiamando la funzione free() : void free(void* ptr);
  • La funzione free comunica al sistema operativo che la memoria puntata da ptr è da deallocare. Se ptr è NULL, la funzione free non fa nulla.
  • Bisogna fare molta attenzione a non utilizzare più i puntatori la cui memoria puntata è stata liberata con una free, l’accesso alla memoria puntata da un puntatore dopo che esso è stato passato alla free genera un undefined behavior.
  • Un puntatore che non si riferisce più a un indirizzo valido è detto dangling pointer , in italiano «puntatore penzolante», ma nessuno lo chiama così.
  • Un undefined behavoir viene generato anche in altri due casi:
    • Quando alla free viene passato un puntatore che è già stato liberato in precedenza
    • Quando alla free viene passato un puntatore che non si riferisce a un’area di memoria allocata dinamicamente in precedenza.

Chiedere memoria al Sistema Operativo

  • Quando si utilizza memoria allocata dinamicamente è molto importante ricordarsi di liberarla quando non è più necessaria, altrimenti si può incorrere in un memory leak.
  • Consideriamo la seguente funzione: int func(void) { int *p = malloc(1000 * sizeof(int)); // operazioni sulle celle puntate da p return *p; // ritorno il primo int puntato da p }
  • In questo caso i 4000 byte allocati non vengono liberati e quando la funzione ritorna al chiamante la memoria che conteneva il puntatore p viene liberata e con essa anche ogni possibilità di poter puntare nuovamente a quei 4000 byte. Ogni riferimento ad essi è perso per sempre. Quindi non posso nemmeno farne la free().
  • Se immaginiamo che la funzione func venga chiamata 100.000 volte durante l’esecuzione del programma finiremmo per avere 400.000. byte (circa 400MB) allocati per il nostro programma senza avere più la possibilità di liberarli.