HOWTO · C++

Cómo declarar un array 2D con el operador new en C++

Este artículo explica cómo asignar dinámicamente memoria para arrays bidimensionales en C++ con el operador new y cómo gestionar sus distintas representaciones.

En esta página

Una matriz cuyo tamaño se determina durante la ejecución puede representarse de varias maneras en C++, pero new no crea un array multidimensional integrado en el lenguaje. Tú eliges cómo se organizan los elementos y quién es responsable de su almacenamiento. Los ejemplos siguientes usan dos filas y tres columnas para mostrar cómo se corresponden los índices con los valores almacenados. El ejemplo de asignación de filas se ejecutó correctamente en el entorno proporcionado.

Cómo se representa un array bidimensional dinámico

Una declaración integrada como int data[2][3] tiene un tamaño fijo. Si las dimensiones se eligen durante la ejecución, las opciones habituales son un int ** cuyas filas se asignan por separado, una única asignación plana con índices calculados o objetos std::vector anidados. Todas estas representaciones permiten acceder por fila y columna, pero difieren en la organización del almacenamiento y en quién es responsable de él. C++ no comprueba que los índices de fila y columna estén dentro de sus límites válidos.

Asignar las filas por separado para acceder con arr[i][j]

Un int ** apunta a un array de punteros a filas; no apunta a un único bloque rectangular de elementos. Primero, asigna el array de punteros y, después, asigna cada fila. Así se conserva la conocida notación matrix[i][j] y también se permiten filas de distintas longitudes, siempre que lleves la cuenta de la longitud de cada una.

Ejecuta este ejemplo:

El código y las etiquetas o salidas visibles de esta sección se mantienen en inglés.

#include <iostream>

int main() {
    const int rows = 2;
    const int columns = 3;
    int **matrix = new int*[rows];

    for (int i = 0; i < rows; ++i) {
        matrix[i] = new int[columns]{};
    }

    for (int i = 0; i < rows; ++i) {
        for (int j = 0; j < columns; ++j) {
            matrix[i][j] = i * columns + j + 1;
            std::cout << matrix[i][j] << (j + 1 == columns ? '\n' : ' ');
        }
    }

    for (int i = 0; i < rows; ++i) {
        delete[] matrix[i];
    }
    delete[] matrix;
}

Cada new[] debe emparejarse con delete[]: libera primero las filas y, después, el array de punteros. Si la asignación falla durante la construcción, una implementación con punteros sin gestión automática necesita lógica adicional para liberar lo que ya se haya asignado. En código de producción, es preferible usar un contenedor propietario o punteros inteligentes para evitar gestionar manualmente la vida útil.

Almacenar los elementos en una única asignación contigua

Una sola asignación new[] almacena juntos todos los elementos. En una disposición por filas, el elemento de la fila i y la columna j está en el desplazamiento i * columns + j. Debes conservar las dimensiones, porque el puntero por sí solo no registra el número de filas ni de columnas. Comprueba que la multiplicación usada para calcular el tamaño de la asignación pueda representarse con los tipos y las dimensiones de tu programa.

Por ejemplo, después de asignar rows * columns elementos en int *data, accede a uno con data[i * columns + j]. Libera la asignación una sola vez con delete[] data. Esta disposición mantiene contiguo el almacenamiento, pero no permite acceder directamente con data[i][j]; una función auxiliar o una abstracción de contenedor puede ocultar el cálculo del desplazamiento. La disposición por sí sola no permite determinar qué opción ofrece mejor rendimiento; si la velocidad es decisiva, mide la carga de trabajo completa.

Usar std::vector para gestionar la memoria automáticamente

En el código habitual de una aplicación, los vectores anidados suelen ofrecer una gestión de la propiedad más clara. Los vectores internos son propietarios de sus respectivas filas, y sus destructores liberan los elementos automáticamente cuando el vector externo sale de ámbito. La siguiente inicialización crea dos filas con tres elementos cada una:

std::vector<std::vector<int>> matrix(2, std::vector<int>(3, 0));

Usa matrix[i][j] para acceder a un elemento o recorre las filas y sus valores con bucles basados en rangos. Cada fila puede cambiar de tamaño por separado, lo que resulta útil para datos irregulares. Al igual que los arrays sin gestión automática, el operador operator[] de los vectores no comprueba los límites; usa at() cuando convenga comprobarlos. Un vector anidado tampoco garantiza que todas las filas ocupen un único bloque contiguo.

Elegir un enfoque y tener en cuenta sus límites

Elige filas asignadas por separado si pueden tener distintas longitudes o si necesitas la forma de puntero a puntero. Elige una única asignación plana cuando tu interfaz se adapte a un solo bloque de memoria y al cálculo explícito de índices por filas. Prefiere std::vector cuando sean importantes la liberación automática, el cambio de tamaño y una gestión clara de la propiedad; por lo general, es un punto de partida más seguro que poseer asignaciones de new sin gestión automática.

Un array integrado de tamaño fijo es sencillo cuando las dimensiones se conocen en tiempo de compilación, por ejemplo, int matrix[2][3]. Para filas irregulares con asignación manual, guarda la longitud de cada fila y úsala para acceder a sus elementos y liberarlos. Con cualquier representación, valida las dimensiones antes de acceder por índice: el acceso fuera de los límites mediante punteros o operator[] tiene comportamiento indefinido. Empareja cada asignación de array hecha con new[] con su correspondiente delete[], y evita copiar punteros propietarios sin gestión automática a menos que la propiedad se transfiera o comparta deliberadamente.