c++ - programacion - vectores en c
¿Cómo uso arrays en C++? (5)
C ++ heredó las matrices de C, donde se utilizan virtualmente 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 surgen tan a menudo como lo hace en C. Sin embargo, cuando lees un código heredado o interactúas con una biblioteca escrita en C, debes tener una idea firme de cómo funcionan los arreglos.
Estas preguntas frecuentes se dividen en cinco partes:
- Arreglos en el nivel de tipo y elementos de acceso.
- creación de matrices e inicialización
- asignación y paso de parámetros
- matrices multidimensionales y matrices de punteros
- trampas comunes cuando se utilizan matrices
Si cree que falta algo importante en esta pregunta frecuente, escriba una respuesta y vincúlela aquí como una 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 declarador C Tenga en cuenta que el uso manual de new
y delete
como se muestra a continuación es extremadamente peligroso frente a las excepciones, pero ese es el tema de otra pregunta frecuente .
(Nota: se pretende que sea una entrada a las Preguntas frecuentes sobre C ++ de Stack Overflow . Si desea criticar la idea de proporcionar una FAQ en este formulario, la publicación en el meta que inició todo esto sería el lugar para hacerlo. Respuestas a esa pregunta se monitorea en la sala de chat de C ++ , donde comenzó la idea de las preguntas frecuentes en primer lugar, por lo que es muy probable que su respuesta sea leída por aquellos a quienes se les ocurrió la idea.)
5. Errores comunes cuando se utilizan matrices.
5.1 Pitfall: Confiando en la vinculación de tipo inseguro.
De acuerdo, se le ha dicho o ha descubierto que las variables globales (variables de alcance del espacio de nombres a las que se puede acceder desde 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 se compila y enlaza bien con MinGW g ++ 4.4.1 y Visual C ++ 10.0.
Como los tipos no coinciden, el programa se bloquea cuando lo ejecutas.
Explicación formal: el programa tiene Comportamiento indefinido (UB) y, en lugar de bloquearse, puede colgarse, o tal vez no hacer nada, o enviar correos electrónicos a los presidentes de EE. UU., Rusia, India China y Suiza, y haz que los Demonios Nasales salgan volando de tu nariz.
Explicación práctica: en main.cpp
la matriz se trata como un puntero, se coloca en la misma dirección que la matriz. Para el ejecutable de 32 bits, esto significa que el primer valor int
en la matriz se trata como un puntero. Es decir, en main.cpp
la variable de numbers
contiene, o parece contener, (int*)1
. Esto hace que el programa acceda a la memoria hacia abajo en la parte inferior del espacio de direcciones, que se reserva convencionalmente y provoca la captura. 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 en la identidad de tipo no requiere un diagnóstico.
El mismo párrafo detalla la variación que está 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 Pitfall: Haciendo optimización prematura ( memset
y amigos).
Aún no escrito
5.3 Trampa: usar el lenguaje C para obtener el número de elementos.
Con la experiencia profunda de C es natural escribir ...
#define N_ITEMS( array ) (sizeof( array )/sizeof( array[0] ))
Dado que una array
decae para apuntar 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 lenguaje C para encontrar los elementos numéricos de la matriz.
Principal trampa: el lenguaje C no es seguro para los tipos. 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_ITEMS
, y 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, llamando display ...
1 elementos.
- El compilador reescribe
int const a[7]
a soloint const a[]
. - El compilador reescribe
int const a[]
toint const* a
. -
N_ITEMS
se invoca por lo tanto con un puntero. - Para un tamaño de ejecutable de 32 bits
sizeof(array)
(tamaño de un puntero) es 4. -
sizeof(*array)
es equivalente asizeof(int)
, que para un ejecutable de 32 bits también es 4.
Para detectar este error en el tiempo de ejecución puedes 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 display ...
La aserción falló: ("N_ITEMS requiere una matriz real como argumento", typeid (a)! = Typeid (& * a)), archivo runtime_detect ion.cpp, línea 16Esta aplicación ha solicitado al Runtime que termine de una manera inusual.
Por favor, 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 ninguna 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, entonces puede hacer eso:
#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 llamar a ''n_items (const int * &)''M: / count> _
Cómo funciona: la matriz se pasa por referencia a n_items
, por lo que no decae al puntero al primer elemento, y la función solo puede devolver el número de elementos especificado por el tipo.
Con C ++ 11 puede usar esto también para arreglos de tipo local, y es el tipo de lenguaje C ++ seguro para encontrar el número de elementos de un arreglo.
5.4 C ++ 11 & C ++ 14 escollo: Usar una función de tamaño de matriz constexpr
.
Con C ++ 11 y versiones posteriores es natural, pero como verá 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, a diferencia de la función C ++ 03, tal constante de tiempo de compilación se puede utilizar 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 versión constexpr
:
// 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 );
}
El escollo: a partir de julio de 2015, lo anterior se compila con MinGW-64 5.1.0 con -pedantic-errors
y, prueba 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 de Windows, no se compila con Visual C ++ 2015. La razón es una declaración de C ++ 11 / C ++ 14 sobre el uso de referencias en expresiones constexpr
:
Una expresión condicional
e
es una expresión constante central a menos que la evaluación dee
, siguiendo las reglas de la máquina abstracta (1.9), evalúe una de las siguientes expresiones:
⋮
- una id-expresión que se refiere a una variable o miembro de datos del 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áticos de un objeto cuya vida comenzó dentro de la evaluación de e;
Siempre se puede escribir el 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 la Collection
no es una matriz en bruto.
Para lidiar con las colecciones que pueden ser no arrays, se necesita la n_items
de sobrecarga de una función n_items
, pero también, para el tiempo de compilación, se necesita una representación de tiempo de compilación del tamaño del array. Y la solución clásica de C ++ 03, que funciona bien también en C ++ 11 y C ++ 14, es permitir 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 como este:
// 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_constant
el resultado se representa directamente como un constexpr
valor, reintroduciendo el problema original. En lugar de una Size_carrier
clase, se puede permitir que la función devuelva directamente una referencia a una matriz. Sin embargo, no todo el mundo está familiarizado con esa sintaxis.
Acerca de la denominación: parte de esta solución al problema constexpr
-invalido por la referencia es hacer que la elección de la constante de compilación sea explícita.
Esperemos que el constexpr
problema se haya solucionado con C ++ 17, pero hasta entonces una macro como la STATIC_N_ITEMS
anterior proporciona portabilidad, por ejemplo, a los compiladores de Clang y Visual C ++, tipo de retención la seguridad.
Relacionados: 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
.
Arreglos en el nivel de tipo
Un tipo de matriz se denota como T[n]
donde T
es el tipo de elemento y n
es 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
La única "conexión" entre T[n]
y T[m]
es que ambos tipos se pueden convertir implícitamente a T*
, y el resultado de esta conversión es un puntero al primer elemento de la matriz. Es decir, en cualquier lugar donde se requiera una T*
, puede proporcionar una 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 forma 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. Contras: dado un puntero al primer (o cualquier otro) elemento de una matriz, no hay manera de detectar qué tan grande es esa matriz o hacia 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 (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 &
produce 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 la matriz completa 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 solo entero (representado como un cuadro pequeño), mientras que el puntero a la matriz completa apunta a un conjunto 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 tipoint*
.
Elementos de acceso
C ++ proporciona dos variaciones sintácticas para acceder a elementos individuales de una matriz. Ninguno de ellos es superior al otro, y debes familiarizarte con ambos.
Aritmética de punteros
Dado un puntero p
al primer elemento de una matriz, la expresión p+i
produce un puntero al elemento i-th de la matriz. Al eliminar la referencia de ese puntero posteriormente, se puede acceder a elementos individuales:
std::cout << *(x+3) << ", " << *(x+7) << std::endl;
Si x
denota una matriz , entonces la decadencia de matriz a puntero se activará, porque agregar una matriz y un entero no tiene sentido (no hay ninguna operación más en las matrices), pero agregar un puntero y un entero tiene sentido:
+---+---+---+---+---+---+---+---+
x: | | | | | | | | | int[8]
+---+---+---+---+---+---+---+---+
^ ^ ^
| | |
| | |
| | |
x+0 | x+3 | x+7 | int*
(Tenga en cuenta que el puntero generado implícitamente no tiene nombre, por lo que escribí x+0
para identificarlo).
Si, por otro lado, x
denota un puntero al primer elemento (o cualquier otro) de una matriz, entonces la desintegración de matriz a puntero no es necesaria, porque el puntero en el que se va a agregar ya existe:
+---+---+---+---+---+---+---+---+
| | | | | | | | | int[8]
+---+---+---+---+---+---+---+---+
^ ^ ^
| | |
| | |
+-|-+ | |
x: | | | x+3 | x+7 | int*
+---+
Tenga en cuenta que en el caso representado, x
es una variable de puntero (discernible por el cuadro pequeño al lado de x
), pero también 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
Dado que la sintaxis *(x+i)
es un poco torpe, C ++ proporciona la sintaxis alternativa x[i]
:
std::cout << x[3] << ", " << x[7] << std::endl;
Debido a que la adición 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 segundo una matriz. Solo cuando el contexto desencadena la caída de matriz a puntero, x
y &x[0]
pueden usar 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, lo cual es trivialmente exitoso. 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), el decaimiento de matriz a puntero se activa de la forma habitual.
Gamas
Una matriz de tipo T[n]
tiene n
elementos, indexados de 0
a n-1
; no hay elemento n
. Y, sin embargo, para admitir rangos semiabiertos (donde el comienzo es inclusivo y el final es exclusivo ), C ++ permite el cálculo de un puntero al elemento n (no existente), pero es ilegal desreferenciar ese puntero:
+---+---+---+---+---+---+---+---+....
x: | | | | | | | | | . int[8]
+---+---+---+---+---+---+---+---+....
^ ^
| |
| |
| |
x+0 | x+8 | int*
Por ejemplo, si desea ordenar una matriz, ambos de los siguientes funcionarán igualmente 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 es equivalente a &*(x+n)
, y la subexpresión *(x+n)
invoca técnicamente un comportamiento indefinido en C ++ (pero no en C99) ).
También tenga en cuenta que simplemente podría proporcionar x
como primer argumento. Eso es un poco demasiado tenso para mi gusto, y también hace que la deducción de los argumentos de la 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. (Una vez más, la caída de matriz a puntero se activa).
Asignación
Por ninguna razón en particular, las matrices no se pueden asignar entre sí. Use std::copy
lugar:
#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::copy
usualmente se especializa en tipos primitivos para dar el máximo rendimiento. Es poco probable que std::memcpy
mejor. En caso de duda, medir.
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 miembro a miembro por el operador de asignación, que el compilador proporciona de forma predeterminada. 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 parametros
Las matrices no se pueden pasar por valor. Puede pasarlos por puntero o por referencia.
Pasar por puntero
Dado que las matrices en sí mismas no se pueden pasar por valor, normalmente un puntero a su primer elemento se pasa por valor en su lugar. Esto a menudo se llama "paso por puntero". Dado que el tamaño de la matriz no es recuperable 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 de iterador 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 una 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);
}
Puede pensar que el compilador reescribe T p[]
a T *p
en el contexto de las listas de parámetros solamente . Esta regla especial es en parte responsable de toda la confusión sobre matrices e indicadores. 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
Pasar 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. Ya que escribir una función que solo acepte arreglos de exactamente 8 elementos es de poca utilidad, los programadores generalmente escriben tales 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 deduce 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 abstraen tanto del tipo de elemento como del tamaño.
Creación e inicialización de matrices.
Al igual que con cualquier otro tipo de objeto C ++, las matrices se pueden almacenar directamente en las variables nombradas (entonces el tamaño debe ser una constante de compilación; C ++ no admite VLA ), o se pueden almacenar de forma anónima en el montón y se puede acceder de forma indirecta a través de punteros (solo así se puede calcular el tamaño en tiempo de ejecución).
Arrays automáticos
Las matrices automáticas (las 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
T
es un POD (comoint
en el ejemplo anterior), no se realiza la inicialización. - De lo contrario, el constructor predeterminado de
T
inicializa todos los elementos. - Si
T
no proporciona un constructor predeterminado accesible, el programa no se compila.
Alternativamente, los valores iniciales pueden especificarse explícitamente en el inicializador de matriz , una lista separada por comas rodeada por corchetes:
int primes[8] = {2, 3, 5, 7, 11, 13, 17, 19};
Dado que 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. Puede ser deducido automáticamente por el compilador:
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 se zero-initialized . Tenga en cuenta que C ++ permite un inicializador de matriz vacío (todos los elementos están inicializados con cero), mientras que C89 no (se requiere al menos un valor). También tenga en cuenta que los inicializadores de matriz solo se pueden utilizar para inicializar matrices; no se pueden usar más tarde en las tareas.
Matrices estáticas
Las matrices estáticas (matrices que viven "en el segmento de datos") son variables de matriz local definidas con la palabra clave static
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 palabra clave static
a su definición tiene un significado obsoleto y completamente diferente ).
Aquí es cómo los arreglos estáticos se comportan de manera diferente a los arreglos automáticos:
- Las matrices estáticas sin un inicializador de matrices se inicializan con cero antes de cualquier posible inicialización adicional.
- Los arreglos de POD estáticos se inicializan exactamente una vez , y los valores iniciales normalmente se incorporan al 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 espacio, y no es requerida por la norma.
- Las matrices estáticas no POD se inicializan la primera vez que el flujo de control pasa a través de su definición. En el caso de matrices estáticas locales, esto puede no ocurrir nunca si la función nunca se llama.
(Ninguna de las anteriores es específica de las matrices. Estas reglas se aplican igualmente 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 ningún medio para inicializar matrices en la lista de inicializadores de miembros , por lo que la inicialización debe simularse 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 en:
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 pueden inicializarse en la lista de inicializadores de miembros gracias a una 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
Los arreglos dinámicos no tienen nombres, por lo tanto, el único medio de acceder a ellos es a través de punteros. Debido a que no tienen nombre, de ahora en adelante me referiré a ellos como "arreglos anónimos".
En C, las matrices anónimas se crean a través de malloc
y amigos. En C ++, las matrices anónimas se crean utilizando la new T[size]
sintaxis new T[size]
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];
La siguiente ilustración ASCII describe 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 se debe almacenar por separado. (También hay algunos gastos generales adicionales en la tienda gratuita).
Tenga en cuenta que aquí no se está produciendo una caída de matriz a puntero. Aunque evaluar el new int[size]
de hecho crea una matriz de enteros, el resultado de la expresión new int[size]
ya es un puntero a un solo entero (el primer elemento), no una matriz de enteros o un puntero a un 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 en 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 activa la inicialización de valores:
int* p = new int[some_computed_size]();
(Tenga en cuenta el par de paréntesis que se encuentra justo antes del punto y coma). De nuevo, C ++ 0x simplifica las reglas y permite especificar valores iniciales para matrices anónimas gracias a una 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 liberarla de nuevo 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, dependiendo del tipo de elemento, una pérdida de recursos), y si se intenta liberarlo varias veces, se obtiene un comportamiento indefinido. Usar el formulario sin matriz delete
(o free
) en lugar de delete[]
para liberar la matriz también es un comportamiento indefinido .
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 no son conscientes del 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 ".
Arreglos multidimensionales nombrados
Cuando se usan matrices multidimensionales nombradas, todas las dimensiones deben ser conocidas 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 simplemente 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]
denotan el mismo elemento! Esto significa que puede tomar matrices multidimensionales y tratarlas como matrices grandes y unidimensionales:
int* p = &connect_four[0][0];
int* q = p + 42;
some_int_sequence_algorithm(p, q);
Anónimos matrices multidimensionales
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 en sí todavía está asignada como un solo bloque en la memoria.
Arreglos de punteros
Puede superar la restricción del ancho fijo introduciendo otro nivel de direccionamiento indirecto.
Nombrados matrices de punteros
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 aquí es cómo se ve en la memoria:
+---+---+---+---+---+
| | | | | |
+---+---+---+---+---+
^
| +---+---+---+---+
| | | | | |
| +---+---+---+---+
| ^
| | +---+---+---+
| | | | | |
| | +---+---+---+
| | ^
| | | +---+---+
| | | | | |
| | | +---+---+
| | | ^
| | | | +---+
| | | | | |
| | | | +---+
| | | | ^
| | | | |
| | | | |
+-|-+-|-+-|-+-|-+-|-+
triangle: | | | | | | | | | | |
+---+---+---+---+---+
Dado que ahora cada línea se asigna de forma individual, la visualización de matrices 2D como matrices 1D ya no funciona.
Anónimos matrices 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 aquí es cómo se ve en la memoria:
+---+---+---+---+---+
| | | | | |
+---+---+---+---+---+
^
| +---+---+---+---+
| | | | | |
| +---+---+---+---+
| ^
| | +---+---+---+
| | | | | |
| | +---+---+---+
| | ^
| | | +---+---+
| | | | | |
| | | +---+---+
| | | ^
| | | | +---+
| | | | | |
| | | | +---+
| | | | ^
| | | | |
| | | | |
+-|-+-|-+-|-+-|-+-|-+
| | | | | | | | | | |
+---+---+---+---+---+
^
|
|
+-|-+
p: | | |
+---+
Conversiones
La desintegración de matriz a puntero se extiende naturalmente a las 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 tal conversión implícita existiera, el resultado sería un puntero al primer elemento de una matriz de h
punteros a T
(cada uno apuntando al primer elemento de una línea en la matriz 2D original), pero esa matriz de punteros no existe En cualquier lugar en la 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 en su lugar, 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;