Disponible la nueva versión "donationware" 7.3 de OrganiZATOR
Descubre un nuevo concepto en el manejo de la información.
La mejor ayuda para sobrevivir en la moderna jungla de datos la tienes aquí.

Curso C++

[Home]  [Inicio]  [Índice]


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).

  Inicio.


[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.