Linguaggio C - Tipi di dato

Generalita'

I tipi del C si possono dividere in due classi principali:
o Tipi fondamentali
 
o char
o short, short int, int, long, long int,
o float, double, long double,
o void
o enum
o Tipi derivati o strutturati
 
o array o vettori
o struct
o union
o puntatori

Inoltre e' anche possibile combinare fra loro i tipi derivati creando delle dichiarazioni complesse che danno origine ai tipi derivati complessi (o tipi strutturati complessi)

Infine e' possibile dichiarare nuovi tipi tramite typedef:

typedef tipo nuovoIdentificatoreTipo;


Tipi fondamentali

o char
 
  Occupano generalmente 1 byte.
Sono usati tipicamente per rappresentare un carattere ASCII o un byte di memoria.
Piu' raramente vengono impiegati per la rappresentazione di piccoli interi con o senza segno; infatti, ad eccezione di qualche caso particolare (come ad esempio la scrittura del codice di un 'kernel' per un sistema operativo), oggigiorno, a differenza di pochi anni fa, non e' piu' necessario spingere all'esasperazione il risparmio di byte, visto le enormi risorse disponibili dei sistemi attuali, sia in termini di memoria fisica che di memoria di massa (capacita' del disco rigido dell'ordine della decina di GB).
il tipo char per default e' equivalente a signed char.
In molti compilatori esiste un'opzione per mezzo della quale si puo' cambiare il default a unsigned char.
In ogni caso il tipo puo' essere specificato come:
[ signed | unsigned ] char
Solamente con char, il tipo risulta essere con o senza segno compatibilmente con l'opzione specificata durante la compilazione.
Nel caso che il tipo venga specificato come signed char o unsigned char, questo prevale sul default assunto durante la compilazione.
Normalmente i valori ammessi sono:
-128..0..127 per signed char
0..255 per unsigned char

o int
 
  Occupano generalmente 2 o 4 byte a secondo dell'implementazione del compilatore e dell'architettura del sistema (16 o 32 bit).
Rappresentazione tipica per numeri interi con o senza segno.
Si possono impiegare i termini short o long per forzare l'intero alla minima o massima dimensione ammessa.
Possono essere utilizzate le parole chiave signed e unsigned per specificare se il tipo sia dotato o no di segno.
Per default i numeri interi sono con segno.
Sintassi ammessa:
[ signed | unsigned ] int
[ signed | unsigned ] short [ int ]
[ signed | unsigned ] long [ int ]
In ogni caso il compilatore deve garantire la relazione:
sizeof(short) <= sizeof(int) <= sizeof(long)

o float e double
 
  Generalmente, i float occupano 4 byte di memoria, mentre i double ne occupano 8.
Il loro impiego e' per la rappresentazione di numeri reali in singola precisione (float) o in doppia precisione (double).
Molti compilatori supportano anche il tipo long double per la rappresentazione di un numero reale in quadrupla precisione.

o void
 
  Questo tipo specifica un insieme vuoto di valori.
Viene utilizzato nella dichiarazione e definizione di funzioni come le funzioni che non ritornano valori (equivalenti alle procedure del Pascal), oppure che non accettano parametri. Altro impiego frequente e' nel dichiarare un puntatore generico, senza cioe' specificare il tipo a cui esso punta.

o enum
 
  Le variabili di tipo enum sono di un unico tipo ed ammettono valori interi.
Ad ogni enumerazione e' associato un insieme di nomi costanti.
Sintassi:
enum {listaNomiCostanti}


Tipi derivati o strutturati

o Array
 
  Un array viene definito con la sintassi
