Array in C++

Un array è una struttura dati simile al tipo vector della libreria del C++ ma offre un compromesso diverso tra prestazioni e flessibilità.

Come un vector, un array è un contenitore di oggetti senza nome di un singolo tipo a cui accediamo tramite indice. A differenza di un vector, gli array hanno dimensione fissa; non possiamo aggiungere elementi a un array. Poiché gli array hanno dimensione fissa, a volte offrono migliori prestazioni a runtime per applicazioni specializzate. Tuttavia, quel vantaggio prestazionale viene al costo di una perdita di flessibilità.

Consiglio

Usare un vector al posto di un array è quasi sempre la scelta migliore.

Se non sai esattamente quanti elementi ti servono, usa un vector.

Concetti Chiave
  • Gli array sono contenitori di oggetti di un singolo tipo a cui si accede tramite indice.
  • Gli array hanno dimensione fissa, che deve essere nota a tempo di compilazione.
  • Gli array non supportano operazioni di copia o assegnamento.
  • Gli array possono essere inizializzati tramite liste di inizializzatori.
  • Gli array possono contenere oggetti di quasi ogni tipo, ma non esistono array di riferimenti.
  • Gli array possono essere usati in modo simile ai vector, ma non offrono la stessa flessibilità.

Definizione e Inizializzazione degli Array

Gli array sono un tipo composto. Un dichiaratore di array ha la forma a[d], dove a è il nome che viene definito e d è la dimensione dell'array. La dimensione specifica il numero di elementi e deve essere maggiore di zero. Il numero di elementi in un array fa parte del tipo dell'array. Di conseguenza, la dimensione deve essere nota a tempo di compilazione, il che significa che la dimensione deve essere un'espressione costante:

// non è un'espressione costante
unsigned cnt = 42;

// espressione costante
constexpr unsigned sz = 42;

// array di dieci int
int arr[10];

// array di 42 puntatori a int
int *parr[sz];

// ERRORE: cnt non è un'espressione costante
string bad[cnt];

// ok se get_size è constexpr, errore altrimenti
string strs[get_size()];

Per impostazione predefinita, gli elementi in un array sono inizializzati per default.

Come per le variabili di tipo built-in, un array di tipo built-in inizializzato per default e definito all'interno di una funzione avrà valori indefiniti.

Quando definiamo un array, dobbiamo specificare un tipo per l'array. Non possiamo usare auto per dedurre il tipo da una lista di inizializzatori. Come per vector, gli array contengono oggetti. Pertanto, non esistono array di riferimenti.

Inizializzazione Esplicita degli Elementi di un Array

Possiamo inizializzare tramite lista gli elementi in un array. Quando lo facciamo, possiamo omettere la dimensione. Se omettiamo la dimensione, il compilatore la deduce dal numero di inizializzatori. Se specifichiamo una dimensione, il numero di inizializzatori non deve superare la dimensione specificata. Se la dimensione è maggiore del numero di inizializzatori, gli inizializzatori vengono usati per i primi elementi e tutti gli elementi rimanenti sono inizializzati per valore:

const unsigned sz = 3;

// array di tre int con valori 0, 1, 2
int ia1[sz] = {0,1,2};

// un array di dimensione 3
int a2[] = {0, 1, 2};

// equivalente a a3[] = {0, 1, 2, 0, 0}
int a3[5] = {0, 1, 2};

// come a4[] = {"ciao", "bye", ""}
string a4[3] = {"ciao", "bye"};

// ERRORE: troppi inizializzatori
int a5[2] = {0,1,2};

Gli Array di Caratteri Sono Speciali

Gli array di caratteri hanno una forma aggiuntiva di inizializzazione: Possiamo inizializzare tali array da un letterale stringa. Quando usiamo questa forma di inizializzazione, è importante ricordare che i letterali stringa terminano con un carattere nullo, \0. Quel carattere nullo viene copiato nell'array insieme ai caratteri nel letterale:

// inizializzazione a lista, nessun nullo esplicito
char a1[] = {'C', '+', '+'};

// inizializzazione a lista, nullo esplicito
char a2[] = {'C', '+', '+', '\0'};

// terminatore nullo aggiunto automaticamente
char a3[] = "C++";

// ERRORE: non c'è spazio per il null!
const char a4[5] = "Mario";

La dimensione di a1 è 3; le dimensioni di a2 e a3 sono entrambe 4. La definizione di a4 è errata. Sebbene il letterale contenga solo cinque caratteri espliciti, la dimensione dell'array deve essere almeno sei, cinque per contenere il letterale e uno per il carattere nullo alla fine.

Nessuna Copia o Assegnamento per gli Array

Non possiamo inizializzare un array come copia di un altro array, né è legale assegnare un array a un altro:

// array di tre int
int a[] = {0, 1, 2};

// ERRORE: non si può inizializzare un array con un altro
int a2[] = a;

// ERRORE: non si può assegnare un array a un altro
a2 = a;

Alcuni compilatori permettono l'assegnamento di array come estensione del compilatore. Di solito è una buona idea evitare di usare funzionalità non standard. I programmi che usano tali funzionalità non funzioneranno con un compilatore diverso.

Comprendere Dichiarazioni di Array Complesse

Come i vector, gli array possono contenere oggetti di quasi ogni tipo. Ad esempio, possiamo avere un array di puntatori. Poiché un array è un oggetto, possiamo definire sia puntatori che riferimenti ad array. Definire array che contengono puntatori è abbastanza semplice, definire un puntatore o riferimento a un array è un po' più complicato:

