Come posso usare le matrici in C ++?


480

C ++ ha ereditato array da C dove sono usati praticamente ovunque. Il C ++ fornisce astrazioni che sono più facili da usare e meno soggette a errori ( std::vector<T>dal C ++ 98 e std::array<T, n>dal C ++ 11 ), quindi la necessità di array non si presenta abbastanza spesso come in C. Tuttavia, quando leggi legacy codice o interagire con una libreria scritta in C, è necessario avere una solida conoscenza del funzionamento degli array.

Questa FAQ è divisa in cinque parti:

  1. matrici a livello di tipo e accesso agli elementi
  2. creazione e inizializzazione di array
  3. assegnazione e passaggio parametri
  4. matrici multidimensionali e matrici di puntatori
  5. insidie ​​comuni quando si usano le matrici

Se ritieni che manchi qualcosa di importante in questa FAQ, scrivi una risposta e collegala qui come parte aggiuntiva.

Nel testo seguente, "array" significa "array C", non il modello di classe std::array. Si presume la conoscenza di base della sintassi del dichiarante C. Si noti che l'utilizzo manuale di newe deletecome dimostrato di seguito è estremamente pericoloso di fronte alle eccezioni, ma questo è l'argomento di un'altra FAQ .

(Nota: questo dovrebbe essere una voce alle FAQ C ++ di Stack Overflow . Se vuoi criticare l'idea di fornire una FAQ in questo modulo, allora la pubblicazione su meta che ha iniziato tutto questo sarebbe il posto dove farlo. tale domanda viene monitorata nella chatroom di C ++ , dove l'idea FAQ è iniziata in primo luogo, quindi è molto probabile che la tua risposta venga letta da coloro che hanno avuto l'idea.)


Sarebbero ancora migliori se i puntatori puntassero sempre all'inizio invece che da qualche parte nel mezzo del loro obiettivo ...
Deduplicatore

È necessario utilizzare il vettore STL perché offre una maggiore flessibilità.
Moiz Sajid,

2
Con la disponibilità combinato di std::arrays, std::vectors e gsl::spans - avrei francamente aspettare una FAQ su come utilizzare gli array in C ++ per dire "A questo punto, si può iniziare a considerare, beh, non li utilizzano."
einpoklum,

Risposte:


302

Matrici a livello di tipo

Un tipo di array è indicato come T[n]dov'è Til tipo di elemento ed nè una dimensione positiva , il numero di elementi nell'array. Il tipo di array è un tipo di prodotto del tipo di elemento e delle dimensioni. Se uno o entrambi questi ingredienti differiscono, ottieni un tipo distinto:

#include <type_traits>

static_assert(!std::is_same<int[8], float[8]>::value, "distinct element type");
static_assert(!std::is_same<int[8],   int[9]>::value, "distinct size");

Si noti che la dimensione fa parte del tipo, ovvero i tipi di array di dimensioni diverse sono tipi incompatibili che non hanno assolutamente nulla a che fare l'uno con l'altro. sizeof(T[n])è equivalente an * sizeof(T) .

Decadimento da matrice a puntatore

L'unica "connessione" tra T[n]e T[m]è che entrambi i tipi possono essere implicitamente convertiti in T*, e il risultato di questa conversione è un puntatore al primo elemento dell'array. Cioè, ovunque T*sia richiesto un, è possibile fornire un T[n]e il compilatore fornirà silenziosamente quel puntatore:

                  +---+---+---+---+---+---+---+---+
the_actual_array: |   |   |   |   |   |   |   |   |   int[8]
                  +---+---+---+---+---+---+---+---+
                    ^
                    |
                    |
                    |
                    |  pointer_to_the_first_element   int*

Questa conversione è nota come "decadimento da array a puntatore" ed è una delle principali fonti di confusione. La dimensione dell'array viene persa in questo processo, poiché non fa più parte del tipo ( T*). Pro: Dimenticare la dimensione di un array a livello di tipo consente a un puntatore di puntare al primo elemento di un array di qualsiasi dimensione. Contro: dato un puntatore al primo (o qualsiasi altro) elemento di un array, non c'è modo di rilevare quanto è grande quell'array o dove il puntatore punta esattamente rispetto ai limiti dell'array. I puntatori sono estremamente stupidi .

Le matrici non sono puntatori

Il compilatore genererà silenziosamente un puntatore al primo elemento di un array ogni volta che viene ritenuto utile, ovvero ogni volta che un'operazione fallisce su un array ma riesce su un puntatore. Questa conversione da array a puntatore è banale, poiché il valore del puntatore risultante è semplicemente l'indirizzo dell'array. Si noti che il puntatore non è memorizzato come parte dell'array stesso (o in qualsiasi altra parte della memoria). Un array non è un puntatore.

static_assert(!std::is_same<int[8], int*>::value, "an array is not a pointer");

Un contesto importante in cui un array non decade in un puntatore al suo primo elemento è quando l' &operatore viene applicato ad esso. In tal caso, l' &operatore fornisce un puntatore all'intero array, non solo un puntatore al suo primo elemento. Sebbene in quel caso i valori (gli indirizzi) siano gli stessi, un puntatore al primo elemento di un array e un puntatore all'intero array sono tipi completamente distinti:

static_assert(!std::is_same<int*, int(*)[8]>::value, "distinct element type");

La seguente arte ASCII spiega questa distinzione:

      +-----------------------------------+
      | +---+---+---+---+---+---+---+---+ |
+---> | |   |   |   |   |   |   |   |   | | int[8]
|     | +---+---+---+---+---+---+---+---+ |
|     +---^-------------------------------+
|         |
|         |
|         |
|         |  pointer_to_the_first_element   int*
|
|  pointer_to_the_entire_array              int(*)[8]

Nota come il puntatore al primo elemento punta solo a un singolo numero intero (rappresentato come una piccola casella), mentre il puntatore all'intero array punta a una matrice di 8 numeri interi (raffigurato come una grande casella).

La stessa situazione si presenta in classe ed è forse più evidente. Un puntatore a un oggetto e un puntatore al suo primo membro di dati hanno lo stesso valore (lo stesso indirizzo), ma sono tipi completamente distinti.

Se non si ha familiarità con la sintassi del dichiaratore C, le parentesi nel tipo int(*)[8]sono essenziali:

  • int(*)[8] è un puntatore a un array di 8 numeri interi.
  • int*[8]è un array di 8 puntatori, ogni elemento di tipo int*.

Accesso agli elementi

C ++ fornisce due varianti sintattiche per accedere ai singoli elementi di un array. Nessuno dei due è superiore all'altro e dovresti familiarizzare con entrambi.

Puntatore aritmetico

Dato un puntatore pal primo elemento di un array, l'espressione p+iproduce un puntatore all'i-esimo elemento dell'array. Dereferenziando successivamente quel puntatore, si può accedere a singoli elementi:

std::cout << *(x+3) << ", " << *(x+7) << std::endl;

Se xdenota un array , il decadimento da array a puntatore entrerà in funzione, perché l'aggiunta di un array e un numero intero non ha senso (non vi è alcuna operazione positiva sugli array), ma l'aggiunta di un puntatore e un numero intero ha senso:

   +---+---+---+---+---+---+---+---+
x: |   |   |   |   |   |   |   |   |   int[8]
   +---+---+---+---+---+---+---+---+
     ^           ^               ^
     |           |               |
     |           |               |
     |           |               |
x+0  |      x+3  |          x+7  |     int*

(Nota che il puntatore generato implicitamente non ha un nome, così ho scritto x+0 per identificarlo.)

Se, d'altra parte, xindica un puntatore al primo (o qualsiasi altro) elemento di un array, allora il decadimento da array a puntatore non è necessario, poiché il puntatore su cui iverrà aggiunto esiste già:

   +---+---+---+---+---+---+---+---+
   |   |   |   |   |   |   |   |   |   int[8]
   +---+---+---+---+---+---+---+---+
     ^           ^               ^
     |           |               |
     |           |               |
   +-|-+         |               |
x: | | |    x+3  |          x+7  |     int*
   +---+

Si noti che nel caso illustrato, xè una variabile puntatore (riconoscibile dalla piccola casella accanto a x), ma potrebbe anche essere il risultato di una funzione che restituisce un puntatore (o qualsiasi altra espressione di tipoT* ).

Operatore di indicizzazione

Poiché la sintassi *(x+i)è un po 'goffa, C ++ fornisce la sintassi alternativa x[i]:

std::cout << x[3] << ", " << x[7] << std::endl;

A causa del fatto che l'addizione è commutativa, il seguente codice fa esattamente lo stesso:

std::cout << 3[x] << ", " << 7[x] << std::endl;

La definizione dell'operatore di indicizzazione porta alla seguente interessante equivalenza:

&x[i]  ==  &*(x+i)  ==  x+i

Tuttavia, &x[0]è generalmente non è equivalente a x. Il primo è un puntatore, il secondo un array. Solo quando il contesto innesca il decadimento da array a puntatore può xed &x[0]essere usato in modo intercambiabile. Per esempio:

T* p = &array[0];  // rewritten as &*(array+0), decay happens due to the addition
T* q = array;      // decay happens due to the assignment

Sulla prima riga, il compilatore rileva un'assegnazione da un puntatore a un puntatore, che ha banalmente successo. Sulla seconda riga, rileva un'assegnazione da un array a un puntatore. Dal momento che questo è insignificante (ma puntatore assegnazione da a puntatore ha senso), il decadimento da matrice a puntatore si avvia come al solito.

Intervalli

Una matrice di tipo T[n]ha nelementi, indicizzati da 0a n-1; non c'è nessun elemento n. Eppure, per supportare intervalli semiaperti (dove l'inizio è inclusivo e la fine è esclusiva ), C ++ consente il calcolo di un puntatore all'elemento n-esimo (inesistente), ma è illegale dedurre tale puntatore:

   +---+---+---+---+---+---+---+---+....