tipo identificatore[numero_elementi];
Es.: int nome_array[10]; /* array di 10 interi di nome nome_array */
Le variabili array possono essere inizializzate indicando una lista di costanti fra parentesi graffe. In tal caso e' possibile omettere il numero degli elementi dell'array. Cosi' facendo si indica al compilatore che la dimensione dell'array e' uguale al numero degli elementi inizializzati.
E' invece possibile inizializzare un numero di elementi inferiore al numero di elementi definiti all'array.
Esempi:
int a1[4]={10, 20, 30, 40}; /* definisco ed inizializzo un array di 4 interi */
int a2[]={10, 20, 30, 40}; /* equivalente alla definizione di a1 */
int a3[30]={10, 20, 30, 40}; /* definisco un'array di 30 interi, ma ne inizializzo solamente 4 */
char s1[5]={'C', 'i', 'a', 'o', '\0'}; /* definisco una stringa costruita con un array di 5 caratteri ed inizializzata con "Ciao" */
char s2[]= "Ciao"; /* stessa cosa di s1 */
Per potere accedere ad un elemento dell'array, si utilizza un indice variabile da 0 (per riferirsi al primo elemento) alla dimensione-1 (per riferirsi all'ultimo elemento).
Esempio:
 ...              
 int a[10];      /* definizione di un array di 10 interi */
 int j;          /* variabile intera */
 ...
 a[0] = 100;     /* assegnamento al primo elemento di a */
 ...
 a[3] = 400;     /* assegnamento al quarto elemento di a */
 ...
 a[9] = 1000;    /* assegnamento al decimo elemento di a */
 ...
 j = 9;
 a[j] = 0;       /* azzeramento del decimo elemento di a */
 ...
 j = 10;
 a[j] = 30;      /* ERRORE GRAVE - undicesimo elemento !! */
 a[10] = 30;     /* ERRORE GRAVE - undicesimo elemento !! */
                  

E' anche possibile definire array multidimensionali.
La memorizzazione avviene per righe, corrispondenti al primo indice.
Elementi contigui in memoria sono individuati dalla variazione piu' veloce dell'ultimo indice.
Per definire l'array multidimensionale si indicano le varie dimensioni fra parentesi quadre, una di seguito all'altra:

tipo identificatore [dimensione1] [dimensione2] ...;
Es.: int nome_array [10] [2]; /* array di interi di nome nome_array, formato da 10 righe e 2 colonne */
Per inizializzare un array multidimensionale, si nidificano le inizializzazioni all'interno delle parentesi graffe; le inizializzazioni avvengono per righe.
In fase di definizione dell'array e' anche possibile non indicare la prima dimensione, lasciando il compito al compilatore di ricavare il numero delle righe in base alle inizializzazioni effettuate. Le altre dimensioni devono essere specificate.
Esempi:
int a1 [3] [4]={ { 10, 20, 30, 40}, /* prima riga */
{ 50, 60, 70, 80}, /* seconda riga */
{ 90, 100, 110, 120} }; /* terza riga */
int a1 [][4]={ { 10, 20, 30, 40}, /* prima riga */
{ 50, 60, 70, 80}, /* seconda riga */
{ 90, 100, 110, 120} }; /* terza riga */
Anche in questa situazione gli indici partono da 0 (zero). Pertanto a1[0][0] accede alla prima riga e prima colonna di a1.

o struct
 
  E' possibile dichiarare e definire una struttura di dati correlati fra loro con il costrutto struct:
struct nomeStruttura { listaDichiarazioneCampi } [nomeVarStruttura1] [, nomeVarStruttura2] ... ;
struct { listaDichiarazioneCampi } nomeVarStruttura1 [, nomeVarStruttura2] ... ;
Per accedere ai vari campi della struttura si impiega l'operatore . (punto).
Una struttura puo' essere inizializzata direttamente in fase di definizione, fornendo fra parentesi graffe la lista dei valori che si intende assegnare ai vari campi.
Esempio:
  struct data {                         /* dichiarazione struttura data */
              int gg;                   /* giorno */
              int mm;                   /* mese */
              int aa;                   /*anno */
              };
  ...
  struct data data1, data2;             /* definizione delle variabili */
  struct data data3 = {1, 1, 2000};
  ...
  data1.gg = data3.gg;                  /* copio i singoli campi */
  data1.mm = data3.mm;
  data1.aa = 2001;
  ...
  data2 = data3;                        /* assegnamenti fra strutture */
  ...
                  
