Linguaggio C, 6e - Capitolo 14

Page 1

14txtI.qxp_Layout 1 14/05/21 09:44 Pagina 227

Puntatori

14

Concetti chiave • • • • • • • • • • •

Definire e utilizzare i puntatori Operatori & e * Lavorare sugli array attraverso i puntatori Aritmetica dei puntatori +, ++, –, –– Passaggio di parametri per indirizzo Funzione per lo scambio dei valori di due variabili Approfondire l’uso delle funzioni: gestione stringhe e conversione da stringa a numero Tecniche per operare con oggetti dinamici: allocazione di memoria, accesso, rimozione L’operatore sizeof e in tipo size_n Qualificatore di tipo const, restrict string.h Funzioni sulle stringhe strcat, strncat, strcmp, strncmp, strcpy, strncpy, strlen, strchr, strrchr, strpbrk,

strtok • stdlib.h Funzioni di conversione atoi, atol, atoll, atof, strtol, strtoll, strtod, strtof • Funzioni di conversione non standard itoa, ltoa • string.h Funzioni sulla memoria memcpy, memmove, memcmp, memchr, memset • stdlib.h Funzioni sulla memoria malloc, calloc, free • stdlib.h Puntatore nullo NULL


14txtI.qxp_Layout 1 14/05/21 09:44 Pagina 228

228 / Capitolo 14

14.1 Definizione di puntatore Indirizzo di memoria

A ogni variabile corrisponde un nome, una locazione di memoria e l’indirizzo della locazione di memoria. Il nome di una variabile è il simbolo attraverso cui si fa riferimento al contenuto della corrispondente locazione di memoria. Così, per esempio, nel frammento di programma: int a; ... a = 5; printf("%d", a);

viene assegnato il valore costante 5 alla variabile di tipo intero a. int a;

a = 5; 5

Operatore &

L’operatore &, introdotto con la funzione scanf, restituisce l’indirizzo di memoria di una variabile. Per esempio, l’espressione &a è un’espressione il cui valore è l’indirizzo della variabile a. Un indirizzo può essere assegnato solo a una speciale categoria di variabili dette puntatori, le quali sono appunto variabili abilitate a contenere un indirizzo. La sintassi di definizione è: tipoBase *varPunt;

dove varPunt è definita come variabile di tipo “puntatore a tipoBase”; in sostanza varPunt è creata per poter mantenere l’indirizzo di variabili di tipo tipoBase, che è uno dei tipi fondamentali già introdotti: char, int, float e double. Il tipo puntatore è un classico esempio di tipo derivato, infatti occorre specificare a quale tipo esso punta. Per esempio, nel seguente caso: int a; char c; int *pi; char *pc; pi = &a; pc = &c;

si ha che pi è una variabile di tipo puntatore a int, e pc è una variabile di tipo puntatore a char. Le variabili pi e pc sono inizializzate rispettivamente con l’indirizzo di a e di c. int a; pi

char c; pc

La capacità di controllo di una variabile o, meglio, la capacità di controllo di una qualsiasi regione di memoria per mezzo di puntatori è una delle caratteristiche


14txtI.qxp_Layout 1 14/05/21 09:44 Pagina 229

Puntatori / 229

salienti del C. È facile sperimentare quanto si accrescano le capacità del linguaggio con l’introduzione dei puntatori. Questi ultimi, d’altra parte, sarebbero poca cosa se non esistesse l’operatore unario *. L’operatore *, detto operatore di indirezione, si applica a una variabile di tipo puntatore e restituisce il contenuto dell’oggetto puntato. Se effettuiamo le operazioni: a = 5; c = 'x';

in memoria abbiamo la situazione illustrata di seguito. int a; pi

5

char c; pc

x

Le istruzioni: printf("a = %d printf("a = %d

c = %c", a, c); c = %c", *pa, *pc);

hanno esattamente lo stesso effetto, quello di visualizzare: a = 5

c = x

Vediamo un altro esempio: char c1, c2; char *pc; ... c1 = 'a'; c2 = 'b'; printf(" c1 = %c, c2 = %c \n", c1, c2); pc = &c1; /* pc contiene l'indirizzo di c1 */ c2 = *pc; /* c2 contiene il carattere 'a' */ printf(" c1 = %c, c2 = %c \n", c1, c2);

Dopo l’assegnazione pc=&c1; i nomi c1 e *pc sono perfettamente equivalenti (sono alias), e si può accedere allo stesso oggetto creato con la definizione char c1 sia con il nome c1 sia con l’espressione *pc. L’effetto ottenuto con l’assegnazione c2=*pc si sarebbe ottenuto, in modo equivalente, con l’assegnazione: c2 = c1;

Un ulteriore esempio di uso di puntatori e dell’operatore di indirezione, riferiti a elementi di un array, è il seguente: int buf[2]; int *p; ... p = &buf[1]; *p = 4;

Operatore *


14txtI.qxp_Layout 1 14/05/21 09:44 Pagina 230

230 / Capitolo 14

Con il puntatore a intero p e l’operatore * si è modificato il contenuto della locazione di memoria buf[1]. buf[0]

buf[1] 4

p

Si sarà certo notato che l’operatore * è usato nella definizione di variabili di tipo “puntatore a”: int *pi; char *pc;

La notazione è perfettamente coerente con la semantica dell’operatore di indirezione. Infatti, se *pi e *pc occupano tanta memoria quanto rispettivamente un int e un char, allora pi e pc saranno dei puntatori a int e a char.

14.2 Array e puntatori Nome dell’array

Gli array e i puntatori in C sono strettamente correlati. Il nome di un array può essere usato come un puntatore al suo primo elemento. Considerando, per esempio: char buf[100]; char *s; s = &buf[0]; oppure s = buf;

si ha che le due assegnazioni s=&buf[0] e s=buf sono perfettamente equivalenti. Infatti in C il nome di un array, come nel nostro caso buf, è una costante – si noti bene: una costante, non una variabile – il cui valore è l’indirizzo del primo elemento dell’array. Allora, come gli elementi di un array vengono scanditi per mezzo dell’indice, in modo equivalente si può avere accesso agli stessi elementi per mezzo di un puntatore. Per esempio, consideriamo il seguente codice: char buf[100]; char *s; ... s = buf; buf[7] = 'a'; printf("buf[7] = %c\n", buf[7]); *(s + 7)= 'b'; printf("buf[7] = %c\n", buf[7]);

