4.9.20c El operador new con matrices
§1 Antecedentes
El Estándar C++ establece cuatro nuevos operadores para crear y destruir objetos persistentes. new, delete, new [ ] y delete [ ]. Los dos primeros se utilizan para objetos de cualquier tipo. Por su parte new [] y delete [] se utilizan para crear y destruir matrices.
Casi todo lo dicho en epígrafes anteriores respecto a new, incluyendo sus precauciones y limitaciones, es también de aplicación a su contrapartida new[] para matrices, de modo que este último, más que un operador independiente, puede considerarse como una versión del primero con el modificador [ ] [2].
§2 Sinopsis
El operador new[ ] permite crear matrices de objetos de cualquier tipo en el montón
( 1.3.2), incluyendo
tipos definidos por el usuario, y devuelve un puntero al primer elemento de la matriz creada
Su utilización exige que el usuario declarare un puntero del tipo adecuado; a continuación este puntero debe ser inicializado
con el valor devuelto por el operador. Si el objeto creado es tipo T, sería algo así (más detalles a continuación
):
T* puntero = valor-devuelto-por-new[];
§3 Sintaxis
La sintaxis para crear una matriz de objetos tipo tipoX es:
<::> new <(situación)> <tipoX> [<dimension>];
<::> new <(situación)> (<tipoX>) [<dimension>];
El argumento <tipoX> es imprecindible.
Indica el tipo de objeto que se guardará en la matriz. Por ejemplo int, long, char, UnaClase, etc.
Si la especificación de tipoX es complicada, se permite englobarla en paréntesis para facilitar al compilador la correcta
interpretación (segunda forma de la sintaxis). Ejemplo:
int* imptr = new int[3]; // crear matriz de 3 enteros
<dimension> este argumento opcional indica la
dimensión de la matriz.
<::> argumento opcional que invoca
la versión global de new[]. Este argumento se utiliza cuando existe una versión específica de usuario (sobrecargada)
pero se desea utilizar la versión global.
<situación>, este especificador opcional
proporciona argumentos adicionales a new. Puede utilizarse solamente si se tiene una versión sobrecargada de new que
coincida con estos argumentos opcionales. Esta opción se ha previsto para aquellos casos en que el usuario desea poder definir el
sitio concreto en que se realizará la reserva de memoria (
4.9.20b).
Como puede verse, el operador new [ ] para
matrices no admite un iniciador como el operador new genérico, de forma que cuando se crea una matriz de objetos de
tipo abstracto, se utiliza siempre el constructor por defecto de la clase para iniciar cada uno de los objetos de la matriz.
Recordar que los objetos creados con new deben ser destruidos necesariamente con delete, y que las matrices
creadas con new[] deben ser borradas con delete[].
Nota: en el caso de matrices de tipos básicos (predefinidos en el lenguaje) siempre es posible iniciar el espacio correspondiente con la función memset de la librería C++ clásica.
§4 Descripción
Una expresión del tipo:
tipoX* ptr = new tipoX [size]
Reserva en el montón un espacio definido por sizeof(tipoX) * size + n; suficiente para alojar una matriz de size elementos de tipoX. El resultado del operador es un puntero que señala al primer elemento de la matriz.
Nota: el valor n representa un espacio adicional que necesita el compilador para incluir información sobre las dimensiones de la matriz y sobre el tamaño reservado [1]. Este valor es dependiente de la implementación, y puede variar de una invocación de new[] a otra. Por supuesto, todo este espacio es liberado cuando se utiliza el operador delete[] con el puntero correspondiente
Cuando se crean matrices multidimensionales con new, deben proporcionarse todas las dimensiones,
aunque la primera dimensión no tiene porqué ser una constante (de tiempo de compilación),
las siguientes sí ( 4.3.8).
void func() {
int* pt1[]; // L.2 Ilegal
extern int* pt2[]; // L.3: Ok.
int* pt3[3]; // L.4: Ok. correcto
...
pt5 = new int[3][10][12]; // L.5: Ok. correcto
pt6 = new int[n][10][12]; // L.6: Ok. correcto
pt7 = new int[3][][12]; // Ilegal
pt8 = new int[][10][12]; // Ilegal
}
L.2 es ilegal porque no especifica el tamaño de la matriz, sin embargo L.3 es correcto (se indica al compilador que el resto de
la información está en un módulo distinto
4.1.8d). L.4 declara una matriz de tres elementos que son punteros-a-int.
L.3 simplemente declara un objeto (existencia semántica); L.4 declara el objeto y lo inicia (reserva espacio en memoria); en este caso en la pila, ya que es un objeto automático.
L.5 y L.6 crean matrices tridimensionales en el montón. Son objetos anónimos (que no tienen identificador), por lo que deben ser accedidos indirectamente. En este caso el acceso deberá realizarse a través de los punteros pt5 y pt6, que suponemos son objetos automáticos, y por tanto situados en la pila; además pt5 y pt6 deben ser de tipos adecuados para señalar a una matriz tridimensional de enteros (ver a continuación).
§5 Asignar el valor devuelto
La forma usual de utilizar el operador new[ ] es en sentencias de asignación. Puesto que new[] devuelve un puntero, es utilizado en el lado derecho (Rvalue) de la asignación. En el lado izquierdo (Lvalue) debe existir un puntero de tipo adecuado para recibir el valor devuelto. Preste atención a las declaraciones de punteros de los siguientes ejemplos:
int* mptr1 = new int[3];
// L.4: Ok.
int (* mptr1) = new int[3];
// L.5: Ok.
int (* mptr2)[] = new int[3][10];
// L.6: Error
int (* mptr2)[10] = new int[3][10]; // L.7: Ok.
int (* mptr3)[][2] = new int[3][10][2]; // L.8: Error
int (* mptr3)[10][2] = new int[3][10][2]; // L.9: Ok.
int (* mptr4)[][2][5] = new int[3][10][2][5]; // L.10: Error
int (* mptr4)[10][2][5] = new int[3][10][2][5]; // Ok.
Comentario
Las líneas 6, 8 y 10 compilan sin dificultad con Borland C++ 5.5, aunque producen error con Visual C++ 6.0 y Linux GCC v
2.95.3. En rigor los tipos de los punteros a la izquierda y derecha de la asignación no son iguales en estas líneas. Por ejemplo,
en L.8 el tipo del Lvalue es int ( *)[ ][2], mientras que el Rvalue es int ( *)[10][2]. En estos casos es posible
hacer un "casting" (
4.9.9) explícito para convertir el tipo de la derecha en el de la izquierda (es lo que hace Borland
automáticamente).
Observe que en L.4 el puntero a matriz de enteros de una dimensión int[3] se define como puntero-a-entero int*.
Que en L.7, el puntero a matriz de enteros de dos dimensiones int[3][10] se define como puntero-a-matriz de enteros de una
dimensión int ( *)[10]. Que en L.9 el puntero a matriz de tres dimensiones int[3][10][2] se define como puntero
a matriz de enteros de dos dimensiones int ( *)[10][2]; y a así sucesivamente
( 4.3.6).
§6 Ejemplo
El programa que sigue muestra el uso del operador new[] asignando espacio para una matriz bidimensional y de delete[ ] para desasignando después.
#include <exception>
#include <iostream.h>
void display(double **); // L3: función auxiliar-1
void borra(double **); // L4: función auxiliar-2
int fil = 3;
// L5: Número de filas
int col = 5;
// L6: Número de columnas
int main(void) { // ==========
double** data; // M1:
try {
// Controlar excepciones
data = new double* [fil]; // M3:
for (int j = 0; j < fil; j++) //
data[j] = new double [col]; // Fase-2: Establecer columnas
}
catch (std::bad_alloc) { // capturar posibles errores
cout << "Imposible asignar espacio. Saliendo...";
exit(-1);
// terminar con error
}
for (int i = 0; i < fil; i++) //
for (int j = 0; j < col; j++)
data[i][j] = i + j;
display(data);
// mostrar datos
borra(data);
// borrar datos
delete[] data;
// A23: Borrar filas
return 0;
// terminar Ok.
}
void display(double **data) { // Función auxiliar-1
for (int i = 0; i < fil; i++) {
for (int j = 0; j < col; j++)
cout << data[i][j] << " ";
cout << "\n";
}
}
void borra(double **data) { // Función auxiliar-2
for (int i = 0; i < fil; i++) // A21:
delete[] data[i];
// A22: Fase-1: Borrar columnas
}
Salida:
0 1 2 3 4
1 2 3 4 5
2 3 4 5 6
Comentario
Se trata de crear; mostrar, y borrar una matriz bidimensional M[3][5]. Los elementos serán de tipo double (podría ser otro tipo cualquiera, simple o abstracto). Las primeras sentencias simplemente declaran dos funciones auxiliares (encargadas de mostrar y borrar los elementos de la matriz), y establecen el número de filas y columnas.
Como todo programa en que se manejen asignaciones de memoria con new, se dispone de un sistema de control para manejar
cualquier posible fallo de este operador, que en tal caso lanzará una excepción bad_alloc
( 4.9.20d).
La matriz M será persistente (se creará en el montón) y estará referenciada mediante el correspondiente puntero. En este caso el puntero debe ser de tipo double** (puntero-a-puntero-a-double); lo denominamos mat y lo declaramos en M1 (mat es un objeto automático creado en la pila).
La primera sentencia realmente interesante es M3:
data = new double* [fil]; // M3:
El puntero data se inicia con el resultado de crear una matriz de fil elementos tipo double* (puntero-a-double). Justamente porque cada elemento de esta matriz contendrá un puntero a una matriz de doubles (cada una de las cuales es una columna).
[1] Esta información es la que permite que posteriormente pueda utilizarse el operador delete[] puntero sin indicar el tamaño de memoria a liberar o las dimensiones de la matriz.
[2] De hecho, el operador new[] para matices es una implementación tardía; inicialmente el lenguaje no disponía de este operador.