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.20a   El operador new,  detalles de funcionamiento

§1  Presentación

Las ideas generales señaladas hasta ahora ( 4.9.20) corresponden a la descripción general que se ofrecen en la mayoría de los libros de texto sobre el operador new. Sin embargo, este operador encierra particularidades especiales que merecen una explicación más detallada. Máxime porque comprender todos sus entresijos [1], requiere hablar de "los" operadores new, porque en realidad, más que de un operador, se trata de una amalgama de funciones bajo una envoltura común.

Recordemos que la mayoría de las afirmaciones incluidas aquí sobre new pueden hacerse extensivas a su versión new[] para matrices.

§2  La función-operador new()

Para entender el asunto habría que empezar recordando que, en el preámbulo de esta sección dedicada a los operadores C++ ( 4.9), señalamos que estos pueden ser considerados como una notación alternativa de ciertas funciones, y que precisamente la sobrecarga de operadores se basa en la sobrecarga de ciertas funciones denominadas función-operador ( 4.9.18). Los operadores new y delete no son una excepción; los compiladores C++ proporcionan una versión por defecto de las funciones-operador new(), new[](), delete() y delete[]().

Ocurre que el compilador declara implícitamente en el espacio global de cada unidad de compilación las siguientes funciones (sus definiciones están incluídas en la Librería Estándar):

void* operator new(std::size_t) throw(std::bad_alloc);    // §2a
void* operator new[](std::size_t) throw(std::bad_alloc);  // §2b
void operator delete(void*) throw();                      // §2c
void operator delete[](void*) throw();                    // §2d


Es importante significar que estas funciones no están en el subespacio std como el resto de entidades de la Librería Estándar ( 4.1.11c2) sino en el espacio global del fichero. Precisamente por esto, es costumbre referirse a ellas como versión global del operador correspondiente.

Nota: esta declaración implícita no introduce los nombres stdstd::bad_alloc  o  std::size_t. De forma que es posible invocar estas funciones ( new(), new[](), delete() y delete[]() ) sin tener que incluir el fichero de cabecera <new>.  Sin embargo, si los utilizamos explícitamente, entonces sí es necesario incluir el fichero de cabecera o un mecanismo equivalente. La razón es que los términos size_t y bad_alloc sí están definidos en la referida cabecera.


De la inspección de los prototipos se desprende que tanto new() como new[]() aceptan como primer argumento un tipo size_t, y que pueden lanzar una excepción de tipo bad_alloc, o derivado de él (más al respecto en 4.9.20d). También que ambas versiones de delete() aceptan un puntero de cualquier tipo como argumento [4], y no pueden lanzar ninguna excepción.

Es siempre posible una invocación explícita de estas funciones con solo utilizar el especificador de ámbito :: para referirnos al espacio global del fichero ( 4.1.11c). De forma que tendríamos ::operator new() y ::operator new[]() para asignar objetos y matrices, y las correspondientes ::operator delete() y ::operator delete[]() para desasignar la memoria reservada con las anteriores.

Por ejemplo, es lícito reservar una cantidad size de bytes en el montón mediante una invocación directa de la función-operador new() en la forma:

void* ptr = ::operator new(size_t(size));   // §2e

aquí utilizamos un modelado explícito para indicar el tamaño de memoria necesaria (bytes) en unidades tipo size_t [2], ya que este es el tipo esperado por la función-operador. También podría asignarse espacio para un objeto de tipoX en la forma:

void* ptr = ::operator new(sizeof(tipoX));  // §2f

Es igualmente posible utilizar la versión global de delete() para la desasignación:

::operator delete(ptr);     // §2g

§2.1  Qué sucede realmente

  Cuando el código contiene una expresión del tipo:

new tipoX

para construir un objeto de una clase tipoX definida por el usuario, que dispone de su propia versión de operator new(), lo que hace realmente el compilador es recabar la memoria necesaria para el objeto mediante una invocación al método de la clase en la forma [5]:

