Guida di C (base)

Le variabili

Nell'ultimo articolo si è visto come la funzione printf() sia in grado di manipolare numeri in modo particolarmente potente. In particolare si sono visti degli esempi con numeri definiti nel testo (1, 2, 3...); tali numeri sono detti costanti.

Si può intuire però che continuare a stampare un numero sempre uguale non è poi così entusiasmante: quello che tenteremo ora di fare è dare un nome ad un numero, in modo che esso possa essere riconosciuto e modificato dal programma in corso di esecuzione. Per capire il significato di ciò può essere utile servirsi di un esempio:

#include <stdio.h>

main()
{
	int anno;

	anno = 2009;
	printf("Siamo nell'anno %d...\n",anno);

	anno = 2010;
	printf("...l'anno prossimo sarà  il %d!\n",anno);
}

Salta subito all'occhio come, all'interno dell'istruzione printf(), la specifica di stampa %d (che, ricordiamo, identifica i numeri interi), non sia relativa ad un vero e proprio numero, quanto, piuttosto, al nome della variabile che lo contiene (anno). Si osserva anche che, nel corso del programma, la stessa variabile assume valori differenti (righe 7 e 10).

Il risultato, prevedibilmente, sarà  che printf() andrà  a stampare per due volte il valore della variabile anno; ciò che il programma stamperà  dipenderà  dal contenuto di anno nel momento in cui è letto il suo valore:

[dario@localhost c]$ ./a.out 
Siamo nell'anno 2009...
...l'anno prossimo sarà  il 2010!

Cosa sono

Proviamo ora a definire con più precisione cosa effettivamente sia una variabile. (Per quanti lo stessero pensando: sì, questa è proprio quella sezione che-si-può-anche-non-leggere... Consiglio di farlo con la stessa libertà  con cui ci si può tirare una bastonata sui denti.)

Premetto, per chi non avesse le idee chiare sull'argomento, che la memoria RAM (Random Access Memory) in un computer è uno spazio addetto a contenere i dati temporanei che i programmi generano nel corso della loro esecuzione. I dati possono essere sovrascritti quando non sono più utilizzati e, in ogni caso, possono rimanere in memoria al più fino a che il computer non viene spento; per questo la RAM è detta memoria volatile.

Detto questo, è facile capire come una variabile sia semplicemente un posto nella memoria, a cui è dato un nome, ed in cui è contenuto un valore numerico. Quando è necessario, un programma può chiedere al sistema di riservargli uno spazio di memoria: è così che si crea una variabile.

Questo tuttavia non è del tutto sufficiente a definire cosa sia una variabile: non sappiamo, ad esempio, come faccia il programma, partendo dal nome, a risalire al suo valore, nè sappiamo quanto spazio occupa in memoria...

Possiamo chiarire brevemente questi aspetti spiegando tre proprietà  di una variabile, tipo, valore ed indirizzo:

  • A seconda dei dati che devono contenere, le variabili si distinguono per tipo. Una variabile di tipo 'int' conterrà  numeri interi, una 'char' conterrà  caratteri e così via...
    C'è tuttavia da fare un'osservazione: tutti sappiamo che i computers lavorano con i soli valori 1 e 0, e naturalmente la memoria non sfugge a questa regola; essa può essere vista come una sterminata distesa di caselline contenenti o 0 o 1, per cui qualsiasi spazio si possa prendere, esso sarà  sempre una successione di zeri ed uni.
    Ma allora cosa distingue un tipo di variabile dall'altro, se tutti in fondo sono riducibili ad una successione di zeri ed uni? Facile: 1) Il modo in cui il calcolatore li interpreta (non approfondiamo, sennò non la finiamo più...). 2) La dimensione dello spazio occupato. Già , perchè, in C, ogni variabile ha a disposizione un certo spazio (ad esempio una variabile int ha a disposizione 4 byte di memoria), oltre il quale non può estendersi. Questo vuole forse dire che le variabili numeriche possono contenere solo numeri più piccoli di un tot? Esattamente, ma lo vedremo meglio in seguito...
  • Naturalmente poi segue il valore, ovvero il contenuto vero e proprio dello spazio di memoria. Quando assegno un valore ad una variabile (anno = 2009;) non faccio altro che "riempire" lo spazio di anno con il valore numerico 1999.
  • Un sottile, ma importante, discorso va poi fatto sull'indirizzo. Bisogna dire infatti che, dietro al nome di una variabile, si cela in realtà  un codice (un numero, tanto per cambiare...) che il programma usa per cercare nella memoria il valore corrispondente. Infatti, anche se sembra impossibile, tutte le "caselline" (siamo nell'ordine dei miliardi) che compongono la memoria RAM di un calcolatore sono numerate (tipicamente a gruppi di 8): per questo motivo la RAM si dice una memoria ad accesso posizionale.

 