Si ha che s e buf sono due sinonimi, con la differenza che s è una variabile puntatore a carattere, mentre buf è una costante. Incrementato di 7 il valore di s si ottiene un valore corrispondente all’indirizzo dell’ottavo elemento, al quale si accede per mezzo dell’operatore di indirezione. Il valore del puntatore s è incrementato di 7 unità, cioè di 7 volte la dimensione dell’oggetto corrispondente al tipo base del puntatore (Figura 14.1).


14txtI.qxp_Layout 1 14/05/21 09:44 Pagina 231

Puntatori / 231

Figura 14.1 Si può far riferimento a buf[7] scrivendo *(s+7). buf s

b

0

1

2

3

4

5

6

7

...

Per le stesse ragioni avremmo potuto riscrivere il frammento di programma: char buf[2]; for (i = 0; i < 2; i++) buf[i] = 'K';

per l’inizializzazione degli elementi di un array come: char *s; char buf[2]; s = buf; for (i = 0; i < 2; i++) *s++ = 'K';

L’istruzione *s++='K' opera nel modo seguente: • •

copia K nella locazione di memoria puntata da s; incrementa quindi s in modo che faccia riferimento all’elemento successivo.

14.3 Aritmetica dei puntatori Un puntatore contiene un indirizzo, quindi le operazioni che possono essere compiute su un puntatore sono quelle che hanno senso per un indirizzo. Di conseguenza, le uniche operazioni ammissibili sono: l’incremento, per andare da un indirizzo più

ERRORI FREQUENTI Assegnamenti fuori dai limiti Se per errore avessimo scritto: char *s; char buf[2]; s = buf; for (i = 0; i < 100; i++) *s++ = 'K';

il compilatore non avrebbe segnalato alcun errore, ma avremmo avuto problemi in esecuzione perché si sarebbe inizializzata con 'K' una regione di memoria al di là del limite allocato con buf. Che si usi una variabile puntatore o si faccia riferimento alla notazione con indice, è sempre dovere del programmatore assicurarsi che le dimensioni di un array vengano rispettate.


14txtI.qxp_Layout 1 14/05/21 09:44 Pagina 232

232 / Capitolo 14

Operatori sui puntatori

basso a uno più alto, e il decremento, per andare da un indirizzo più alto a uno più basso. Gli indirizzi sono per la memoria quello che gli indici sono per l’array. Gli operatori ammessi per una variabile di tipo puntatore sono: + ++ – ––

Ma qual è l’esatto significato dell’incremento o decremento di un puntatore? Il valore numerico del puntatore corrispondente a un indirizzo non viene incrementato come una qualunque altra costante numerica. Per esempio, se pc vale 10, dove pc è stato dichiarato: char *pc;

non è detto che pc++ valga 11. Nell’aritmetica dei puntatori quello che conta è il tipo base. Incrementare di 1 un puntatore significa far saltare il puntatore alla prossima locazione corrispondente a un elemento di memoria il cui tipo coincide con quello base. Per esempio, in: int a[10]; char b[10]; int *pi; char *pc; pi = a; pc = b; pi = pi + 3; pc = pc + 3;

le ultime due istruzioni che incrementano di 3 i puntatori pi e pc debbono essere interpretate in modo diverso. La prima: pi = pi + 3;

significa spostare in avanti pi di tre posizioni, dove ogni posizione occupa lo spazio di un int. La seconda: pc = pc + 3;

significa spostare in avanti pc di tre posizioni, dove ogni posizione occupa lo spazio di un char. Più in generale si ha che, quando un operatore aritmetico è applicato a un puntatore p di un certo tipo e p punta a un elemento di un array di oggetti di quel tipo, p+1 significa “prossimo elemento del vettore” mentre p–1 significa “elemento precedente del vettore”. La sottrazione tra puntatori è definita solamente quando entrambi i puntatori puntano a elementi dello stesso array. La sottrazione di un puntatore da un altro produce un numero intero corrispondente al numero di posizioni tra i due elementi dell’array. Si osservi invece come addizionando o sottraendo un intero da un puntatore si ottenga ancora un puntatore. Si considerino i tre esempi seguenti. int v1[10]; int v2[10];


14txtI.qxp_Layout 1 14/05/21 09:44 Pagina 233

Puntatori / 233

int i; int *p; i = &v1[5] – &v1[3]; printf("%d\n", i);

/* 1 ESEMPIO */ /* i vale 2 */

i = &v1[5] – &v2[3]; printf("%d\n", i);

/* 2 ESEMPIO */ /* il risultato è indefinito */

p = v2 – 2;

/* 3 ESEMPIO */ /* dove va a puntare p ? */

1. Sottrazione tra indirizzi dello stesso vettore: i = &v1[5] – &v1[3];

corrispondente a un caso perfettamente legale. 2. Sottrazione tra indirizzi di array diversi: i = &v1[5] – &v2[3];

corrispondente a un caso il cui risultato non è prevedibile. 3. Sottrazione di una costante da un indirizzo ma nella direzione sbagliata: p = v2 – 2; il puntatore p va a puntare due interi prima dell’inizio del vettore v2 (per

come sono avvenute le definizioni probabilmente si sconfina nello spazio riservato a v1, ma non è detto).

14.4 Passaggio di parametri per indirizzo Abbiamo osservato nel capitolo dedicato alle funzioni che in C non è possibile passare un array a una funzione. Eppure esistono molti casi in cui è necessario non solo passare un array ma anche restituire una struttura dati più complessa della semplice variabile char o int. All’apparenza le funzioni sembrano essere limitate dal meccanismo del passaggio parametri per valore. Il programmatore C risolve questa apparente mancanza con un metodo semplice: passa per valore l’indirizzo della variabile – array o altro – che si vuol leggere o modificare tramite la funzione. Passare un indirizzo a una funzione significa renderle nota la locazione dell’oggetto corrispondente all’indirizzo. In tale maniera le istruzioni all’interno di una funzione possono modificare il contenuto della variabile il cui indirizzo è stato passato alla funzione. Questo meccanismo è noto con il nome di passaggio di parametri per indirizzo. Consideriamo, per esempio, nel Listato 14.1 la funzione scambia, che ha l’effetto di scambiare il valore dei suoi parametri. La chiamata di questa funzione non produce alcun effetto sui parametri attuali; cioè la chiamata: scambia(x, y);

