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.20   Operador new

§1  Presentación

Podría decirse que los objetos C++ son efímeros por naturaleza. De hecho, su persistencia es automática por defecto ( 4.1.5), y salvo que se adopten medidas en contrario, los objetos creados en el ámbito de un bloque o función son automáticamente destruidos al salir del mismo. Esta destrucción tiene lugar porque el compilador invoca los destructores adecuados al salir del ámbito.

La razón última de este comportamiento es económica y de eficacia; de no ser así, la ejecución del programa conllevaría la existencia de un número creciente de objetos persistentes que ocuparían más y más memoria. La mayoría de las veces de forma inútil, ya que los objetos creados en muchas rutinas y funciones no vuelven a ser utilizados en la vida del programa, y la mayoría de veces en que vuelven a utilizarse, lo hacen con unos valores iniciales distintos de la última utilización.

A pesar que el lenguaje C++ no dispone de objetos persistentes de forma nativa [1], es evidente que en algunas situaciones es deseable y conveniente esta persistencia, por lo que su creador lo dotó de los oportunos mecanismos lógicos y físicos para la creación, almacenamiento y eventual destrucción de objetos persistentes. Observe que en este contexto, la persistencia consiste en que una vez salido del ámbito en que se creó el objeto, conserva los valores que tenía si vuelve a ser utilizado.

En el capítulo correspondiente ( 2.2.6) vimos que los programas C++ disponen básicamente de dos sitios donde almacenar objetos: el montón y la pila, y que dependiendo del que se utilice, los objetos tienen distinta duración ("Lifetime" 4.1.5). Los objetos persistentes (también denominados dinámicos) se alojan en el montón, y con independencia de las acciones concretas del programador (crearlos y destruirlos), el gestor de memoria del compilador ofrece cierto grado de control sobre esta zona, proporcionando algunas facilidades para la correcta creación y destrucción de tales objetos.

El C clásico proporciona formas específicas de solicitar al compilador que reserve y/o inicie espacio de tamaño arbitrario en el montón ( 1.3.2). También dispone de formas para rehusarlo cuando ya no se considera necesario. Son las funciones de librería malloc, calloc, realloc y free. C++ mantiene (por compatibilidad) las formas clásicas, pero introduce cuatro nuevos operadores: new, delete, new [ ] y delete [ ], que no solo crean y rehúsan espacio de forma más cómoda y eficiente, sino que incluso pueden crear objetos persistentes. Los dos primeros se utilizan para objetos de cualquier tipo, mientras que new [ ] y delete [ ] se usan con matrices.


  Las diferencias de new con las funciones malloc y calloc de las librerías C tradicionales comportan una clara ventaja a favor de la primera:

  • malloc y calloc devuelven un puntero-a-void, mientras que new devuelve un puntero conformado al tipo específico del objeto creado.

  • malloc y calloc simplemente reservan memoria (calloc la inicializa a 0), mientras que new construye el objeto, lo que hace que este operador esté estrechamente relacionado con los constructores cuando se aplica a clases.

  • new y delete pueden ser sobrecargados como el resto de los operadores, lo que comporta ciertas ventajas adicionales.

Nota:  Aunque el operador new supone un avance sobre la forma C "clásica" de asignar espacio persistente, en muchos casos la STL ( 5.1) hace innecesaria su utilización directa (por ejemplo para crear matrices), ya que esta librería dispone de mecanismos que realizan esta función de forma más cómoda para el programador. Incluso con la ventaja adicional de no tener que preocuparse de rehusar posteriormente los objetos creados.

§2  Sinopsis

El operador new (palabra clave C++) proporciona espacio de almacenamiento persistente, similar pero superior a la función de Librería Estándar malloc. Este operador permite crear un objeto de cualquier tipo, incluyendo tipos definidos por el usuario, y devuelve un puntero (del tipo adecuado) al objeto creado.

Su utilización exige que el usuario declarare un puntero del tipo adecuado; a continuación 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-el-operador;

§3  Sintaxis

<::> new <(situación)>  tipoX  <(iniciador)>
<::> new <(situación)> (tipoX) <(iniciador)>

El argumento tipoX  es imprescindible. Indica el tipo de objeto para el que se obtendrá espacio de almacenamiento. Ejemplo:

ClaseA* ptr = new ClaseA;

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: el tipo de una matriz de diez punteros a función que no reciben argumentos y devuelven un int es: int(*[10])(). Sin embargo, la invocación a su construcción con new:

new int(*[10])();        // Error

produce un error, ya que el compilador es incapaz de interpretar correctamente una sentencia como la anterior; en su lugar interpretaría:

(new int) (*[10])();     // Error

