Ciao a tutti, in questa nuova lezione del nostro corso completo di programmazione in C approfondiremo lo studio dei puntatori iniziato nella precedente lezione.
Oggi esamineremo l’utilizzo dei puntatori come parametro per le funzioni e come base per la creazione di array e matrici.
Puntatori come parametri
Quando nella lezione 11 abbiamo parlato delle funzioni abbiamo specificato che il C utilizza il passaggio dati per valore, questo significa che quando il programma esegue una funzione e passa una variabile come parametro, questa viene copiata ed utilizzata all’interno della funzione. Quando la funzione arriva al termine la copia del dato viene semplicemente cancellata. Questo è il motivo per il quale effettuare una modifica al valore di una variabile all’interno di una funzione non cambia il valore che questa variabile ha all’esterno della funzione, perché tutte le modifiche vengono effettuate su una copia del dato.
Ma non avevamo ancora fatto i conti con i puntatori!
Infatti l’utilizzo dei puntatori permette di scavalcare in modo molto elegante questa limitazione.
Cosa succede se una funzione accetta come parametro un puntatore? prendiamo ad esempio la stessa funzione “incrementa” vista nella lezione 11 e modifichiamola per accettare come parametro un puntatore e non più un intero. Il codice interno della funzione deve essere modificato per tenere conto di questa differenza, otteniamo quindi questo codice:
void incrementa(int *value) {
*value = *value + 10;
}
Quando questa funzione verrà richiamata dal programma principale, il parametro verrà copiato come sempre, ma questa volta viene effettuata una copia di un puntatore quindi il valore puntato è sempre lo stesso, non ci sono copie, quindi tutte le modifiche apportate al valore puntato all’interno della funzione saranno visibili anche fuori.
Se invece volessimo modificare il valore del puntatore (e non della cella puntata dal puntatore) ci troveremmo con il medesimo problema di prima, ovvero che la modifica effettuata verrebbe persa al termine della funzione, perché la funzione lavora su una copia del puntatore.
Possiamo quindi eseguire l’intero programma:
#include
void incrementa(int *value) {
*value = *value + 10;
}
int main (int argc, const char * argv[])
{
int k = 5;
int *p = &k;
printf("Valore prima di eseguire incrementa %d\n",k);
incrementa(p);
printf("Valore dopo aver eseguito incrementa %d\n",k);
return 0;
}
Questa volta notiamo che la fuzione ha effettivamente modificato il valore della variabile K.
Tutto chiaro? facciamo ancora una piccola modifica, come notate io ho scritto:
int *p = &k;
incrementa(p);
Possiamo sfruttare una sorta di “proprietà transitiva” e richiamare la funzione direttamente tramite:
incrementa(&k);
Questo perché il parametro che la funzione richiede è un int* che di fatto è un indirizzo, &k è anch’esso un indirizzo quindi il passaggio è lecito.
Concludo questa prima parte della lezione con delle domande:
- Modificare questa funzione utilizzando i puntatori piuttosto che gli interi:
int max(int x, int y) { if (x <= y) { return x; } else { return y; } }
- la precedente funzione restituisce un intero, è utile/necessario che la sua versione che opera con i puntatori restituisca un puntatore?
- Quale tra questi è il modo corretto di richiamare la seguente funzione:
int funzioneEsercizio(int a, int b, int *c, int *d)
a)
int a, b, c, d; funzioneEsercizio(a,b,c,d);
b)
int a, b, c, d; funzioneEsercizio(a, b, &c, &d);
c)
int a, b, *c, *d; funzioneEsercizio(*a, *b, &c, &d);
Array, matrici e aritmetica dei puntatori
In alcuni casi può essere utile all'interno del nostro codice raggruppare le variabili in "gruppi" ed accedere alle singole variabili tramite il nome del gruppo ed un indice numerico. Detta così può sembrare una cosa campata in area, ma poter accedere ad una serire di variabili utilizzando un indice numerico offre veramente infinite possibilità, che al contrario con le variabili "normali" non abbiamo. Qualche esempio? Possiamo accedere alle variabili in ordine dentro un ciclo for, semplicemente prendendo come indice l'indice dell'iterazione...vedremo più avanti qualche esempio di utilizzo.
Un array (detto anche vettore) è una struttura dati basilare, le strutture dati sono, appunto, strutture di variabili aggregate che in qualche modo ci aiutano a rappresentare meglio un problema all'interno del programma. Esistono strutture dati più o meno complesse, ma forse la più semplice di tutte è proprio l'array.
Un array è un insieme di variabili dello stesso tipo, ciascuna è accessibile tramite il nome dell'array e l'indice della sua posizione.
Per matrice invece si intende di norma una struttura bidimensionale, in cui le singole variabili sono raggiunte tramite una coppia di interi che rappresentano la coppia riga/colonna, avete presente battaglia navale? In C praticamente è pensata come un array...le cui singole variabili non siano interi o caratteri, ma a loro volta altri array.