non ha effetto sulle variabili intere x e y. Infatti i valori di x e y sono copiati nei parametri formali a e b e, quindi, sono stati scambiati i valori dei parametri formali, non i valori originali di x e y. Affinché scambia abbia un qualche effetto


14txtI.qxp_Layout 1 14/05/21 09:44 Pagina 234

234 / Capitolo 14

Listato 14.1 Il passaggio dei parametri per indirizzo.

#include <stdio.h> void scambia(int, int); int main(void) { int x, y; x = 8; y = 16; printf("Prima dello scambio\n"); printf("x = %d, y = %d\n", x, y); scambia(x, y); printf("Dopo lo scambio\n"); printf("x = %d, y = %d\n", x, y); } /* Versione KO di scambia */ void scambia(int a, int b) { int temp; temp = a; a = b; b = temp; }

deve essere modificata in modo da ricevere gli indirizzi, anziché i valori, delle variabili (Listato 14.2). La relativa rappresentazione grafica del passaggio di parametri per indirizzo è data in Figura 14.2. La simbologia int *a e int *b, usata nella definizione dei parametri formali, dichiara a e b come variabili di tipo puntatore a un intero. L’invocazione della funzione scambia deve essere modificata in modo da passare l’indirizzo delle variabili da scambiare: scambia(&x, &y); Parametro array

La medesima strategia del passaggio per valore di un indirizzo si può sfruttare con gli array (Listato 14.3). L’array dell’esempio è una stringa, cioè un array di char che termina con il carattere terminatore \0. Con l’inizializzazione: char str[] = "BATUFFO";

il primo elemento dell’array di caratteri str è inizializzato a puntare al primo elemento della costante di tipo stringa "BATUFFO".


14txtI.qxp_Layout 1 14/05/21 09:44 Pagina 235

Puntatori / 235

Listato 14.2 Ancora sullo scambio di valori.

#include <stdio.h> void scambia(int *, int *); int main(void) { int x, y; x = 8; y = 16; printf("Prima dello scambio\n"); printf("x = %d, y = %d\n", x, y); scambia(&x, &y); printf("Dopo lo scambio\n"); printf("x = %d, y = %d\n", x, y); } /* Versione OK di scambia */ void scambia(int *a, int *b) { int temp; temp = *a; *a = *b; *b = temp; }

L’accorgimento di valutare una stringa per mezzo del puntatore char * è particolarmente utile nella scrittura di funzioni che manipolano stringhe. Nell’esempio la funzione strlen conta il numero di caratteri (escluso \0) di una stringa: int strlen(char *p) { Figura 14.2 Passaggio di parametri per indirizzo. Spazio dati di main

Spazio dati di scam bia

x

a

8 y

b

16

Parametri attuali

Parametri formali


14txtI.qxp_Layout 1 14/05/21 09:44 Pagina 236

236 / Capitolo 14

Listato 14.3 Passaggio di un array.

#include <stdio.h> char str[] = "BATUFFO"; int strlen(char *); int main(void) { printf("la stringa %s ha lunghezza %d\n", str, strlen(str)); } int strlen(char *p) { int i = 0; while (*p++) i++; return i; }

int i = 0; while (*p++) i++; return i; }

La funzione pone il contatore i a zero e comincia a contare caratteri finché non trova il carattere nullo. Un’implementazione alternativa di strlen che usa la sottrazione di puntatori è: int strlen(char *p ) { char *q = p; while (*q++); return (q–p–1); }

Le funzioni di manipolazione stringa gestiscono le stringhe sempre per mezzo di puntatori a carattere. Il C fornisce un vasto insieme di funzioni di manipolazione stringa, dichiarate nel file di include string.h. Perciò, per poter usare la libreria di manipolazione stringhe del C, occorre premettere la direttiva: #include <string.h>

Si riportano di seguito le dichiarazioni delle più importanti funzioni di manipolazione stringa, alcune delle quali utilizzate nel Capitolo 13, allo scopo di riflettere sull’uso che viene fatto del passaggio dei parametri per indirizzo. char *strcat(char * restrict string1, const char * restrict string2);


14txtI.qxp_Layout 1 14/05/21 09:44 Pagina 237

Puntatori / 237

concatena le stringhe puntate da string1 e string2 attaccando string2 in coda a string1. Per brevità, nelle successive descrizioni al posto di “la stringa puntata da string1”, “la stringa puntata da string2” scriviamo in breve string1, string2. char *strncat(char * restrict string1, const char * restrict string2, size_t n);

concatena le stringhe string1 e string2 attaccando n caratteri della stringa string2 in coda a string1. Il tipo size_t è un tipo intero senza segno definto in string.h e stddef.h e in altri file d’intestazione (i tipi senza segno sono introdotti nel Paragrafo 18.2). int strcmp(const char *string1, const char *string2);

confronta string1 con string2. Ritorna 0 se le stringhe sono identiche, un numero minore di zero se string1 è minore di string2, e un numero maggiore di zero se string1 è maggiore di string2. int strncmp(const char *string1, const char *string2, size_t n);

confronta i primi n caratteri di string1 con string2. Ritorna 0 se le sottostringhe di n caratteri sono identiche, un numero minore di zero se sottostring1 è minore di sottostring2, e un numero maggiore di zero se sottostring1 è maggiore di sottostring2. char *strcpy(char *string1, const char *string2);

copia string2 su string1. char *strncpy(char *string1, const char *string2, size_t n);

copia i primi n caratteri di string2 su string1. size_t strlen(const char *string);

conta il numero di caratteri di string, escluso il carattere nullo. char *strchr(const char *string, int c);

ritorna il puntatore alla prima occorrenza di c convertito a carattere in string. char *strrchr(const char *string, int c);

ritorna il puntatore all’ultima occorrenza di c convertito a carattere in string. char *strpbrk(const char *string1, const char *string2);

ritorna un puntatore alla prima occorrenza della stringa string2 in string1. char *strtok(char * restrict string1, const char * restrict string2);


14txtI.qxp_Layout 1 14/05/21 09:44 Pagina 238

238 / Capitolo 14

RICHIAMO Tipi Capitolo 18 Paragrafo 3

