¿Cómo uso matrices en C ++?

480

C ++ heredó matrices de C donde se usan prácticamente en todas partes. C ++ proporciona abstracciones que son más fáciles de usar y menos propensas a errores ( std::vector<T>desde C ++ 98 y std::array<T, n>desde C ++ 11 ), por lo que la necesidad de matrices no surge con tanta frecuencia como en C. Sin embargo, cuando lee el legado código o interactuar con una biblioteca escrita en C, debe tener una idea clara de cómo funcionan las matrices.

Estas preguntas frecuentes se dividen en cinco partes:

  1. matrices en el nivel de tipo y elementos de acceso
  2. creación e inicialización de matrices
  3. asignación y paso de parámetros
  4. matrices multidimensionales y matrices de punteros
  5. trampas comunes al usar matrices

Si cree que falta algo importante en estas preguntas frecuentes, escriba una respuesta y enlácela aquí como parte adicional.

En el siguiente texto, "matriz" significa "matriz C", no la plantilla de clase std::array. Se asume el conocimiento básico de la sintaxis del declarante C. Tenga en cuenta que el uso manual de newy deletecomo se demuestra a continuación es extremadamente peligroso ante las excepciones, pero ese es el tema de otras preguntas frecuentes .

(Nota: Esto está destinado a ser una entrada a las preguntas frecuentes de C ++ de Stack Overflow . Si desea criticar la idea de proporcionar preguntas frecuentes en este formulario, entonces la publicación en meta que comenzó todo esto sería el lugar para hacerlo. Respuestas a esa pregunta se monitorea en la sala de chat de C ++ , donde la idea de las preguntas frecuentes comenzó en primer lugar, por lo que es muy probable que su respuesta sea leída por aquellos a quienes se les ocurrió la idea).

flujo libre
fuente
Sin embargo, serían aún mejores si los punteros siempre apuntaran al comienzo en lugar de estar en el medio de su objetivo ...
Deduplicador
Debe usar el vector STL porque le brinda mayor flexibilidad.
Moiz Sajid
2
Con la disponibilidad combinada de std::arrays, std::vectorsy gsl::spans, francamente esperaría una pregunta frecuente sobre cómo usar matrices en C ++ para decir "Por ahora, puede comenzar a considerar simplemente, bueno, no usarlos".
einpoklum

Respuestas:

302

Matrices en el nivel de tipo

Un tipo de matriz se denota como T[n]donde Testá el tipo de elemento y nes un tamaño positivo , el número de elementos en la matriz. El tipo de matriz es un tipo de producto del tipo de elemento y el tamaño. Si uno o ambos ingredientes difieren, obtienes 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");

Tenga en cuenta que el tamaño es parte del tipo, es decir, los tipos de matriz de diferente tamaño son tipos incompatibles que no tienen absolutamente nada que ver entre sí. sizeof(T[n])es equivalente a n * sizeof(T).

Decaimiento de matriz a puntero

El único "conexión" entre T[n]y T[m]es que ambos tipos implícitamente pueden ser convertidos a T*, y el resultado de esta conversión es un puntero al primer elemento de la matriz. Es decir, en cualquier lugar donde T*se requiera un, puede proporcionar un T[n], y el compilador proporcionará silenciosamente ese puntero:

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

Esta conversión se conoce como "decaimiento de matriz a puntero", y es una fuente importante de confusión. El tamaño de la matriz se pierde en este proceso, ya que ya no es parte del tipo ( T*). Pro: olvidar el tamaño de una matriz en el nivel de tipo permite que un puntero apunte al primer elemento de una matriz de cualquier tamaño. Con: dado un puntero al primer elemento (o cualquier otro) de una matriz, no hay forma de detectar qué tan grande es esa matriz o dónde apunta exactamente el puntero en relación con los límites de la matriz. Los punteros son extremadamente estúpidos .

Las matrices no son punteros