x: |   |   |   |   |   |   |   |   |   .   int[8]
   +---+---+---+---+---+---+---+---+....
     ^                               ^
     |                               |
     |                               |
     |                               |
x+0  |                          x+8  |     int*

Ad esempio, se si desidera ordinare un array, entrambi i seguenti funzionerebbero ugualmente bene:

std::sort(x + 0, x + n);
std::sort(&x[0], &x[0] + n);

Si noti che è illegale fornire &x[n]come secondo argomento poiché questo equivale a &*(x+n), e la sottoespressione *(x+n)invoca tecnicamente un comportamento indefinito in C ++ (ma non in C99).

Si noti inoltre che è possibile fornire semplicemente xcome primo argomento. Questo è un po 'troppo conciso per i miei gusti, e rende anche la deduzione dell'argomento template un po' più difficile per il compilatore, perché in quel caso il primo argomento è un array ma il secondo argomento è un puntatore. (Anche in questo caso, inizia il decadimento da array a puntatore.)


I casi in cui l'array non si decompone in un puntatore è illustrato qui come riferimento.
legends2k

@fredoverflow Nella parte Access o Ranges vale la pena ricordare che gli array C funzionano con C ++ 11 basato su range per i loop.
gnzlbg,

135

I programmatori spesso confondono gli array multidimensionali con gli array di puntatori.