trova la prossima sequenza di caratteri (token) circoscritta dai caratteri string2 nella stringa string1. Una sequenza di chiamate di strtok divide string1 in una sequenza di token. Il separatore string2 può cambiare da chiamata a chiamata. È utile riflettere sull’uso di puntatori a char che si fa nelle funzioni di manipolazione stringa. Per poter utilizzare le funzioni di conversione da stringa a numero dobbiamo includere stdlib.h. #include <stdlib.h> int atoi(const char *string); long int atol(const char *string); long long int atoll(const char *string); double atof(const char *string);

convertono string (la stringa puntata da) rispettivamente in un valore int, long int, long long int e double. Le funzioni ritornano il risultato della conversione. Le variabili di tipo long int, che contengono valori interi la cui dimensione può essere maggiore o uguale al tipo int, vengono dichiarate facendole precedere da long int, anche abbreviato in long. Le variabili di tipo long long int contengono valori interi la cui dimensione può essere maggiore o uguale al tipo long int. Le precedenti funzioni non garantiscono alcuna diagnostica dell’errore che potrebbero produrre nel caso in cui, per esempio, a un certo punto non sia più possibile interpretare correttamente i successivi caratteri della stringa ai fini della conversione. Al contrario, le seguenti funzioni alternative rispondono a questa necessità. long int strtol(const char * restrict string, char ** restrict endptr, int base); long int strtoll(const char * restrict string, char ** restrict endptr, int base);

convertono string rispettivamente in un valore long int e long long int. Se il valore di base è compreso tra 2 e 36 le funzioni si aspettano che string

APPROFONDIMENTO const e restrict

Le parole chiave const e restrict sono qualificatori di tipo. const indica che la funzione non può modificare le locazioni di memoria puntate dal puntatore; nell’esempio const char * string2, la funzione non può modificare l’oggetto puntato da string2. Con tale precisazione, semplicemente leggendo il prototype, si capisce quali sono argomenti che non sono modificati dalla funzione di manipolazione. Con restrict il programmatore informa il compilatore che solo quel puntatore o un puntatore direttamente derivato da esso (come puntatore+1) sarà usato per accedere all’oggetto per tutta la vita del puntatore stesso; per esempio, con char * restrict string2, si avverte che sarà usato solo string2 per accedere all’oggetto puntato.


14txtI.qxp_Layout 1 14/05/21 09:44 Pagina 239

Puntatori / 239

contenga una sequenza di lettere e numeri che rappresentano un intero espresso in quella base. I valori dalla a (o A) alla z (o Z) sono i valori da 10 a 35. Sono permessi esclusivamente lettere e numeri minori della base. Restituiscono il valore convertito oppure il valore zero se non è stato possibile ottenere la conversione. Se nell’elaborazione le funzioni raggiungono un carattere non riconducibile a un intero, terminano restituendo in endptr il puntatore a tale carattere. La ripetizione del carattere asterisco ** sta per “il puntatore di puntatore”, nello specifico a un carattere. Lo approfondiremo nel Capitolo 20. double strtod(const char * restrict string, char ** restrict endptr); double strtof(const char * restrict string, char ** restrict endptr);

Convertono string rispettivamente in un valore double e float, se raggiungono un carattere non riconducibile a un valore in virgola mobile terminano restituendo in endptr il puntatore a tale carattere.

14.5 Oggetti dinamici I puntatori sono usati nella creazione e manipolazione di oggetti dinamici. Mentre gli oggetti statici vengono creati specificandoli in una definizione, gli oggetti dinamici sono creati durante l’esecuzione del programma. Il numero degli oggetti dinamici non è definito dal testo del programma come quello degli oggetti creati attraverso una definizione: essi vengono creati o distrutti durante l’esecuzione del programma, non durante la compilazione. Gli oggetti dinamici, inoltre, non hanno un nome esplicito, ma occorre fare riferimento a essi per mezzo di puntatori. Il valore NULL, che può essere assegnato a qualsiasi tipo di puntatore, indica che nessun oggetto è puntato da quel puntatore. È un errore usare questo valore in riferimento a un oggetto dinamico. Il puntatore NULL è un indirizzo di memoria che corrisponde al valore convenzionale di puntatore che non punta a nulla, e la sua definizione può essere diversa da macchina a macchina. Per esempio: #include <stdio.h> int main(void)

APPROFONDIMENTO Funzioni di conversione Non fanno invece parte dello standard ISO/IEC le funzioni inverse ad atoi e alle altre funzioni simili. Molte delle attuali implementazioni includono in stdlib.h anche funzioni di conversione da valori numerici a stringhe. Tra queste troviamo char * itoa(long num, char * string, int base) che converte l’intero num in una stringa puntata da string nella base indicata da base, generalmente compresa tra 2 e 36, e l’analoga funzione ltoa.

Valore NULL


14txtI.qxp_Layout 1 14/05/21 09:44 Pagina 240

240 / Capitolo 14