El compilador generará silenciosamente un puntero al primer elemento de una matriz siempre que se considere útil, es decir, cuando una operación falle en una matriz pero tenga éxito en un puntero. Esta conversión de matriz a puntero es trivial, ya que el valor del puntero resultante es simplemente la dirección de la matriz. Tenga en cuenta que el puntero no se almacena como parte de la matriz en sí (o en cualquier otro lugar de la memoria). Una matriz no es un puntero.

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

Un contexto importante en el que una matriz no se desintegra en un puntero a su primer elemento es cuando &se le aplica el operador. En ese caso, el &operador arroja un puntero a toda la matriz, no solo un puntero a su primer elemento. Aunque en ese caso los valores (las direcciones) son los mismos, un puntero al primer elemento de una matriz y un puntero a toda la matriz son tipos completamente distintos:

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

El siguiente arte ASCII explica esta distinción:

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

Observe cómo el puntero al primer elemento solo apunta a un único entero (representado como un cuadro pequeño), mientras que el puntero a toda la matriz apunta a una matriz de 8 enteros (representado como un cuadro grande).

La misma situación surge en las clases y es quizás más obvia. Un puntero a un objeto y un puntero a su primer miembro de datos tienen el mismo valor (la misma dirección), pero son tipos completamente distintos.

Si no está familiarizado con la sintaxis del declarador C, los paréntesis en el tipo int(*)[8]son esenciales:

  • int(*)[8] es un puntero a una matriz de 8 enteros.
  • int*[8]es una matriz de 8 punteros, cada elemento de tipo int*.

Acceso a elementos

C ++ proporciona dos variaciones sintácticas para acceder a elementos individuales de una matriz. Ninguno de los dos es superior al otro, y debes familiarizarte con ambos.

Aritmética de puntero

Dado un puntero pal primer elemento de una matriz, la expresión p+iproduce un puntero al elemento i-ésimo de la matriz. Al hacer referencia a ese puntero posteriormente, se puede acceder a elementos individuales:

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

Si xdenota una matriz , entonces la descomposición de matriz a puntero se activará, porque agregar una matriz y un entero no tiene sentido (no hay una operación más en las matrices), pero tiene sentido agregar un puntero y un entero:

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

(Tenga en cuenta que el puntero generado implícitamente no tiene nombre, así que escribí x+0para identificarlo).

Si, por otro lado, xdenota un puntero al primer elemento (o cualquier otro) de una matriz, entonces la descomposición de matriz a puntero no es necesaria, porque el puntero en el que ise va a agregar ya existe:

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

Tenga en cuenta que en el caso representado, xes una variable de puntero (discernible por el pequeño cuadro al lado x), pero podría ser el resultado de una función que devuelve un puntero (o cualquier otra expresión de tipo T*).

Operador de indexación

Como la sintaxis *(x+i)es un poco torpe, C ++ proporciona la sintaxis alternativa x[i]:

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

Debido al hecho de que la suma es conmutativa, el siguiente código hace exactamente lo mismo:

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

La definición del operador de indexación conduce a la siguiente equivalencia interesante:

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

Sin embargo, &x[0]generalmente no es equivalente a x. El primero es un puntero, el último una matriz. Sólo cuando la caries los factores desencadenantes de contexto array-a-puntero puede xy &x[0]ser utilizado de manera intercambiable. Por ejemplo:

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

En la primera línea, el compilador detecta una asignación de un puntero a un puntero, que tiene éxito trivialmente. En la segunda línea, detecta una asignación de una matriz a un puntero. Como esto no tiene sentido (pero la asignación de puntero a puntero tiene sentido), la descomposición de matriz a puntero se activa como de costumbre.

Rangos

Una matriz de tipos T[n]tiene nelementos, indexados de 0a n-1; No hay elemento n. Y, sin embargo, para admitir rangos medio abiertos (donde el comienzo es inclusivo y el final es exclusivo ), C ++ permite el cálculo de un puntero al elemento n-ésimo (inexistente), pero es ilegal desreferenciar ese puntero:

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

Por ejemplo, si desea ordenar una matriz, los dos siguientes funcionarían igual de bien:

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