Matrici multidimensionali

La maggior parte dei programmatori ha familiarità con array multidimensionali denominati, ma molti non sono consapevoli del fatto che un array multidimensionale può anche essere creato in modo anonimo. Le matrici multidimensionali sono spesso chiamate "matrici di matrici" o " vere matrici multidimensionali".

Matrici multidimensionali nominate

Quando si utilizzano matrici multidimensionali denominate, tutte le dimensioni devono essere note al momento della compilazione:

int H = read_int();
int W = read_int();

int connect_four[6][7];   // okay

int connect_four[H][7];   // ISO C++ forbids variable length array
int connect_four[6][W];   // ISO C++ forbids variable length array
int connect_four[H][W];   // ISO C++ forbids variable length array

Ecco come appare un array multidimensionale con nome in memoria:

              +---+---+---+---+---+---+---+
connect_four: |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+

Si noti che le griglie 2D come quelle sopra sono solo visualizzazioni utili. Dal punto di vista di C ++, la memoria è una sequenza "piatta" di byte. Gli elementi di un array multidimensionale sono memorizzati nell'ordine delle righe principali. Cioè, connect_four[0][6]e connect_four[1][0]sono vicini nella memoria. In effetti, connect_four[0][7]e connect_four[1][0]denota lo stesso elemento! Ciò significa che puoi prendere array multidimensionali e trattarli come grandi array monodimensionali:

int* p = &connect_four[0][0];
int* q = p + 42;
some_int_sequence_algorithm(p, q);

Matrici multidimensionali anonime

Con matrici multidimensionali anonime, tutte le dimensioni tranne la prima devono essere conosciute al momento della compilazione:

int (*p)[7] = new int[6][7];   // okay
int (*p)[7] = new int[H][7];   // okay

int (*p)[W] = new int[6][W];   // ISO C++ forbids variable length array
int (*p)[W] = new int[H][W];   // ISO C++ forbids variable length array

Ecco come appare un array multidimensionale anonimo in memoria:

              +---+---+---+---+---+---+---+
        +---> |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |
      +-|-+
   p: | | |
      +---+

Si noti che l'array stesso è ancora allocato come singolo blocco in memoria.

Matrici di puntatori

È possibile superare la limitazione della larghezza fissa introducendo un altro livello di riferimento indiretto.

Matrici nominate di puntatori

Ecco un array denominato di cinque puntatori che sono inizializzati con array anonimi di diverse lunghezze:

int* triangle[5];
for (int i = 0; i < 5; ++i)
{
    triangle[i] = new int[5 - i];
}

// ...

for (int i = 0; i < 5; ++i)
{
    delete[] triangle[i];
}

Ed ecco come appare in memoria:

          +---+---+---+---+---+
          |   |   |   |   |   |
          +---+---+---+---+---+
            ^
            | +---+---+---+---+
            | |   |   |   |   |
            | +---+---+---+---+
            |   ^
            |   | +---+---+---+
            |   | |   |   |   |
            |   | +---+---+---+
            |   |   ^
            |   |   | +---+---+
            |   |   | |   |   |
            |   |   | +---+---+
            |   |   |   ^
            |   |   |   | +---+
            |   |   |   | |   |
            |   |   |   | +---+
            |   |   |   |   ^
            |   |   |   |   |
            |   |   |   |   |
          +-|-+-|-+-|-+-|-+-|-+
triangle: | | | | | | | | | | |
          +---+---+---+---+---+

Poiché ora ogni riga è allocata singolarmente, la visualizzazione di array 2D come array 1D non funziona più.

Matrici anonime di puntatori

Ecco un array anonimo di 5 (o qualsiasi altro numero di) puntatori che sono inizializzati con array anonimi di diverse lunghezze:

int n = calculate_five();   // or any other number
int** p = new int*[n];
for (int i = 0; i < n; ++i)
{
    p[i] = new int[n - i];
}

// ...

for (int i = 0; i < n; ++i)
{
    delete[] p[i];
}
delete[] p;   // note the extra delete[] !

Ed ecco come appare in memoria:

          +---+---+---+---+---+
          |   |   |   |   |   |
          +---+---+---+---+---+
            ^
            | +---+---+---+---+
            | |   |   |   |   |
            | +---+---+---+---+
            |   ^
            |   | +---+---+---+
            |   | |   |   |   |
            |   | +---+---+---+
            |   |   ^
            |   |   | +---+---+
            |   |   | |   |   |
            |   |   | +---+---+
            |   |   |   ^
            |   |   |   | +---+
            |   |   |   | |   |
            |   |   |   | +---+
            |   |   |   |   ^
            |   |   |   |   |
            |   |   |   |   |
          +-|-+-|-+-|-+-|-+-|-+
          | | | | | | | | | | |
          +---+---+---+---+---+
            ^
            |
            |
          +-|-+
       p: | | |
          +---+

conversioni

Il decadimento da matrice a puntatore si estende naturalmente alle matrici di matrici e alle matrici di puntatori:

int array_of_arrays[6][7];
int (*pointer_to_array)[7] = array_of_arrays;

int* array_of_pointers[6];
int** pointer_to_pointer = array_of_pointers;