tipoX::operator new( sizeof( tipoX ) )

A continuación invoca al constructor de la clase para construir el objeto en el sitio recién asignado. Si la utilización de new es como la anterior (sin iniciador explícito), entonces se utiliza el constructor por defecto (sin argumentos). Por contra, si se hubiesen utilizado iniciadores, se invocaría el constructor que coincidiese con los argumentos suministrados (). En nuestro caso, el compilador incluiría una invocación al constructor en la forma:

tipoX::tipoX();

En caso de ser tipoX un tipo abstracto ( 2.2) que no dispone de su propia versión sobrecargada, entonces la invocación se refiere a la versión global:

::operator new( sizeof( tipoX ) )

A continuación se invocaría el constructor por defecto tipoX::tipoX() para iniciar un objeto en la zona asignada [9].

Cuando se pretende crear una matriz de objetos de un tipo abstracto, se sigue un proceso análogo al anterior: En principio, la versión global de new asigna espacio para los elementos de la matriz ( 4.9.20c); a continuación se utiliza el constructor por defecto de la clase para iniciar cada uno de los objetos de la matriz. Por ejemplo, la sentencia:

new tipoX[5];

asignaría 5 espacios contiguos para 5 objetos tipoX. A continuación se invocaría cinco veces al constructor por defecto de la clase para iniciar un objeto tipoX en cada una de las posiciones, pero recuerde que el operador new[] para matrices no permite ningún tipo de iniciador, por lo que se utiliza siempre el constructor por defecto para iniciar los miembros de la matriz creada.


  La destrucción de objetos con delete sigue el proceso inverso. Cuando el compilador encuentra una sentencia como:

delete ptr;

en la que ptr es un puntero a un objeto tipoX que ha sido creado previamente con new, primero invoca al destructor de la clase (observe que en cualquier caso siempre existirá un destructor de oficio 4.11.2d):

ptr->tipoX::~tipoX();

A continuación, si la clase dispone de una versión particular del operador delete, se invoca dicho método:

tipoX::operator delete(ptr);

En caso contrario es invocada la versión global:

::operator delete(ptr);

Si se trata de la destrucción de un objeto creado con el operador new[] para matrices, entonces la sentencia

delete[] ptr;

provoca la sucesiva invocación del destructor

ptr-> tipoX::~tipoX();

una vez para cada miembro de la matriz. A continuación es invocada a la versión particular o global de la función operator delete[]() correspondiente, según que la clase disponga o no de una versión sobrecargada de este operador.

tipoX::operator delete[](ptr);  // Existe una versión específica
::operator delete[](ptr);       // tipoX no tiene su propia versión de delete[]

§2.2  new con tipos fundamentales

Es significativo que las versiones globales también sirven para construir objetos que no son clases, estructuras o uniones, sino tipos preconstruidos en el lenguaje. Es lo que ocurre cuando se utiliza new para crear tipos simples. Por ejemplo, en las expresiones:

int* iptr = new int;
*iptr = 5;
const char* cptr = new char('Z');
...
delete iptr;
delete cptr;

§2.3  Más funciones-operador

En realidad los compiladores C++ ofrecen diversas versiones (sobrecargadas) de las funciones-operador globales señaladas anteriormente. Para cada una de las formas §2a, §2b reseñadas , existen otras dos formas denominadas respectivamente en-posición ("In-place") y sin-excepción ("No-throw").

La forma en-posición responde a los prototipos:

void* operator new(std::size_t, void*) throw();     // §2.2a
void* operator new[](std::size_t, void*) throw();   // §2.2b


Como puede comprobarse, estas versiones se distinguen de las anteriores en que aceptan como segundo argumento un puntero de cualquier tipo, que sirve para señalar la dirección a partir de la cual se creará el objeto. También en que no lanzan una excepción en caso de fallo.