// ptrs è un array di dieci puntatori a int
int *ptrs[10];

// ERRORE: non esistono array di riferimenti
int &refs[10] = /* ? */;

// Parray è un puntatore a un array di dieci int
int (*Parray)[10] = &arr;

// arrRef si riferisce a un array di dieci int
int (&arrRef)[10] = arr;

Per impostazione predefinita, i modificatori di tipo si associano da destra a sinistra. Leggere la definizione di ptrs da destra a sinistra è facile: Vediamo che stiamo definendo un array di dimensione 10, chiamato ptrs, che contiene puntatori a int.

Leggere la definizione di Parray da destra a sinistra non è altrettanto utile. Poiché la dimensione dell'array segue il nome che viene dichiarato, può essere più facile leggere le dichiarazioni di array dall'interno verso l'esterno piuttosto che da destra a sinistra. Leggere dall'interno verso l'esterno rende molto più facile comprendere il tipo di Parray. Iniziamo osservando che le parentesi intorno a *Parray significano che Parray è un puntatore. Guardando a destra, vediamo che Parray punta a un array di dimensione 10. Guardando a sinistra, vediamo che gli elementi in quell'array sono int. Quindi, Parray è un puntatore a un array di dieci int. Similmente, (&arrRef) dice che arrRef è un riferimento. Il tipo a cui si riferisce è un array di dimensione 10. Quell'array contiene elementi di tipo int.

Naturalmente, non ci sono limiti a quanti modificatori di tipo possono essere usati:

// arry è un riferimento a un array di dieci puntatori
int *(&arry)[10] = ptrs;

Leggendo questa dichiarazione dall'interno verso l'esterno, vediamo che arry è un riferimento. Guardando a destra, vediamo che l'oggetto a cui arry si riferisce è un array di dimensione 10. Guardando a sinistra, vediamo che il tipo elemento è puntatore a int. Quindi, arry è un riferimento a un array di dieci puntatori.

Può essere più facile comprendere le dichiarazioni di array partendo dal nome dell'array e leggendole dall'interno verso l'esterno.

Esercizi

  • Quali sono i valori nei seguenti array?

    string sa[10];
    int ia[10];
    int main() {
        string sa2[10];
        int ia2[10];
    }
    

    Soluzione:

    Gli array sa e sa2 sono array di stringhe definite a livello globale e locale, rispettivamente. Entrambi gli array sono inizializzati per default, quindi tutti i loro elementi sono stringhe vuote ("").

    Gli array ia e ia2 sono array di interi definiti a livello globale e locale, rispettivamente. L'array ia, essendo definito a livello globale, è inizializzato per default, quindi tutti i suoi elementi sono 0. Tuttavia, l'array ia2, essendo definito all'interno della funzione main, non è inizializzato per default e contiene valori indefiniti (spazzatura).

Accesso agli Elementi di un Array

Come per i tipi vector e string della libreria, possiamo usare un range for o l'operatore indice per accedere agli elementi di un array. Come al solito, gli indici partono da 0. Per un array di dieci elementi, gli indici vanno da 0 a 9, non da 1 a 10.

Quando usiamo una variabile per indicizzare un array, normalmente dovremmo definire quella variabile di tipo size_t. size_t è un tipo unsigned specifico della macchina che è garantito essere abbastanza grande da contenere la dimensione di qualsiasi oggetto in memoria. Il tipo size_t è definito nell'header cstddef, che è la versione C++ dell'header stddef.h della libreria C.

Con l'eccezione che gli array hanno dimensione fissa, usiamo gli array in modi simili a come usiamo i vector. Ad esempio, possiamo reimplementare il nostro programma di valutazione realizzato nella lezione sui vector per usare un array per contenere i contatori dei cluster:

// conta il numero di voti per cluster di dieci:
// 0--9, 10--19, . . . 90--99, 100

// 11 bucket, tutti inizializzati per valore a 0
unsigned punteggi[11] = {};
unsigned voto;

while (cin >> voto) {
     if (voto <= 100)
          ++punteggi[voto/10]; // incrementa il contatore per il cluster corrente
}

L'unica differenza ovvia tra questo programma e quello implementato con i vector è la dichiarazione di punteggi. In questo programma punteggi è un array di 11 elementi unsigned. La differenza non così ovvia è che l'operatore indice in questo programma è quello definito come parte del linguaggio. Questo operatore può essere usato su operandi di tipo array. L'operatore indice usato nel programma con i vector era definito dal template vector della libreria e si applica agli operandi di tipo vector.

Come nel caso di string o vector, è meglio usare un range for quando vogliamo scorrere l'intero array. Ad esempio, possiamo stampare i punteggi risultanti come segue:

// per ogni contatore in punteggi
for (auto i : punteggi)
    // stampa il valore di quel contatore
    cout << i << " ";
cout << endl;

Poiché la dimensione fa parte di ogni tipo array, il sistema sa quanti elementi ci sono in punteggi. Usare un range for significa che non dobbiamo gestire l'attraversamento da soli.

Controllo degli Indici

Come per string e vector, spetta al programmatore assicurare che l'indice sia nell'intervallo, cioè, che il valore dell'indice sia uguale o maggiore di zero e minore della dimensione dell'array. Niente impedisce a un programma di oltrepassare un limite dell'array eccetto un'attenta attenzione ai dettagli e test approfonditi del codice. È possibile che i programmi compilino ed eseguano ma siano comunque fatalmente errati.