Para evitar el error es necesario incluir la expresión en paréntesis:

new (int (*[10])());     // Ok.


El resto de argumentos, opcionales, son los siguientes:

  <::> Operador que invoca la versión global de new [2]. Este argumento opcional se utiliza cuando existe una versión específica de usuario (sobrecargada) pero se desea utilizar la versión global (en caso contrario no es necesario).

  <(situación)>, este especificador opcional permite especificar el sitio concreto en que se realizará la reserva de memoria ( 4.9.20b). Ejemplo:

tipoX* xp = new (z) tipoX;   // definir sitio de almacenamiento

  <(iniciador)>, en su caso, se utiliza para inicializar el almacenamiento ( 4.9.20a). Ejemplo:

tipoX* xp = new tipoX (z);   // definir valor inicial

En la página adjunta se muestra la gramática C++ para este operador ( Gramática).

Nota: las matrices no pueden ser creadas con este operador, ya que disponen de su propia versión new[ ] para este propósito ( 4.9.20c). No obstante, casi todo lo que digamos sobre los operadores new y delete, puede ser aplicado también a las versiones para matrices new[ ] y delete[ ].

&4  Descripción

En la expresión

new ClaseC;

el operador new intenta asignar un espacio de tamaño sizeof(ClaseC) en la zona de memoria dinámica ( 2.2.6). A continuación intenta crear en esta posición una instancia de la clase utilizando el constructor adecuado. Como resultado de estas dos operaciones se obtiene la dirección (puntero) del objeto creado. Este puntero devuelto por new es del tipo correcto: puntero-a-ClaseC, sin que se necesaria ninguna conversión de tipo ("casting") explícita.

Aunque el operador tiene algunas limitaciones , puede utilizarse con tipos calificados ( 2.2). Por ejemplo, es válido:

new const ClaseC;


Los objetos creados con new son persistentes, es decir, la vida del nuevo objeto es desde el punto de creación hasta el final del programa o hasta que el programador lo destruya explícitamente con el operador delete ( 4.9.21). Este último desasigna la zona de memoria ocupada por el objeto, de forma que queda disponible para nuevo uso. Las sucesivas invocaciones de este operador van reservando zonas de memoria en el montón para los objetos sucesivamente creados. El gestor de memoria del compilador se encarga de mantener una tabla con los sitios ocupados y libres sin que haya conflictos hasta que la memoria se ha agota, o no existe espacio contiguo suficiente para el nuevo objeto. En cuyo caso se lanza una excepción como indicativo del error .

El operador new puede aceptar un inicializador opcional para que rellene el espacio reservado con el valor suministrado. Sin embargo, su versión para matrices new[ ] no acepta iniciador ( 4.9.20c). En caso de no proporcionarse iniciador, el objeto creado contiene basura.

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

  • Los objetos estáticos aunque son de carácter permanente, tienen su propia zona de almacenamiento distinta del montón ( 1.3.2), por lo que no se crean con este operador.

  • Por razones evidentes, no es posible crear un objeto de un tipo ClaseC que sea una clase abstracta ( 4.11.8c).


Podemos resumir lo anterior diciendo que la invocación del operador new en una sentencia como

ClaseC* cPtr = new ClaseC;

implica tres operaciones que son realizadas automáticamente:

  1. Se reserva espacio suficiente en el montón para el objeto [3]. new utiliza el operador sizeof para determinar el espacio adecuado a reservar. A continuación invoca al constructor del objeto (ver punto 3º ).
  2. Se crea un puntero cPtr adecuado al tipo (en este caso su tipo es puntero-a-ClaseC). Este puntero se inicia con el valor de la dirección del espacio reservado en el punto anterior.
  3. Si se utiliza un iniciador opcional, new invoca el constructor correspondiente (que debe corresponder con los argumentos utilizados); en caso contrario se utiliza el constructor por defecto. Así pues, si tipoX es un tipo abstracto, definido por el usuario, debe existir un constructor por defecto (que pueda ser invocado sin argumentos 4.11.2d1), pues new lo utilizará para crear el objeto cuando sea invocado sin especificador de inicio.
§5  Peligros

 La persistencia de los objetos creados con new y su independencia del ámbito desde el que han sido creados, es muy importante y de tener en cuenta, pues suele ser motivo de pérdidas de memoria en el programa si olvidamos destruirlos cuando ya no son necesarios (ver ejemplos: 4.11.2d2). Hay que prestar especial atención, porque en una sentencia como:

void func() {
  ...
  tipoX* Xptr = new tipoX;
  ...
}