Las versiones "In-place" se invocan cuando se utilizan los operadores new/new[] con el especificador opcional de situación ( 4.9.20b). Por ejemplo:

 Forma utilizada  Función invocada
 new (ptr) tipoX;  operator new(std::size_t  sizeof(tipoX), ptr)
 new (ptr) tipoX[5];  operator new[](std::size_t sizeof(tipoX) * 5 + m, ptr)


La forma sin-excepción ("No-throw") responde a los prototipos:

void* operator new(std::size_t) throw();     // §2.2c
void* operator new[](std::size_t) throw();   // §2.2d


Estas versiones no pueden lanzar excepciones ( 1.6.4), y son invocadas cuando se desea alterar el comportamiento estándar de new/new[ ] frente a la ocurrencia de un error. De forma que en lugar de lanzar una excepción bad_alloc, se devuelve un puntero nulo. Sería el caso de una invocación del tipo:

char* cptr = new (std::nothrow) char[5];

Como veremos al tratar del manejo de errores ( 4.9.20d), estas versiones se diseñaron de forma que los nuevos operadores new pudieran utilizarse con un mínimo de modificación en programas antiguos, diseñados de forma que la comprobación de error no se realizaba recibiendo una excepción, sino comparando el valor del puntero devuelto con NULL.

§2.3  Formas de uso y funciones invocadas

Hemos visto que en realidad, debajo del las diversas sintaxis de uso de los operadores new y new[] subyace un conjunto de funciones-operador proporcionadas por defecto por el compilador (que en muchos casos pueden ser sobrecargadas). Para ofrecer una visión de conjunto, relacionamos a continuación las formas de utilización (en el fuente), junto con la función-operador realmente utilizada por el compilador. Sin olvidar que, como hemos indicado , estas últimas también pueden ser utilizadas directamente.

Nota: en el cuadro suponemos que la clase T no dispone de su propia versión sobrecargada del operador, por lo que es invocada la versión global.

Uso

Funciones invocadas

new T;

::operator new(std::size_t sizeof(T)) throw(std::bad_alloc);

T::T();       // constructor

new T[5];

::operator new[](std::size_t sizeof(T) * 5 + n) throw(std::bad_alloc);

// ver nota [6]

T:T();        // constructor (5 veces)

new (pos) T;

::operator new(std::size_t sizeof(T), void* pos) throw();

T::T();       // constructor

new (pos) T [5];

::operator new[](std::size_t sizeof(T) * 5 + m, void* ptr) throw();

T:T();        // constructor (5 veces)

new (std::nothrow) T;

::operator new(std::size_t sizeof(T), const std::nothrow_t&) throw();

T:T();        // constructor

new (std::nothrow) T[5]

::operator new[](std::size_t sizeof(T), const std::nothrow_t&) throw();

T:T();        // constructor (5 veces)


§3  Sobrecarga del operador new global

Es posible sobrecargar la versión global de new proporcionada por el compilador [3], de forma que existan distintas versiones en un mismo programa, pero cada nueva instancia debe tener una firma ("Signature") distinta. Las nuevas versiones tendrían el siguiente aspecto:

void* operator new(size_t sz) { ... }


La definición de las nuevas funciones deberá atenerse a las siguientes normas:

  • Deben declararse en el espacio global. No pueden declararse en un subespacio distinto, ni pueden declararse static ( 4.1.8c) en el espacio global.

  • Deben devolver un puntero genérico void*, y aceptar como primer argumento un tipo size_t, que no podrá tener un valor por defecto y que será interpretado como el tamaño del espacio solicitado.

  • Salvo que se trate de una versión "in-place" , la función devolverá la dirección inicial de un espacio contiguo de al menos, el tamaño solicitado en el primer argumento.

  • La solicitud de un espacio de tamaño 0 devolverá un puntero no NULO, y distinto al de cualquier otro objeto (en el caso del compilador Borland C++ las peticiones sucesivas en este sentido, devuelven punteros "no nulos" distintos).

  • En caso de fallo en la asignación solicitada, la nueva función puede invocar al manejador instalado en su caso mediante la función set_new_handler ( 4.9.20d). Si no se hubiese indicado ningún manejador, entonces deberá lanzar una excepción bad_alloc o de una clase derivada de ella.