La fonte più comune di problemi di sicurezza sono i bug di buffer overflow. Tali bug si verificano quando un programma non controlla un indice e usa erroneamente memoria al di fuori dell'intervallo di un array o di una struttura dati simile.

Esercizi

  • Identifica gli errori di indicizzazione nel seguente codice:

    constexpr size_t array_size = 10;
    int ia[array_size];
    for (size_t ix = 1; ix <= array_size; ++ix)
        ia[ix] = ix;
    

    Soluzione:

    Il ciclo for inizia con ix uguale a 1 e continua fino a quando ix è minore o uguale a array_size. Poiché gli indici degli array partono da 0, l'indice massimo valido per l'array ia è 9. Quando ix è uguale a 10, il programma tenta di accedere a ia[10], che è fuori dai limiti dell'array, causando un comportamento indefinito. Per correggere questo errore, il ciclo dovrebbe iniziare con ix uguale a 0 e continuare fino a quando ix è minore di array_size:

    for (size_t ix = 0; ix < array_size; ++ix)
        ia[ix] = ix;
    
  • Scrivi un programma per definire un array di dieci int. Assegna a ciascun elemento lo stesso valore della sua posizione nell'array.

    Soluzione:

    #include <iostream>
    using namespace std;
    
    int main() {
        const size_t array_size = 10;
        int arr[array_size];
    
        // Assegna a ciascun elemento il valore della sua posizione
        for (size_t i = 0; i < array_size; ++i) {
            arr[i] = i;
        }
    
        // Stampa gli elementi dell'array
        for (size_t i = 0; i < array_size; ++i) {
            cout << "arr[" << i << "] = " << arr[i] << endl;
        }
    
        return 0;
    }
    
  • Copia l'array che hai definito nell'esercizio precedente in un altro array. Riscrivi il tuo programma per usare vector.

    Soluzione:

    Usando array:

    #include <iostream>
    using namespace std;
    
    int main() {
        const size_t array_size = 10;
        int arr1[array_size];
        int arr2[array_size];
    
        // Assegna a ciascun elemento il valore della sua posizione in arr1
        for (size_t i = 0; i < array_size; ++i) {
            arr1[i] = i;
        }
    
        // Copia arr1 in arr2
        for (size_t i = 0; i < array_size; ++i) {
            arr2[i] = arr1[i];
        }
    
        // Stampa gli elementi di arr2
        for (size_t i = 0; i < array_size; ++i) {
            cout << "arr2[" << i << "] = " << arr2[i] << endl;
        }
    
        return 0;
    }
    

    Usando vector:

    #include <iostream>
    #include <vector>
    using namespace std;
    
    int main() {
        const size_t vector_size = 10;
        vector<int> vec1(vector_size);
        vector<int> vec2(vector_size);
    
        // Assegna a ciascun elemento il valore della sua posizione in vec1
        for (size_t i = 0; i < vector_size; ++i) {
            vec1[i] = i;
        }
    
        // Copia vec1 in vec2
        vec2 = vec1;
    
        // Stampa gli elementi di vec2
        for (size_t i = 0; i < vector_size; ++i) {
            cout << "vec2[" << i << "] = " << vec2[i] << endl;
        }
    
        return 0;
    }
    

Puntatori e Array

In C++ puntatori e array sono strettamente intrecciati. In particolare, come vedremo, quando usiamo un array, il compilatore normalmente converte l'array in un puntatore.

Normalmente, otteniamo un puntatore a un oggetto usando l'operatore indirizzo (&). Generalmente parlando, l'operatore indirizzo può essere applicato a qualsiasi oggetto. Gli elementi in un array sono oggetti. Quando indicizziamo un array, il risultato è l'oggetto in quella posizione nell'array. Come per qualsiasi altro oggetto, possiamo ottenere un puntatore a un elemento di array prendendo l'indirizzo di quell'elemento:

// array di stringhe
string numeri[] = {"uno", "due", "tre"};

// p punta al primo elemento in numeri
string *p = &numeri[0];

Tuttavia, gli array hanno una proprietà speciale: nella maggior parte dei casi quando usiamo un array, il compilatore sostituisce automaticamente un puntatore al primo elemento:

string *p2 = numeri; // Equivalente a p2 = &numeri[0];

Nella maggior parte delle espressioni, quando usiamo un oggetto di tipo array, stiamo realmente usando un puntatore al primo elemento in quell'array.

Ci sono varie implicazioni del fatto che le operazioni sugli array sono spesso realmente operazioni sui puntatori. Una tale implicazione è che quando usiamo un array come inizializzatore per una variabile definita usando auto, il tipo dedotto è un puntatore, non un array:

// ia è un array di dieci int
int ia[] = {0,1,2,3,4,5,6,7,8,9};

// ia2 è un int* che punta al primo elemento in ia
auto ia2(ia);

// ERRORE: ia2 è un puntatore, e non possiamo assegnare un int a un puntatore
ia2 = 42;

Sebbene ia sia un array di dieci int, quando usiamo ia come inizializzatore, il compilatore tratta quell'inizializzazione come se avessimo scritto

auto ia2(&ia[0]); // ora è chiaro che ia2 ha tipo int*

Vale la pena notare che questa conversione non avviene quando usiamo decltype. Il tipo restituito da decltype(ia) è array di dieci int:

// ia3 è un array di dieci int
decltype(ia) ia3 = {0,1,2,3,4,5,6,7,8,9};

// ERRORE: non si può assegnare un int* a un array
ia3 = p;

// OK: assegna il valore di i a un elemento in ia3
ia3[4] = i;

I Puntatori Sono Iteratori

I puntatori che indirizzano elementi in un array hanno operazioni aggiuntive oltre a quelle che abbiamo descritto. In particolare, i puntatori agli elementi di array supportano le stesse operazioni degli iteratori su vector o string. Ad esempio, possiamo usare l'operatore di incremento per spostarci da un elemento in un array al successivo:

int arr[] = {0,1,2,3,4,5,6,7,8,9};

// p punta al primo elemento in arr
int *p = arr;

// p punta a arr[1]
++p;

Proprio come possiamo usare gli iteratori per attraversare gli elementi in un vector, possiamo usare i puntatori per attraversare gli elementi in un array. Naturalmente, per farlo, dobbiamo ottenere puntatori al primo e oltre l'ultimo elemento. Come abbiamo appena visto, possiamo ottenere un puntatore al primo elemento usando l'array stesso o prendendo l'indirizzo del primo elemento. Possiamo ottenere un puntatore off-the-end usando un'altra proprietà speciale degli array. Possiamo prendere l'indirizzo dell'elemento inesistente uno oltre l'ultimo elemento di un array:

// puntatore appena oltre l'ultimo elemento in arr
int *e = &arr[10];

Qui abbiamo usato l'operatore indice per indicizzare un elemento inesistente; arr ha dieci elementi, quindi l'ultimo elemento in arr è alla posizione di indice 9. L'unica cosa che possiamo fare con questo elemento è prenderne l'indirizzo, cosa che facciamo per inizializzare e. Come un iteratore off-the-end, un puntatore off-the-end non punta a un elemento. Di conseguenza, non possiamo dereferenziare o incrementare un puntatore off-the-end.

Usando questi puntatori possiamo scrivere un ciclo per stampare gli elementi in arr come segue:

for (int *b = arr; b != e; ++b)
    // stampa gli elementi in arr
    cout << *b << endl;

Le Funzioni begin ed end della Libreria

Sebbene possiamo calcolare un puntatore off-the-end, farlo è soggetto a errori. Per rendere più facile e sicuro usare i puntatori, la libreria standard del C++ include due funzioni, chiamate begin ed end. Queste funzioni agiscono come i membri del contenitore con nome simile. Tuttavia, gli array non sono tipi classe, quindi queste funzioni non sono funzioni membro. Invece, prendono un argomento che è un array:

// ia è un array di dieci int
int ia[] = {0,1,2,3,4,5,6,7,8,9};

// puntatore al primo elemento in ia
int *beg = begin(ia);

// puntatore uno oltre l'ultimo elemento in ia
int *last = end(ia);

begin restituisce un puntatore al primo, ed end restituisce un puntatore uno oltre l'ultimo elemento nell'array dato: Queste funzioni sono definite nell'header iterator.

Usando begin ed end, è facile scrivere un ciclo per elaborare gli elementi in un array. Ad esempio, assumendo che arr sia un array che contiene valori int, potremmo trovare il primo valore negativo in arr come segue:

// pbeg punta al primo e pend punta appena oltre l'ultimo elemento in arr
int *pbeg = begin(arr), *pend = end(arr);
// trova il primo elemento negativo,
// fermandosi se abbiamo visto tutti gli elementi
while (pbeg != pend && *pbeg >= 0)
    ++pbeg;

Iniziamo definendo due puntatori int chiamati pbeg e pend. Posizioniamo pbeg per denotare il primo elemento e pend per puntare uno oltre l'ultimo elemento in arr. La condizione del while usa pend per sapere se è sicuro dereferenziare pbeg. Se pbeg punta a un elemento, lo dereferenziamo e controlliamo se l'elemento sottostante è negativo. Se sì, la condizione fallisce e usciamo dal ciclo. In caso contrario, incrementiamo il puntatore per guardare l'elemento successivo.

Un puntatore "uno oltre" la fine di un array built-in si comporta nello stesso modo dell'iteratore restituito dall'operazione end di un vector. In particolare, non possiamo dereferenziare o incrementare un puntatore off-the-end.

Aritmetica dei Puntatori

I puntatori che indirizzano elementi di array possono usare tutte le operazioni che gli iteratori supportano. Queste operazioni, dereferenziamento, incremento, confronti, addizione di un valore intero, sottrazione di due puntatori, hanno lo stesso significato quando applicate a puntatori che puntano a elementi in un array built-in come quando applicate agli iteratori.

Quando aggiungiamo (o sottraiamo) un valore intero a (o da) un puntatore, il risultato è un nuovo puntatore. Quel nuovo puntatore punta all'elemento il numero dato di posizioni avanti (o indietro) rispetto al puntatore originale:

constexpr size_t sz = 5;
int arr[sz] = {1,2,3,4,5};

// equivalente a int *ip = &arr[0]
int *ip = arr;

// ip2 punta a arr[4], l'ultimo elemento in arr
int *ip2 = ip + 4;

Il risultato dell'aggiunta di 4 a ip è un puntatore che punta all'elemento quattro elementi più avanti nell'array rispetto a quello a cui ip punta attualmente.

