______________
< int say_moo; >
 --------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Le variabili si possono definire come aree di memoria di dimensioni fisse dedicate a contenere dati di tipo prefissato. In C infatti a ciascuna variabile è sempre associato un tipo dati. Il contenuto di queste aree di memoria può essere modificato arbitrariamente durante l’esecuzione del programma da qui deriva il nome di variabile. Da un numero molto esiguo di tipi dati fondamentali possiamo ottenere una varietà di tipi dati derivati. I tipi fondamentali sono:

charcontiene un valore numerico che può rappresentare un carattere
intun intero la cui dimensione dipende dall’implementazione
floatnumerico a virgola mobile a singola precisione
doublenumerico a virgola mobile a doppia precisione

A questi tipi può essere aggiunto un qualificatore. Nel caso di interi ad esempio possiamo applicare short e long, risparmiando memoria o ampliando la dimensione della variabile a seconda delle esigenze:

// Integer variable definitions
   int           a;
   short int     b;
   long int      c;
   long long int d;
   unsigned int  e;

Inoltre è possibile definire un tipo dati puntatore, un’area di memoria che contiene l’indirizzo dei dati. Gli indirizzi della memoria sono dei valori numerici, pertanto anche la variabili di tipo puntatore sono da considerare delle variabili numeriche; ciò comporta che con queste variabili sono lecite le operazioni di tipo aritmetico e di confronto. La definizione di una variabile puntatore è analoga alla definizione di uno dei tipi fondamentali, a cui si aggiunge il simbolo *.

// Pointer variable definition
//   +----------------+        +-------+
// p | memory address |  --> a | value |  
//   +----------------+        +-------+
  int a;
  int *p;
  p = &a;

Una variabile puntatore è da considerare a tutti gli effetti una variabile, anch’essa è allocata in un’area di memoria ben definita con il suo indirizzo. Quest’indirizzo può memorizzata in una variabile: in C è pertanto possibile definire un puntatore a puntatore.

// Pointer to pointer variable definition
//    +---------+       +---------+        +-------+
// pp | address | --> p | address |  --> a | value |  
//    +---------+       +---------+        +-------+
  int a;
  int *p;
  int **pp;
  p  = &a;
  pp = &p;

Ciascuna variabile occupa uno spazio ben definito in memoria e che caratterizza il tipo della variabile. La memoria del computer si misura in byte, ovvero lo spazio occupato da una sequenza di otto bit che possono assumere due stati, identificati rispettivamente con 0 e 1. Le sequenze di stati possono essere utilizzate per codificare dei valori mediante codifiche standardizzate.

Il tipo dati intero

Il tipo dati intero è utilizzato per gestire numeri interi mediante una codifica che corrisponde ormai universalmente alla codifica binaria. Per rappresentare un intero di tipo int vengono occupati un numero di byte che dipende dall’implementazione. Nel caso di un intero a 4 byte, ovvero 32 bit, 31 di questi sono usati per rappresentare il valore numerico e un byte ne indica il suo segno: il primo bit del byte più rappresentativo è 0 se il numero è positivo e 1 se è negativo. Questo modo di rappresentare i numeri presenta alcuni inconvenienti in ordine alla ridondanza e alla dispendiosità dei calcoli, per cui la codifica utilizzata nella pratica è il cosiddetto “complemento a uno”[*].

Non sussiste uno standard fisso sul numero di byte che occupa ciascun tipo di dato intero; è definito solo una dimensione minima, che per gli int e gli short int è 2 byte, mentre per i long int è di 4 byte.

L’occupazione in byte si può verificare facendo seguire nelle definizioni questo frammento di codice:

   printf("int           %d\n",sizeof(a));
   printf("short int     %d\n",sizeof(b));
   printf("long int      %d\n",sizeof(c));
   printf("long long int %d\n",sizeof(d));
   printf("unsigned int  %d\n",sizeof(e));

