13.2.3. Stash para punteros

Esta nueva versión de la clase Stash, llamada PStash, almacena punteros a objetos existentes en el montículo, a diferencia de la vieja versión, que guardaba una copia por valor de los objetos. Usando new y delete, es fácil y seguro almacenar punteros a objetos creados en el montículo.

He aquí el fichero de cabecera para «Stash para punteros»:

//: C13:PStash.h
// Holds pointers instead of objects
#ifndef PSTASH_H
#define PSTASH_H

class PStash {
  int quantity; // Number of storage spaces
  int next; // Next empty space
   // Pointer storage:
  void** storage;
  void inflate(int increase);
public:
  PStash() : quantity(0), storage(0), next(0) {}
  ~PStash();
  int add(void* element);
  void* operator[](int index) const; // Fetch
  // Remove the reference from this PStash:
  void* remove(int index);
  // Number of elements in Stash:
  int count() const { return next; }
};
#endif // PSTASH_H ///:~

Listado 13.5. C13/PStash.h


Los elementos de datos subyacentes no han cambiado mucho, pero ahora el almacenamiento se hace sobre un array de punteros void, que se obtiene mediante new en lugar de malloc(). En la expresión

void** st = new void*[ quantity + increase ];

el tipo del objeto asignado es void*, de modo que la expresión asigna espacio para un array de punteros a void.

El destructor de la clase libera el espacio en el que se almacenan los punteros sin tratar de borrar los objetos a los que hacen referencia, ya que esto, como ya se ha dicho, liberaría el espacio asignado a los objetos, pero no se produciría la necesaria llamada a sus destructores por la falta de información de tipo.

El otro cambio realizado es el reemplazo de la función fetch() por operator [], que tiene más sentido desde el punto de vista sintáctico. Su tipo de retorno es nuevamente void*, por lo que el usuario deberá recordar el tipo de los objetos a que se refieren y efectuar la adecuada conversión al extraerlos del contenedor. Este problema se resolverá en capítulos posteriores.

Sigue la definición de los métodos de PStash:

//: C13:PStash.cpp {O}
// Pointer Stash definitions
#include "PStash.h"
#include "../require.h"
#include <iostream>
#include <cstring> // 'mem' functions
using namespace std;

int PStash::add(void* element) {
  const int inflateSize = 10;
  if(next >= quantity)
    inflate(inflateSize);
  storage[next++] = element;
  return(next - 1); // Index number
}

// No ownership:
PStash::~PStash() {
  for(int i = 0; i < next; i++)
    require(storage[i] == 0, 
      "PStash not cleaned up");
  delete []storage; 
}

// Operator overloading replacement for fetch
void* PStash::operator[](int index) const {
  require(index >= 0,
    "PStash::operator[] index negative");
  if(index >= next)
    return 0; // To indicate the end
  // Produce pointer to desired element:
  return storage[index];
}

void* PStash::remove(int index) {
  void* v = operator[](index);
  // "Remove" the pointer:
  if(v != 0) storage[index] = 0;
  return v;
}

void PStash::inflate(int increase) {
  const int psz = sizeof(void*);
  void** st = new void*[quantity + increase];
  memset(st, 0, (quantity + increase) * psz);
  memcpy(st, storage, quantity * psz);
  quantity += increase;
  delete []storage; // Old storage
  storage = st; // Point to new memory
} ///:~

Listado 13.6. C13/PStash.cpp


La función add() es, en efecto, la misma que antes si exceptuamos el hecho de que lo que se almacena ahora es un puntero a un objeto en lugar de una copia del objeto.

El código de inflate() ha sido modificado para gestionar la asignación de memoria para un array de void*, a diferencia del diseño previo, que sólo trataba con bytes. Aquí, en lugar de usar el método de copia por el índice del array, se pone primero a cero el array usando la función memset() de la librería estándar de C, aunque esto no sea estrictamente necesario ya que, presumiblemente, PStash manipulará la memoria de forma adecuada, pero normalmente no está de más añadir un poco de precaución extra. A continuación, se copian al nuevo array usando memcpy() los datos existentes en el antiguo. Con frecuencia, funciones como memcpy() y memset() han sido optimizadas con el paso del tiempo, de modo que pueden ser más rápidas que los bucles anteriormente vistos. No obstante, una función como inflate() no es probable que sea llamada con la frecuencia necesaria para que la diferencia sea palpable. En cualquier caso, el hecho de que las llamadas a función sean más concisas que los bucles puede ayudar a prevenir errores de programación.