Tuttavia, non esiste una conversione implicita da T[h][w]a T**. Se esistesse una tale conversione implicita, il risultato sarebbe un puntatore al primo elemento di una matrice di hpuntatori a T(ciascuno che punta al primo elemento di una linea nella matrice 2D originale), ma tale matrice di puntatori non esiste da nessuna parte in memoria ancora. Se si desidera tale conversione, è necessario creare e riempire manualmente l'array di puntatori richiesto:

int connect_four[6][7];

int** p = new int*[6];
for (int i = 0; i < 6; ++i)
{
    p[i] = connect_four[i];
}

// ...

delete[] p;

Si noti che ciò genera una vista dell'array multidimensionale originale. Se invece hai bisogno di una copia, devi creare array extra e copiare i dati da solo:

int connect_four[6][7];

int** p = new int*[6];
for (int i = 0; i < 6; ++i)
{
    p[i] = new int[7];
    std::copy(connect_four[i], connect_four[i + 1], p[i]);
}

// ...

for (int i = 0; i < 6; ++i)
{
    delete[] p[i];
}
delete[] p;

Come suggerimento: è necessario sottolineare che int connect_four[H][7];, int connect_four[6][W]; int connect_four[H][W];così come int (*p)[W] = new int[6][W];e int (*p)[W] = new int[H][W];sono dichiarazioni valide, quando He Wsono noti al momento della compilazione.
RobertS supporta Monica Cellio il

88

Incarico

Per nessun motivo particolare, le matrici non possono essere assegnate l'una all'altra. Usa std::copyinvece:

#include <algorithm>

// ...

int a[8] = {2, 3, 5, 7, 11, 13, 17, 19};
int b[8];
std::copy(a + 0, a + 8, b);

Questo è più flessibile di quello che potrebbe fornire una vera assegnazione di array poiché è possibile copiare sezioni di array più grandi in array più piccoli. std::copydi solito è specializzato per tipi primitivi per offrire le massime prestazioni. È improbabile chestd::memcpy funzioni meglio. In caso di dubbi, misurare.

Sebbene non sia possibile assegnare direttamente le matrici, tu possibile assegnare strutture e classi che contengono membri dell'array. Questo perché i membri dell'array vengono copiati in modo membro dall'operatore di assegnazione, che viene fornito come predefinito dal compilatore. Se si definisce manualmente l'operatore di assegnazione per i propri tipi di struttura o classe, è necessario ricorrere alla copia manuale per i membri dell'array.

Passaggio dei parametri

Le matrici non possono essere passate per valore. Puoi passarli per puntatore o per riferimento.

Passa per puntatore

Poiché le matrici stesse non possono essere passate per valore, di solito viene invece passato un puntatore al loro primo elemento per valore. Questo è spesso chiamato "passa per puntatore". Poiché la dimensione della matrice non è recuperabile tramite quel puntatore, è necessario passare un secondo parametro che indica la dimensione della matrice (la soluzione C classica) o un secondo puntatore che punta dopo l'ultimo elemento della matrice (la soluzione iteratore C ++) :

#include <numeric>
#include <cstddef>

int sum(const int* p, std::size_t n)
{
    return std::accumulate(p, p + n, 0);
}

int sum(const int* p, const int* q)
{
    return std::accumulate(p, q, 0);
}

Come alternativa sintattica, puoi anche dichiarare i parametri come T p[], e significa esattamente la stessa cosa che T* p nel contesto degli elenchi di parametri :

int sum(const int p[], std::size_t n)
{
    return std::accumulate(p, p + n, 0);
}

Si può pensare di compilatore come riscrittura T p[]a T *p nel contesto di sole liste di parametri . Questa regola speciale è in parte responsabile dell'intera confusione su matrici e puntatori. In ogni altro contesto, dichiarare qualcosa come un array o come un puntatore rende enorme differenza .

Sfortunatamente, puoi anche fornire una dimensione in un parametro array che viene silenziosamente ignorato dal compilatore. Cioè, le seguenti tre firme sono esattamente equivalenti, come indicato dagli errori del compilatore:

int sum(const int* p, std::size_t n)

// error: redefinition of 'int sum(const int*, size_t)'
int sum(const int p[], std::size_t n)

// error: redefinition of 'int sum(const int*, size_t)'
int sum(const int p[8], std::size_t n)   // the 8 has no meaning here

Passa per riferimento

Le matrici possono anche essere passate per riferimento:

int sum(const int (&a)[8])
{
    return std::accumulate(a + 0, a + 8, 0);
}

In questo caso, la dimensione dell'array è significativa. Poiché scrivere una funzione che accetta solo matrici di esattamente 8 elementi è di scarsa utilità, i programmatori di solito scrivono funzioni come template:

template <std::size_t n>
int sum(const int (&a)[n])
{
    return std::accumulate(a + 0, a + n, 0);
}

Si noti che è possibile chiamare tale modello di funzione solo con un array effettivo di numeri interi, non con un puntatore a un numero intero. La dimensione dell'array viene automaticamente dedotta e per ogni dimensione nviene istanziata una funzione diversa dal modello. Puoi anche scrivere modelli di funzioni piuttosto utili che si astraggono sia dal tipo di elemento che dalla dimensione.


2
Potrebbe valere la pena aggiungere una nota che anche se void foo(int a[3]) asembra che si stia passando l'array per valore, la modifica aall'interno di foomodificherà l'array originale. Ciò dovrebbe essere chiaro perché le matrici non possono essere copiate, ma potrebbe valerne la pena rinforzarlo.
gnzlbg,

C ++ 20 haranges::copy(a, b)
LF

