Cuando se ejecuta una expresión con new
, ocurren dos
cosas. Primero se asigna la memoria al ejecutar el código del operator
new()
y después se realiza la llamada al constructor. En el caso de una
expresión con delete
, se llama primero al destructor
y después se libera la memoria con el operador operator
delete()
. Las llamadas al constructor y destructor no están bajo el
control del programador, pero se pueden cambiar las funciones
opertator new()
y operatator delete()
.
El sistema de asignación de memoria usado por new
y delete
es un
sistema de propósito general. En situaciones especiales, puede que no funcione
como se requiere. Frecuentemente la razón para cambiar el asignador es la
eficiencia; puede que se necesite crear y destruir tantos objetos de la misma
clase que lo haga ineficaz en términos de velocidad: un cuello de botella. En C++
es posible sobrecargar new
y delete
para implementar un esquema
particular más adecuado que permita manejar situaciones como ésta.
Otra cuestión es la fragmentación del montículo. Cuando los objetos tienen tamaños diferentes es posible llegar a dividir de tal modo el área de memoria libre que se vuelva inútil. Es decir, el espacio puede estar disponible, pero debido al nivel de fragmentación alcanzado, no exista ningún bloque del tamaño requerido. Es posible asegurarse de que esto no llegue a ocurrir mediante la creación de un asignador para una clase específica.
En los sistemas de tiempo real y en los sistemas integrados, suele ser necesario que
los programas funcionen por largo tiempo con recursos muy limitados. Tales sistemas
pueden incluso requerir que cada asignación tome siempre la misma cantidad de
tiempo, y que no esté permitida la fragmentación ni el agotamiento en el área
dinámica. La solución a este problema consiste en utilizar un asignador
«personalizado»; de otro modo, los programadores evitarían usar
new
y delete
es estos casos y desperdiciarían un recurso muy
valioso de C++.
A la hora de sobrecargar operator new()
y operator
delete()
es importante tener en cuenta que lo único que se está
cambiando es la forma en que se realiza la asignación del espacio. El compilador
llamará a la nueva versión de new
en lugar de al original, para asignar
espacio, llamando después al constructor que actuará sobre él. Así que, aunque el
compilador convierte una expresión new
en código para asignar el espacio
y para llamar al constructor, todo lo que se puede cambiar al sobrecargar
new
es la parte correspondiente a la asignación. delete
tiene
una limitación similar.
Cuando se sobrecarga operator new()
, se está reemplazando
también el modo de tratar los posibles fallos en la asignación de la memoria. Se
debe decidir qué acciones va a realizar en tal caso: devolver cero, un bucle de
reintento con llamada al new-handler, o lo que es
más frecuente, disparar una excepción bad_alloc (tema que se
trata en el Volumen 2).
La sobrecarga de new
y delete
es como la de cualquier otro
operador. Existe la posibilidad de elegir entre sobrecarga global y sobrecarga
para una clase determinada.
Este es el modo más drástico de abordar el asunto, resulta útil cuando el
comportamiento de new
y delete
no es satisfactorio para la mayor
parte del sistema. Al sobrecargar la versión global, quedan inaccesibles las
originales, y ya no es posible llamarlas desde dentro de las funciones
sobrecargadas.
El new
sobrecargado debe tomar un argumento del tipo size_t
(el estándar de C) para tamaños. Este argumento es generado y pasado por el
compilador, y se refiere al tamaño del objeto para el que ahora tenemos la
responsabilidad de la asignación de memoria. Debe devolver un puntero a un bloque
de ese tamaño, (o mayor, si hubiera motivos para hacerlo así), o cero en el caso
de no se encontrara un bloque adecuado. Si eso sucede, no se producirá la llamada
al constructor. Por supuesto, hay que hacer algo más informativo que sólo devolver
cero, por ejemplo llamar al «new-handler» o disparar una excepción,
para indicar que hubo un problema.
El valor de retorno de operator new()
es void*
,
no un puntero a un tipo particular. Lo que hace es obtener un bloque de memoria,
no un objeto definido, no hasta que que sea llamado el constructor, un acto que el
compilador garantiza y que está fuera del control de este operador.
El operador operator delete()
toma como argumento un puntero
void*
a un bloque obtenido con el operator
new()
. Es un void*
ya que el delete
obtiene el
puntero sólo después de que haya sido llamado el destructor,
lo que efectivamente elimina su caracter de objeto convirtiéndolo en un simple
bloque de memoria. El tipo de retorno para delete
es void
.
A continuación se expone un ejemplo del modo de sobrecargar globalmente
new
y delete
:
//: C13:GlobalOperatorNew.cpp // Overload global new/delete #include <cstdio> #include <cstdlib> using namespace std; void* operator new(size_t sz) { printf("operator new: %d Bytes\n", sz); void* m = malloc(sz); if(!m) puts("out of memory"); return m; } void operator delete(void* m) { puts("operator delete"); free(m); } class S { int i[100]; public: S() { puts("S::S()"); } ~S() { puts("S::~S()"); } }; int main() { puts("creating & destroying an int"); int* p = new int(47); delete p; puts("creating & destroying an s"); S* s = new S; delete s; puts("creating & destroying S[3]"); S* sa = new S[3]; delete []sa; } ///:~
Listado 13.8. C13/GlobalOperatorNew.cpp
Aquí puede verse la forma general de sobrecarga de operadores new
y
delete
. Estos operadores sustitutivos usan las funciones
malloc()
y free()
de la biblioteca
estándar de C, que es probablemente lo que ocurre en los operadores
originales. Imprimen también mensajes sobre lo que están haciendo. Nótese que no se
han usado iostreams
sino printf()
y
puts()
. Esto se hace debido a que los objetos
iostream
como los globales cin
,
cout
y cerr
llaman a new
para obtener
memoria [73]. Usar
printf()
evita el fatal bloqueo, ya que no hace llamadas a
new
.
En main()
, se crean algunos objetos de tipos básicos para
demostrar que también en estos casos se llama a los operadores new
y
delete
sobrecargados. Posteriormente, se crean un objeto simple y un
vector, ambos de tipo S
. En el caso del vector se puede
ver, por el número de bytes pedidos, que se solicita algo de memoria extra para
incluir información sobre el número de objetos que tendrá. En todos los casos se
efectúa la llamada a las versiones globales sobrecargadas de new
y
delete
.
Aunque no es necesario poner el modificador static
, cuando se sobrecarga
new
y delete
para una clase se están creando métodos estáticos
(métodos de clase). La sintaxis es la misma que para cualquier otro
operador. Cuando el compilador encuentra una expresión new
para crear un
objeto de una clase, elige, si existe, un método de la clase llamado
operator new()
en lugar del new
global. Para el
resto de tipos o clases se usan los operadores globales (a menos que tengan
definidos los suyos propios).
En el siguiente ejemplo se usa un primitivo sistema de asignación de
almacenamiento para la clase Framis
. Se reserva un bloque
de memoria en el área de datos estática FIXME , y se usa esa memoria para asignar
alojamiento para los objetos de tipo Framis
. Para
determinar qué bloques se han asignado, se usa un sencillo vector de bytes, un
byte por bloque.
//: C13:Framis.cpp // Local overloaded new & delete #include <cstddef> // Size_t #include <fstream> #include <iostream> #include <new> using namespace std; ofstream out("Framis.out"); class Framis { enum { sz = 10 }; char c[sz]; // To take up space, not used static unsigned char pool[]; static bool alloc_map[]; public: enum { psize = 100 }; // frami allowed Framis() { out << "Framis()\n"; } ~Framis() { out << "~Framis() ... "; } void* operator new(size_t) throw(bad_alloc); void operator delete(void*); }; unsigned char Framis::pool[psize * sizeof(Framis)]; bool Framis::alloc_map[psize] = {false}; // Size is ignored -- assume a Framis object void* Framis::operator new(size_t) throw(bad_alloc) { for(int i = 0; i < psize; i++) if(!alloc_map[i]) { out << "using block " << i << " ... "; alloc_map[i] = true; // Mark it used return pool + (i * sizeof(Framis)); } out << "out of memory" << endl; throw bad_alloc(); } void Framis::operator delete(void* m) { if(!m) return; // Check for null pointer // Assume it was created in the pool // Calculate which block number it is: unsigned long block = (unsigned long)m - (unsigned long)pool; block /= sizeof(Framis); out << "freeing block " << block << endl; // Mark it free: alloc_map[block] = false; } int main() { Framis* f[Framis::psize]; try { for(int i = 0; i < Framis::psize; i++) f[i] = new Framis; new Framis; // Out of memory } catch(bad_alloc) { cerr << "Out of memory!" << endl; } delete f[10]; f[10] = 0; // Use released memory: Framis* x = new Framis; delete x; for(int j = 0; j < Framis::psize; j++) delete f[j]; // Delete f[10] OK } ///:~
Listado 13.9. C13/Framis.cpp
El espacio de almacenamiento para el montículo Framis
se
crea sobre el bloque obtenido al declarar un vector de tamaño suficiente para
contener psize
objetos de clase
Framis
. Se ha declarado también una variable lógica para
cada uno de los psize
bloques en el vector. Todas estas
variables lógicas son inicializadas a false
usando el truco
consistente en inicializar el primer elemento para que el compilador lo haga
automáticamente con los restantes iniciándolos a su valor por defecto,
false
, en el caso de variables lógicas.
El operador new()
local usa la misma sintaxis que el
global. Lo único que hace es buscar una posición libre, es decir, un valor
false
en el mapa de localización
alloc_map
. Si la encuentra, cambia su valor a
true
para marcarla como ocupada, y devuelve la dirección del
bloque correspondiente. En caso de no encontrar ningún bloque libre, envía un
mensaje al fichero de trazas y dispara una excepción de tipo
bad_alloc
.
Este es el primer ejemplo con excepción que aparece en este libro. En
el Volumen 2 se verá una discusión detallada del tratamiento de
excepciones, por lo que en este ejemplo se hace un uso muy simple del
mismo. En el operador new
hay dos expresiones
relacionadas con el tratamiento de excepciones. Primero, a la lista de
argumentos de función le sigue la expresión
throw(bad_alloc)
, esto informa al compilador que la
función puede disparar una excepción del tipo indicado. En segundo
lugar, si efectivamente se agota la memoria, la función alcanzará la
sentencia throw bad_alloc()
lanzando la excepción. En el
caso de que esto ocurra, la función deja de ejecutarse y se cede el
control del programa a la rutina de tratamiento de excepción que se ha
definido en una cláusula catch(bad_alloc)
.
En main()
se puede ver la cláusula
try-catch que es la otra parte del mecanismo. El
código que puede lanzar la excepción queda dentro del bloque
try
; en este caso, llamadas a new
para objetos
Framis
. Justo a continuación de dicho bloque
sigue una o varias cláusulas catch
, especificando en cada una
la excepción a la que se destina. En este caso,
catch(bad_alloc)
indica que en ese bloque se
tratarán las excepciones de tipo bad_alloc
. El código de
este bloque sólo se ejecutará si se dispara la excepción, continuando
la ejecución del programa justo después de la última del grupo de
cláusulas catch
que existan. Aquí sólo hay una, pero podría
haber más.
En este ejemplo, el uso de iostream
es correcto ya
que el operator new()
global no ha sido modificado.
El operator delete()
asume que la dirección de
Framis
ha sido obtenida de nuestro almacén
particular. Una asunción justa, ya que cada vez que se crea un objeto
Framis
simple se llama al operator
new()
local; pero cuando se crea un vector de tales objetos
se llama al new
global. Esto causaría problemas si el usuario
llamara accidentalmente al operador delete
sin usar la
sintaxis para destrucción de vectores. Podría ser que incluso
estuviera tratando de borrar un puntero a un objeto de la pila. Si
cree que estas cosas puedan suceder, conviene pensar en añadir una
línea que asegurare que la dirección está en el intervalo correcto
(aquí se demuestra el potencial que tiene la sobrecarga de los
operadores new
y delete
para la localización de
fugas de memoria).
operador delete()
calcula el bloque al que el
puntero representa y después pone a false
la
bandera correspondiente en el mapa de localización, para indicar que
dicho bloque está libre.
En la función main()
, se crean dinámicamente
suficientes objetos Framis
para agotar la
memoria. Con esto se prueba el comportamiento del programa en este
caso. A continuación, se libera uno de los objetos y se crea otro para
mostrar la reutilización del bloque recién liberado.
Este esquema específico de asignación de memoria es probablemente
mucho más rápido que el esquema de propósito general que usan los
operadores new
y delete
originales. Se debe
advertir, no obstante, que este enfoque no es automáticamente
utilizable cuando se usa herencia, un tema que verá en el Capítulo 14 (FIXME).
Si se sobrecargan los operadores new
y
delete
para una clase, esos operadores se
llaman cada vez que se crea un objeto simple de esa clase. Sin
embargo, al crear un vector de tales objetos se llama al
operator new()
global para obtener el
espacio necesario para el vector, y al operator
delete()
global para liberarlo. Es posible
controlar también la asignación de memoria para vectores
sobrecargando los métodos operator new[]
y operator delete[]
; se trata de
versiones especiales para vectores. A continuación se expone
un ejemplo que muestra el uso de ambas versiones.
//: C13:ArrayOperatorNew.cpp // Operator new for arrays #include <new> // Size_t definition #include <fstream> using namespace std; ofstream trace("ArrayOperatorNew.out"); class Widget { enum { sz = 10 }; int i[sz]; public: Widget() { trace << "*"; } ~Widget() { trace << "~"; } void* operator new(size_t sz) { trace << "Widget::new: " << sz << " bytes" << endl; return ::new char[sz]; } void operator delete(void* p) { trace << "Widget::delete" << endl; ::delete []p; } void* operator new[](size_t sz) { trace << "Widget::new[]: " << sz << " bytes" << endl; return ::new char[sz]; } void operator delete[](void* p) { trace << "Widget::delete[]" << endl; ::delete []p; } }; int main() { trace << "new Widget" << endl; Widget* w = new Widget; trace << "\ndelete Widget" << endl; delete w; trace << "\nnew Widget[25]" << endl; Widget* wa = new Widget[25]; trace << "\ndelete []Widget" << endl; delete []wa; } ///:~
Listado 13.10. C13/ArrayOperatorNew.cpp
Si exceptuamos la información de rastreo que se añade aquí,
las llamadas a las versiones globales de
new
y delete
causan el
mismo efecto que si estos operadores no se hubieran
sobrecargado. Como se ha visto anteriormente, es posible usar
cualquier esquema conveniente de asignación de memoria en
estos operadores modificados.
Se puede observar que la sintaxis de new
y
delete
para vectores es la misma que la
usada para objetos simples añadiéndoles el operador subíndice
[]
. En ambos casos se le pasa a
new
como argumento el tamaño del bloque de
memoria solicitado. A la versión para vectores se le pasa el
tamaño necesario para albergar todos sus componentes. Conviene
tener en cuenta que lo único que se requiere del
operator new()
es que devuelva un puntero
a un bloque de memoria suficientemente grande. Aunque es
posible inicializar el bloque referido, eso es trabajo del
constructor, que se llamará automáticamente por el compilador.
El constructor y el destructor simplemente imprimen mensajes para que pueda verse que han sido llamados. A continuación se muestran dichos mensajes:
new Widget Widget::new: 40 bytes * delete Widget ~Widget::delete new Widget[25] Widget::new: 1004 bytes ************************* delete []Widget ~~~~~~~~~~~~~~~~~~~~~~~~~Widget::delete[]
La creación de un único objeto Widget
requiere 40 bytes, tal y como se podría esperar para una
máquina que usa 32 bits para un int
. Se invoca al
operator new()
y luego al constructor,
que se indica con la impresión del carácter
«*». De forma complementaria, la llamada a
delete
provoca primero la invocación del
destructor y sólo después, la de operator
delete()
.
Cuando lo que se crea es un vector de objetos
Widget
, se observa el uso de la versión
de operator new()
para vectores, de acuerdo
con lo dicho anteriormente. Se observa que el tamaño del
bloque solicitado en este caso es cuatro bytes mayor que el
esperado. Es en estos cuatro bytes extra donde el compilador
guarda la información sobre el tamaño del vector. De ese
modo, la expresión
delete []Widget;
informa al compilador que se trata de un vector, con lo cual,
generará el código para extraer la información que indica el
número de objetos y para llamar otras tantas veces al
destructor. Obsérvese que aunque se llame solo una vez a
operator new()
y operator
delete()
para el vector, se llama al constructor y
al destructor una vez para cada uno de los objetos del vector.
Considerando que
MyType* f = new MyType;
llama a new
para obtener un bloque del
tamaño de MyType
invocando después a su
constructor, ¿qué pasaría si la asignación de memoria falla en
new
?. En tal caso, no habrá llamada al
constructor al que se le tendría que pasar un puntero
this
nulo, para un objeto que no se ha
creado . He aquí un ejemplo que lo
demuestra:
//: C13:NoMemory.cpp // Constructor isn't called if new fails #include <iostream> #include <new> // bad_alloc definition using namespace std; class NoMemory { public: NoMemory() { cout << "NoMemory::NoMemory()" << endl; } void* operator new(size_t sz) throw(bad_alloc){ cout << "NoMemory::operator new" << endl; throw bad_alloc(); // "Out of memory" } }; int main() { NoMemory* nm = 0; try { nm = new NoMemory; } catch(bad_alloc) { cerr << "Out of memory exception" << endl; } cout << "nm = " << nm << endl; } ///:~
Listado 13.11. C13/NoMemory.cpp
Cuando se ejecuta, el programa imprime los mensajes del
operator new()
y del manejador de
excepción, pero no el del constructor. Como
new
nunca retorna, no se llama al
constructor y por tanto no se imprime su mensaje.
Para asegurar que no se usa indebidamente, Es importante
inicializar nm
a cero, debido a que
new
no se completa. El código de manejo de
excepciones debe hacer algo más que imprimir un mensaje y
continuar como si el objeto hubiera sido creado con
éxito. Idealmente, debería hacer algo que permitiera al
programa recuperarse del fallo, o al menos, provocar la salida
después de registrar un error.
En las primeras versiones de C++, el comportamiento estándar
consistía en hacer que new
retornara un
puntero nulo si la asignación de memoria fallaba. Esto podía
impedir que se llamara al constructor. Si se intenta hacer
esto con un compilador que sea conforme al estándar actual, le
informará de que en lugar de devolver un valor nulo, debe
disparar una excepción de tipo bad_alloc
.
He aquí otros dos usos, menos comunes, para la sobrecarga de
operador new()
:
Puede ocurrir que necesite emplazar un objeto en un lugar específico de la memoria. Esto puede ser importante en programas en los que algunos de los objetos se refieren o son sinónimos de componentes hardware mapeados sobre una zona de la memoria.
Si se quiere permitir la elección entre varios
asignadores de memoria (allocators) en la llamada a
new
.
Ambas situaciones se resuelven mediante el mismo mecanismo: la
función operator new()
puede tomar más de
un argumento. Como se ha visto, el primer argumento de
new
es siempre el tamaño del objeto,
calculado en secreto y pasado por el compilador. El resto de
argumentos puede ser de cualquier otro tipo que se necesite:
la dirección en la que queremos emplazar el objeto, una
referencia a una función de asignación de memoria, o
cualquiera otra cosa que se considere conveniente.
Al principio puede parecer curioso el modo en que se pasan los
argumentos extra al operator
new()
. Después de la palabra clave
new
y antes del nombre de clase del objeto
que se pretende crear, se pone la lista de argumentos, sin
contar con el correspondiente al size_t
del
objeto, que le pasa el compilador. Por ejemplo, la expresión:
X* xp = new(a) X;
pasará a
como segundo argumento al operador
operator new()
. Por supuesto, sólo
funcionará si ha sido declarado el operator
new()
adecuado.
He aquí un ejemplo demostrativo de cómo se usa esto para colocar un objeto en una posición particular:
//: C13:PlacementOperatorNew.cpp // Placement with operator new() #include <cstddef> // Size_t #include <iostream> using namespace std; class X { int i; public: X(int ii = 0) : i(ii) { cout << "this = " << this << endl; } ~X() { cout << "X::~X(): " << this << endl; } void* operator new(size_t, void* loc) { return loc; } }; int main() { int l[10]; cout << "l = " << l << endl; X* xp = new(l) X(47); // X at location l xp->X::~X(); // Explicit destructor call // ONLY use with placement! } ///:~
Listado 13.12. C13/PlacementOperatorNew.cpp
Observe que lo único que hace el operador
new
es retornar el puntero que se
pasa. Por tanto, es posible especificar la dirección en la
que se quiere construir el objeto.
Aunque este ejemplo muestra sólo un argumento adicional, nada impide añadir otros, si se considera conveniente para sus propósitos.
Al tratar de destruir estos objetos surge un problema. Sólo hay una
versión del operador delete
, de modo que no hay forma de
decir: "Usa mi función de liberación de memoria para este objeto".
Se requiere llamar al destructor, pero sin utilizar el mecanismo de
memoria dinámica, ya que el objeto no está alojado en el montículo.
La solución tiene una sintaxis muy especial. Se debe llamar explícitamente al destructor, tal como se muestra:
xp->X::~X(); //Llamada explícita al destructor
Hay que hacer una llamada de atención al respecto. Algunas
personas ven esto como un modo de destruir objetos en algún
momento anterior al determinado por las reglas de ámbito, en
lugar de ajustar el ámbito, o más correctamente, en lugar de
usar asignación dinámica como medio de determinar la duración
del objeto en tiempo de ejecución. Esto es un error, que puede
provocar problemas si se trata de destruir de esta manera un
objeto ordinario creado en la pila, ya que el destructor será
llamado de nuevo cuando se produzca la salida del ámbito
correspondiente. Si se llama de esta forma directa al
destructor de un objeto creado dinámicamente, se llevará a
cabo la destrucción, pero no la liberación del bloque de
memoria, lo que probablemente no es lo que se desea. La única
razón para este tipo de llamada explícita al destructor es
permitir este uso especial del operador
new
, para emplazamiento en memoria.
Existe también una forma de operador delete
de emplazamiento que sólo es llamada en caso de que el
constructor dispare una excepción, con lo que la memoria se
libera automáticamente durante la excepción. El operador
delete
de emplazamiento usa una lista de
argumentos que se corresponde con la del operador
new
de emplazamiento que fue llamado
previamente a que el constructor lanzase la excepción. Este
asunto se tratará en el Volumen 2, en un capítulo dedicado al
tratamiento de excepciones.