Il risultato dell'aggiunta di un valore intero a un puntatore deve essere un puntatore a un elemento nello stesso array, o un puntatore appena oltre la fine dell'array:

// OK: arr viene convertito in un puntatore al suo primo elemento;
// p punta uno oltre la fine di arr
int *p = arr + sz; // Attenzione: non dereferenziare p!
int *p2 = arr + 10; // ERRORE: arr ha solo 5 elementi; p2 ha valore indefinito

Quando aggiungiamo sz ad arr, il compilatore converte arr in un puntatore al primo elemento in arr. Quando aggiungiamo sz a quel puntatore, otteniamo un puntatore che punta sz posizioni (cioè, 5 posizioni) oltre la prima. Cioè, punta uno oltre l'ultimo elemento in arr. Calcolare un puntatore più di uno oltre l'ultimo elemento è un errore, anche se il compilatore difficilmente rileverà tali errori.

Come per gli iteratori, sottrarre due puntatori ci dà la distanza tra quei puntatori. I puntatori devono puntare a elementi nello stesso array:

// n è 5, il numero di elementi in arr
auto n = end(arr) - begin(arr);

Il risultato della sottrazione di due puntatori è un tipo di libreria chiamato ptrdiff_t. Come size_t, il tipo ptrdiff_t è un tipo specifico della macchina ed è definito nell'header cstddef. Poiché la sottrazione potrebbe produrre una distanza negativa, ptrdiff_t è un tipo intero con segno.

Possiamo usare gli operatori relazionali per confrontare puntatori che puntano a elementi di un array, o uno oltre l'ultimo elemento in quell'array. Ad esempio, possiamo attraversare gli elementi in arr come segue:

int *b = arr, *e = arr + sz;
while (b < e) {
    // usa *b
    ++b;
}

Non possiamo usare gli operatori relazionali su puntatori a due oggetti non correlati:

int i = 0, sz = 42;
int *p = &i, *e = &sz;
// indefinito: p ed e non sono correlati;
// il confronto è privo di significato!
while (p < e)

Sebbene l'utilità possa essere oscura a questo punto, vale la pena notare che l'aritmetica dei puntatori è valida anche per i puntatori null e per i puntatori che puntano a un oggetto che non è un array. Nel secondo caso, i puntatori devono puntare allo stesso oggetto, o uno oltre quell'oggetto. Se p è un puntatore null, possiamo aggiungere o sottrarre un'espressione costante intera il cui valore è 0 a p. Possiamo anche sottrarre due puntatori null l'uno dall'altro, nel qual caso il risultato è 0.

Interazione tra Dereferenziamento e Aritmetica dei Puntatori

Il risultato dell'aggiunta di un valore intero a un puntatore è esso stesso un puntatore. Assumendo che il puntatore risultante punti a un elemento, possiamo dereferenzare il puntatore risultante:

// array con 5 elementi di tipo int
int ia[] = {0,2,4,6,8};

// OK: inizializza last a 8, il valore di ia[4]
int last = *(ia + 4);

L'espressione *(ia + 4) calcola l'indirizzo quattro elementi oltre ia e dereferenzia il puntatore risultante. Questa espressione è equivalente a scrivere ia[4].

Ricorda che che le parentesi sono richieste nelle espressioni che contengono operatori di dereferenziamento e punto. Similmente, le parentesi intorno a questa addizione di puntatori sono essenziali. Scrivere:

last = *ia + 4; // OK: last = 4, equivalente a ia[0] + 4

significa dereferenziare ia e aggiungere 4 al valore dereferenziato. Tratteremo le ragioni di questo comportamento nelle prossime lezioni.

Indicizzazione e Puntatori

Come abbiamo visto, nella maggior parte dei casi quando usiamo il nome di un array, stiamo realmente usando un puntatore al primo elemento in quell'array. Un posto in cui il compilatore fa questa trasformazione è quando indicizziamo un array. Dato:

// array con 5 elementi di tipo int
int ia[] = {0,2,4,6,8};

se scriviamo ia[0], quella è un'espressione che usa il nome di un array. Quando indicizziamo un array, stiamo realmente indicizzando un puntatore a un elemento in quell'array:

int i = ia[2]; // equivalente a int i = *(ia + 2);

int *p = ia;   // p punta al primo elemento in ia
i = *(p + 2);  // equivalente a ia[2]

Possiamo usare l'operatore indice su qualsiasi puntatore, purché quel puntatore punti a un elemento (o uno oltre l'ultimo elemento) in un array:

int *p = &ia[2]; // p punta all'elemento indicizzato da 2
int j = p[1];    // p[1] è equivalente a *(p + 1),
                 // p[1] è lo stesso elemento di ia[3]
int k = p[-2];   // p[-2] è lo stesso elemento di ia[0]

Quest'ultimo esempio evidenzia una differenza importante tra array e tipi di libreria come vector e string che hanno operatori indice. I tipi di libreria forzano l'indice usato ad essere un valore unsigned. L'operatore indice built-in non lo fa. L'indice usato con l'operatore indice built-in può essere un valore negativo. Naturalmente, l'indirizzo risultante deve puntare a un elemento in (o uno oltre la fine di) l'array a cui punta il puntatore originale.

A differenza degli indici per vector e string, l'indice dell'operatore indice built-in non è un tipo unsigned.