el área de almacenamiento señalada por el puntero es persistente, pero Xptr que es una variable local automática no lo es. Si olvidamos destruir el objeto creado (con delete) antes de salir del ámbito, el área de almacenamiento permanecerá ocupando espacio en el montón y no podrá ser recuperada nunca, pues el puntero Xptr habrá desaparecido.

La cuestión de pérdida de memoria no es solo cuestión de que el programador "recuerde" utilizar delete antes de salir del ámbito del puntero. También puede producirse por otras circunstancias. Por ejemplo, el mecanismo de lanzamiento y captura de excepciones C++ ( 1.6) puede funcionar como un salto, goto o break multinivel, que saque abruptamente de ámbito al puntero con la consiguiente pérdida irreparable. La situación puede ser esquematiza como sigue  (la figura adjunta muestra el estado de la pila y el alcance del desmontaje -"Stack unwinding"- caso de producirse un error).

  throw_
foo     |
fun2    |
try     |
fun1 ___V
...
...
...
...
main

fun1() {

  try { fun2(); }

  catch (...) {

     ...

  }

}

 

fun2() {

   A* aptr = new A;  // crear objeto

   foo();

   ...

   delete aptr;      // Ok. destruir el objeto

}                    // antes de salir de ámbito

 

foo() {

  ...

  if (x) throw "Error";

  ...

}

La situación anterior es de gran peligro potencial. Si se produce la condición x, y se lanza la excepción en foo, la pila será desmontada hasta el punto de la sentencia catch en fun1 que recibirá el control. La secuencia de ejecución no pasará nunca por el delete de fun2, con lo que el espacio del objeto A se perderá irremediablemente.

Conscientes del deficiente maridaje entre el operador new y el sistema C++ de excepciones, los diseñadores de la Librería Estándar, han incluido en esta un puntero "inteligente" auto_ptr, que resuelve este tipo de problemas ( 4.12.2b1).

  Tema relacionado
  • Control de recursos ( 4.1.5a).
§6  Limitaciones

En la especificación del tipo de objeto que debe crearse con new, no están permitidos los especificadores de almacenamiento static, auto y register. Tampoco typedef.

Ejemplo:

int* iptr = new static int;        // Error!!
int* iptr = new typedef int;       // Error!!
int* iptr = new auto int;          // Error!!
int* iptr = new register int;      // Error!!

Tampoco es posible crear referencias con new, ya que las referencias ( 4.2.3) no son objetos sino nombres alternativos (alias) de objetos.

Aunque el Estándar no indica nada al respecto, algunos compiladores no permiten utilizar el operador new con los especificadores const ( 3.2.1c) o volatile ( 3.2.1d). Por ejemplo, MS VC++ 6.0 no permite las siguientes sentencias:

int *const i1p = new const int;
volatile int * viptr = new volatile int;

que sin embargo, son perfectamente correctas con BC++ 5.5, mientras que GNU Cpp 2.95.2 produce error en la primera pero acepta la segunda.

§7  Como se controla la operación

Si la operación tiene éxito, new devuelve un puntero no nulo (distinto de cero) al objeto. Si la asignación falla (como en caso de no haber suficiente espacio en memoria dinámica), se lanza la excepción bad_alloc, a menos que se defina un nuevo manejador de excepciones. Antes de intentar usar el nuevo objeto, el programa debe estar siempre preparado para capturar dicha excepción (más detalles en 4.9.20d).

  Puesto que la invocación de new con éxito devuelve un puntero no nulo, la petición de espacio para 0 bytes devuelve un puntero "no nulo", y las peticiones sucesivas en este sentido, devuelven punteros "no nulos" distintos.

§8  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:

UnaClase*  punt1 = new UnaClase;    // Ok.
int* punt2 = new int;               // Ok.
char* punt3 = new int;              // Ilegal
char* punt4 = new char;             // Ok.

Nota: aunque los objetos creados en estos ejemplos son en su mayoría tipos simples, no es esta desde luego la utilización habitual del operador. Lo normal es utilizarlo con matrices o con tipos abstractos (estructuras y clases).

  Inicio.


[1]  Otra forma de decirlo es señalar que la persistencia no es un atributo que pueda ser identificado como tal en los objetos C++.

[2]  Tener en cuenta que como la mayoría de operadores, new también puede ser sobrecargado ( 4.9.18), y que la versión no sobrecargada de un operador se denomina versión global.

[3]  new no se limita a asignar el espacio estrictamente necesario para el objeto. El compilador incluye algún espacio adicional para indicar "cuanto" espacio hay reservado a partir de la posición señalada por el puntero. Este espacio suele ser de una palabra, y es la razón por la que más tarde, el operador delete sepa "cuanto" espacio de memoria queda libre a partir de la dirección que se rehúsa. En el caso de matrices se incluye también información sobre las dimensiones.