Condividi tramite


Matrici (C++)

Una matrice è una sequenza di oggetti dello stesso tipo che occupano un'area contigua di memoria. Le matrici di tipo C tradizionali sono l'origine di molti bug, ma sono ancora comuni, soprattutto nelle codebase meno recenti. In C++ moderno è consigliabile usare std::vector o std::array anziché matrici di tipo C descritte in questa sezione. Entrambi questi tipi di libreria standard archiviano i relativi elementi come blocco contiguo di memoria. Tuttavia, forniscono maggiore sicurezza dei tipi e supportano iteratori che puntano a una posizione valida all'interno della sequenza. Per altre informazioni, vedere Contenitori.

Dichiarazioni di stack

In una dichiarazione di matrice C++ la dimensione della matrice viene specificata dopo il nome della variabile, non dopo il nome del tipo come in altri linguaggi. Nell'esempio seguente viene dichiarata una matrice di 1000 double da allocare nello stack. Il numero di elementi deve essere fornito come valore letterale integer oppure come espressione costante. Ciò è dovuto al fatto che il compilatore deve conoscere la quantità di spazio dello stack da allocare; non può usare un valore calcolato in fase di esecuzione. A ogni elemento della matrice viene assegnato un valore predefinito pari a 0. Se non si assegna un valore predefinito, ogni elemento contiene inizialmente qualsiasi valore casuale si trovi in tale posizione di memoria.

    constexpr size_t size = 1000;

    // Declare an array of doubles to be allocated on the stack
    double numbers[size] {0};

    // Assign a new value to the first element
    numbers[0] = 1;

    // Assign a value to each subsequent element
    // (numbers[1] is the second element in the array.)
    for (size_t i = 1; i < size; i++)
    {
        numbers[i] = numbers[i-1] * 1.1;
    }

    // Access each element
    for (size_t i = 0; i < size; i++)
    {
        std::cout << numbers[i] << " ";
    }

Il primo elemento nella matrice è l'elemento zeroth. L'ultimo elemento è l'elemento (n-1), dove n è il numero di elementi che la matrice può contenere. Il numero di elementi nella dichiarazione deve essere di un tipo integrale e deve essere maggiore di 0. È responsabilità dell'utente assicurarsi che il programma non passi mai un valore all'operatore pedice maggiore di (size - 1).

Una matrice di dimensioni zero è valida solo quando la matrice è l'ultimo campo in un struct oggetto o union e quando le estensioni Microsoft sono abilitate (/Za o /permissive- non è impostata).

Le matrici basate su stack sono più veloci da allocare e accedere rispetto alle matrici basate su heap. Tuttavia, lo spazio dello stack è limitato. Il numero di elementi della matrice non può essere così grande che usa una quantità eccessiva di memoria dello stack. Quanto dipende troppo dal programma. È possibile usare gli strumenti di profilatura per determinare se una matrice è troppo grande.

Dichiarazioni di heap

Potrebbe essere necessaria una matrice troppo grande da allocare nello stack o le cui dimensioni non sono note in fase di compilazione. È possibile allocare questa matrice nell'heap usando un'espressione new[] . L'operatore restituisce un puntatore al primo elemento. L'operatore pedice funziona sulla variabile del puntatore nello stesso modo in cui viene eseguita su una matrice basata su stack. È anche possibile usare l'aritmetica del puntatore per spostare il puntatore a qualsiasi elemento arbitrario nella matrice. È responsabilità dell'utente assicurarsi che:

  • si mantiene sempre una copia dell'indirizzo del puntatore originale in modo da poter eliminare la memoria quando non è più necessaria la matrice.
  • non si incrementa o decrementa l'indirizzo del puntatore oltre i limiti della matrice.

Nell'esempio seguente viene illustrato come definire una matrice nell'heap in fase di esecuzione. Illustra come accedere agli elementi della matrice usando l'operatore pedice e usando l'aritmetica del puntatore:

void do_something(size_t size)
{
    // Declare an array of doubles to be allocated on the heap
    double* numbers = new double[size]{ 0 };

    // Assign a new value to the first element
    numbers[0] = 1;

    // Assign a value to each subsequent element
    // (numbers[1] is the second element in the array.)
    for (size_t i = 1; i < size; i++)
    {
        numbers[i] = numbers[i - 1] * 1.1;
    }

    // Access each element with subscript operator
    for (size_t i = 0; i < size; i++)
    {
        std::cout << numbers[i] << " ";
    }

    // Access each element with pointer arithmetic
    // Use a copy of the pointer for iterating
    double* p = numbers;

    for (size_t i = 0; i < size; i++)
    {
        // Dereference the pointer, then increment it
        std::cout << *p++ << " ";
    }

    // Alternate method:
    // Reset p to numbers[0]:
    p = numbers;

    // Use address of pointer to compute bounds.
    // The compiler computes size as the number
    // of elements * (bytes per element).
    while (p < (numbers + size))
    {
        // Dereference the pointer, then increment it
        std::cout << *p++ << " ";
    }

    delete[] numbers; // don't forget to do this!

}
int main()
{
    do_something(108);
}

Inizializzazione di matrici

È possibile inizializzare una matrice in un ciclo, un elemento alla volta o in una singola istruzione. Il contenuto delle due matrici seguenti è identico:

    int a[10];
    for (int i = 0; i < 10; ++i)
    {
        a[i] = i + 1;
    }

    int b[10]{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

Passaggio di matrici alle funzioni

Quando una matrice viene passata a una funzione, viene passata come puntatore al primo elemento, indipendentemente dal fatto che si tratti di una matrice basata su stack o di heap. Il puntatore non contiene altre informazioni sulle dimensioni o sul tipo. Questo comportamento è detto decadimento del puntatore. Quando si passa una matrice a una funzione, è sempre necessario specificare il numero di elementi in un parametro separato. Questo comportamento implica anche che gli elementi della matrice non vengono copiati quando la matrice viene passata a una funzione. Per impedire alla funzione di modificare gli elementi, specificare il parametro come puntatore agli const elementi.

Nell'esempio seguente viene illustrata una funzione che accetta una matrice e una lunghezza. Il puntatore punta alla matrice originale, non a una copia. Poiché il parametro non constè , la funzione può modificare gli elementi della matrice.

void process(double *p, const size_t len)
{
    std::cout << "process:\n";
    for (size_t i = 0; i < len; ++i)
    {
        // do something with p[i]
    }
}

Dichiarare e definire il parametro p della matrice in const modo da renderlo di sola lettura all'interno del blocco di funzioni:

void process(const double *p, const size_t len);

La stessa funzione può anche essere dichiarata in questi modi, senza alcuna modifica del comportamento. La matrice viene comunque passata come puntatore al primo elemento:

// Unsized array
void process(const double p[], const size_t len);

// Fixed-size array. Length must still be specified explicitly.
void process(const double p[1000], const size_t len);

Matrici multidimensionali

Le matrici generate da altre matrici sono matrici multidimensionali. Queste matrici multidimensionali sono specificate posizionando più espressioni costanti tra parentesi in sequenza. Ad esempio, considerare la dichiarazione riportata di seguito:

int i2[5][7];

Specifica una matrice di tipo int, disposta concettualmente in una matrice bidimensionale di cinque righe e sette colonne, come illustrato nella figura seguente:

Layout concettuale di una matrice multidimensionale.

L'immagine è una griglia 7 celle larga e 5 celle alte. Ogni cella contiene l'indice della cella. Il primo indice di cella è etichettato 0,0. La cella successiva in tale riga è 0,1 e così via fino all'ultima cella di tale riga, ovvero 0,6. La riga successiva inizia con l'indice 1,0. La cella successiva ha un indice pari a 1,1. L'ultima cella della riga è 1,6. Questo criterio si ripete fino all'ultima riga, che inizia con l'indice 4,0. L'ultima cella dell'ultima riga ha un indice pari a 4,6.

È possibile dichiarare matrici multidimensionali con un elenco di inizializzatori (come descritto in Inizializzatori). In queste dichiarazioni è possibile omettere l'espressione costante che specifica i limiti per la prima dimensione. Ad esempio:

// arrays2.cpp
// compile with: /c
const int cMarkets = 4;
// Declare a float that represents the transportation costs.
double TransportCosts[][cMarkets] = {
   { 32.19, 47.29, 31.99, 19.11 },
   { 11.29, 22.49, 33.47, 17.29 },
   { 41.97, 22.09,  9.76, 22.55 }
};

La dichiarazione precedente definisce una matrice costituita da tre righe e da quattro colonne. Le righe rappresentano le ditte e le colonne rappresentano i mercati di destinazione delle ditte. I valori sono i costi di trasporto dalle ditte ai mercati. La prima dimensione della matrice viene lasciata fuori, ma il compilatore la completa esaminando l'inizializzatore.

L'uso dell'operatore di riferimento indiretto (*) in un tipo di matrice n-dimensionale produce una matrice dimensionale n-1. Se n è 1, viene restituito un elemento scalare (o matrice).

Le matrici C++ sono archiviate per lunghezza di riga. L'ordine per lunghezza di riga significa che l'ultimo indice varia più velocemente.

Esempio

È anche possibile omettere la specifica dei limiti per la prima dimensione di una matrice multidimensionale nelle dichiarazioni di funzione, come illustrato di seguito:

// multidimensional_arrays.cpp
// compile with: /EHsc
// arguments: 3
#include <limits>   // Includes DBL_MAX
#include <iostream>

const int cMkts = 4, cFacts = 2;

// Declare a float that represents the transportation costs
double TransportCosts[][cMkts] = {
   { 32.19, 47.29, 31.99, 19.11 },
   { 11.29, 22.49, 33.47, 17.29 },
   { 41.97, 22.09,  9.76, 22.55 }
};

// Calculate size of unspecified dimension
const int cFactories = sizeof TransportCosts /
                  sizeof( double[cMkts] );

double FindMinToMkt( int Mkt, double myTransportCosts[][cMkts], int mycFacts);

using namespace std;

int main( int argc, char *argv[] ) {
   double MinCost;

   if (argv[1] == 0) {
      cout << "You must specify the number of markets." << endl;
      exit(0);
   }
   MinCost = FindMinToMkt( *argv[1] - '0', TransportCosts, cFacts);
   cout << "The minimum cost to Market " << argv[1] << " is: "
       << MinCost << "\n";
}

double FindMinToMkt(int Mkt, double myTransportCosts[][cMkts], int mycFacts) {
   double MinCost = DBL_MAX;

   for( size_t i = 0; i < cFacts; ++i )
      MinCost = (MinCost < TransportCosts[i][Mkt]) ?
         MinCost : TransportCosts[i][Mkt];

   return MinCost;
}
The minimum cost to Market 3 is: 17.29

La funzione FindMinToMkt viene scritta in modo che l'aggiunta di nuove factory non richieda modifiche al codice, ma solo una ricompilazione.

Inizializzazione di matrici

Le matrici di oggetti con un costruttore di classe vengono inizializzate dal costruttore . Quando sono presenti meno elementi nell'elenco di inizializzatori rispetto agli elementi nella matrice, il costruttore predefinito viene usato per gli elementi rimanenti. Se per la classe non è definito alcun costruttore predefinito, l'elenco di inizializzatori deve essere completo, ovvero deve essere presente un inizializzatore per ogni elemento della matrice.

Si consideri la classe Point che definisce due costruttori:

// initializing_arrays1.cpp
class Point
{
public:
   Point()   // Default constructor.
   {
   }
   Point( int, int )   // Construct from two ints
   {
   }
};

// An array of Point objects can be declared as follows:
Point aPoint[3] = {
   Point( 3, 3 )     // Use int, int constructor.
};

int main()
{
}

Il primo elemento aPoint viene costruito usando il costruttore Point( int, int ); i due elementi rimanenti vengono costruiti usando il costruttore predefinito.

Le matrici membro statiche (indipendentemente dal fatto che const ) possano essere inizializzate nelle relative definizioni (all'esterno della dichiarazione di classe). Ad esempio:

// initializing_arrays2.cpp
class WindowColors
{
public:
    static const char *rgszWindowPartList[7];
};

const char *WindowColors::rgszWindowPartList[7] = {
    "Active Title Bar", "Inactive Title Bar", "Title Bar Text",
    "Menu Bar", "Menu Bar Text", "Window Background", "Frame"   };
int main()
{
}

Accesso agli elementi della matrice

È possibile accedere ai singoli elementi di una matrice tramite l'operatore di indice di matrice ([ ]). Se si usa il nome di una matrice unidimensionale senza un pedice, viene valutato come puntatore al primo elemento della matrice.

// using_arrays.cpp
int main() {
   char chArray[10];
   char *pch = chArray;   // Evaluates to a pointer to the first element.
   char   ch = chArray[0];   // Evaluates to the value of the first element.
   ch = chArray[3];   // Evaluates to the value of the fourth element.
}

Quando si utilizzano matrici multidimensionali, è possibile utilizzare varie combinazioni nelle espressioni.

// using_arrays_2.cpp
// compile with: /EHsc /W1
#include <iostream>
using namespace std;
int main() {
   double multi[4][4][3];   // Declare the array.
   double (*p2multi)[3];
   double (*p1multi);

   cout << multi[3][2][2] << "\n";   // C4700 Use three subscripts.
   p2multi = multi[3];               // Make p2multi point to
                                     // fourth "plane" of multi.
   p1multi = multi[3][2];            // Make p1multi point to
                                     // fourth plane, third row
                                     // of multi.
}

Nel codice precedente è multi una matrice tridimensionale di tipo double. Il p2multi puntatore punta a una matrice di tipo double 3. In questo esempio la matrice viene utilizzata con uno, due e tre indici. Sebbene sia più comune specificare tutti i pedici, come nell'istruzione cout , a volte è utile selezionare un subset specifico di elementi di matrice, come illustrato nelle istruzioni che seguono cout.

Overload dell'operatore pedice

Analogamente ad altri operatori, l'operatore pedice ([]) può essere ridefinito dall'utente. Il comportamento predefinito dell'operatore di indice, se non sottoposto a overload, è di combinare il nome della matrice e l'indice utilizzando il seguente metodo:

*((array_name) + (subscript))

Come in tutti gli altri tipi di puntatore, la scalabilità viene eseguita automaticamente per regolare le dimensioni del tipo. Il valore risultante non è n byte dall'origine di array_name; è invece l'n elemento th della matrice. Per altre informazioni su questa conversione, vedere Operatori additivi.

Analogamente, per le matrici multidimensionali, l'indirizzo viene derivato utilizzando il seguente metodo:

((array_name) + (subscript1 * max2 * max3 * ... * maxn) + (subscript2 * max3 * ... * maxn) + ... + subscriptn))

Matrici nelle espressioni

Quando un identificatore di un tipo di matrice viene visualizzato in un'espressione diversa da sizeof, address-of (&) o inizializzazione di un riferimento, viene convertito in un puntatore al primo elemento della matrice. Ad esempio:

char szError1[] = "Error: Disk drive not ready.";
char *psz = szError1;

Il puntatore psz fa riferimento al primo elemento della matrice szError1. Le matrici, a differenza dei puntatori, non sono modificabili l-values. Ecco perché l'assegnazione seguente è illegale:

szError1 = psz;

Vedi anche

std::array