Ora, dopo le dovute premesse teoriche, si può passare alla parte pratica, imparando come utilizzare le variabili all'interno dei programmi.

Come si usano

Come si è visto, prima di poter utilizzare una variabile, è necessario in qualche modo comunicare al programma che deve "occupare" lo spazio di memoria necessario a contenere il suo valore. Questa operazione è detta dichiarazione della variabile:

Dichiarazione

	int anno;

La sintassi non è complessa: a sinistra va scritto il tipo di variabile da dichiarare (nel nostro caso int, un numero intero), in modo che il programma sappia quanto spazio richiedere, e come interpretare e visualizzare i dati contenuti in quello spazio. A destra (anno) invece si andrà  a scrivere il nome con cui, da qui alla fine del programma, il programmatore si riferirà  alla variabile.

Ma cosa succederebbe se omettessi la dichiarazione? Ecco come risponde il gcc, se provo a compilare il programma iniziale, dopo avervi eliminato la riga 5 (int anno;):

[dario@localhost c]$ gcc variabili.c
variabili.c: In function ‘main’:
variabili.c:7: error: ‘anno’ undeclared (first use in this function)
variabili.c:7: error: (Each undeclared identifier is reported only once
variabili.c:7: error: for each function it appears in.)
[dario@localhost c]$ 

Come previsto: gcc non riesce a compilare il programma, perchè trova, alla riga 7, una variabile 'anno' che non è stata precedentemente dichiarata.

Convinti della necessità  di dichiarare una variabile prima del suo utilizzo, passiamo oltre. Viene quindi spontaneo il desiderio di riempire la variabile appena definita con un valore; questa operazione è detta assegnamento:

Assegnamento

	anno = 2009;

In questo modo, come si è già  detto prima, viene inserito il numero 2009 all'interno dello spazio di nome anno, che è stato precedentemente riservato con la dichiarazione.

Vedremo tra qualche paragrafo che, come avviene nella maggior parte dei linguaggi, anche nel C ogni espressione ha un suo valore numerico. Alle assegnazioni è attribuito il valore del numero assegnato, quindi, ad esempio, l'espressione anno = 2009; ha, da un lato, l'effetto di assegnare '2009' alla variabile anno, e, dall'altro, il valore intrinseco del numero 2009 (vedi: "Operare con gli interi").

Come è molto frequente nel C, anche in questo caso è consentito operare dichiarazioni ed assegnamenti multipli utilizzando delle forme contratte, ed in particolare è possibile:

  • Dichiarare più variabili allo stesso tempo, es. int a,b,c;.
  • Assegnare lo stesso valore a più variabili contemporaneamente, es. a = b = c = 7; (naturalmente le variabili in questione devono già  essere state dichiarate).
  • àˆ inoltre possibile unire in un'unica istruzione le operazioni di dichiarazione ed assegnamento, es. int a = 3;. Queste due operazioni assieme costituiscono quella che è detta inizializzazione di una variabile.

Operare con gli interi

Il C è in grado di gestire tutte le principali operazioni tra numeri:

  • +: addizione
  • -: sottrazione
  • *: moltiplicazione
  • /: divisione (occhio! Se si opera con gli interi mancherà  la parte decimale, quindi, ad esempio, 3/2 = 1!)
  • %: resto della divisione intera (es. 5/2 = 2 con resto di 1, quindi 5%2 = 1).