Esercizi

  • Dato che p1 e p2 puntano a elementi nello stesso array, cosa fa il seguente codice? Ci sono valori di p1 o p2 che rendono questo codice illegale?

    p1 += p2 - p1;
    

    Soluzione:

    L'espressione p2 - p1 calcola la distanza tra i puntatori p1 e p2, restituendo un valore di tipo ptrdiff_t. Questo valore rappresenta il numero di elementi tra i due puntatori. Quando aggiungiamo questo valore a p1 con l'operatore +=, stiamo effettivamente spostando p1 in avanti di quella distanza, portandolo a puntare alla stessa posizione di p2.

    Questo codice è legale fintanto che p1 e p2 puntano a elementi dello stesso array o uno oltre la fine di quell'array. Se p1 e p2 puntano a oggetti non correlati, il comportamento è indefinito.

  • Usando i puntatori, scrivi un programma per impostare gli elementi in un array a zero.

    Soluzione:

    int arr[5] = {1, 2, 3, 4, 5};
    int *p = arr;
    while (p < arr + 5) {
        *p = 0;
        p++;
    }
    
  • Scrivi un programma per confrontare due array per uguaglianza. Scrivi un programma simile per confrontare due vector.

    Soluzione:

    Per array:

    #include <iostream>
    using namespace std;
    
    bool arraysEqual(int arr1[], int arr2[], size_t size) {
        for (size_t i = 0; i < size; ++i) {
            if (arr1[i] != arr2[i]) {
                return false;
            }
        }
        return true;
    }
    
    int main() {
        int arr1[] = {1, 2, 3, 4, 5};
        int arr2[] = {1, 2, 3, 4, 5};
        size_t size = sizeof(arr1) / sizeof(arr1[0]);
    
        if (arraysEqual(arr1, arr2, size)) {
            cout << "Gli array sono uguali." << endl;
        } else {
            cout << "Gli array sono diversi." << endl;
        }
    
        return 0;
    }
    

    Per vector:

    #include <iostream>
    #include <vector>
    using namespace std;
    
    bool vectorsEqual(const vector<int>& vec1, const vector<int>& vec2) {
        if (vec1.size() != vec2.size()) {
            return false;
        }
        for (size_t i = 0; i < vec1.size(); ++i) {
            if (vec1[i] != vec2[i]) {
                return false;
            }
        }
        return true;
    }
    
    int main() {
        vector<int> vec1 = {1, 2, 3, 4, 5};
        vector<int> vec2 = {1, 2, 3, 4, 5};
    
        if (vectorsEqual(vec1, vec2)) {
            cout << "I vettori sono uguali." << endl;
        } else {
            cout << "I vettori sono diversi." << endl;
        }
    
        return 0;
    }
    

Stringhe di Caratteri in Stile C

Sebbene C++ supporti le stringhe in stile C, non dovrebbero essere usate nei programmi C++. Le stringhe in stile C sono una fonte sorprendentemente ricca di bug e sono la causa principale di molti problemi di sicurezza. Sono anche più difficili da usare!

I letterali stringa di caratteri sono un'istanza di un costrutto più generale che C++ eredita da C: stringhe di caratteri in stile C. Le stringhe in stile C non sono un tipo. Invece, sono una convenzione su come rappresentare e usare stringhe di caratteri. Le stringhe che seguono questa convenzione sono memorizzate in array di caratteri e sono terminate con il carattere nullo \0. Con questo intendiamo che l'ultimo carattere nella stringa è seguito da un carattere nullo ('\0'). Normalmente usiamo puntatori per manipolare queste stringhe.

Funzioni per Stringhe della Libreria C

La libreria C standard fornisce un insieme di funzioni, elencate nella tabella sottostante, che operano su stringhe in stile C. Queste funzioni sono definite nell'header cstring, che è la versione C++ dell'header C string.h.

Funzione Descrizione
strlen(p) Restituisce la lunghezza di p, non contando il carattere nullo.
strcmp(p1, p2) Confronta p1 e p2 per uguaglianza. Restituisce 0 se p1 == p2, un valore positivo se p1 > p2, un valore negativo se p1 < p2.
strcat(p1, p2) Aggiunge p2 a p1. Restituisce p1.
strcpy(p1, p2) Copia p2 in p1. Restituisce p1.
Tabella 1: Funzioni per Stringhe di Caratteri in Stile C

Le funzioni nella tabella di sopra non verificano i loro parametri stringa.

I puntatori passati a queste routine devono puntare ad array terminati con il carattere nullo. Inoltre, per strcat e strcpy, il chiamante è responsabile di assicurare che l'array di destinazione sia abbastanza grande da contenere la stringa risultante:

// non terminato con `\0`
char ca[] = {'C', '+', '+'};

// DISASTRO: ca non è terminato con il carattere nullo
cout << strlen(ca) << endl;

In questo caso, ca è un array di char ma non è terminato con il carattere nullo. Il risultato è indefinito. L'effetto più probabile di questa chiamata è che strlen continuerà a cercare attraverso la memoria che segue ca fino a quando incontra un carattere \0.

Confronto di Stringhe

Confrontare due stringhe in stile C viene fatto in modo molto diverso da come confrontiamo le stringhe di libreria. Quando confrontiamo due stringhe di libreria, usiamo i normali operatori relazionali o di uguaglianza:

string s1 = "Un esempio di stringa";
string s2 = "Una stringa diversa";
if (s1 < s2) // falso: s2 è minore di s1