Tenga en cuenta que es ilegal proporcionar &x[n]como segundo argumento, ya que esto es equivalente a &*(x+n), y la sub-expresión *(x+n)técnicamente invoca un comportamiento indefinido en C ++ (pero no en C99).

También tenga en cuenta que simplemente podría proporcionar xcomo primer argumento. Eso es un poco demasiado breve para mi gusto, y también hace que la deducción de argumentos de plantilla sea un poco más difícil para el compilador, porque en ese caso el primer argumento es una matriz pero el segundo argumento es un puntero. (Nuevamente, la descomposición de matriz a puntero se activa).

flujo libre
fuente
Los casos en los que la matriz no se descompone en un puntero se ilustra aquí como referencia.
legends2k
@fredoverflow En la parte de Acceso o Rangos, vale la pena mencionar que los arreglos en C funcionan con C ++ 11 basados ​​en rangos para bucles.
gnzlbg
135

Los programadores a menudo confunden matrices multidimensionales con matrices de punteros.

Matrices multidimensionales

La mayoría de los programadores están familiarizados con las matrices multidimensionales con nombre, pero muchos desconocen el hecho de que la matriz multidimensional también se puede crear de forma anónima. Las matrices multidimensionales a menudo se denominan "matrices de matrices" o " matrices multidimensionales verdaderas ".

Matrices multidimensionales con nombre

Cuando se utilizan matrices multidimensionales con nombre, todas las dimensiones deben conocerse en tiempo de compilación:

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

Así es como se ve una matriz multidimensional con nombre en la memoria:

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

Tenga en cuenta que las cuadrículas 2D como las anteriores son meramente visualizaciones útiles. Desde el punto de vista de C ++, la memoria es una secuencia "plana" de bytes. Los elementos de una matriz multidimensional se almacenan en orden de fila mayor. Es decir, connect_four[0][6]y connect_four[1][0]son vecinos en la memoria. De hecho, connect_four[0][7]y connect_four[1][0]denota el mismo elemento! Esto significa que puede tomar matrices multidimensionales y tratarlas como matrices unidimensionales grandes:

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

Matrices multidimensionales anónimas

Con matrices multidimensionales anónimas, todas las dimensiones, excepto la primera, deben conocerse en tiempo de compilación:

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

Así es como se ve una matriz multidimensional anónima en la memoria:

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

Tenga en cuenta que la matriz misma todavía está asignada como un bloque único en la memoria.

Matrices de punteros

Puede superar la restricción de ancho fijo introduciendo otro nivel de indirección.

Conjuntos de punteros nombrados

Aquí hay una matriz con nombre de cinco punteros que se inicializan con matrices anónimas de diferentes longitudes:

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];
}

Y así es como se ve en la memoria:

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

Como cada línea se asigna individualmente ahora, ver matrices 2D como matrices 1D ya no funciona.

Matrices anónimas de punteros

Aquí hay una matriz anónima de 5 (o cualquier otro número de) punteros que se inicializan con matrices anónimas de diferentes longitudes:

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[] !

Y así es como se ve en la memoria:

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

Conversiones

La descomposición de matriz a puntero se extiende naturalmente a matrices de matrices y matrices de punteros:

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;

Sin embargo, no hay conversión implícita de T[h][w]a T**. Si existiera tal conversión implícita, el resultado sería un puntero al primer elemento de una matriz de hpunteros a T(cada uno apuntando al primer elemento de una línea en la matriz 2D original), pero esa matriz de puntero no existe en ninguna parte memoria todavía. Si desea dicha conversión, debe crear y completar la matriz de punteros requerida manualmente:

int connect_four[6][7];

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

// ...

delete[] p;

Tenga en cuenta que esto genera una vista de la matriz multidimensional original. Si necesita una copia, debe crear matrices adicionales y copiar los datos usted mismo:

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;
flujo libre
fuente
Como sugerencia: debe señalar que int connect_four[H][7];, int connect_four[6][W]; int connect_four[H][W];así como int (*p)[W] = new int[6][W];y int (*p)[W] = new int[H][W];son declaraciones válidas, cuándo Hy Wse conocen en tiempo de compilación.
RobertS apoya a Monica Cellio
88

