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.20b  Definir el sitio de almacenamiento

§1  Sinopsis

Como se ha indicado (4.9.20), el operador new proporciona espacio en memoria para un objeto, y esta asignación de memoria se realiza en la zona de memoria dinámica ( 2.2.6) que es gestionada de forma automática por el gestor de memoria del compilador. Sin embargo, hay situaciones donde se desea determinar con más precisión el sitio en que deseamos crear el objeto. Esta situación puede presentarse cuando al principio reservamos un espacio de memoria determinado, y queremos usarlo y rehusarlo con el fin de disminuir los problemas de fragmentación ( 1.3.2) o por cualquier otra causa.

§2  Uso

Recordemos la sintaxis de los operadores new/new []

<::> new <(situación)>  tipoX  <(iniciador)>    // §2a
<::> new <(situación)> (tipoX)  <(iniciador)>   // §2b
<::> new <(situación)>  tipoX  [<(dimension)>]  // §2c
<::> new <(situación)> (tipoX) [<(dimension)>]  // §2d

Los especificadores <::><(situación)> e <(iniciador)> son opcionales. Preste atención a las expresiones siguientes que tienen significados distintos:

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


Para señalar una situación específica, a partir de la cual queremos que new realice la construcción del objeto, se utiliza el especificador <(situación)>. Recordemos que este especificador esta constituido por una lista de expresiones (que puede estar vacía) entre paréntesis y separadas por comas.

La idea es indicar aquí una dirección adecuada en la que tengamos seguridad de que:

  1. Pertenece al montón
  2. El sistema la tiene reservada (el gestor de memoria no la utilizará para otro objeto).
  3. Hay suficiente espacio para nuestros propósitos.


La forma de asegurarlo es realizando una primera invocación que reserve el espacio adecuado, y a partir de aquí, reusarlo mediante invocaciones sucesivas con el especificador de situación. Por ejemplo, supongamos que necesitamos un área de 64 KB para ciertos usos. Existen varias formas para reservar este espacio; la más general es la versión global de la función-operador new():

void* ptri = ::operator new(size_t(64000));    // §2e

Aunque también son posibles otras:

char* ptri = ::new char (size_t(64000));    // §2f
void* ptri = ::new char [size_t(64000)];    // §2g


En las tres formas hemos utilizado un modelado para transformar la constante numérica 64000 en un tipo size_t. A partir de aquí podemos utilizar esta zona para alojar objetos de cualquier tipo, aunque desde luego, es nuestra responsabilidad procurar que tales objetos no rebasen el tamaño límite de 64 KB:

A* pt1 = new (ptri) A;    // crear un objeto en ptri
...                       // usar objeto
pt1->A::~A();             // destruir objeto
B* pt2 = new (ptri) B;    // crear otro objeto en ptri
...
pt2->B::~B();             // destruir el nuevo objeto
...                       // etc.
delete[] ptri;            // liberar definitivamente espacio

Observe que el indicador de posición (ptri) que acompaña a new debe incluirse entre paréntesis, y que la sentencia elegida para liberar definitivamente la memoria reservada, supone que la asignación se efectuó con la expresión §2g.

Preste atención que los objetos intermedios A y B se destruyen mediante mecanismos especiales (en este caso llamadas explícitas a los destructores de clase), pero no pueden destruirse utilizando la versión global de delete. En estos casos, si se utiliza el asignador de memoria con especificador de posición, debe disponerse también un desasignador particular. Es decir, debemos construir también nuestra propia versión del operador delete, ya que entonces no puede usarse la versión global ( 4.9.21).

Si se trata de un objeto y la destrucción se realiza mediante una llamada a su destructor (como en el ejemplo), debemos proceder con extrema precaución, y solo en situaciones especiales. Recordando que si se realiza una llamada explícita al destructor de un objeto, que haya sido construido en la pila, antes que salga de ámbito (no es nuestro caso), el destructor será llamado de nuevo cuando sea liberado el marco de la pila correspondiente a dicho ámbito ( 4.4.6b). Es la situación del ejemplo siguiente:

void func() {
  A a;          // Ok. objeto en la pila
  A* apt = &a;
  ...           // Ok. usar el objeto
  ptr->A::~A(); // Ok? destruir el objeto
  ...
}               // Peligro: volverá a ser invocado el destructor de a 

§3  Observaciones

Recordemos ( 4.9.20a) que el compilador ofrece por defecto una versión global ("In-place") de las funciones-operador ::operator new() y ::operator new[]() que aceptan un segundo argumento en el que se indica la dirección donde se creará el objeto. Este segundo argumento debe ser un puntero a la dirección deseada.