int sum( int size_, int a[size_]);- da (credo) C99 in poi
Chef Gladiator il

73

5. Insidie ​​comuni quando si usano le matrici.

5.1 Insidie: fidarsi dei collegamenti non sicuri di tipo.

OK, ti è stato detto, o hai scoperto te stesso, che i globali (variabili dell'ambito dello spazio dei nomi a cui è possibile accedere all'esterno dell'unità di traduzione) sono Evil ™. Ma sapevi quanto sono veramente cattivi? Considera il seguente programma, costituito da due file [main.cpp] e [numbers.cpp]:

// [main.cpp]
#include <iostream>

extern int* numbers;

int main()
{
    using namespace std;
    for( int i = 0;  i < 42;  ++i )
    {
        cout << (i > 0? ", " : "") << numbers[i];
    }
    cout << endl;
}

// [numbers.cpp]
int numbers[42] = {1, 2, 3, 4, 5, 6, 7, 8, 9};

In Windows 7 questo compila e collega bene sia con MinGW g ++ 4.4.1 che con Visual C ++ 10.0.

Poiché i tipi non corrispondono, il programma si arresta in modo anomalo quando lo si esegue.

La finestra di dialogo di arresto anomalo di Windows 7

Spiegazione formale: il programma ha Undefined Behaviour (UB), e invece di bloccarsi può quindi semplicemente bloccarsi, o forse non fare nulla, oppure può inviare e-mail minacciose ai presidenti degli Stati Uniti, Russia, India, La Cina e la Svizzera e fai volare i demoni nasali dal tuo naso.

Spiegazione pratica: main.cppnell'array viene trattato come un puntatore, posizionato allo stesso indirizzo dell'array. Per eseguibile a 32 bit ciò significa che il primo intvalore nell'array viene trattato come un puntatore. Vale a dire, nel main.cppla numbersvariabile contiene, o sembra contenere, (int*)1. Questo fa sì che il programma acceda alla memoria in fondo allo spazio degli indirizzi, che è convenzionalmente riservato e causa trap. Risultato: si ottiene un arresto anomalo.

I compilatori hanno pienamente i loro diritti di non diagnosticare questo errore, poiché C ++ 11 §3.5 / 10 dice, sul requisito dei tipi compatibili per le dichiarazioni,

[N3290 §3.5 / 10]
Una violazione di questa regola sull'identità del tipo non richiede una diagnostica.

Lo stesso paragrafo descrive in dettaglio la variazione consentita:

… Le dichiarazioni per un oggetto array possono specificare tipi di array che differiscono per la presenza o l'assenza di un limite dell'array principale (8.3.4).

Questa variazione consentita non include la dichiarazione di un nome come matrice in un'unità di traduzione e come puntatore in un'altra unità di traduzione.

5.2 Trabocchetto: ottimizzazione prematura (memset e amici).

Non ancora scritto

5.3 Trappola: usare il linguaggio C per ottenere il numero di elementi.

Con una profonda esperienza in C è naturale scrivere ...

#define N_ITEMS( array )   (sizeof( array )/sizeof( array[0] ))

Poiché un arraydecadimento punta al primo elemento dove necessario, l'espressione sizeof(a)/sizeof(a[0])può anche essere scritta come sizeof(a)/sizeof(*a). Significa lo stesso, e non importa come sia scritto, è il linguaggio C per trovare gli elementi numerici dell'array.

Trabocchetto principale: il linguaggio C non è dilemma. Ad esempio, il codice ...

#include <stdio.h>

#define N_ITEMS( array ) (sizeof( array )/sizeof( *array ))

void display( int const a[7] )
{
    int const   n = N_ITEMS( a );          // Oops.
    printf( "%d elements.\n", n );
}

int main()
{
    int const   moohaha[]   = {1, 2, 3, 4, 5, 6, 7};

    printf( "%d elements, calling display...\n", N_ITEMS( moohaha ) );
    display( moohaha );
}

passa un puntatore a N_ITEMS, e quindi molto probabilmente produce un risultato sbagliato. Compilato come eseguibile a 32 bit in Windows 7, produce ...

7 elementi, chiamata display ...
1 elementi.

  1. Il compilatore riscrive int const a[7]solo int const a[].
  2. Il compilatore riscrive int const a[]inint const* a .
  3. N_ITEMS viene quindi invocato con un puntatore.
  4. Per un eseguibile a 32 bit sizeof(array) (dimensione di un puntatore) è quindi 4.
  5. sizeof(*array)è equivalente a sizeof(int), che per un eseguibile a 32 bit è anche 4.

Per rilevare questo errore in fase di esecuzione puoi fare ...

#include <assert.h>
#include <typeinfo>

#define N_ITEMS( array )       (                               \
    assert((                                                    \
        "N_ITEMS requires an actual array as argument",        \
        typeid( array ) != typeid( &*array )                    \
        )),                                                     \
    sizeof( array )/sizeof( *array )                            \
    )

7 elementi, chiamata display ...
Asserzione non riuscita: ("N_ITEMS richiede un array effettivo come argomento", typeid (a)! = Typeid (& * a)), file runtime_detect ion.cpp, riga 16

Questa applicazione ha richiesto a Runtime di terminarlo in modo insolito.
Per ulteriori informazioni, contattare il team di supporto dell'applicazione.

Il rilevamento degli errori di runtime è meglio di nessun rilevamento, ma spreca un po 'di tempo del processore e forse molto più tempo del programmatore. Meglio con il rilevamento in fase di compilazione! E se sei felice di non supportare array di tipi locali con C ++ 98, puoi farlo:

#include <stddef.h>

typedef ptrdiff_t   Size;

template< class Type, Size n >
Size n_items( Type (&)[n] ) { return n; }

#define N_ITEMS( array )       n_items( array )

Compilando questa definizione sostituita nel primo programma completo, con g ++, ho ottenuto ...

M: \ count> g ++ compile_time_detection.cpp
compile_time_detection.cpp: nella funzione 'void display (const int *)':
compile_time_detection.cpp: 14: errore: nessuna funzione corrispondente per la chiamata a 'n_items (const int * &)'

M: \ count> _

Come funziona: la matrice viene passata con riferimento allan_items , e quindi non decade al puntatore al primo elemento, e la funzione può semplicemente restituire il numero di elementi specificati dal tipo.

Con C ++ 11 puoi usarlo anche per array di tipo locale, ed è il linguaggio sicuro C ++ di tipo per trovare il numero di elementi di un array.

5.4 Trabocchetto C ++ 11 e C ++ 14: utilizzo di una constexprfunzione di dimensione dell'array.

Con C ++ 11 e versioni successive è naturale, ma come vedrai pericoloso !, sostituire la funzione C ++ 03

typedef ptrdiff_t   Size;

template< class Type, Size n >
Size n_items( Type (&)[n] ) { return n; }

con

using Size = ptrdiff_t;

template< class Type, Size n >
constexpr auto n_items( Type (&)[n] ) -> Size { return n; }

dove il cambiamento significativo è l'uso di constexpr, che consente a questa funzione di produrre una costante di tempo di compilazione .

Ad esempio, contrariamente alla funzione C ++ 03, una tale costante di tempo di compilazione può essere utilizzata per dichiarare un array della stessa dimensione di un altro:

// Example 1
void foo()
{
    int const x[] = {3, 1, 4, 1, 5, 9, 2, 6, 5, 4};
    constexpr Size n = n_items( x );
    int y[n] = {};
    // Using y here.
}

Ma considera questo codice usando la constexprversione:

// Example 2
template< class Collection >
void foo( Collection const& c )
{
    constexpr int n = n_items( c );     // Not in C++14!
    // Use c here
}

auto main() -> int
{
    int x[42];
    foo( x );
}

La trappola: a partire da luglio 2015 il precedente si compila con MinGW-64 5.1.0 con -pedantic-errorse, testando con i compilatori online su gcc.godbolt.org/ , anche con clang 3.0 e clang 3.2, ma non con clang 3.3, 3.4. 1, 3.5.0, 3.5.1, 3.6 (rc1) o 3.7 (sperimentale). E importante per la piattaforma Windows, non si compila con Visual C ++ 2015. Il motivo è un'istruzione C ++ 11 / C ++ 14 sull'uso dei riferimenti nelle constexprespressioni:

C ++ 11 C ++ 14 $ 5.19 / 2 nove ° trattino

Un condizionale espressione e è un'espressione costante nucleo a meno che la valutazione e, secondo le regole della macchina astratta (1.9), sarebbe valutare una delle seguenti espressioni:
        ⋮

  • un id-espressione che si riferisce ad un elemento variabile o dati di riferimento di tipo a meno che il riferimento ha un'inizializzazione precedente e sia
    • è inizializzato con un'espressione costante o
    • è un membro di dati non statico di un oggetto la cui durata è iniziata nell'ambito della valutazione di e;

Si può sempre scrivere il più dettagliato

// Example 3  --  limited

using Size = ptrdiff_t;

template< class Collection >
void foo( Collection const& c )
{
    constexpr Size n = std::extent< decltype( c ) >::value;
    // Use c here
}

... ma questo non riesce quando Collection non è un array grezzo.

Per gestire raccolte che possono essere non array, è necessaria la sovraccaricabilità di una n_itemsfunzione, ma anche, per un utilizzo in fase di compilazione, è necessaria una rappresentazione in fase di compilazione della dimensione dell'array. E la classica soluzione C ++ 03, che funziona bene anche in C ++ 11 e C ++ 14, è di lasciare che la funzione riporti il ​​suo risultato non come un valore ma tramite il suo tipo di risultato della funzione . Ad esempio in questo modo:

// Example 4 - OK (not ideal, but portable and safe)

#include <array>
#include <stddef.h>

using Size = ptrdiff_t;

template< Size n >
struct Size_carrier
{
    char sizer[n];
};

template< class Type, Size n >
auto static_n_items( Type (&)[n] )
    -> Size_carrier<n>;
// No implementation, is used only at compile time.

template< class Type, size_t n >        // size_t for g++
auto static_n_items( std::array<Type, n> const& )
    -> Size_carrier<n>;
// No implementation, is used only at compile time.

#define STATIC_N_ITEMS( c ) \
    static_cast<Size>( sizeof( static_n_items( c ).sizer ) )

template< class Collection >
void foo( Collection const& c )
{
    constexpr Size n = STATIC_N_ITEMS( c );
    // Use c here
    (void) c;
}

auto main() -> int
{
    int x[42];
    std::array<int, 43> y;
    foo( x );
    foo( y );
}

Informazioni sulla scelta del tipo restituito per static_n_items: questo codice non viene utilizzato std::integral_constant perché con std::integral_constantil risultato viene rappresentato direttamente come constexprvalore, reintroducendo il problema originale. Invece di una Size_carrierclasse si può lasciare che la funzione restituisca direttamente un riferimento a un array. Tuttavia, non tutti hanno familiarità con questa sintassi.