Nella listaDichiarazioneCampi e' possibile dichiarare dei campi di bit o bit-field o field. Essi consentono di suddividere una word in piu' campi di ampiezze pari al numero di bit specificati. Il principale uso dei campi di bit e' nel dichiarare di un insieme di flag.
La dichiarazione dei campi di bit, consiste nel descrivere il tipo che deve essere int e che per motivi di portabilita' deve essere specificato se unsigned o signed, il nomeDelCampo (elemento opzionale) ed infine l'ampiezza preceduta dal carattere ':' conformemente alla sintassi seguente:
unsigned|signed int [identificatore] :ampiezza;
Se identificatore e' omesso, allora viene riservata memoria pari ad ampiezza senza che questa sia riferibile ad un campo. Questa e' la tecnica impiegata per creare dei buchi fra campi di bit.
L'ampiezza nulla forza un allineamento alla word successiva.
N.B. - L'ordine dei campi di bit specificati dal programmatore e la corrispondenza con la rappresentazione interna dell'elaboratore dipende fortemente dalla implementazione, pertanto i campi di bit sono poco adatti a comunicare i dati con il mondo esterno. Vengono invece impiegati per la rappresentazione interna di dati.
N.B. - Ai campi di bit non e' applicabile l'operatore indirizzo & poiche', per definizione, non possiedono alcun indirizzo.
Esempio di dichiarazione di una struttura con campi di bit:
  struct esempio
    {
    unsigned int  field1   :1;
    unsigned int  field2   :1;
    unsigned int  field3   :1;
    unsigned int           :1;
    unsigned int  field4   :3;
    unsigned int           :0;
    };
                  

o union
 
  Il C permette a piu' campi di condivedere la stessa area di memoria, purche' un solo campo alla volta possa presentare valori significativi. Le informazioni possono essere cosi' strutturate per mezzo del tag union con una sintassi simile a quella utilizzata per la struct.
union nomeUnion { listaDichiarazioneCampi } [nomeVarUnion1] [, nomeVarUnion2] ... ;
union { listaDichiarazioneCampi } nomeVarUnion1 [, nomeVarUnion2] ... ;
La dimensione in memoria della union coincide con la dimensione del campo che occupa piu' spazio in memoria.
La union puo' venire inizializzata solamente fornendo il valore del tipo del suo primo campo.
Analogamente alla struct, per accedere ai campi della union, viene impiegato l'operatore . (punto).
E' responsabilita' del programmatore accedere al campo della union che presenta il valore significativo.
Esempio:
  ...
  enum {MM, IN} metrica;        /* metrica in uso (millimetri o inch) */
  ...
  union misura {
    long   mm;                  /* millimetri */
    double in;                  /* inch */
    };
  ...
  union misura mis;
  double in;
  ...
  metrica=MM;                   /* uso i mm per specificare le misure */
  mis.mm=1000;                  /* 1 metro */
  in=mis.in;                    /* ERRORE GRAVE !! */
                  
Nell'esempio si e' definita una union che possa contenere o una misura in millimetri di tipo long, oppure una misura in pollici di tipo double. In seguito e' stata impostata la metrica in millimetri ed e' stato assegnato un valore al membro mm della union. L'errore consiste nell'accedere al membro in che, condividendo l'area di memoria con il membro mm, non puo' presentare alcun valore significativo.

o Puntatori
 
  Il puntatore ad un tipo e' definito tramite la sintassi:
tipo *varPuntatore;
Es.: int *ptrInt; /* definisce prtInt come puntatore a intero */
Il puntatore contiene l'indirizzo di memoria dell'area a cui punta.
Per allocare dinamicamente memoria dall'heap, usualmente viene impiegata la funzione di libreria malloc(), la quale accetta come parametro la quantita' di memoria da allocare. In caso di successo viene tornato l'indirizzo dell'area allocata. In caso di fallimento (es. memoria insufficiente) la malloc() torna il valore NULL.
L'indirizzo tornato dalla malloc() deve venire assegnato alla variabile puntatore per potere accedere alla memoria allocata.
L'accesso alla memoria tramite il puntatore viene effettuato per mezzo dell'operatore di indirezione * (asterisco). L'operazione di accesso alla memoria e' detta di dereferencing.
Per liberare nuovamente la memoria precedentemente allocata, viene chiamata la funzione free() della libreria standard, passando come argomento il valore del puntatore dell'area di memoria da deallocare.
Un puntatore inizializzato a NULL, per definizione non punta ad un indirizzo di memoria valido.


Indice-C Indice linguaggio C
At Home Umberto Zappi Home Page