Asignación

Sin ninguna razón en particular, las matrices no pueden asignarse entre sí. Use en su std::copylugar:

#include <algorithm>

// ...

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

Esto es más flexible de lo que podría proporcionar la verdadera asignación de matriz porque es posible copiar segmentos de matrices más grandes en matrices más pequeñas. std::copygeneralmente está especializado para tipos primitivos para dar el máximo rendimiento. Es poco probable que std::memcpyfuncione mejor. En caso de duda, mida.

Aunque no puede asignar matrices directamente, puede asignar estructuras y clases que contienen miembros de la matriz. Esto se debe a que los miembros de la matriz son copiados por el operador de asignación, que el compilador proporciona como predeterminado. Si define el operador de asignación manualmente para sus propios tipos de estructura o clase, debe recurrir a la copia manual para los miembros de la matriz.

Paso de parámetros

Las matrices no se pueden pasar por valor. Puede pasarlos por puntero o por referencia.

Pase por puntero

Dado que las matrices en sí mismas no se pueden pasar por valor, generalmente se pasa un puntero a su primer elemento por valor. Esto a menudo se llama "pasar por puntero". Dado que el tamaño de la matriz no se puede recuperar a través de ese puntero, debe pasar un segundo parámetro que indica el tamaño de la matriz (la solución clásica de C) o un segundo puntero que apunta después del último elemento de la matriz (la solución iteradora de 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);
}

Como alternativa sintáctica, también puede declarar parámetros como T p[], y significa exactamente lo mismo que T* p en el contexto de las listas de parámetros solamente :

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

Se puede pensar en el compilador como la reescritura T p[]de T *p en el contexto de las listas de parámetros únicos . Esta regla especial es en parte responsable de toda la confusión sobre las matrices y los punteros. En cualquier otro contexto, declarar algo como una matriz o como un puntero hace una gran diferencia.

Desafortunadamente, también puede proporcionar un tamaño en un parámetro de matriz que el compilador ignora silenciosamente. Es decir, las siguientes tres firmas son exactamente equivalentes, como lo indican los errores del compilador:

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

Pase por referencia

Las matrices también se pueden pasar por referencia:

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

En este caso, el tamaño de la matriz es significativo. Dado que escribir una función que solo acepta matrices de exactamente 8 elementos es de poca utilidad, los programadores generalmente escriben funciones como plantillas:

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

Tenga en cuenta que solo puede llamar a dicha plantilla de función con una matriz real de enteros, no con un puntero a un entero. El tamaño de la matriz se infiere automáticamente y, para cada tamaño n, se crea una instancia de una función diferente de la plantilla. También puede escribir plantillas de funciones bastante útiles que se resumen tanto del tipo de elemento como del tamaño.

flujo libre
fuente
2
Podría valer la pena agregar una nota de que incluso aunque void foo(int a[3]) aparezca que uno está pasando la matriz por valor, la modificación ainterna foomodificará la matriz original. Esto debería estar claro porque las matrices no se pueden copiar, pero puede valer la pena reforzar eso.
gnzlbg
C ++ 20 tieneranges::copy(a, b)
LF
int sum( int size_, int a[size_]);- desde (creo) C99 en adelante
Chef Gladiator
73

5. Errores comunes al usar matrices.

5.1 Peligro: Confiando en el tipo de enlace inseguro.

Bien, le han dicho, o se ha enterado usted mismo, que los globales (variables de alcance del espacio de nombres a las que se puede acceder fuera de la unidad de traducción) son Evil ™. ¿Pero sabías cuán verdaderamente malvados son? Considere el siguiente programa, que consta de dos archivos [main.cpp] y [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};

En Windows 7 esto compila y enlaza bien tanto con MinGW g ++ 4.4.1 como con Visual C ++ 10.0.

Como los tipos no coinciden, el programa se bloquea cuando lo ejecuta.

El cuadro de diálogo de bloqueo de Windows 7