Informazioni sulla denominazione: parte di questa soluzione al problema constexpr-invalid-due-to-reference è rendere esplicita la scelta del tempo di compilazione.

Si spera che il constexprproblema relativo al problema sia stato risolto con C ++ 17, ma fino ad allora una macro come quella STATIC_N_ITEMSsopra produce portabilità, ad esempio per i compilatori clang e Visual C ++, mantenendo il tipo sicurezza.

Correlati: le macro non rispettano gli ambiti, quindi per evitare le collisioni di nomi può essere una buona idea usare un prefisso di nome, ad es MYLIB_STATIC_N_ITEMS.


1
+1 Ottimo test di codifica C: ho trascorso 15 minuti su VC ++ 10.0 e GCC 4.1.2 cercando di risolvere il Segmentation fault... Ho finalmente trovato / capito dopo aver letto le tue spiegazioni! Si prega di scrivere la sezione §5.2 :-) Saluti
olibre

Buona. Un nit: il tipo restituito per countOf dovrebbe essere size_t anziché ptrdiff_t. Vale probabilmente la pena ricordare che in C ++ 11/14 dovrebbe essere constexpr e noexcept.
Ricky65,

@ Ricky65: Grazie per aver menzionato le considerazioni su C ++ 11. Il supporto per queste funzionalità è in ritardo per Visual C ++. Per quanto riguarda size_t, ciò non ha alcun vantaggio che io conosca per le piattaforme moderne, ma presenta una serie di problemi dovuti alle regole implicite di conversione dei tipi di C e C ++. Cioè, ptrdiff_tè usato molto intenzionalmente, per evitare i problemi con size_t. Bisogna comunque essere consapevoli del fatto che g ++ ha un problema nel far corrispondere le dimensioni dell'array al parametro template a meno che non lo sia size_t(non penso che questo problema specifico del compilatore con non size_tsia importante, ma YMMV).
Saluti e hth. - Alf

@Alf. In Standard Working Draft (N3936) 8.3.4 Ho letto - Il limite di un array è ... "un'espressione costante convertita di tipo std :: size_t e il suo valore deve essere maggiore di zero".
Ricky65,

@Ricky: se ti riferisci all'incoerenza, questa affermazione non è presente nell'attuale standard C ++ 11, quindi è difficile indovinare il contesto, ma la contraddizione (un array allocato dinamicamente può essere associato a 0, per C + +11 §5.3.4 / 7) probabilmente non finirà in C ++ 14. I progetti sono proprio questo: bozze. Se invece stai chiedendo a cosa "si riferisce", si riferisce all'espressione originale, non a quella convertita. Se in terzo luogo lo dici perché pensi che forse una frase del genere significhi che si dovrebbe usare size_tper indicare dimensioni di matrici, ovviamente no.
Saluti e hth. - Alf,

72

Creazione e inizializzazione di array

Come con qualsiasi altro tipo di oggetto C ++, le matrici possono essere archiviate direttamente nelle variabili con nome (quindi la dimensione deve essere una costante di compilazione; C ++ non supporta i VLA ), oppure possono essere archiviate in modo anonimo sull'heap e accessibili indirettamente tramite puntatori (solo allora la dimensione può essere calcolata in fase di esecuzione).

Matrici automatiche

Le matrici automatiche (matrici che vivono "in pila") vengono create ogni volta che il flusso del controllo passa attraverso la definizione di una variabile di matrice locale non statica:

void foo()
{
    int automatic_array[8];
}