Una vez definida la versión sobrecargada según las pautas anteriores , las invocaciones a new utilizarían la nueva versión, aunque todavía se podrían utilizar las versiones globales mediante el operador :: de acceso a ámbito ( 4.9.19).

int* iptr = new int (123);      // invoca versión sobrecargada
int* iptr = ::new int (123);    // invoca versión global

§4  Sobrecarga del operador new para objetos de una clase

Igual que ocurre con el resto de operadores, la sobrecarga del operador new para tipos abstractos, se realiza definiendo versiones particulares de las funciones-operador correspondientes. Como base pueden utilizarse los siguientes prototipos:

void* operator new(size_t Type_size);     // General
void* operator new[](size_t Type_size);   // Para matrices

Nota: ver en ( 4.9.21) la forma de sobrecargar el operador delete en tipos abstractos.


Ejemplo:

struct E {
  void* operator new(size_t sz) { ... }
  void operator delete(void* ptr) { ... }
  ...
};

Lo normal es que desde estos métodos se invoquen las versiones globales de new y new[]. Pero recuerde que ellos mismos ocultan las versiones globales. Ejemplo:

struct E {
  void* operator new(size_t sz, int val) { ... }
  ...
};
...
E* ePtr = new E;   // Error!!


la última sentencia es transformada por el compilador en una invocación a E::operator new(sizeof(E));. Pero no existe ningún método E::operator new() que responda a estos argumentos, porque E::operator new(size_t, int) ha ocultado a ::operator new(size_t).

En una definición como la anterior (§4 ), la estructura E dispone de su propia versión de los operadores new y delete, de forma que las expresiones:

E* eptr = new E;
...
delete eptr;


crean un objeto de tipo E y posteriormente lo destruyen utilizando los métodos E::operator new() y E::operator delete(), antes que las versiones globales. Solo aquellas clases que no dispongan de sus propias versiones de new() y delete(), invocan las versiones suministradas por defecto por el compilador. Pero recuerde que las versiones de estos operadores definidas para una clase son heredadas por las subclases. Es muy frecuente que en las jerarquías de clases las versiones de new() y delete() de toda la estirpe se encuentren definidas en las superclases.

Estudie con detenimiento el ejemplo propuesto ( 4.9.20b), y observe que el método E::operator new() no realiza él mismo todo el trabajo involucrado en la sentencia new E. En realidad se limita a asignar el espacio y devolver un puntero a la zona reservada. La segunda parte (la correcta inicialización del objeto) es encomendada a los constructores E::E(). Estos pasos son realizados automáticamente por el "operador" new.


Las limitaciones para las nuevas definiciones de tipoX::operator new() son análogas a las declaradas para la versión global:

  • Deben devolver un puntero genérico void*, que corresponderá lo la dirección del espacio reservado. Y aceptar como primer argumento un tipo size_t, que será interpretado como el tamaño del espacio solicitado (además de este, puede utilizarse cualquier número de argumentos adicionales).

  • La función devolverá la dirección inicial de un espacio contiguo de al menos, el tamaño solicitado en el primer argumento.

  • La solicitud de un espacio de tamaño 0 devolverá un puntero no NULO, y distinto al de cualquier otro objeto (las peticiones sucesivas en este sentido, devolverán punteros "no nulos" distintos).

  • En caso de fallo en la asignación solicitada, la nueva función puede invocar al manejador instalado (en su caso) mediante la función set_new_handler ( 4.9.20d). Si no se hubiese indicado ninguno, entonces deberá lanzar una excepción bad_alloc o de una clase derivada de ella.


  Recuerde que estas funciones-miembro tipoX::operator new();  tipoX::operator new[]();  tipoX::operator delete() y tipoX::operator delete[]() son declaradas estáticas por el compilador [7]. La razón es que son invocadas antes que el constructor y después que el destructor. En realidad estas funciones no operan sobre objetos de la clase, sino sobre el gestor de memoria. new se limita a reservar una zona de memoria sobre la que posteriormente trabajará el constructor para transformarla en un objeto de la clase. delete procede a la liberación de una zona de memoria en la que previamente ha trabajado el destructor.