Explicación formal: el programa tiene Comportamiento Indefinido (UB), y en lugar de fallar, por lo tanto, simplemente puede colgar, o tal vez no hacer nada, o puede enviar correos electrónicos amenazantes a los presidentes de los EE. UU., Rusia, India, China y Suiza, y haz que los Demonios Nasales salgan volando de tu nariz.

Explicación en main.cppla práctica: en la matriz se trata como un puntero, ubicado en la misma dirección que la matriz. Para el ejecutable de 32 bits, esto significa que el primer intvalor de la matriz se trata como un puntero. Es decir, en main.cppla numbersvariable contiene, o parece contener, (int*)1. Esto hace que el programa acceda a la memoria en la parte inferior del espacio de direcciones, que está convencionalmente reservado y causa trampa. Resultado: tienes un accidente.

Los compiladores tienen pleno derecho a no diagnosticar este error, porque C ++ 11 §3.5 / 10 dice, sobre el requisito de tipos compatibles para las declaraciones,

[N3290 §3.5 / 10]
Una violación de esta regla sobre la identidad de tipo no requiere un diagnóstico.

El mismo párrafo detalla la variación permitida:

… Las declaraciones para un objeto de matriz pueden especificar tipos de matriz que difieren por la presencia o ausencia de un límite de matriz principal (8.3.4).

Esta variación permitida no incluye declarar un nombre como una matriz en una unidad de traducción y como un puntero en otra unidad de traducción.

5.2 Peligro: Hacer optimización prematura ( memsety amigos).

Aún no escrito

5.3 Dificultad: usar el lenguaje C para obtener el número de elementos.

Con experiencia profunda en C es natural escribir ...

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

Como un arraydecaimiento del puntero al primer elemento donde sea necesario, la expresión sizeof(a)/sizeof(a[0])también se puede escribir como sizeof(a)/sizeof(*a). Significa lo mismo, y no importa cómo esté escrito, es el idioma C para encontrar los elementos numéricos de la matriz.

Principal escollo: el idioma C no es seguro. Por ejemplo, el código ...

#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 );
}

pasa un puntero a N_ITEMSy, por lo tanto, lo más probable es que produzca un resultado incorrecto. Compilado como un ejecutable de 32 bits en Windows 7 produce ...

7 elementos, pantalla de llamada ...
1 elementos.

  1. El compilador reescribe int const a[7]a just int const a[].
  2. El compilador reescribe int const a[]a int const* a.
  3. N_ITEMS Por lo tanto, se invoca con un puntero.
  4. Para un ejecutable de 32 bits sizeof(array)(tamaño de un puntero) es entonces 4.
  5. sizeof(*array)es equivalente a sizeof(int), que para un ejecutable de 32 bits también es 4.

Para detectar este error en tiempo de ejecución, puede hacer ...

#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 elementos, llamando a la pantalla ...
Error de aserción: ("N_ITEMS requiere una matriz real como argumento", typeid (a)! = Typeid (& * a)), archivo runtime_detect ion.cpp, línea 16

Esta aplicación ha solicitado el tiempo de ejecución para terminarlo de una manera inusual.
Póngase en contacto con el equipo de soporte de la aplicación para obtener más información.

La detección de errores en tiempo de ejecución es mejor que la ausencia de detección, pero desperdicia un poco de tiempo de procesador y quizás mucho más tiempo de programador. ¡Mejor con detección en tiempo de compilación! Y si está contento de no admitir matrices de tipos locales con C ++ 98, puede hacerlo:

#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 esta definición sustituida en el primer programa completo, con g ++, obtuve ...

M: \ count> g ++ compile_time_detection.cpp
compile_time_detection.cpp: En la función 'void display (const int *)':
compile_time_detection.cpp: 14: error: no hay función coincidente para la llamada a 'n_items (const int * &)'

M: \ cuenta> _

Cómo funciona: la matriz se pasa por referencia a n_items, y por lo tanto no decae al puntero al primer elemento, y la función solo puede devolver el número de elementos especificados por el tipo.

Con C ++ 11 puede usar esto también para matrices de tipo local, y es el idioma seguro de C ++ para encontrar el número de elementos de una matriz.