{ char *p; ... p = NULL ... if (p != NULL { ... } else { ... } }

Allocazione di memoria

Il valore di puntatore nullo NULL è una costante universale che si applica a qualsiasi tipo di puntatore (puntatore a char, a int...). La definizione di NULL è contenuta in stddef.h, stdlib.h, stdio.h e in altri file d’intestazione. È una direttiva del precompilatore espansa in un valore costante corrispondente a un puntatore nullo che dipende dall’implementazione. Un esempio di definizione potrebbe essere: #define NULL 0. La memoria è allocata dinamicamente per mezzo delle funzioni di allocazione malloc e calloc, che hanno le seguenti specifiche: void *malloc(size_t num); num è la quantità di memoria allocata. void *calloc(size_t numEle, size_t eleDim); numEle è il numero di elementi; eleDim è la quantità di memoria per ogni ele-

mento.

Operatore sizeof

Sia malloc sia calloc ritornano un puntatore a carattere che punta alla memoria allocata. Se l’allocazione di memoria non ha successo – o perché non c’è memoria sufficiente, o perché si sono passati parametri sbagliati – le funzioni ritornano il puntatore NULL. Per stabilire la quantità di memoria da allocare è molto spesso utile usare l’operatore sizeof, che si applica nei modi seguenti: sizeof(espressione)

nel qual caso restituisce la quantità di memoria richiesta per memorizzare espressione, oppure: sizeof(T)

nel qual caso restituisce la quantità di memoria richiesta per valori di tipo T. Vediamo un esempio: int main(void) { int i; char a[] = "Quanti caratteri ho?";


14txtI.qxp_Layout 1 14/05/21 09:44 Pagina 241

Puntatori / 241

i = sizeof(int); printf("Gli interi hanno dimensione %d\n", i); i = sizeof(a); printf("L'array: %s ha dimensione = %d", a, i); }

Generalmente l’operatore sizeof dà come risultato la memoria occupata dall’operando a cui è applicato. Il risultato è un valore di tipo size_t. Per esempio, se l’implementazione prevede 4 byte per la memorizzazione del tipo int, sizeof(int) e sizeof(i), dove i è una variabile di tipo int, danno entrambe 4. L’operatore sizeof applicato a un array dà come risultato il numero di byte richiesti dell’array. Caso particolare è sizeof applicato ai char che dà come risultato sempre 1. Gli allocatori malloc e calloc ritornano un puntatore all’oggetto dinamico creato. In realtà, gli allocatori ritornano dei puntatori a void, cioè a tipo generico, perciò devono essere esplicitamente convertiti in un tipo specifico. Il valore che ritorna dagli allocatori di memoria è molto importante perché è solo attraverso di esso che si può far riferimento agli oggetti dinamici. Consideriamo, per esempio, l’istruzione: pi = (int *) malloc (sizeof(int));

che alloca una quantità di memoria sufficiente per accogliere un intero. Questo intero, allocato dinamicamente e di cui non si conosce il nome, può essere raggiunto per mezzo del puntatore pi. L’indirizzo dell’intero è assegnato a pi dopo aver esplicitamente convertito il tipo void *, ritornato da malloc, nel tipo int *, il tipo della variabile pi, mediante la semplice espressione (int *) detta cast. Graficamente l’oggetto dinamico puntato da pi potrebbe essere rappresentato come segue. pi

La scatola vuota simboleggia lo spazio riservato dall’intero. Per poter utilizzare le funzioni di allocazione è necessario includere stdlib.h: #include <stdlib.h>

In implementazioni del C meno recenti la libreria da includere potrebbe essere la libreria malloc.h o stddef.h. Si accede a un oggetto dinamico tramite un puntatore e l’operatore di indirezione. Così, nell’esempio, si accede all’intero puntato da pi con il nome *pi. Per esempio: *pi = 55;

Graficamente l’effetto della precedente assegnazione può essere rappresentato come segue. pi

55

Puntatore all’oggetto dinamico


14txtI.qxp_Layout 1 14/05/21 09:44 Pagina 242

242 / Capitolo 14

Lo stesso valore del puntatore può essere assegnato a più di una variabile puntatore. In tal modo si può far riferimento a un oggetto dinamico con più di un puntatore. Un oggetto cui si fa riferimento con due o più puntatori possiede degli alias. Per esempio, il risultato dell’assegnazione qi = pi;

è di creare due puntatori allo stesso oggetto, cioè due alias. Graficamente l’effetto della precedente assegnazione può essere rappresentato come segue: pi qi

Deallocazione di memoria

55

Gli oggetti dinamici devono essere esplicitamente deallocati dalla memoria se si vuole recuperare dello spazio. La deallocazione esplicita dello spazio di memoria avviene con la funzione free, così specificata: void free(void *ptr)

Se non si esegue questa operazione e si perde il riferimento a quell’area di memoria, lo spazio di memoria rimarrà allocato senza che esso possa essere però utilizzato o riallocato. Per esempio: free(pi);

libera la memoria occupata dall’intero puntato da pi. Occorre prestare molta attenzione a evitare errori del tipo: “fare riferimento a un oggetto che è già stato deallocato”. Alcuni linguaggi, come Lisp, Snobol, Python e Java, prevedono meccanismi automatici di recupero della memoria detti “spazzini” (garbage collector). In C il programmatore deve raccogliere la “spazzatura” da solo.

14.6 Indirizzamento assoluto della memoria Il C è un linguaggio tipicamente usato per la programmazione di sistema, cioè per la programmazione di dispositivi hardware. Un classico problema della programmazione di sistema è l’indirizzamento diretto della memoria. Usando i puntatori e il cast è molto semplice fare riferimento a un indirizzo assoluto di memoria. Per esempio, supponiamo che pt sia un puntatore di tipo T *;

SUGGERIMENTI DI PROGRAMMAZIONE Puntatori multipli allo stesso oggetto Un uso smodato degli alias può deteriorare la leggibilità di un programma. Il fatto di accedere allo stesso oggetto con puntatori diversi può rendere difficoltosa l’analisi locale del programma, cioè la lettura di una porzione di codice senza avere conoscenza del tutto.


14txtI.qxp_Layout 1 14/05/21 09:44 Pagina 243

Puntatori / 243

SUGGERIMENTI DI PROGRAMMAZIONE Uso di oggetti dinamici non allocati Occorre fare attenzione: uno degli errori più frequenti è il tentativo di usare oggetti non allocati in memoria. Sperimenteremo ampiamente e in diverse direzioni i puntatori nei capitoli dedicati alle strutture dati come le liste, gli alberi e i grafi; se modificando uno degli esempi proposti o risolvendo gli esercizi riscontrassimo dei problemi, una delle prime cose da chiederci sarà: abbiamo allocato la memoria (con malloc, calloc o altre funzioni atte allo scopo)?

questo puntatore lo si fa puntare alla locazione in memoria 0777000 nel seguente modo: pt = (T *) 0777000;

Questa tecnica è comunemente usata nella costruzione di driver. Anche nel caso del vecchio sistema operativo MS-DOS era pratica comune accedere direttamente alla memoria video per la gestione degli output su video. Occorre però tenere presente che la maggior parte dei sistemi operativi impedisce al programmatore la gestione diretta dell’hardware. È infatti il sistema operativo che offre l’interfaccia verso l’hardware, mettendo a disposizione delle funzioni le cui invocazioni sono dette chiamate di sistema.

14.7 Funzioni sulla memoria Sempre nella libreria string.h sono disponibili alcune funzioni standard per la gestione della memoria. Una caratteristica comune a queste funzioni è che lavorano su dati in formato binario: non si preoccupano del tipo degli oggetti puntati dai loro parametri, il che è reso possibile dal fatto che i puntatori alle zone di memoria sono di tipo void. Questa caratteristica potrebbe risultare interessante per la realizzazione di codice di basso livello che svolga compiti tanto elementari quanto essenziali come la copia del contenuto di un blocco di memoria da una posizione a un’altra, senza che nulla sia noto sui valori in gioco. memcpy(destinazione, sorgente, n);

copia i valori di n byte a partire dalla locazione puntata da sorgente al blocco di memoria puntato da destinazione, restituisce il puntatore all’oggetto risultante. La funzione copia esattamente n caratteri senza verificare la presenza di un eventuale carattere terminatore; n è di tipo size_t. Se sorgente e destinazione coincidono il risultato non è definito, in tali casi si usa la funzione memmove. Restituisce un puntatore a destinazione. Il Listato 14.4 mostra un esempio d’uso di memcpy; la sua esecuzione visualizza: sorgente: BATUFFO


14txtI.qxp_Layout 1 14/05/21 09:44 Pagina 244

244 / Capitolo 14

Listato 14.4 Esempio di uso della funzione memcpy.

/* Esempio di uso di memcpy */ #include <stdio.h> #include <string.h> int main(void) { char sorg[]="BATUFFO"; char dest[40]; char dest2[40]; char pausa; memcpy (dest, sorg, strlen(sorg)+1); memcpy (dest2, "Copia riuscita", 15); printf ("sorgente: %s\ndestinazione: %s\n", sorg, dest); printf ("destinazione2: %s\n", dest2); scanf("%c", &pausa); }

destinazione: BATUFFO destinazione2: Copia riuscita memmove(destinazione, sorgente, n);

copia i valori di n byte a partire dalla locazione puntata da sorgente al blocco di memoria puntato da destinazione, restituisce il puntatore all’oggetto risultante. La copia è eseguita come se i caratteri dell’oggetto puntato da sorgente fossero ricopiati prima in un buffer temporaneo e da questo nell’oggetto puntato da destinazione, in modo che sorgente e destinazione possano coincidere. Per il resto agisce come memcpy. Si veda l’esempio d’uso di memmove del Listato 14.5 la cui esecuzione visualizza: Prima: gattonava cespuglio per nascondersi fuggire Dopo: gattonava cespuglio cespuglio per nascondersi fuggire r = memcmp(uno, due, n);

confronta i primi n caratteri degli oggetti puntati da uno e due, restituisce un valore uguale, minore o maggiore di zero rispettivamente se uno è uguale, minore o maggiore di due. Il Listato 14.6 contiene un esempio d’utilizzo di memcmp; la sua esecuzione visualizza: –1 1

Il valore numerico del risultato può dipendere dall’implementazione; nell’esempio 1 poteva essere –25 25, quel che conta è il segno negativo/positivo. r = memcmp(uno, due, 1);


14txtI.qxp_Layout 1 14/05/21 09:44 Pagina 245

Puntatori / 245

Listato 14.5 Esempio di uso della funzione memmove.

/* Esempio di uso di memmove */ #include <stdio.h> #include <string.h> int main(void) { char sorgDest[100]= "gattonava cespuglio per nascondersi fuggire"; char pausa; printf ("Prima: %s\n", sorgDest); memmove(sorgDest+20, sorgDest+10, 34); printf ("Dopo: %s\n", sorgDest); scanf("%c", &pausa); }

restituice –1 poiché A è minore di Z. r = memcmp(due, uno, 1);

restituisce 1, cioè +1, poiché Z è maggiore di A. memchr(s, valore, n);

cerca nei primi n caratteri dell’oggetto puntato da s la prima occorrenza di valore, convertito in un unsigned char (tipo carattere senza segno). In caso di Listato 14.6 Esempio di uso della funzione memcmp.

/* Esempio di uso di memcmp */ #include <stdio.h> #include <string.h> int main(void) { char uno[]= "A"; char due[]= "Z"; int r; char pausa; r = memcmp(uno, due, 1); printf ("%i ", r); r = memcmp(due, uno, 1); printf ("%i\n", r); scanf("%c", &pausa); }

RICHIAMO unsigned int e unsigned char Capitolo 18 Paragrafo 2


14txtI.qxp_Layout 1 14/05/21 09:44 Pagina 246

246 / Capitolo 14

esito positivo è restituito un puntatore alla locazione di valore nell’oggetto puntato da s, in caso contrario NULL. memset(s, valore, n);

assegna valore (convertito in un unsigned char) ai primi n caratteri del blocco di memoria puntato da s. Restituisce un puntatore all’oggetto risultante.

Verifica le conoscenze 1. Che cosa si intende per puntatore? 2. Come si dichiarano le variabili di tipo puntatore e come si può fare riferimento al valore di una variabile tramite puntatore? 3. Quali sono le differenze tra l’operatore & e l’operatore *? 4. Che cos’è il nome di un array? In che cosa differisce da un puntatore? Spiegarlo con un esempio. 5. Nel caso si utilizzi un puntatore per far riferimento agli elementi di un array, si hanno maggiori garanzie da parte del compilatore sul controllo di non “sconfinamento” dei limiti dell’array? 6. Quali operatori possono essere applicati ai puntatori e quando possono essere utilizzati? 7. Che cosa si intende per passaggio dei parametri per indirizzo? 8. Quando serve il passaggio dei parametri per indirizzo? Descrivere alcuni casi in cui il passaggio per valore non otterrebbe l’effetto desiderato. 9. Esistono delle alternative al passaggio dei parametri per indirizzo per fare in modo che una funzione modifichi il valore di una variabile del programma chiamante? 10. Scrivere il prototype di almeno cinque funzioni di libreria di manipolazione stringa. 11. Che cos’è la conversione esplicita di tipo? Indicare due casi in cui serve il cast. 12. Che cosa sono gli oggetti dinamici? Come si allocano e deallocano? 13. Quale significato ha il tipo void *? 14. Come si effettua l’indirizzo assoluto di memoria, e a che cosa serve? Normalmente, i sistemi operativi permettono di accedere direttamente alla memoria fisica?

Applica le abilità Soluzioni sul sito web www.mheducation.it

Risolvere i seguenti problemi utilizzando i puntatori. 1. Scrivere un programma che esegua la scansione e la visualizzazione di un vettore di interi. 2. Scrivere un programma che esegua la scansione e la visualizzazione di un vettore di stringhe.


14txtI.qxp_Layout 1 14/05/21 09:44 Pagina 247

Puntatori / 247

3. Scrivere una funzione che ritorni un puntatore alla prima occorrenza della stringa t in una stringa s. Se la stringa t non è contenuta in s allora la funzione deve ritornare un puntatore NULL. 4. Scrivere almeno tre differenti versioni di una funzione che effettui la copia di una stringa su un’altra. 5. Scrivere un programma che prenda in ingresso la dimensione di un buffer e la allochi dinamicamente. 6. Utilizzando la funzione memcpy copiare i primi quattro caratteri di cifre in decimali: char cifre[]="1234567890"; char decimali[10]; e visualizzare decimali.

7. Data la definizione: char y[100]= "1234567890"; modificare il contenuto di y in "1234123490" utilizzando la funzione memmove e visualizzare y. 8. Utilizzare memcmp per verificare se “santo” è maggiore o minore di “santi”. 9. Utilizzare memchr per verificare se ‘2’ è un valore presente in "1 2 3". Suc-

cessivamente si provi a trovare anche la posizione in cui è presente. 10. Utilizzando la funzione memset per copiare ‘9’ nei primi due caratteri di cifre: char cifre[]="1234567890"; e visualizzare cifre.


14txtI.qxp_Layout 1 14/05/21 09:44 Pagina 248

Caso di studio II Gestione di una sequenza con uso dei puntatori

Problema Riprendiamo il Caso di studio I in cui abbiamo realizzato un programma per la gestione di una sequenza tramite un menu con le opzioni di immissione, ordinamento, ricerca completa e ricerca binaria. In quella sede l’array che conteneva la sequenza era una variabile globale a cui tutte le funzioni accedevano direttamente. Adesso apportiamo le modifiche necessarie perché il tutto avvenga mediante un array locale alla funzione gestioneSequenza con il passaggio esplicito del suo indirizzo a tutte le altre funzioni. Questa soluzione è migliore in termini di chiarezza, modularità e riusabilità del codice: le funzioni non dipendono da nessuna variabile generale, la “frontiera” – l’interfaccia con il chiamante – è chiaramente espressa nei parametri in ingresso e dal risultato di ritorno.

Analisi e progetto

Dichiarazione delle funzioni

Aggiungiamo ai parametri delle funzioni l’indirizzo dell’array in modo che queste vi possano accedere direttamente. Dichiariamo le funzioni in modo da includere il parametro puntatore all’array: int immissione(int *); void ordinamento(int, int *); int ricerca(int, int , int *); int ricBin(int, int , int *); void visualizzazione(int, int *);

Alle dichiarazioni abbiamo aggiunto int *, per indicare che quel parametro sarà un puntatore a un oggetto di tipo int. Definiamo l’array sequenza locale alla funzione gestioneSequenza (invece che all’inizio del programma prima del main, come abbiamo fatto nel Caso di studio I): void gestioneSequenza() { int sequenza[MAX_ELE]; /* array che ospita la sequenza */ ...

Sostituiamo in gestioneSequenza al precedente nome il nuovo nome dell’array: sequenza.


14txtI.qxp_Layout 1 14/05/21 09:44 Pagina 249

Gestione di una sequenza con uso dei puntatori  / 249

Al momento della chiamata delle funzioni passiamo l’array come parametro attuale: case 1: n = immissione(sequenza); case 2: ordinamento(n, sequenza); posizione = ricerca(n, ele, sequenza); posizione = ricBin(n, ele, sequenza); case 5: visualizzazione(n, sequenza);

Nella definizione delle funzioni, a sua volta, esplicitiamo un nuovo parametro formale: int immissione(int *vet) void ordinamento(int n, int *vet) int ricBin(int n, int ele, int *vet) void visualizzazione(int n, int *vet)

Sorprendentemente, all’interno di ogni funzione, non cambia niente; infatti vet è una variabile puntatore all’array: i sottoprogrammi possono accedere e modificare il contenuto dell’array. Essendo vet una parametro in ingresso, le funzioni agiscono sul valore locale di vet, ma non possono modificare il parametro attuale, nell’esempio l’indirizzo di sequenza.

Diagramma delle interazioni fra i moduli Il significato di questo tipo di diagramma è già stato spiegato nel Caso di studio I. Si noti come le funzioni di immissione, ordinamento, ricerca e visualizzazione accettino in ingresso un parametro in più rispetto al Caso di studio I: int *vet, il puntatore all’array che contiene la sequenza. int immissione(int *vet) int main(void)

n

void gestioneSequenza()

Posizione Pos iz

ione

void ordinamento(int n, int *vet)

int ricerca(int n, int ele, int *vet)

int ricBin( intn, int ele, int *vet)

void visualizzazione(int n, int *vet)

Invocazione delle funzioni

Definizione delle funzioni


14txtI.qxp_Layout 1 14/05/21 09:44 Pagina 250

250 / Caso di studio II

SUGGERIMENTI DI PROGRAMMAZIONE Interazione chiara con l’utente L’utente prima di utilizzare la ricerca binaria deve ricordarsi di ordinare il vettore. Per prevenire un possibile errore in questo senso potremmo imporre l’ordinamento aggiungendo la chiamata a ordinamento all’inizio di ricBin. int ricBin(int n, int ele, int *vet) { ... ordinamento(n, vet); ...

In questo modo però ordineremmo il vettore tutte le volte che si effettua una ricerca, il che non è accettabile in generale. Negli esercizi sono indicate altre strade più consone a questo fine, alcune delle quali costringono ad alcune modifiche importanti al programma.

Sviluppo Listato Caso di studio II Gestione di una sequenza con il passaggio di un puntatore ad array alle funzioni di immissione, ordinamento, ricerca completa, ricerca binaria e visualizzazione.

#include <stdio.h> #define MAX_ELE 1000 /* massimo numero di elementi */ void gestioneSequenza(void); int immissione(int *); void ordinamento(int, int *);tt int ricerca(int, int , int *); int ricBin(int, int , int *); void visualizzazione(int, int *); int main(void) { gestioneSequenza(); } void gestioneSequenza() { int sequenza[MAX_ELE]; /* array che ospita la sequenza */ int n; int scelta = –1; char invio; int ele, posizione;


14txtI.qxp_Layout 1 14/05/21 09:44 Pagina 251

Gestione di una sequenza con uso dei puntatori  / 251

while(scelta != 0) { printf("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"); printf("\t\t\t GESTIONE SEQUENZA"); printf("\n\n\n\t\t\t 1. Immissione"); printf("\n\n\t\t\t 2. Ordinamento"); printf("\n\n\t\t\t 3. Ricerca completa"); printf("\n\n\t\t\t 4. Ricerca binaria"); printf("\n\n\t\t\t 5. Visualizzazione"); printf("\n\n\t\t\t 0. fine"); printf("\n\n\n\t\t\t\t Scegliere una opzione: "); scanf("%d", &scelta); scanf("%c", &invio); printf("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"); switch(scelta) { case 1: n = immissione(sequenza); break; case 2: ordinamento(n, sequenza); break; case 3: printf("Elemento da ricercare: "); scanf("%d", &ele); scanf("%c", &invio); posizione = ricerca(n, ele, sequenza); if(ele = = sequenza[posizione]) printf("\nElem. %d presente in posizione %d\n", ele, posizione); else printf("\nElemento non presente!\n"); printf("\n\n Premere Invio per continuare..."); scanf("%c", &invio); break; case 4: printf("Elemento da ricercare: "); scanf("%d", &ele); scanf("%c", &invio); posizione = ricBin(n, ele, sequenza); if(posizione != –1) printf("\nElem. %d presente in posizione %d\n", ele, posizione); else printf("\nElemento non presente!\n"); printf("\n\n Premere Invio per continuare..."); scanf("%c", &invio); break; case 5: visualizzazione(n, sequenza); break; } }


14txtI.qxp_Layout 1 14/05/21 09:44 Pagina 252

252 / Caso di studio II

} int immissione(int *vet) { int i, n; do { printf("\nNumero elementi: "); scanf("%d", &n); } while (n < 1 || n > MAX_ELE); for(i = 0; i < n; i++) { printf("\nImmettere un intero n.%d: ",i); scanf("%d", &vet[i]); } return(n); } void ordinamento(int n, int *vet) { int i, p, k, n1; int aux; p = n; n1 = p; do { k = 0; for(i = 0; i < n1-1; i++) if(vet[i] > vet[i+1]) { aux = vet[i]; vet[i] = vet[i+1]; vet[i+1] = aux; k = 1; p = i + 1; } n1 = p; } while (k = = 1 && n1>1); } /* Ricerca sequenziale */ int ricerca(int n, int ele, int *vet) { int i; i = 0; while (ele != vet[i] && i < n-1) ++i; return(i); } /* ricerca binaria */ int ricBin(int n, int ele, int *vet) { int i, alto, basso, pos;


14txtI.qxp_Layout 1 14/05/21 09:44 Pagina 253

Gestione di una sequenza con uso dei puntatori  / 253

alto = 0; basso = n – 1; pos = –1; do { i = (alto+basso)/2; if(vet[i] = = ele) pos = i; else if(vet[i] < ele) alto = i + 1; else basso = i – 1; } while(alto <= basso && pos = = –1); return(pos); } void visualizzazione( int n, int *vet) { int i; char invio; for(i = 0; i < n; i++) printf("\n%d", vet[i]); printf("\n\n Premere Invio per continuare..."); scanf("%c", &invio); }

Esercizi Soluzioni sul sito web www.mheducation.it

1. Realizzare un programma che: a) richieda all’utente una sequenza di numeri interi e la memorizzi in un array; b) richieda all’utente un’altra sequenza di numeri interi e la memorizzi in un array; c) effettui il merge delle due sequenze in un terzo array; d) visualizzi i tre array. Per i punti a) e b) si utilizzi la stessa funzione passandogli l’indirizzo dell’array. 2. Come l’Esercizio 1 facendo in modo che all’inizio l’utente immetta due array, si effettui il merge e si visualizzi, e che in seguito l’utente possa immettere un altro vettore e il programma effettui il merge fra questo e il risultato del precedente merge. Il procedimento si ripete fino a che l’utente non decide di smettere. 3. Modificare il programma di gestione sequenza di questo Caso di studio in modo che, se l’utente sceglie la ricerca binaria prima di aver ordinato l’array, appaia il messaggio: Ricerca binaria non applicabile, l'array deve essere ordinato. Premere Invio per continuare...