Una variabile int può essere utilizzata nel codice esattamente come un qualsiasi numero intero. Non c'è differenza infatti tra costanti, interi e, addirittura, espressioni; quello che conta è il valore di questi elementi.

Mi spiego meglio, se io scrivo 2, scrivo un'espressione numerica che ha valore '2'. Se scrivo 1+1 scrivo comunque un'espressione numerica che ha valore '2'. Parimenti se scrivo (2+2)/2 sto comunque producendo un'espressione numerica di valore '2'. Anche se scrivo a = 2 produco un'espressione di valore '2', ma, in più, ottengo l'effetto che alla variabile a sarà  stato assegnato il valore '2'.

Chiariamo definitivamente questo discorso con un esempio:

#include <stdio.h>

main() {
	int a = 5;                                  /* Inizializzazione: 'a' vale 5*/

	printf("2 + 2 fa %d \n",2+2);               /* stampa: 2 + 2 fa 4 */
	printf("a/2 fa %d, resto %d \n",a/2,a%2);   /* stampa: a/2 fa 2, resto 1 */

	printf("a vale ancora %d \n",a);            /* stampa: a vale ancora 5 */
	printf("Assegnamento: a = %d \n",a=7);      /* stampa: Assegnamento: a = 7 */

	printf("Ora a vale %d \n",a);               /* stampa: Ora a vale 7 */
}
[dario@localhost c]$ ./a.out 
2 + 2 fa 4 
a/2 fa 2, resto 1 
a vale ancora 5 
Assegnamento: a = 7 
Ora a vale 7 
[dario@localhost c]$ 

Osserviamo che, alla riga 10, printf() usa l'argomento (a=7) per stampare il suo valore ('7'). Poichè l'espressione è un assegnamento, questo ha anche l'effetto di assegnare il valore '7' ad a.

[CustomTags Error]: Tag - tag resource not found.

I linguaggi eleganti ed intelligenti, come il C, spesso prevedono delle scorciatoie per velocizzare le operazioni più comuni.

Operatori speciali di assegnazione

Un esempio? Ecco come utilizzare un operatore speciale per dimezzare il valore di una variabile a, senza utilizzare la normale scrittura a = a/2:

	a /= 2;			/* a = a/2; */

Naturalmente questo vale anche per tutti gli altri operatori:

	a += 5;			/* a = a + 5; */
	a *= 4;			/* a = a * 4; */
	/* ... */

Pre-incremento e post-incremento

Una delle operazioni più frequenti (molto più di quanto si possa immaginare) in programmazione, è quella di incrementare di 1 una variabile:

	a = a + 1;

Come abbiamo appena visto, ciò si può abbreviare con la scrittura a += 1, ma ad un programmatore C questo non basta... Vediamo alcuni modi squisitamente stringati, efficienti e compatti per ottenere il medesimo risultato:

	a++;			/* a += 1; */
	++a;			/* a += 1; */

	a--;			/* a -= 1; */
	--a;			/* a -= 1; */

Molto intuitivo: le prime due righe utilizzano l'operatore ++ per sommare 1 alla variabile a, mentre, alle ultime due, l'operatore -- è utilizzato per sottrarle 1.

La domanda però sorge spontanea: che differenza c'è tra a++ e ++a, se tutte e due fanno la stessa identica cosa?

La differenza sta nel fatto che, nel primo caso, la variabile verrà  incrementata dopo essere stata usata:

	a = 2;
	b = a++;
	/* ora 'a' vale 3...
	 * ... e 'b' vale 2
	 * perchè 'a' è stata incrementata *dopo* il suo utilizzo nell'espressione 'b = a++' */

Mentre, nel secondo caso, la variabile viene incrementata prima di essere usata in un'eventuale espressione.

	a = 2;
	b = ++a;
	/* ora 'a' vale 3...
	 * ... ed anche 'b' vale 3
	 * perchè 'a' è stata incrementata *prima* del suo utilizzo nell'espressione 'b = ++a' */