5.4 Peligro de C ++ 11 y C ++ 14: uso de una constexprfunción de tamaño de matriz

Con C ++ 11 y versiones posteriores es natural, ¡pero como verás peligroso !, reemplazar la función 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; }

donde el cambio significativo es el uso de constexpr, que permite que esta función produzca una constante de tiempo de compilación .

Por ejemplo, en contraste con la función C ++ 03, dicha constante de tiempo de compilación se puede usar para declarar una matriz del mismo tamaño que otra:

// 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.
}

Pero considere este código usando la constexprversión:

// 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 trampa: a partir de julio de 2015, lo anterior se compila con MinGW-64 5.1.0 con -pedantic-errors, y, probando con los compiladores en línea en gcc.godbolt.org/ , también con clang 3.0 y clang 3.2, pero no con clang 3.3, 3.4. 1, 3.5.0, 3.5.1, 3.6 (rc1) o 3.7 (experimental). E importante para la plataforma Windows, no se compila con Visual C ++ 2015. La razón es una declaración C ++ 11 / C ++ 14 sobre el uso de referencias en constexprexpresiones:

C ++ 11 ++ 14 C $ 5.19 / 2 nueve º tablero

Una expresión condicional e es una expresión constante central a menos que la evaluación de e, siguiendo las reglas de la máquina abstracta (1.9), evalúe una de las siguientes expresiones:
        ⋮

  • una expresión de identificación que se refiere a una variable o miembro de datos de tipo de referencia a menos que la referencia tenga una inicialización anterior y
    • se inicializa con una expresión constante o
    • es un miembro de datos no estático de un objeto cuya vida útil comenzó dentro de la evaluación de e;

Uno siempre puede escribir más detallado

// 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
}

... pero esto falla cuando Collectionno es una matriz en bruto.

Para manejar colecciones que pueden ser no matrices, se necesita la capacidad de sobrecarga de una n_itemsfunción, pero también, para el tiempo de compilación, se necesita una representación en tiempo de compilación del tamaño de la matriz. Y la solución clásica de C ++ 03, que funciona bien también en C ++ 11 y C ++ 14, es dejar que la función informe su resultado no como un valor sino a través de su tipo de resultado de función . Por ejemplo así:

// 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 );
}

Acerca de la elección del tipo de retorno para static_n_items: este código no se usa std::integral_constant porque con std::integral_constantel resultado se representa directamente como un constexprvalor, reintroduciendo el problema original. En lugar de una Size_carrierclase, se puede dejar que la función devuelva directamente una referencia a una matriz. Sin embargo, no todos están familiarizados con esa sintaxis.

Acerca de la nomenclatura: parte de esta solución al problema constexpr-invalido-debido a una referencia es hacer explícita la elección del tiempo de compilación constante.

Afortunadamente, oops-there-was-a-reference-implicate-in-your- constexprissue se solucionará con C ++ 17, pero hasta entonces una macro como la STATIC_N_ITEMSanterior proporciona portabilidad, por ejemplo, a los compiladores clang y Visual C ++, conservando el tipo la seguridad.

Relacionado: las macros no respetan los ámbitos, por lo que para evitar colisiones de nombres puede ser una buena idea usar un prefijo de nombre, por ejemplo MYLIB_STATIC_N_ITEMS.

