13.2.3. Stash para punteros

Esta nueva versión de la clase Stash, que llamamos 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 archivo 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.4. C13/PStash.h


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

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

se asigna espacio para un vector 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, insistimos, 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 [], más significativo sintácticamente. 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. Resolveremos este problema 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.5. 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 vector 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 vector, se pone primero a cero el vector usando la función memset() de la biblioteca estándar de C, aunque esto no sea estrictamente necesario ya que, presumiblemente, PStash manipulará la memoria de forma adecuada, pero a veces no es muy costoso añadir un poco más de seguridad. A continuación, se copian al nuevo vector usando memcpy() los datos existentes en el antiguo. Con frecuencia verá que las funciones memcpy() y memset() han sido optimizadas en cuanto al tiempo de proceso, 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 prueba 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 mas 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.6. C13/PStashTest.cpp


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

intStash.add(new int(i));

Se ha usado una forma de pseudo constructor en la expresión new int(i), con lo que además de crear un objeto int en el área de memoria dinámica, le asigna el valor inicial 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 de PStatsh 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 lee línea a línea el propio archivo fuente. Mediante getline() se lee cada línea de texto en una variable de cadena, de la que se crea una copia independiente. Si le hubiéramos pasado cada vez la dirección de line, tendríamos un montón de copias del mismo puntero, referidas a la última línea leída.

En, 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 el string* es de-referenciado y es visto por el compilador como un objeto string que se envía 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 al carecer de 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 pueden resolverse mediante el uso de plantillas, que veremos en el capítulo 16. FIXME:ref