A partire dai byte occupati si possono ricavare i valori massimi e minimi che una variabile intera può contenere. Per una variabile signed, detto N il numero di byte che occupa, i valori che può contenere sono compresi tra -2(8 N-1) e 2(8 N-1) -1. Ad esempio un intero a 4 byte, ovvero 32 bit, potrà contenere valori che vanno da -231 = -2147483648 e 231 -1 = 2147483647.

Questi valori possono essere calcolati, sono anche definiti come costanti nell’header limits.h. Il seguente è un esempio delle definizioni che possiamo trovare in questo file, dove il minimo delle variabili unsigned non è indicato poiché ovviamente è pari a 0. I valori di seguito riportati possono variare in base alla macchina e al compilatore.

#  define CHAR_BIT	8

#  define SCHAR_MIN	(-128)
#  define SCHAR_MAX	127
#  define UCHAR_MAX	255

#  ifdef __CHAR_UNSIGNED__
#   define CHAR_MIN	0
#   define CHAR_MAX	UCHAR_MAX
#  else
#   define CHAR_MIN	SCHAR_MIN
#   define CHAR_MAX	SCHAR_MAX
#  endif

#  define SHRT_MIN	(-32768)
#  define SHRT_MAX	32767

#  define USHRT_MAX	65535
#  define INT_MIN	(-INT_MAX - 1)
#  define INT_MAX	2147483647
#  define UINT_MAX	4294967295U

#  if __WORDSIZE == 64
#   define LONG_MAX	9223372036854775807L
#  else
#   define LONG_MAX	2147483647L
#  endif
#  define LONG_MIN	(-LONG_MAX - 1L)

#  if __WORDSIZE == 64
#   define ULONG_MAX	18446744073709551615UL
#  else
#   define ULONG_MAX	4294967295UL
#  endif

#  ifdef __USE_ISOC99
#   define LLONG_MAX	9223372036854775807LL
#   define LLONG_MIN	(-LLONG_MAX - 1LL)
#   define ULLONG_MAX	18446744073709551615ULL
#  endif 

Variabili a virgola mobile

Con le variabili in virgola mobile è possibile rappresentare i numeri decimali. È possibile usare due tipi dati primitivi: il float e il double.

void main()
{
    float           f;
    double          g;
    long double     h;
    printf("float       %d\n", sizeof(f));
    printf("double      %d\n", sizeof(g));
    printf("long double %d\n", sizeof(h));
}

Il tipo float è detto anche a precisione singola, tipicamente 4 byte, mentre il double è un numero a doppia precisione, ovvero occupa 8 byte. I valori numerici sono codificati (tipicamente) attraverso lo standard IEEE-754[*], ovvero dei bit disponibili alcuni si utilizzano per rappresentare la mantissa del numero e altri per l’esponente (in base 2). Comprendere il funzionamento di questa codifica numerica permette di tener conto delle problematiche ne scaturiscono nelle conversioni e nel calcolo. Anche in questo caso è possibile utilizzare il modificatore di tipo long, il cui effetto dipende dall’implementazione del compilatore.

Dichiarazione e definizione

Le variabili in C sono sempre esplicitamente dichiarate e sono sempre di un tipo dati ben definito: non è possibile definire implicitamente una variabile né modificarne il tipo. Ad ogni variabile è associata un nome, una sequenza formata da numeri e lettere nonché il carattere _ (underscore) che soddisfano alcune condizioni: ovviamente non è possibile usare come nome di variabile una parola chiave del linguaggio C (if, else, while ecc. non sono nomi leciti per una variabile). Inoltre i nomi delle variabili non possono iniziare con un numero ma solo lettere, (è possibile far iniziare una variabile con underscore). Si possono usare caratteri minuscoli o maiuscoli e il compilatore applica una logica case sensitive (le variabili a e A sono considerate differenti). Infine la lunghezza della nome è limitata, ovvero solo i primi caratteri sono significativi, pertanto due variabili i cui nomi differiscono solo per i caratteri finali potrebbero risultare indistinguibili da parte del compilatore. Molti compilatori moderni sono elastici riguardo quest’ultimo vincolo ma è bene evitare variabili con più di 31 caratteri nel nome (quelli previsti dallo standard ANSI) per questioni di retrocompatibilità.