Saludos y hth. - Alf
fuente
1
+1 Gran prueba de codificación C: he pasado 15 minutos en VC ++ 10.0 y GCC 4.1.2 tratando de arreglar el Segmentation fault... ¡Finalmente encontré / entendí después de leer sus explicaciones! Por favor escriba su sección §5.2 :-) Saludos
olibre
Bueno. Una cosa: el tipo de retorno para countOf debe ser size_t en lugar de ptrdiff_t. Probablemente valga la pena mencionar que en C ++ 11/14 debería ser constexpr y noexcept.
Ricky65
@ Ricky65: Gracias por mencionar las consideraciones de C ++ 11. El soporte para estas características ha tardado en llegar para Visual C ++. En cuanto a size_teso, eso no tiene ventajas que conozco para las plataformas modernas, pero tiene varios problemas debido a las reglas de conversión de tipo implícito de C y C ++. Es decir, ptrdiff_tse usa de manera muy intencional, para evitar los problemas con size_t. Sin embargo, se debe tener en cuenta que g ++ tiene un problema al hacer coincidir el tamaño de la matriz con el parámetro de la plantilla a menos que sea size_t(no creo que este problema específico del compilador con no size_tsea ​​importante, pero YMMV).
Saludos y hth. - Alf
@Alf. En el Borrador de trabajo estándar (N3936) 8.3.4 Leí: el límite de una matriz es ... "una expresión constante convertida de tipo std :: size_t y su valor será mayor que cero".
Ricky65
@Ricky: si te refieres a la inconsistencia, esta declaración no está en el estándar actual de C ++ 11, por lo que es difícil adivinar el contexto, pero la contradicción (una matriz asignada dinámicamente puede ser de límite 0, por C + +11 §5.3.4 / 7) probablemente no terminará en C ++ 14. Los borradores son solo eso: borradores. Si, en cambio, está preguntando a qué se refiere "its", se refiere a la expresión original, no a la convertida. Si, por otro lado, menciona esto porque cree que tal oración significa que uno debería usar size_tpara denotar tamaños de matrices, no, por supuesto, no lo hace.
Saludos y hth. - Alf
72

Creación e inicialización de matrices

Al igual que con cualquier otro tipo de objeto C ++, las matrices se pueden almacenar directamente en variables con nombre (entonces el tamaño debe ser una constante de tiempo de compilación; C ++ no admite VLA ), o se pueden almacenar de forma anónima en el montón y acceder indirectamente a través de punteros (solo entonces se puede calcular el tamaño en tiempo de ejecución).

Matrices automáticas

Las matrices automáticas (matrices que viven "en la pila") se crean cada vez que el flujo de control pasa a través de la definición de una variable de matriz local no estática:

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

La inicialización se realiza en orden ascendente. Tenga en cuenta que los valores iniciales dependen del tipo de elemento T:

  • Si Tes un POD (como inten el ejemplo anterior), no se lleva a cabo la inicialización.
  • De lo contrario, el constructor predeterminado de Tinicializa todos los elementos.
  • Si Tno proporciona un constructor predeterminado accesible, el programa no se compila.

Alternativamente, los valores iniciales se pueden especificar explícitamente en el inicializador de matriz , una lista separada por comas rodeada de llaves:

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

Como en este caso el número de elementos en el inicializador de matriz es igual al tamaño de la matriz, especificar el tamaño manualmente es redundante. El compilador puede deducirlo automáticamente:

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

También es posible especificar el tamaño y proporcionar un inicializador de matriz más corto:

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

En ese caso, los elementos restantes están inicializados en cero . Tenga en cuenta que C ++ permite un inicializador de matriz vacío (todos los elementos están inicializados en cero), mientras que C89 no (se requiere al menos un valor). También tenga en cuenta que los inicializadores de matriz solo se pueden usar para inicializar matrices; luego no se pueden usar en tareas.

Matrices estáticas

Las matrices estáticas (matrices que viven "en el segmento de datos") son variables de matriz local definidas con la staticpalabra clave y las variables de matriz en el ámbito del espacio de nombres ("variables globales"):

int global_static_array[8];

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

(Tenga en cuenta que las variables en el ámbito del espacio de nombres son implícitamente estáticas. Agregar la staticpalabra clave a su definición tiene un significado completamente diferente y obsoleto ).

Así es como las matrices estáticas se comportan de manera diferente a las automáticas:

  • Las matrices estáticas sin un inicializador de matrices se inicializan a cero antes de cualquier inicialización potencial adicional.
  • Las matrices POD estáticas se inicializan exactamente una vez , y los valores iniciales generalmente se hornean en el ejecutable, en cuyo caso no hay costo de inicialización en tiempo de ejecución. Sin embargo, esta no siempre es la solución más eficiente en cuanto al espacio y no es requerida por el estándar.
  • Las matrices estáticas sin POD se inicializan la primera vez que el flujo de control pasa por su definición. En el caso de las matrices estáticas locales, eso nunca puede suceder si nunca se llama a la función.