Ejemplo recopilatorio  ( Ejemplo)

§5  Valor inicial

Hemos señalado ( 4.9.20), que la sintaxis de new permite utilizar un especificador opcional <(iniciador)>, que se utiliza para iniciar el objeto creado. Este iniciador puede estar constituido por una lista de expresiones separadas por comas y situadas entre paréntesis. Ejemplo:

ClaseC cPtr = new ClaseC (arg1, arg2);


En realidad, cuando el compilador encuentra una expresión como la anterior, reserva espacio de almacenamiento, y a continuación crea en este espacio un objeto mediante una invocación al constructor adecuado. Para ello utiliza las expresiones del iniciador como argumentos del constructor (por supuesto se exige la existencia de un constructor que se corresponda con estos argumentos!!). En el caso del ejemplo se realizaría la invocación:

ClaseC::ClaseC(arg1, arg2);

Si el especificador está vacío ( ) o es inexistente, entonces se invoca el constructor por defecto. Por ejemplo, la sentencia:

ClaseC cPtr = new ClaseC ();

conducirá a la invocación:

ClaseC::ClaseC();


Ejemplo:

#include <iostream>
using namespace std;

class C {
  public:
  int x;
  void* operator new(size_t size) {
    return ::operator new(size);
  }
  C (int n=10): x(n) {}  // constructor
};

int main() {    // ================
  C* cPt1 = new C;       // M1:
  C* cPt2 = new C (20);  // M2:
  cout << "Valor x = " << cPt1->x << endl;
  cout << "Valor x = " << cPt2->x << endl;
  return 0;
}

Salidas:

Valor x = 10
Valor x = 20

Comentario:

La sentencia M1 es descompuesta por el compilador en una invocación al método operador C::operator new() con un argumento, y otra al constructor sin argumentos:

C::operator new(sizeof(C));
C::C();

A su vez la sentencia M2 es descompuesta en una invocación al método  C::operator new() con un argumento, y otra al constructor también con un argumento:

C::operator new(sizeof(C));
C::C(20);


Preste especial atención, las expresiones siguientes tienen significados totalmente distintos ( 4.9.20b):

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


§5.1  Hemos señalado , el operador new puede ser utilizado incluso con los tipos básicos (preconstruidos en el lenguaje), aunque no sea desde luego una situación frecuente. Es significativo que el compilador también proporciona constructores adecuados para estos tipos.

Ejemplo:

int* ip1 = new int (13);
cout << *ip1;            // -> 13
int* ip2 = new int ();
cout << *ip2;            // -> 0

Ejemplo:

void func() {
  int x = 13;                  // L1:
  int* ip = new int (x);       // L2:
  cout << *ip;                 // -> 13
  int** ipp = new (int*) (ip); // L3:
  cout << **ipp << endl;       // -> 13
}


Comentario

En L1. se crea un objeto automático x, tipo int, en la pila, al que se inicia con el valor 13. En L2 se crean dos nuevos objetos: un tipo puntero-a-int ip automático, y un entero (al que llamaremos i1) en el montón. i1 se inicia con el valor de x. A su vez ip se inicia con la dirección de i1.

En L3 se crean dos nuevos objetos: un puntero-a-puntero-a-int ipp en la pila, y un puntero-a-int (que llamaremos ip1) en el montón. ip1 se inicia con el valor de ip. A su vez ipp se inicia con un valor que es la dirección de ip1.

§5.2  Ejemplo

#include <iostream>
using namespace std; 