L'inizializzazione viene eseguita in ordine crescente. Si noti che i valori iniziali dipendono dal tipo di elemento T:

  • Se Tè un POD (come intnell'esempio sopra), non ha luogo l'inizializzazione.
  • Altrimenti, il costruttore predefinito di Tinizializza tutti gli elementi.
  • Se Tnon fornisce alcun costruttore predefinito accessibile, il programma non viene compilato.

In alternativa, i valori iniziali possono essere esplicitamente specificati nell'inizializzatore di array , un elenco separato da virgole circondato da parentesi graffe:

    int primes[8] = {2, 3, 5, 7, 11, 13, 17, 19};

Poiché in questo caso il numero di elementi nell'inizializzatore di array è uguale alla dimensione dell'array, specificare manualmente la dimensione è ridondante. Può essere automaticamente dedotto dal compilatore:

    int primes[] = {2, 3, 5, 7, 11, 13, 17, 19};   // size 8 is deduced

È anche possibile specificare la dimensione e fornire un inizializzatore di array più corto:

    int fibonacci[50] = {0, 1, 1};   // 47 trailing zeros are deduced

In tal caso, gli elementi rimanenti vengono inizializzati con zero . Si noti che C ++ consente un inizializzatore di array vuoto (tutti gli elementi sono inizializzati a zero), mentre C89 no (è richiesto almeno un valore). Si noti inoltre che gli inizializzatori di array possono essere utilizzati solo per inizializzare array; non possono essere successivamente utilizzati negli incarichi.

Matrici statiche

Le matrici statiche (matrici che vivono "nel segmento di dati") sono variabili di matrice locale definite con la staticparola chiave e le variabili di matrice nell'ambito dello spazio dei nomi ("variabili globali"):

int global_static_array[8];

void foo()
{
    static int local_static_array[8];
}

(Si noti che le variabili nell'ambito del namespace sono implicitamente statiche. L'aggiunta della staticparola chiave alla loro definizione ha un significato completamente diverso e deprecato .)

Ecco come le matrici statiche si comportano diversamente dalle matrici automatiche:

  • Gli array statici senza inizializzatore di array vengono inizializzati con zero prima di qualsiasi ulteriore inizializzazione potenziale.
  • Le matrici POD statiche vengono inizializzate esattamente una volta e i valori iniziali sono in genere inseriti nell'eseguibile, nel qual caso non vi sono costi di inizializzazione in fase di esecuzione. Tuttavia, questa non è sempre la soluzione più efficiente in termini di spazio e non è richiesta dallo standard.
  • Le matrici statiche non POD vengono inizializzate la prima volta che il flusso di controllo passa attraverso la loro definizione. Nel caso di array statici locali, ciò non può mai accadere se la funzione non viene mai chiamata.

(Nessuna delle precedenti è specifica per le matrici. Queste regole si applicano ugualmente bene ad altri tipi di oggetti statici.)

Membri dei dati dell'array

I membri dei dati dell'array vengono creati quando viene creato il loro oggetto proprietario. Sfortunatamente, C ++ 03 non fornisce alcun mezzo per inizializzare le matrici nell'elenco di inizializzazione dei membri , quindi l'inizializzazione deve essere falsata con assegnazioni:

class Foo
{
    int primes[8];

public:

    Foo()
    {
        primes[0] = 2;
        primes[1] = 3;
        primes[2] = 5;
        // ...
    }
};

In alternativa, è possibile definire un array automatico nel corpo del costruttore e copiare gli elementi su:

class Foo
{
    int primes[8];

public:

    Foo()
    {
        int local_array[] = {2, 3, 5, 7, 11, 13, 17, 19};
        std::copy(local_array + 0, local_array + 8, primes + 0);
    }
};

In C ++ 0x, le matrici possono essere inizializzate nell'elenco di inizializzazione dei membri grazie all'inizializzazione uniforme :

class Foo
{
    int primes[8];

public:

    Foo() : primes { 2, 3, 5, 7, 11, 13, 17, 19 }
    {
    }
};

Questa è l'unica soluzione che funziona con tipi di elementi che non hanno un costruttore predefinito.

Matrici dinamiche

Le matrici dinamiche non hanno nomi, quindi l'unico mezzo per accedervi è tramite puntatori. Poiché non hanno nomi, mi riferirò a loro come "array anonimi" da ora in poi.

In C, le matrici anonime vengono create tramite malloce amici. In C ++, gli array anonimi vengono creati utilizzando la new T[size]sintassi che restituisce un puntatore al primo elemento di un array anonimo:

std::size_t size = compute_size_at_runtime();
int* p = new int[size];

La seguente arte ASCII mostra il layout di memoria se la dimensione viene calcolata come 8 in fase di esecuzione:

             +---+---+---+---+---+---+---+---+
(anonymous)  |   |   |   |   |   |   |   |   |
             +---+---+---+---+---+---+---+---+
               ^
               |
               |
             +-|-+
          p: | | |                               int*
             +---+

Ovviamente, gli array anonimi richiedono più memoria rispetto agli array con nome a causa del puntatore aggiuntivo che deve essere memorizzato separatamente. (C'è anche qualche sovraccarico aggiuntivo nel negozio gratuito.)

Si noti che non è in corso alcun decadimento da array a puntatore. Sebbene la valutazione new int[size]crei effettivamente una matrice di numeri interi, il risultato dell'espressione new int[size]è già un puntatore a un singolo numero intero (il primo elemento), non una matrice di numeri interi o un puntatore a una matrice di numeri interi di dimensione sconosciuta. Ciò sarebbe impossibile, perché il sistema di tipi statici richiede che le dimensioni degli array siano costanti di tempo di compilazione. (Quindi, non ho annotato l'array anonimo con informazioni di tipo statico nella foto.)

Per quanto riguarda i valori predefiniti per gli elementi, le matrici anonime si comportano in modo simile alle matrici automatiche. Normalmente, gli array POD anonimi non vengono inizializzati, ma esiste una sintassi speciale che attiva l'inizializzazione del valore:

int* p = new int[some_computed_size]();

(Notare la coppia di parentesi finale proprio prima del punto e virgola.) Ancora una volta, C ++ 0x semplifica le regole e consente di specificare valori iniziali per array anonimi grazie all'inizializzazione uniforme:

int* p = new int[8] { 2, 3, 5, 7, 11, 13, 17, 19 };

Se hai finito di usare un array anonimo, devi rilasciarlo di nuovo sul sistema:

delete[] p;

È necessario rilasciare ogni array anonimo esattamente una volta e non toccarlo mai più in seguito. Non rilasciarlo affatto si traduce in una perdita di memoria (o più in generale, a seconda del tipo di elemento, una perdita di risorsa) e provare a rilasciarlo più volte provoca un comportamento indefinito. L'uso del modulo non array delete(o free) invece di delete[]rilasciare l'array è anche un comportamento indefinito .


2
La deprecazione staticdell'utilizzo nell'ambito dello spazio dei nomi è stata rimossa in C ++ 11.
legends2k

Poiché newè un operatore, potrebbe sicuramente restituire l'array assegnato per riferimento. Non ha senso ...
Deduplicatore,

@Deduplicator No, non è possibile, perché storicamente newè molto più vecchio dei riferimenti.
Fredoverflow,

@FredOverflow: Quindi c'è un motivo per cui non è stato possibile restituire un riferimento, è completamente diverso dalla spiegazione scritta.
Deduplicatore,

2
@Deduplicator Non credo che esista un riferimento a una matrice di limiti sconosciuti. Almeno g ++ si rifiuta di compilareint a[10]; int (&r)[] = a;
fredoverflow il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.