// correct name for variables
int n;                 // alfabetic char
int N;                 // same variable in different case
int first_value;       // with underscore
int NextValue;         // capital letter
int Counter1;          // numbers
int _parameter_;       // beginning or ending with underscore
int _n_From_0_TO_2000_;// all this together

// worng name for variables
int 10_year;           // beginning with number
int first year;        // with a space inside
int while              // reserved word

La dichiarazione di una variabile permette di riservare un’area di memoria di dimensioni coerenti con il suo tipo. La sintassi per dichiarare una variabile è già stata presentata in precedenza ed è abbastanza intuitiva: si fa seguire il nome della variabile al tipo. È possibile dichiarare più variabili dello stesso tipo contemporaneamente:

// Declaration of an integer variable c
   int c;
// Declare two variables
   int x, y;

Per queste variabili saranno allocate delle aree di memoria, ma inizialmente il loro contenuto non è definito e il loro valore potrebbe essere completamente casuale. Per questo motivo prima di accedere al suo valore (mediante operazioni algebriche o per la stampa) la variabile deve essere definita, ovvero deve essere valorizzata esplicitamente. Anche se i moderni compilatori prevedono l’inizializzazione di default di tutte le variabili a 0, l’uso di una variabile non definita è da considerarsi sempre un errore!

La definizione di una variabile può avvenire al momento della dichiarazione o in qualunque punto del codice successivo alla sua dichiarazione, ma una variabile può anche essere definita in maniera indiretta tramite il proprio indirizzo.

#include <stdio.h>
void add(int m, int n, int *ps)
{
        int s;
        s = m + n;
        *ps = s;
}

void main()
{
        int a = 1;         // declare and define variable a
        int b, c;          // declare variables b and c
        b = 2;             // define variable b
        add(a, b, &c);     // define variable c
        printf("%d %d %d\n", a, b, c);
}

Le variabili dichiarate all’interno di una funzione sono dette variabili locali (anche il main è una funzione), la loro visibilità (scope) è limitata all’interno della funzione in cui sono definite e lo spazio in memoria a loro esclusivo è allocato fintanto che il loro contesto è attivo. Ciò è dovuto al fatto che la variabile è allocata nel cosiddetto stack, un’area di memoria in cui i dati sono gestiti con una logica FIFO[*]. Prendiamo in considerazione l’esempio precedente in cui sono presenti due funzioni: main e add. Nel main sono dichiarate le variabili a, b e c; la loro visibilità è limitata alla funzione in cui sono dichiarate per cui è possibile usare i loro nomi solo all’interno del main e non sono visibili in add. Lo stesso vale per le variabili dichiarate all’interno della funzione add, ad esempio la variabile s può essere usata solo all’interno di questa funzione. Si tenga presente comunque che quando viene effettuata la chiamata della funzione add il contesto della funzione main rimane nella memoria stack ed è accessibile da tutte le funzioni che eventualmente vi si sovrappongono.

                            +-----------+
                        add |  m n sp s | 
     +-----------+          +-----------+
main |  a  b  c  | --> main |  a  b  c  | 
     +-----------+          +-----------+

Nella funzione add quindi sono visibili solo le variabili m, n, sp e s, ma è possibile accedere anche alle variabili a, b e c poiché le loro corrispondenti aree di memoria rimangono allocate. Per questo motivo è possibile accedervi tramite l’indirizzo ed eventualmente anche modificarne il valore. Questo permette il passaggio per riferimento ad una funzione[*]. Si noti infine che una volta terminata l’esecuzione della funzione add il suo contesto viene deallocato dallo stack e con esso tutte le variabili locali dichiarate in questa funzione. Ciò significa che anche avendo a disposizione l’indirizzo della variabile s, all’interno del main è un errore provare ad accedervi e ancor più errato tentare di modificarne il valore! Infatti modificando il precedente codice in modo che la funzione add restituisca l’indirizzo di s, probabilmente verranno evidenziati dei warning al momento della compilazione. Durante l’esecuzione verrà segnalato un errore di segmentazione (segmentation fault) e l’esecuzione sarà interrotta: si tratta di un errore estremamente grave!