class A {
  public:
  int x, y;
  A (int n = 0) { x = y = n; }        // constructor por defecto
  A (int a, int b) { x = a; y = b; }  // constructor-2
};

int main (void) {  // =============
  A* a1p = new A;            // M.1
  A* a2p = new A (10);       // M.2
  A* a3p = new A (20, 30);   // M.3
  cout << "a1 == " << a1p->x << "," << a1p->y << endl;
  cout << "a2 == " << a2p->x << "," << a2p->y << endl;
  cout << "a3 == " << a3p->x << "," << a3p->y << endl;
  int* i1p = new int;        // M.7
  int* i2p = new int (123);  // M.8

  int x = 123;               // M.9
  cout << "i1 == " << *i1p << endl;
  cout << "i2 == " << *i2p << endl;
  return 0;

Salida:

a1 == 0,0
a2 == 10,10
a3 == 20,30
i1 == 4302004
i2 == 123


Comentario

Las tres primeras sentencias de main muestran tres formas de invocación del operador new para crear tres instancias a1, a2 y a3, de un tipo definido por el usuario (clase A). Las formas M.2 y M.3 utilizan un iniciador opcional. Los objetos creados son accesibles mediante tres punteros adecuados a1p, a2p y a3p, que reciben los valores devueltos por new.

En las sentencias M.1 y M.2 se utiliza el constructor por defecto. En M.3 es invocado el segundo constructor de la clase.

Las sentencias M.7 y M.8 son un método algo heterodoxo para crear sendos punteros a un tipo básico ( 2.2.1) utilizando el operador new. Sirven como demostración de que incluso los tipos preconstruidos en el lenguaje disponen de un constructor adecuado. A este respecto, hay que señalar que el constructor por defecto del tipo int, crea el objeto pero no inicializa el espacio con ningún valor. El resultado obtenido en i1 es basura (contenido previo de esta zona del montón).

Obviamente, existe una diferencia substancial entre la sentencia M.8 y M.9. Aunque ambos representan el mismo valor, el objeto señalado en M8 es persistente y está situado el montón, mientras que x es un objeto automático creado en la pila.

  Inicio.


[1]  En mi opinión es, con diferencia, el operador de más difícil comprensión del lenguaje C++.

[2]  Recuerde que el resultado del operador sizeof ( 4.9.13) es size_t, un entero sin signo cuya definición depende de la implementación, y que puede encontrarse en los ficheros de cabecera MALLOC.H y MEMORY.H entre otros. En el caso de MS Visual C++ 6.0 y Borland C++, el tipo de size_t es unsigned int.

[3]  Aunque es posible sobrecargar la versión global del operador new, el propio Stroustrup nos indica que no es aconsejable para personas de espíritu frágil: "However, replacing the global operator new() and operator delete() is not for the fainthearted".  [TC++PL-00] §15.6.  Además indica que si dos programadores escriben sus propias versiones de estos operadores, su código no podrá ser integrado en proyectos más generales sin un esfuerzo adicional.

[4]  Con las excepciones señaladas en ( 4.2.1d). Por ejemplo, no puede pasárseles la dirección de una función.

[5]  El hecho de que una invocación a new suponga en realidad una invocación a la función-operador, hace que coloquialmente operador new y función-operador new() sean casi sinónimos. El Estándar las denomina funciones asingadoras y desasignadoras ("allocation functions" y "deallocations functions").

[6]  Ver el operador new[] para matrices ( 4.9.20c) para una explicación del valor n.

[7]  B. Stroustrup [TC++PL-00] §15.6:  "Member operator new() and operator delete() are implicitly static members". En consecuencia, no pueden ser declarados virtual ( 4.11.7).

[9]  En realidad, esta invocación del constructor es un tanto especial, y en cierta forma distinta de la que podemos realizar manualmente, ya que crea el objeto en un sitio concreto. Algo que no podemos hacer con un constructor normal (a menos naturalmente que utilicemos new).