Limiti e Overflow

Avevamo già  detto che una variabile è uno spazio che il programma si riserva nella memoria RAM. Ogni tipo di variabile occupa un certo spazio in memoria, ovvero un certo numero di 'caselline', che ormai possiamo chiamare bit (1 byte = 8 bit).

Lo spazio occupato da una variabile int non è definito negli standard del C, ma è a scelta del compilatore. Tipicamente un intero utilizza 4 byte (32 bit), circa un miliardesimo della memoria totale di un moderno calcolatore.

Questo però vuol dire che, per forza, i numeri interi che possiamo memorizzare in una variabile int non sono infiniti. Essi infatti possono variare da un minimo di -2.147.483.647 ad un massimo di 2.147.483.647.

Perchè proprio questi valori? Perchè il massimo numero di combinazioni differenti che possiamo formare con 32 cifre di zeri ed uni, è 232: 4.294.967.296 (metà  positivi e metà  negativi, più lo zero).

E cosa succederebbe se scrivessi un programma di prova che incrementa una variabile int contenente 2.147.483.647? Nessuno vieta di provare e "toccare con mano" ciò che in informatica è detto overflow...

Per motivi unicamente nostalgici, ricordo che i vecchi compilatori per DOS, come il TurboC, utilizzavano interi di 16 bit, che quindi, al massimo, potevano spaziare da -32.767 a 32.767. Il motivo è semplice: i processori montati sui calcolatori dell'epoca (PC IBM) lavoravano con la massima efficienza se usavano numeri a 16 bit, mentre i "nostri" possono operare con numeri lunghi 32, o addirittura 64 bit.

Altri tipi

Non ci addentriamo nell'argomento, ma per completezza riporto la tabella di tutti i tipi di variabile definiti nel C:

                 Tipo Bytes    bits                       Intervallo di valori Specifica di stampa
--------------------------------------------------------------------------------------------------
            short int     2      16          -32,768 -> +32,767          (32K)              %d, %i
   unsigned short int     2      16                0 -> +65,535          (64K)                  %u
         unsigned int     4      32                0 -> +4,294,967,295   ( 4G)                  %u
                  int     4      32   -2,147,483,648 -> +2,147,483,647   ( 2G)              %d, %i
             long int     4      32   -2,147,483,648 -> +2,147,483,647   ( 2G)             %l, %ld
          signed char     1       8             -128 -> +127                                    %c
        unsigned char     1       8                0 -> +255                                    %c
                float     4      32                                                             %f
               double     8      64                                                 %e, %E, %g, %G
          long double    12      96                                                 %e, %E, %g, %G

Conclusioni

Come di consueto, riassumo qui quanto ho scritto qua sopra:

  • Come in tutti i linguaggi, in C è possibile conservare dei valori (numerici e non) in appositi spazi di memoria detti variabili.
  • Il programmatore, nel creare una variabile, deve scegliere un nome (che il compilatore assocerà  all'indirizzo), un tipo (quantità  di spazio occupato), ed un valore iniziale. Tale operazione è detta inizializzazione: int anno = 2009
  • Nel corso del programma il valore di una variabile può essere modificato attraverso gli assegnamenti. Un assegnamento è un'istruzione nella forma nome = [espressione], dove nome è una variabile, ed [espressione] è un numero, o una qualsiasi espressione dotata di valore numerico (che può quindi essere utilizzata dovunque si possa usare un numero.
  • Un assegnamento è a sua volta un'espressione, e vale il valore assegnato.
  • Nessun tipo di variabile è illimitato (semplicemente perchè la RAM non lo è). Quando il valore di una variabile supera il limite si verifica overflow.
  1. Introduzione
  2. Preparare l'ambiente
  3. Il primo programma
  4. La funzione printf()
  5. Le variabili
  6. Input da tastiera
  7. Esecuzione condizionale: if
  8. Cicli
  9. Esempi
  10. Bibliografia