#include <stdio.h>
int *add(int m, int n, int *ps)
{
        int s;
        s = m + n;
        *ps = s;
        return &s; // It's wrong!
}
void main()
{
        int c;
        int *p;
        p = add(1, 2, &c);        // will be executed
        printf("%d %d\n", c, *p); // generate a segfault
}

Va da se che è possibile dichiarare delle variabili con lo stesso nome purché appartengano a funzioni diverse, esse saranno fisicamente distinte, il loro nome è sa considerare unicamente come un’etichetta che avrà senso solo nella funzione in cui a variabile è dichiarata. Esiste inoltre la possibilità di dichiarare delle variabili il cui comportamento risulta più articolato, ovvero le variabili globali e le variabili statiche.

Le variabili globali

Le variabili globali vengono dichiarate in maniera del tutto analoga alle variabili globali ma esternamente a qualunque funzione. In questo modo per loro viene allocata un’area di memoria non inclusa in nessun blocco dello stack, bensì in un settore denominato Data, che viene allocato all’avvio del programma e deallocato solo quando il programma termina. Per questo motivo una variabile globale è accessibile e modificabile in qualunque punto del programma.

#include <stdio.h>
int gCounter;
void Count()
{
        gCounter++;
}
void main()
{
        printf("%d\n", gCounter);
        Count();
        printf("%d\n", gCounter);
        Count();
        printf("%d\n", gCounter);
        Count();
        printf("%d\n", gCounter);
        Count();
        printf("%d\n", gCounter);
}

Nell’esempio precedente la variabile gCounter è dichiarata all’esterno di qualunque funzione, pertanto è una variabile globale, infatti è stata utilizzata sia nella funzione main che in Count, e sarebbe visibile a qualunque altra funzione definita in questo stesso file sorgente. È da notare inoltre che la variabile globale non è stato assegnato nessun valore iniziale, infatti le variabili globali sono inizializzate a zero. Assegnare loro un valore al momento della dichiarazione è comunque una buona prassi.

Dal momento che l’area di memoria dedicata a questa variabile rimane allocata per tutta la durata del programma la stessa variabile può essere usata anche in funzioni definite in un file diverso. Affinché queste funzioni possano accedere alla variabile quest’ultima dovrà essere di nuovo dichiarata anche nel nuovo file sorgente ma con la clausola extern, in questo modo non viene allocata altra memoria ma si avvisa il compilatore che si sta usando un’area di memoria dichiarata altrove.

// --------------------------------------------
// File main.c
// --------------------------------------------
#include <stdio.h>
int gCounter;
void Count();
void Reset();

void main()
{
        printf("%d\n", gCounter);
        Count();
        Count();
        Count();
        printf("%d\n", gCounter);
        Reset();
        printf("%d\n", gCounter);
}
// -END FILE-----------------------------------

// --------------------------------------------
// File counter.c
// --------------------------------------------
extern int gCounter;
void Count()
{
        gCounter++;
}

void Reset()
{
        gCounter = 0;
}
// -END FILE-----------------------------------

La compilazione separata dei due file avverrà senza errori perché le dichiarazioni della variabile si due file sono coerenti tra di loro:

$ gcc -c -o counter.o counter.c 
$ gcc -c -o main.o main.c

Ed con il linkaggio viene associata la variabile esterna a quella dichiarata.

$ gcc -o counter main.o counter.o