14txtI.qxp_Layout 1 14/05/21 09:44 Pagina 254

254 / Caso di studio II

e si torna al menu principale. Naturalmente se l’utente ha anteriormente ordinato l’array, il messaggio non appare e la ricerca viene permessa. 4. Realizzare un programma che utilizzi funzioni e il passaggio di array per indirizzo per consentire all’utente di svolgere le operazioni di questo menu: 1. Immettere due matrici 2. Moltiplicare la prima matrice per la seconda 3. Visualizzare la prima matrice 4. Visualizzare la seconda matrice 5. Visualizzare la matrice prodotto 0. Fine Scegliere una opzione:

Per l’immissione delle due matrici si utilizzi una funzione che richieda correttamente le dimensioni (solo due dimensioni non quattro... si veda il Capitolo 10) e che invoca due volte la funzione di immissioneMatrice per caricare la prima e la seconda matrice, passandogli le dimensioni corrette. Per la visualizzazione delle tre matrici si utilizzi la stessa funzione passandogli di volta in volta in ingresso l’indirizzo della matrice d’interesse. 5. Realizzare un programma che utilizzi funzioni e il passaggio di array per indirizzo per consentire all’utente di svolgere le operazioni di questo menu: 1. Immettere una matrice 2. Ricercare un elemento nella matrice 3. Visualizzare la matrice 0. Fine Scegliere una opzione:

Se e solo se è richiesta la ricerca di un elemento prima dell’immissione della matrice il programma avvia la funzione di immissione della matrice e poi richiede all’utente l’elemento da ricercare. La funzione di ricerca in caso di esito positivo restituisce la riga e la colonna dove ha reperito l’elemento. 6. Realizzare un programma che utilizzi funzioni e il passaggio di array per indirizzo per consentire all’utente di svolgere le operazioni di questo menu: 1. Immettere una matrice 2. Ordinare le colonne 3. Ordinare le righe 4. Visualizzare la matrice 0. Fine Scegliere una opzione:

L’ordinamento delle colonne ordina i valori di ogni colonna, lo stesso dicasi per le righe, naturalmente un ordinamento in generale scombina l’altro. 7. Modificare il programma gestioneSequenza del Caso di studio II in modo che la funzione di immissione sequenza non ritorni alcun valore ma abbia in ingresso il puntatore alla variabile intera n di gestioneSequenza per poterla modificare.


Turn static files into dynamic content formats.

Create a flipbook
Issuu converts static files into: digital portfolios, online yearbooks, online catalogs, digital photo albums and more. Sign up and create your flipbook.