Para dejar definitivamente la responsabilidad de la limpieza de los objetos sobre los hombros del programador cliente, se proporcionan dos formas de acceder a los punteros en PStash: el operador [], que devuelve el puntero sin eliminarlo del contenedor, y un segundo método remove() que además de devolver el puntero lo elimina del contenedor, poniendo a cero la posición que ocupaba. Cuando se produce la llamada al destructor de PStash, se comprueba si han sido previamente retirados todos los punteros; si no es así, se notifica, de modo que es posible prevenir la fuga de memoria. Se verán otras soluciones más elegantes en capítulos posteriores.

Una prueba

Aquí aparece el programa de prueba de Stash, reescrito para PStash:

//: C13:PStashTest.cpp
//{L} PStash
// Test of pointer Stash
#include "PStash.h"
#include "../require.h"
#include <iostream>
#include <fstream>
#include <string>
using namespace std;

int main() {
  PStash intStash;
  // 'new' works with built-in types, too. Note
  // the "pseudo-constructor" syntax:
  for(int i = 0; i < 25; i++)
    intStash.add(new int(i));
  for(int j = 0; j < intStash.count(); j++)
    cout << "intStash[" << j << "] = "
         << *(int*)intStash[j] << endl;
  // Clean up:
  for(int k = 0; k < intStash.count(); k++)
    delete intStash.remove(k);
  ifstream in ("PStashTest.cpp");
  assure(in, "PStashTest.cpp");
  PStash stringStash;
  string line;
  while(getline(in, line))
    stringStash.add(new string(line));
  // Print out the strings:
  for(int u = 0; stringStash[u]; u++)
    cout << "stringStash[" << u << "] = "
         << *(string*)stringStash[u] << endl;
  // Clean up:
  for(int v = 0; v < stringStash.count(); v++)
    delete (string*)stringStash.remove(v);
} ///:~

Listado 13.7. C13/PStashTest.cpp


Igual que antes, se crean y rellenan varios objetos Stash, pero esta vez con los punteros obtenidos con new. En el primer caso, véase la línea:

intStash.add(new int(i));

La expresión new int(i) usa la forma de pseudoconstructor, de modo que se crea en el montículo espacio para un nuevo objeto int y éste se inicializa con el valor i.

Para imprimir, es necesario convertir al tipo adecuado el puntero obtenido de PStash::operator[]; lo mismo se repite con el resto de los objetos PStash del programa. Es la consecuencia indeseable del uso de punteros void como representación subyacente, que se corregirá en capítulos posteriores.

En la segunda prueba, se abre el propio fichero fuente y se lee línea a línea en otro PStash. Mediante getline() se lee cada línea de texto en un string, a partir del cual se crea un nuevo string que es una copia independiente de esa línea. Si se pasara cada vez la dirección de line, se obtendría un montón de punteros a line, que sólo contendría la última línea leída del fichero.

En la recuperación de los punteros, verá la expresión:

*(string*)stringStash[v];

El puntero obtenido por medio de operator[] debe ser convertido a string* para tener el tipo adecuado. Después se dereferencia el string*, de modo que la expresión se evalúa como un objeto, y el compilador ve un objeto string que enviar a cout.

Antes de destruir los objetos, se han de eliminar las referencias correspondientes mediante el uso de remove(). De no hacerse así, PStash notificará que no se ha efectuado la limpieza correctamente. Véase que en el caso de los punteros a int no es necesaria la conversión de tipo, ya que un int no tiene destructor y lo único que se necesita es liberar la memoria:

delete intStash.remove(k);

En cambio, para los punteros a string, hace falta la conversión de tipo, so pena de crear otra (silenciosa) fuga de memoria, de modo que el molde es esencial:

delete (string*) stringStash.remove(k);

Algunas de estas dificultades (pero no todas) pueden resolverse mediante el uso de plantillas, que se tratan en el Capítulo 16.