In caso di progetti complessi può essere utile includere in un file di header tutto ciò che va condiviso nel progetto, tra cui appunto le variabili globali, e con l’uso opportuno delle direttive di preprocessione è possibile rendere estremamente compatta la gestione delle variabili globali:

// --------------------------------------------
// File counter.h
// --------------------------------------------
#ifdef DEFINE_GLOBAL
#define EXTERN
#else
#define EXTERN extern
#endif /* DEFINE_GLOBAL */

EXTERN int gCounter;
void Count();
void Reset();
// -END FILE-----------------------------------

// --------------------------------------------
// File main.c
// --------------------------------------------
#include <stdio.h>
#define DEFINE_GLOBAL
#include "counter.h"

void main()
{
        printf("%d\n", gCounter);
        Count();
        Count();
        Count();
        printf("%d\n", gCounter);
        Reset();
        printf("%d\n", gCounter);
}
// -END FILE-----------------------------------

// --------------------------------------------
// File counter.c
// --------------------------------------------
#include "counter.h"
void Count()
{
        gCounter++;
}
void Reset()
{
        gCounter = 0;
}
// -END FILE-----------------------------------

In alcuni casi le variabili globali possono essere utili perché semplificano il passaggio dei parametri, ma è necessario limitarne l’uso alle situazioni di reale necessità ovvero solo per le variabili usate un po’ ovunque e che non influenzano la logica della funzione. È questo il caso di parametri di configurazione come il livello di log o il file pointer da utilizzare; utilizzare variabili globali che possono influire nella logica delle funzioni rende poco comprensibile il codice. Inoltre è possibile assegnare ad una variabile globale e ad una locale lo stesso nome, ma certamente non è consigliabile ancora per ragioni di comprensibilità del codice.

#include <stdio.h>
int samename = 2;
void showvalue()
{
    printf("%d\n", samename);
}

void main()
{
    int samename = 1;
    showvalue();
    printf("%d\n", samename);
}

Come si può verificare eseguendo questo programma viene considerato il valore della Variabili locale con priorità rispetto alla variabile globale; infatti l’output è il seguente:

2
1

Per cui è bene distinguere le variabili globali attraverso una precisa naming convention[*]; come è stato fatto negli esempi precedenti, si può anteporre al nome della variabile la lettera g.

Variabili statiche

Le variabili statiche hanno un comportamento simile alle variabili globali, ovvero l’area di memoria a loro dedicata si trova nel blocco Data, ovvero l’area di memoria che viene allocata all’inizio dell’esecuzione e viene deallocata solo alla fine del programma, pertanto il suo contenuto non viene perso tra una chiamata e l’altra della funzione in cui è dichiarata.

#include <stdio.h>

void Count()
{
        static int counter;     // At the biginning is 0
        counter++;
        printf("counter value = %d\n", counter);
}

void main()
{
        // first increment
        Count();                // now his value is 1
        Count();                // now his value is 2
        Count();                // now his value is 3
}

Come per la variabile globale non è necessario inizializzarla ma viene posta a zero quando per essa viene allocata memoria, ma a differenza la variabile statica è visibile solo all’interno della funzione dove è dichiarata; per essa è comunque riservata un’area di memoria unica e ben definita e il suo valore si conserva tra una chiamata e l’altra della funzione. L’output di questo codice è il seguente:

counter value = 1
counter value = 2
counter value = 3

Variabili costanti

Aggiungendo alla dichiarazione di una qualsiasi variabili il qualificatore const specifica che la variabile non verrà modificata: il seguente codice quindi non è valido

const int ZERO = 0;
ZERO = 1;

Nel caso si provi a modificare il valore di questa variabile il risultato è tipicamente un messaggio di errore in fase di compilazione (anche se il comportamento dipende dalla particolare implementazione).

Si può definire come costante anche un array, in questo caso ogni componente dell’array sarà considerato in sola lettura e il seguente codice comporta un errore in fase di compilazione

const char s[] = "Constant";
s[0]='c';