Usare questi operatori su stringhe in stile C definite in modo simile confronta i valori dei puntatori, non le stringhe stesse:

const char ca1[] = "Un esempio di stringa";
const char ca2[] = "Una stringa diversa";
if (ca1 < ca2) // indefinito: confronta due indirizzi non correlati

Ricorda che quando usiamo un array, stiamo realmente usando un puntatore al primo elemento nell'array. Quindi, questa condizione confronta effettivamente due valori const char *. Quei puntatori non indirizzano lo stesso oggetto, quindi il confronto è indefinito.

Per confrontare le stringhe, piuttosto che i valori dei puntatori, possiamo chiamare strcmp. Quella funzione restituisce 0 se le stringhe sono uguali, o un valore positivo o negativo, a seconda che la prima stringa sia maggiore o minore della seconda:

// stesso effetto del confronto string s1 < s2
if (strcmp(ca1, ca2) < 0) {
    // ca1 è minore di ca2
}

Il Chiamante È Responsabile della Dimensione di una Stringa di Destinazione

Concatenare o copiare stringhe in stile C è anche molto diverso dalle stesse operazioni su stringhe di libreria. Ad esempio, se volessimo concatenare le due stringhe s1 e s2 definite sopra, possiamo farlo direttamente:

// inizializza largeStr come concatenazione di s1, uno spazio e s2
string largeStr = s1 + " " + s2;

Fare lo stesso con i nostri due array, ca1 e ca2, sarebbe un errore. L'espressione ca1 + ca2 cerca di aggiungere due puntatori, il che è illegale e privo di significato.

// inizializza largeStr come concatenazione di ca1, uno spazio e ca2
char largeStr[100]; // Assicurati che largeStr sia abbastanza grande
strcpy(largeStr, ca1);
strcat(largeStr, " ");
strcat(largeStr, ca2);

Invece possiamo usare strcat e strcpy. Tuttavia, per usare queste funzioni, dobbiamo passare un array per contenere la stringa risultante. L'array che passiamo deve essere abbastanza grande da contenere la stringa generata, incluso il carattere nullo alla fine. Il codice che mostriamo qui, sebbene un pattern di utilizzo comune, è pieno errori gravi potenziali:

// disastroso se calcoliamo male la dimensione di largeStr
strcpy(largeStr, ca1); // copia ca1 in largeStr
strcat(largeStr, " "); // aggiunge uno spazio alla fine di largeStr
strcat(largeStr, ca2); // concatena ca2 su largeStr

Il problema è che possiamo facilmente calcolare male la dimensione necessaria per largeStr. Inoltre, ogni volta che cambiamo i valori che vogliamo memorizzare in largeStr, dobbiamo ricordarci di ricontrollare che ne abbiamo calcolato correttamente la dimensione. Sfortunatamente, programmi simili a questo codice sono ampiamente distribuiti. I programmi con tale codice sono soggetti a errori e spesso portano a gravi falle di sicurezza.

Per la maggior parte delle applicazioni, oltre ad essere più sicuro, è anche più efficiente usare stringhe di libreria piuttosto che stringhe in stile C.

Esercizi

  • Cosa fa il seguente programma?

    const char ca[] = {'c', 'i', 'a', 'o'};
    const char *cp = ca;
    while (*cp) {
        cout << *cp << endl;
        ++cp;
    }
    

    Soluzione:

    Il programma tenta di stampare i caratteri dell'array ca uno per uno fino a quando non incontra un carattere nullo ('\0'). Tuttavia, l'array ca non è terminato con un carattere nullo, quindi il ciclo while (*cp) continuerà a dereferenziare il puntatore cp fino a quando non incontra casualmente un carattere nullo in memoria, il che può portare a un comportamento indefinito. In pratica, il programma potrebbe stampare i caratteri 'c', 'i', 'a', 'o' seguiti da caratteri casuali fino a incontrare un carattere nullo.

  • In questa sezione, abbiamo notato che non era solo illegale ma anche privo di significato cercare di sommare due puntatori. Perché l'aggiunta di due puntatori sarebbe priva di significato?

    Soluzione:

    L'aggiunta di due puntatori è priva di significato perché i puntatori rappresentano indirizzi di memoria. Sommare due indirizzi di memoria non produce un indirizzo valido o utile. Invece, l'operazione di somma tra puntatori non è definita nel contesto della memoria, poiché non ha senso combinare due indirizzi per ottenere un terzo indirizzo. Le operazioni valide sui puntatori includono la sottrazione (per calcolare la distanza tra due puntatori) e l'aggiunta o sottrazione di un intero (per spostarsi avanti o indietro in un array).

  • Scrivi un programma per confrontare due stringhe. Ora scrivi un programma per confrontare i valori di due stringhe di caratteri in stile C.

    Soluzione:

    Per confrontare due stringhe di libreria:

    #include <iostream>
    #include <string>
    using namespace std;
    
    int main() {
        string str1 = "Hello";
        string str2 = "World";
    
        if (str1 == str2) {
            cout << "Le stringhe sono uguali." << endl;
        } else if (str1 < str2) {
            cout << "str1 è minore di str2." << endl;
        } else {
            cout << "str1 è maggiore di str2." << endl;
        }
    
        return 0;
    }
    

    Per confrontare due stringhe in stile C:

    #include <iostream>
    #include <cstring>
    using namespace std;
    
    int main() {
        const char *str1 = "Hello";
        const char *str2 = "World";
    
        int result = strcmp(str1, str2);
        if (result == 0) {
            cout << "Le stringhe sono uguali." << endl;
        } else if (result < 0) {
            cout << "str1 è minore di str2." << endl;
        } else {
            cout << "str1 è maggiore di str2." << endl;
        }
    
        return 0;
    }
    
  • Scrivi un programma per definire due array di caratteri inizializzati da letterali stringa. Ora definisci un terzo array di caratteri per contenere la concatenazione dei due array. Usa strcpy e strcat per copiare i due array nel terzo.

    Soluzione:

    #include <iostream>
    #include <cstring>
    using namespace std;
    
    int main() {
        const char str1[] = "Hello, ";
        const char str2[] = "World!";
    
        // Calcola la dimensione necessaria per il terzo array
        size_t size = strlen(str1) + strlen(str2) + 1; // +1 per il carattere nullo
        char result[size];
    
        // Copia str1 in result
        strcpy(result, str1);
        // Concatena str2 a result
        strcat(result, str2);
    
        cout << "La stringa concatenata è: " << result << endl;
    
        return 0;
    }
    