En realidad una expresión como:

tipoX* xp = new (pos) tipoX;

es traducida por el compilador a una invocación del tipo:

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

Si la clase tipoX no dispone de su propia versión, entonces la función invocada es:

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

lo que significa que en caso de fallo, no se lanzará ninguna excepción ( 1.6.4).


§3.1 Un punto especialísimo a considerar es que estas versiones "in-place" de new no reservan espacio, limitándose a crear el objeto en la zona señalada (por esta razón no pueden ser rehusadas con delete).

Nota: la afirmación de que no reservan espacio se refiere al gestor de memoria dinámica del compilador, en el sentido de que este no toma nota de la zona como ocupada porque supone que se trabaja sobre una zona reservada previamente.


§3.2  La versión global del operador new "in-place" no puede ser sobrecargada, aunque nada impide que se defina una versión particular para objetos de una clase que será utilizado en la creación de tales objetos (ver ejemplo a continuación ).


§3.3  El argumento <situacion> debe ser una dirección de memoria adecuada.  Ejemplo:

void func () {
  int x = 13;                    // L1:
  cout << x;                     // L2: -> 13
  int* ip = new (&x) int (130);  // L3:
  cout << *ip;                   // L4: -> 130
  cout << x;                     // L5: -> 130
  int* ix = new (&x+6000) int;   // L6: Error!
}

Comentario

El ejemplo, aparte de su posible utilidad didáctica, es un caso de lo que el Dr. Stroustrup denominaría "tweak the language". Hemos indicado que no es usual utilizar tipos simples con este operador. Además, presenta una curiosa singularidad que comentamos.

En L1 se crea un objeto automático x en la pila (tipo int), y se inicia con un valor. En L2 se comprueba que el resultado es el esperado.

En L3 se define un puntero-a-int ip (un objeto automático creado en la pila), que se inicia con el resultado de invocar new para crear un tipo int con dos especificadores:  un valor (130), y una dirección (la del objeto x).

Sorpresivamente, a pesar que esta es una dirección de la pila, no recibimos ninguna protesta del compilador [2]. En L4 comprobamos que la inicialización es correcta, y en L5, que efectivamente se ha creado en la dirección solicitada. El valor de x se ha modificado!!.

El programa compila correctamente, pero en L6 se produce un error fatal, de runtime, al pretender crear el objeto en una dirección fuera de la permitida por el Sistema a la instancia en ejecución.

§4  Ejemplo

El ejemplo que sigue muestra el uso del especificador opcional de situación para el operador new.

#include <iostream>
using namespace std;
 
class Alpha {
  union { char ch; char buf[10]; };
  public:
  Alpha(char c = '\0') : ch(c) {  // constructor C1
    cout << "Constructor de caracteres" << endl;
  }
  Alpha(char* s) {                // constructor C2
    cout << "Constructor de cadenas" << endl;
    strcpy(buf, s);
  }
  ~Alpha() {                      // destructor
    cout << "Alpha::~Alpha() " << endl;
  }
  void* operator new(size_t size, void* buf) {
    cout << "Alpha::new()" << endl;
    return buf;
  }
};

void main() {   // ================
  char* str = new char[sizeof(Alpha)];   // M1:
  Alpha* ptr = new (str) Alpha ('X');    // M2:
  cout << "str[0] = " << str[0] << endl;
  ptr->Alpha::~Alpha();                  // M4:
  ptr = new (str) Alpha ("mi cadena");   // M5:
  cout << "str = " << str << endl;       // M6:
  ptr->Alpha::~Alpha();                  // M7:
  delete[] str;                          // M8:
}

Salida:

Alpha::new()
Constructor de caracteres
str[0] = X
Alpha::~Alpha()
Alpha::new()
Constructor de cadenas
str = mi cadena
Alpha::~Alpha()

Comentario

La clase Alpha tiene un único miembro privado que es una unión ( 4.7). Los componentes de esta unión son un carácter ch, y una matriz de 10 caracteres buff. El tamaño de Alpha corresponde por tanto a buff que es el mayor. La clase tiene dos constructores: C1 y C2. El primero es un constructor por defecto que acepta un char para crear el miembro carácter de la unión. C2 acepta un puntero a carácter y crea el miembro matriz utilizando la función strcpy de la Librería Estándar.

Además de lo anterior, la clase dispone de su propia versión de la función-operador operator new(). Recordemos que este método es estático por defecto, de forma que su definición equivale a:

static void* operator new(size_t size, void* buf) {  ...  }

Este método acepta dos argumentos, y en realidad no hace gran cosa ya que su diseño es muy pobre.  Simplemente nos advierte de su invocación y devuelve el segundo argumento sin mayor transformación (recuerde que este tipo de funciones debe devolver un puntero).

  En M1 se define un puntero-a-char str, y se inicia con la dirección de un objeto creado con la versión global del operador new para matrices. En realidad estamos creando una matriz de caracteres de n elementos, siendo n el tamaño de Alpha. Observe que aquí cabe una matriz de n caracteres o una instancia de Alpha, y que str señala la primera posición de esta matriz.

  La sentencia M2 es una representación bastante completa de las posibilidades sintácticas del operador new. Crea un puntero-a-Alpha ptr, que se inicia con el resultado de new al crear una instancia de Alpha. El argumento tipoX de la sintaxis está representado por el miembro Alpha. El iniciador es ('X') [1], y el especificador de situación está representado por (str).

Esta sentencia es sustituida por el compilador por otras dos:

Alpha::operator new(sizeof(Alpha), str);
Alpha::Alpha('X');

La primera, una invocación a la versión particular de operator new() de Alpha con los argumentos apropiados, es responsable de la primera salida, y como resultado se obtiene el valor utilizado como segundo argumento. Observe que en este ejemplo el método Alpha::operator new() no hace realmente nada, excepto confirmar su invocación; el espacio ya había sido reservado previamente en M1 por la versión global de new.

Nota: generalmente el diseño se hace de tal modo que en el interior de Alpha::operator new() se realice todo el trabajo, incluyendo una invocación a la versión global de new para que el gestor de memoria del compilador realice la asignación de la memoria correspondiente.

A continuación el compilador intenta crear un objeto en el espacio recién asignado mediante una llamada al constructor correspondiente; en este caso C1 es el que ajusta con el argumento utilizado. Esta invocación es responsable de la segunda salida.

El resultado es que la nueva instancia de Alpha (en realidad una matriz de 10 caracteres de la que solo se utiliza la primera posición), se coloca en la posición indicada por el especificador (str). Observe que en este momento, los punteros str y ptr señalan a la misma posición de memoria, aunque técnicamente son objetos de tipo distinto. str es puntero-a-char, mientras que ptr es puntero-a-Alpha.

  La tercera salida, responsabilidad de M3, es una comprobación de la afirmación anterior.

  M4 destruye el objeto creado en M2 mediante la invocación explícita de su destructor, que es responsable de la cuarta salida.

  En M5 se realiza una nueva asignación a ptr mediante una invocación a new similar a la realizada en M2. Aunque en esta ocasión se invoca el constructor C2 de Alpha. Esta sentencia es responsable de las salidas quinta y sexta.

  M6 es similar a M3; muestra el nuevo contenido de la posición str en la séptima salida.

  M7 es similar a M4; destruye el objeto creado en la sentencia anterior y es responsable de la octava salida.

Al llegar a este punto el espacio reservado en la primera sentencia ha sido reutilizado dos veces (albergando dos objetos que han sido destruidos después). Esta zona sigue estando reservada por el sistema, de forma que, salvo asignaciones explícitas e intencionadas, como las realizadas en M2 y M5, será respetada por el gestor de memoria del compilador.

En M8 se libera la zona reservada en M1. Si el programa continuara, esta zona del montón podría ser reutilizada por el Sistema para otros usos. A continuación el programa termina con la destrucción de todos los objetos automáticos creados en main.

  Inicio.


[1]  Observe que en la expresión:

Alpha* ptr = new (str) Alpha ('X');  // M2:

el componente Alpha('X') no es interpretado como una invocación al constructor, sino como un indicador de tipo Alpha, y un iniciador ('X') con un solo miembro. Posteriormente, cuando se ha reservado el almacenamiento sí será transformado en una invocación al constructor, Alpha::Alpha('X'), para crear el objeto.

[2]  El ejemplo se ejecuta sin dificultad con los compiladores Borland C++ 5.5 y MS VC++ 6.0 (que muestran idéntico comportamiento). Por su parte GNU Cpp 2.95.2 no permite la utilización de un especificador de situación con los tipos simples. Su comportamiento queda resumido como sigue:

int* ip1 = new (poisic) int (valor);  // Error: too many arguments...
int* ip2 = new int (valor);           // Ok.


Un tiempo más tarde, compruebo que la versión 3.4.2 de GNU c++ ya acepta sin problema este tipo de sentencia.