(Ninguno de los anteriores es específico para las matrices. Estas reglas se aplican igualmente bien a otros tipos de objetos estáticos).

Miembros de datos de matriz

Los miembros de datos de matriz se crean cuando se crea su objeto propietario. Desafortunadamente, C ++ 03 no proporciona medios para inicializar matrices en la lista de inicializadores de miembros , por lo que la inicialización debe ser falsificada con asignaciones:

class Foo
{
    int primes[8];

public:

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

Alternativamente, puede definir una matriz automática en el cuerpo del constructor y copiar los elementos sobre:

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);
    }
};

En C ++ 0x, las matrices se pueden inicializar en la lista de inicializadores de miembros gracias a la inicialización uniforme :

class Foo
{
    int primes[8];

public:

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

Esta es la única solución que funciona con tipos de elementos que no tienen un constructor predeterminado.

Matrices dinámicas

Las matrices dinámicas no tienen nombres, por lo tanto, el único medio para acceder a ellas es a través de punteros. Como no tienen nombre, de ahora en adelante me referiré a ellos como "matrices anónimas".

En C, se crean matrices anónimas a través de mallocy amigos. En C ++, las matrices anónimas se crean utilizando la new T[size]sintaxis que devuelve un puntero al primer elemento de una matriz anónima:

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

El siguiente arte ASCII muestra el diseño de la memoria si el tamaño se calcula como 8 en tiempo de ejecución:

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

Obviamente, las matrices anónimas requieren más memoria que las matrices con nombre debido al puntero adicional que debe almacenarse por separado. (También hay algunos gastos generales adicionales en la tienda gratuita).

Tenga en cuenta que no hay una descomposición de matriz a puntero aquí. Aunque la evaluación new int[size]de hecho crea una matriz de enteros, el resultado de la expresión yanew int[size] es un puntero a un único entero (el primer elemento), no una matriz de enteros o un puntero a una matriz de enteros de tamaño desconocido. Eso sería imposible, porque el sistema de tipo estático requiere que los tamaños de matriz sean constantes de tiempo de compilación. (Por lo tanto, no anoté la matriz anónima con información de tipo estático en la imagen).

Con respecto a los valores predeterminados para los elementos, las matrices anónimas se comportan de manera similar a las matrices automáticas. Normalmente, las matrices anónimas de POD no se inicializan, pero hay una sintaxis especial que desencadena la inicialización del valor:

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

(Observe el par de paréntesis final justo antes del punto y coma). Nuevamente, C ++ 0x simplifica las reglas y permite especificar valores iniciales para matrices anónimas gracias a la inicialización uniforme:

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

Si ha terminado de usar una matriz anónima, debe devolverla al sistema:

delete[] p;

Debe liberar cada matriz anónima exactamente una vez y luego nunca volver a tocarla. Si no se libera en absoluto, se produce una pérdida de memoria (o, más generalmente, según el tipo de elemento, una pérdida de recursos), e intentar liberarlo varias veces se traduce en un comportamiento indefinido. Usar el formulario sin matriz delete(o free) en lugar de delete[]liberar la matriz también es un comportamiento indefinido .

flujo libre
fuente
2
La obsolescencia del staticuso en el ámbito del espacio de nombres se eliminó en C ++ 11.
legends2k
Debido a newque soy un operador, sin duda podría devolver la matriz localizada por referencia. Simplemente no tiene sentido ...
Deduplicator
@Deduplicator No, no podría, porque históricamente newes mucho más antiguo que las referencias.
fredoverflow
@FredOverflow: Entonces, hay una razón por la que no pudo devolver una referencia, es completamente diferente de la explicación escrita.
Deduplicador
2
@Dupuplicator No creo que exista una referencia a una matriz de límites desconocidos. Al menos g ++ se niega a compilarint a[10]; int (&r)[] = a;
fredoverflow