Interfacciamento con Codice Più Vecchio

Molti programmi C++ sono stati scritti prima che la libreria standard fosse disponibile e non usano i tipi string e vector. Inoltre, molti programmi C++ si interfacciano a programmi scritti in C o altri linguaggi che non possono usare la libreria C++. Quindi, i programmi scritti in C++ moderno potrebbero dover interfacciarsi a codice che usa array e/o stringhe di caratteri in stile C. La libreria C++ offre strutture per rendere l'interfaccia più facile da gestire.

Mescolamento di string di Libreria e Stringhe in Stile C

Nelle lezioni precedenti abbiamo visto che possiamo inizializzare una string da un letterale stringa:

string s("Ciao, Mondo!"); // s contiene Ciao, Mondo!

Più in generale, possiamo usare un array di caratteri terminato con il carattere nullo ovunque possiamo usare un letterale stringa:

  • Possiamo usare un array di caratteri terminato con il carattere nullo per inizializzare o assegnare una string.
  • Possiamo usare un array di caratteri terminato con il carattere nullo come un operando (ma non entrambi gli operandi) dell'operatore di addizione di string o come operando di destra nell'operatore di assegnamento composto di string (+=).

La funzionalità inversa non è fornita: Non c'è un modo diretto di usare una string di libreria quando è richiesta una stringa in stile C. Ad esempio, non c'è modo di inizializzare un puntatore a carattere da una string. Tuttavia, c'è una funzione membro di string chiamata c_str che possiamo spesso usare per ottenere ciò che vogliamo:

string s("Ciao, Mondo!");

// ERRORE: non si può inizializzare un char* da una string
char *str = s;

// OK: usa c_str per ottenere un array di caratteri in stile C
const char *str = s.c_str();

Il nome c_str indica che la funzione restituisce una stringa di caratteri in stile C. Cioè, restituisce un puntatore all'inizio di un array di caratteri terminato con il carattere nullo che contiene gli stessi dati dei caratteri nella string. Il tipo del puntatore è const char *, che ci impedisce di cambiare i contenuti dell'array.

L'array restituito da c_str non è garantito essere valido indefinitamente. Qualsiasi uso successivo di s che potrebbe cambiare il valore di s può invalidare questo array.

Se un programma necessita di accesso continuo ai contenuti dell'array restituito da c_str, il programma deve copiare l'array restituito da c_str.

Uso di un Array per Inizializzare un vector

Prima abbiamo visto che non possiamo inizializzare un array built-in da un altro array. Né possiamo inizializzare un array da un vector. Tuttavia, possiamo usare un array per inizializzare un vector. Per farlo, specifichiamo l'indirizzo del primo elemento e uno oltre l'ultimo elemento che desideriamo copiare:

int int_arr[] = {0, 1, 2, 3, 4, 5};
// ivec ha sei elementi;
// ciascuno è una copia dell'elemento corrispondente in int_arr
vector<int> ivec(begin(int_arr), end(int_arr));

I due puntatori usati per costruire ivec marcano l'intervallo di valori da usare per inizializzare gli elementi in ivec. Il secondo puntatore punta uno oltre l'ultimo elemento da copiare. In questo caso, abbiamo usato le funzioni begin ed end della libreria standard per passare puntatori al primo e uno oltre l'ultimo elemento in int_arr. Di conseguenza, ivec avrà sei elementi ciascuno dei quali avrà lo stesso valore dell'elemento corrispondente in int_arr.

L'intervallo specificato può essere un sottoinsieme dell'array:

// copia tre elementi: int_arr[1], int_arr[2], int_arr[3]
vector<int> subVec(int_arr + 1, int_arr + 4);

Questa inizializzazione crea subVec con tre elementi. I valori di questi elementi sono copie dei valori in int_arr[1] fino a int_arr[3].

Consiglio

Usare i tipi di libreria invece di array e puntatori built-in

I puntatori e gli array sono spesso soggetti a errori. Parte del problema è concettuale: I puntatori sono usati per manipolazioni di basso livello ed è facile fare errori di conto. Altri problemi derivano dalla sintassi, in particolare dalla sintassi di dichiarazione usata con i puntatori.

I programmi C++ moderni dovrebbero usare vector e iteratori invece di array built-in e puntatori, e usare string piuttosto che stringhe di caratteri basate su array in stile C.