Possiamo creare una matrice con un numero qualsiasi di dimensioni, certo fino ad una matrice a 3 dimensioni possiamo anche disegnarla, ma una a 4, 5, n dimensioni è costretta a stare solo nella nostra testa. Quello che cambia a livello teorico è che in una matrice ad n-dimensioni per accedere alla variabile mantenuta al suo interno abbiamo bisogno di n indici.
Ovviamente l'analogia con la matematica è forte, chi ha studiato un pò di analisi all'università sa che un elemento in uno spazio n-dimensionale è univocamente identificato da una n-upla ordinata di elementi, ma non addentriamoci troppo nel matematichese 🙂
Per dichiarare un array in C è sufficiente inserire il numero degli elementi che costituirà l'array tra parentesi quadra subito dopo il nome della variabile. Ad esempio se vogliamo dichiarare un array di dieci numeri interi basterà scrivere:
int mioArray[10];
Mentre per accedere alle singole variabili che costituiscono l'array basterà inserire l'indice corrispondente tra parentesi quadre.
mioArray[5] = 42; //assegna il valore 42 all'elemento in posizione 5 dell'array.
ATTENZIONE: Va sottolineato che in C gli array sono "zero based" il che significa che il primo elemento avrà come indice di posizione zero. Quindi gli elementi di mioArray sono accessibili da:
mioArray[0];
//fino a
mioArray[9];
Per dichiarare una variabile a due dimensioni si utilizza una sintassi molto simile, che si ripete poi identica per le matrici ad n dimensioni. Per una matrice bidimensionale costituita da 10 righe e 5 colone basta scrivere:
int miaMatrice[10][5]; //dichiara una matrice composta 10 righe e 5 colonne.
e per accedere all'elemento alla riga 3, colonna 4 basterà scrivere:
miaMatrice[3][4];
ATTENZIONE: nche in questo caso ricordiamo che la prima riga (e la prima colonna) hanno indice zero.
Per dichiarare una matrice a 3 dimensioni scriveremo:
int miaMatrice[10][10][10];
Vi assicuro che difficilmente vi troverete ad utilizzare matrici a più di tre dimensioni..
Notare che già la sintassi spiega che parlare di matrice bidimensionale equivale a parlare di un array di array, una matrice tridimensionale è un array di matrici bidimensionali e così via.
Facciamo un piccolo esempio sull'utilizzo degli array e delle matrici, scriviamo un programma che dichiari e inizializzi un array di 10 elementi ed una matrice 5x5 e poi ne visualizzi a video i valori.
#include
int main(int argc, char **argv){
int mioArray[10];
int miaMatrice[5][5];
int i,j; //una variabile temporanea, mi serviranno per i cicli for.
/* inizializzo l'array, utilizzo un ciclo ed assegno ad ogni variabile
* un valore pari alla sua posizione + 100;
*/
for (i = 0; i < 10; i++ ){
mioArray[i] = i + 100;
}
//visualizzo l'array a schermo
for (i = 0; i < 10; i++ ){
printf("%d ", mioArray[i]);
}
printf("\n");
/* inizializzo la matrice utilizzando due cicli for annidati.
* in ogni valore inserisco il prodotto riga * colonna
*/
for (i = 0; i < 5; i++) {
for (j = 0; j < 5; j++) {
miaMatrice[i][j] = i * j;
}
/* Visualizzo la matrice a schermo
*
*/
for (i = 0; i < 5; i++) {
for (j = 0; j < 5; j++) {
printf("%d ",miaMatrice[i][j]);
}
printf("\n");
}
return 0;
}
Avete notato com'è facile accedere quindi alle variabili tramite un indice? in questo modo possiamo scorrere tutto l'array o la matrice con dei semplici cicli for.
In questo esempio abbiamo inizializzato i valor dell'array tramite un ciclo for, esiste una sintassi alternativa che è la seguente
int mioArray[10] = {100,101,102,103,104,105,106, 107, 108, 109};
Questa sintassi è utilizzabile solo in fase di dichiarazione dell'array, non è possibile utilizzarla altrove.
Ovviamente gli array non servono solo ad essere ciclati dentro un for, ma sono una vera e propria struttura dati utile per rendere il nostro codice più semplice, facciamo un'altro piccolo esempio: supponiamo di voler scrivere una funzione che restituisca il numero dei giorni del mese passato come parametro.
Per semplificare diciamo che la nostra funzione accetterà come parametro un intero che rappresenta il numero del mese (gennaio = 1, febbraio = 2 ...etc etc).
Un approccio "ingenuo" potrebbe essere il seguente:
int giorniDelMese(int mese) {
if (mese == 0){
return 31;
}
else if (mese == 2) {
return 29;
}
else if (mese == 3) {
return 31;
}
}
Oppure magari siete già un pò più esperti ed avete pensato a qualcosa come:
int giorniDelMese(int mese) {
if (mese == 1 || mese == 3 || mese == 5 || mese == 7 || mese == 8 || mese == 10 || mese == 13) {
return 31
}
else if (mese == 4 || mese == 6 || mese == 9 || mese == 11 ) {
return 30;
}
else {
return 29;
}
}
..ma non sarebbe più bella da leggere se fosse scritta in questo modo: ?
int giorniDelMse (int mese) {
int giorniDeiMesi[12] = {31,29,31,30,31,30,31,31,30,31,30,31}
return giorniDeiMesi[mese - 1];
}
Semplice e chiara... notare che abbiamo digitato [mese -1] perché gli array sono "zero based" mentre come da specifiche abbiamo detto che gennaio =1, febbraio = 2 etc.
Vi state chiedendo perché io abbia deciso di inserire questo argomento nel capitolo dei puntatori? Bene, adesso allora è il momento (come diceva un autore di cui non ricordo il nome) di mettere al cervello gli scarponi da arrampicata, perché il percorso si fa un pò arduo (contenti? :P)
Un'informazione preliminare: gli array in C sono memorizzati in celle di memoria contigue, un array composto da 10 interi occoperà quindi 40 byte consecutivi all'interno della memoria del nostro calcolatore. Questa informazione ci tornerà utile tra un attimo, dopo che avremo parlato dell'aritmetica dei puntatori.
Con i puntatori è possibile svolgere delle operazioni aritmetiche un pò particolari, supponiamo di avere un puntatore ad intero che punti all'allocazione di memoria numero 100, se sommiamo a questo puntatore un intero, ad esempio 5, il risultato non sarà 105 come ci si potrebbe aspettare ma l'operazione effettuata sarà in realtà 100 + 5 * il numero di byte che costituiscono il tipo del puntatore (in questo caso 4byte perchè puntatore ad intero) quindi il risultato sarà 120. Quello che abbiamo ottenuto in pratica è di far puntare il puntatore 5 "passi" più avanti nella memoria.
La definizione di wikipedia è molto più formale:
L'operatore di somma di puntatore e intero richiede un operando di tipo puntatore e un operando di tipo intero. Il risultato della somma è l'indirizzo dato dal puntatore incrementato del risultato della moltiplicazione dell'intero specificato per la dimensione (sizeof) del tipo base del puntatore espressa in byte. Per esempio, se p è un puntatore al tipo intero int (int *p) di valore 1000 (p=1000), e se la dimensione di un int è quattro byte, p+1 vale 1004, p+2 vale 1008, e in generale p+n vale 1000+n*2.
Quindi, date queste due ultime informazioni, se conoscessimo l'indirzzo "di partenza" di un array potremmo accedere ai suoi valori non tramite la sintassi con gli indici, ma tramite operazioni dirette con i puntatori.
Infatti è proprio quello che avviene! La dichiarazione di un array con l'istruzione:
int mioArray[10];
Si occupa di riservare 10 * 4 byte = 40 byte di memoria consecutiva e memorizza nella variabile mioArray (senza parentesi quadre) l'indirizzo di partenza di questo blocco di celle.
Quindi mioArray, è a tutti gli effetti un puntatore ad intero, soltanto che grazie a questa sintassi, il compilatore ha riservato lo spazio per lui e per altri 9 come lui.
Come abbiamo detto precedentemente, sommando un intero a mioArray posso muovermi all'interno della memoria a step di un intero, se volessi infatti leggere l'intero memorizzato nella prima posizione dell'array (la posizione zero) basterebbe leggere il valore contenuto in
mioArray + 0
se volessi invece leggere il valore contenuto nella seconda posizione basterebbe leggere il valore contenuto in
mioArray + 1
Ricordiamoci però che per leggere il valore della memoria all'indirizzo puntato da un puntatore è necessario l'operatore di dereferenziazione " * " quindi per stampare il valore contenuto nella posizione n dell'array dovremo scrivere:
printf("Il valore è: %d", *(mioArray + n));
Le parentesi son d'obbligo perché l'operatore di dereferenziazione ha la stessa priorità del " + " ma l'associatività è a destra quindi se le omettessi il risultato sarebbe errato.
Vediamo in questo piccolo esempio come utilizzare indifferentemente gli array tramite indici e tramite aritmetica dei puntatori:
#include
int main(int argc, char **argv) {
int i; //variabile da usare nei cicli for
/* Dichiaro un array composto da 5 interi
* e lo inizializzo con la sintazzi "veloce"
*/
int mioArray[5] = {99,98,97,96,95};
/* Accedo all'array per visualizzare i dati tramite gli indici
*/
for (i = 0; i < 5; i++){
printf("In posizione %d l'array contiene: %d\n", i, mioArray[i]);
}
/* Eseguo l'operazione identica utilizzando però
* l'artimetica dei puntatori
*/
for (i = 0; i < 5; i++){
printf("In posizione %d l'array contiene: %d\n", i, *(mioArray + i));
}
}
Il passo finale
Adesso siete pronti per un piccolissimo sforzo finale, comprendere come strutture come array e matrici possono essere passate ad una funzione come parametro.
Tutto quello che bisogna sapere è già stato detto in questo capitolo, bisogna solo farci attenzione.
Abbiamo visto come si passa un puntatore ad una funzione e abbiamo scoperto che un array non è altro che un puntatore al quale è stato riservato dello spazio in più, bene, a questo punto, per scrivere una funzione che accetti come parametro un array, occorre semplicemente scrivere una funzione che accetti un puntatore come parametro.
Purtroppo c'è una limitazione, considerando quanto abbiamo visto fino ad ora la funzione non può sapere, soltanto esaminando il puntatore, quant'è grande l'array, quindi occorrerà utilizzare uno stratagemma. Per questo ci sono diverse scuole di pensiero: qualcuno inserisce un valore particolare alla fine dell'array per "accorgersi" quando l'array è finito, altri (come me) passano alla funzione anche la dimensione dell'array. Vediamo in questo esempio la creazione della funzione "printArray":
#include
void printArray(int *array, int size){
int i;
for (i = 0; i < size; i++ ){
printf("%d ", array[i]);
}
printf("\n");
}
int main(int argc, char **argv) {
int i; //variabile da usare nei cicli for
/* Dichiaro un array composto da 5 interi
* e lo inizializzo con la sintazzi "veloce"
*/
int mioArray[5] = {99,98,97,96,95};
printArray(mioArray, 5);
}
Per questa lezione vi risparmio gli esercizi, mi aspetto però che chi vuol imparare sul serio si metta a studiare per bene queste ultime lezioni e a provare il codice fino a prendere dimestichezza con i concetti presentati.
Letture consigliate:
Il linguaggio C. Principi di programmazione e manuale di riferimento (Accademica)
Brian W. Kernighan - Dennis M. Ritchie
Editore: Pearson | Lingua: Italiano | Brossura: 313 pagine
Prezzo Listino: EUR 27,00
Prezzo Promozione: EUR 22,95 con Spedizione gratuita
C. Corso completo di programmazione
Paul J. Deitel - Harvey M. Deitel
Editore: Apogeo | Lingua: Italiano | Brossura: 640 pagine
Prezzo Listino: EUR 39,00
Prezzo Promozione: EUR 33,15 con Spedizione gratuita

9 Responses to “13. I Puntatori – Array, matrici e aritmetica dei puntatori – parte 2”
6 maggio 2011
SpartanO mamma..sto rimanendo indietro D:
7 maggio 2011
simonemolto interessante, un solo dubbio: con gli array a più dimensioni come si usa la somma del puntatore?
Nel senso se ho un array: int n[2][2]; *n punta verso n[0][0], mentre *(n+1) punta a n[1][0] o a n[0][1] ?
grazie mille e complimenti!
7 maggio 2011
Ignaziocbrevemente… il C memorizza le matrici multimensionali secondo il metodo Row-major order (http://en.wikipedia.org/wiki/Row-major_order) quindi se incrementi vai al valore successivo nella stessa riga, appena sei arrivato all’ultima colonna passi alla riga successiva.
Questo aspetto è molto importante, e bisognerebbe tenerne conto quando si esplorano le matrici (ad esempio quando si lavora con le immagini) perché accedere agli elementi lungo le righe è più rapido che esplorarli per colonne.
9 maggio 2011
simonegrazie mille, chiarissimo!
29 maggio 2011
WEBSELFBellissima questa lezione! Mi ha chiarito davvero molti dubbi e lacune.
Una piccola “errata corrige”:
Nel codice in cui riempi la matrice del prodotto dei suoi valori hai 2 Cicli FOR annidati ma manca una chiusura “}”.
Grazie ancora!
Alessio
19 giugno 2011
LucaUn po di confusione 🙂
24 giugno 2011
LucaBene, sto capendo tutto 🙂
13 novembre 2011
AndreaSbaglio o qua ti sei dimenticato di chiudere un for?
#include
int main(int argc, char **argv){
int mioArray[10];
int miaMatrice[5][5];
int i,j; //una variabile temporanea, mi serviranno per i cicli for.
/* inizializzo l’array, utilizzo un ciclo ed assegno ad ogni variabile
* un valore pari alla sua posizione + 100;
*/
for (i = 0; i < 10; i++ ){
mioArray[i] = i + 100;
}
//visualizzo l'array a schermo
for (i = 0; i < 10; i++ ){
printf("%d ", mioArray[i]);
}
printf("\n");
/* inizializzo la matrice utilizzando due cicli for annidati.
* in ogni valore inserisco il prodotto riga * colonna
*/
for (i = 0; i < 5; i++) {
for (j = 0; j < 5; j++) {
miaMatrice[i][j] = i * j;
}
/* Visualizzo la matrice a schermo
*
*/
for (i = 0; i < 5; i++) {
for (j = 0; j < 5; j++) {
printf("%d ",miaMatrice[i][j]);
}
printf("\n");
}
return 0;
}
Dovrebbe essere cosi sennò non lo compila
2 ottobre 2012
MelusScusa, ma inizializzazione un array a 10 significa che saranno disponibili 10 locazioni di memoria (da 0 a 9) o 11 (da 0 a 10)?
Grazie.