Tanto la sobrecarga de funciones como los argumentos por defecto hacen más cómodas las llamadas a funciones. Sin embargo, a veces puede resultar confuso saber qué técnica utilizar. Por ejemplo, estudie la siguiente herramienta que está diseñada para tratar automáticamente bloques de memoria:
//: C07:Mem.h #ifndef MEM_H #define MEM_H typedef unsigned char byte; class Mem { byte* mem; int size; void ensureMinSize(int minSize); public: Mem(); Mem(int sz); ~Mem(); int msize(); byte* pointer(); byte* pointer(int minSize); }; #endif // MEM_H ///:~
Listado 7.9. C07/Mem.h
El objeto Mem contiene un bloque de octetos y se asegura de que tiene suficiente memoria. El constructor por defecto no reserva memoria pero el segundo constructor se asegura de que hay sz octetos de memoria en el objeto Mem. El destructor libera la memoria, msize() le dice cuántos octetos hay actualmente en Mem y pointer() retorna un puntero al principio de la memoria reservada (Mem es una herramienta de bastante bajo nivel). Hay una versión sobrecargada de pointer() que los programadores clientes pueden utilizar para obtener un puntero que apunta a un bloque de memoria con al menos el tamaño minSize, y el método lo asegura.
El constructor y el método pointer() utilizan el método privado ensureMinSize() para incrementar el tamaño del bloque de memoria (note que no es seguro mantener el valor de retorno de pointer() si se cambia el tamaño del bloque de memoria).
He aquí la implementación de la clase:
//: C07:Mem.cpp {O} #include "Mem.h" #include <cstring> using namespace std; Mem::Mem() { mem = 0; size = 0; } Mem::Mem(int sz) { mem = 0; size = 0; ensureMinSize(sz); } Mem::~Mem() { delete []mem; } int Mem::msize() { return size; } void Mem::ensureMinSize(int minSize) { if(size < minSize) { byte* newmem = new byte[minSize]; memset(newmem + size, 0, minSize - size); memcpy(newmem, mem, size); delete []mem; mem = newmem; size = minSize; } } byte* Mem::pointer() { return mem; } byte* Mem::pointer(int minSize) { ensureMinSize(minSize); return mem; } ///:~
Listado 7.10. C07/Mem.cpp
Puede observar que ensureMinSize() es la única función responsable de reservar memoria y que la utilizan tanto el segundo constructor como la segunda versión sobrecargada de pointer(). Dentro de ensureMinSize() no se hace nada si el tamaño es lo suficientemente grande. Si se ha de reservar más memoria para que el bloque sea más grande (lo que también ocurre cuando el bloque tiene tamaño cero después del constructor por defecto), la nueva porción de más se pone a cero utilizando la función de la librería estándar de C memset(), que fue presentada en el Capítulo 5. La siguiente llamada es a la función de la librería estándar de C memcpy(), que en este caso copia los octetos existentes de mem a newmem (normalmente de una manera eficiente). Finalmente, se libera la memoria antigua y se asignan a los atributos apropiados la nueva memoria y su tamaño.
La clase Mem se ha diseñado para su utilización como herramienta dentro de otras clases para simplificar su gestión de la memoria (también se podría utilizar para ocultar un sistema de gestión de memoria más avanzado proporcionado, por ejemplo, por el sistema operativo). Esta clase se comprueba aquí con una simple clase de tipo string:
//: C07:MemTest.cpp // Testing the Mem class //{L} Mem #include "Mem.h" #include <cstring> #include <iostream> using namespace std; class MyString { Mem* buf; public: MyString(); MyString(char* str); ~MyString(); void concat(char* str); void print(ostream& os); }; MyString::MyString() { buf = 0; } MyString::MyString(char* str) { buf = new Mem(strlen(str) + 1); strcpy((char*)buf->pointer(), str); } void MyString::concat(char* str) { if(!buf) buf = new Mem; strcat((char*)buf->pointer( buf->msize() + strlen(str) + 1), str); } void MyString::print(ostream& os) { if(!buf) return; os << buf->pointer() << endl; } MyString::~MyString() { delete buf; } int main() { MyString s("My test string"); s.print(cout); s.concat(" some additional stuff"); s.print(cout); MyString s2; s2.concat("Using default constructor"); s2.print(cout); } ///:~
Listado 7.11. C07/MemTest.cpp
Todo lo que puede hacer con esta clase es crear un MyString, concatenar texto e imprimir a un ostream. La clase sólo contiene un puntero a un Mem, pero note la diferencia entre el constructor por defecto, que pone el puntero a cero, y el segundo constructor, que crea un Mem y copia los datos dentro del mismo. La ventaja del constructor por defecto es que puede crear, por ejemplo, un array grande de objetos MyString vacíos con pocos recursos, pues el tamaño de cada objeto es sólo un puntero y la única sobrecarga en el rendimiento del constructor por defecto es la de asignarlo a cero. El coste de un MyString sólo empieza a aumentar cuando concatena datos; en ese momento el objeto Mem se crea si no ha sido creado todavía. Sin embargo, si utiliza el constructor por defecto y nunca concatena ningún dato, la llamada al destructor todavía es segura porque llamar a delete con un puntero a cero está definido de forma que no intenta liberar memoria ni causa ningún otro problema.
Si mira los dos constructores, en principio, podría parecer que son candidatos para utilizar argumentos por defecto. Sin embargo, si elimina el constructor por defecto y escribe el constructor que queda con un argumento por defecto:
MyString(char* str = "");
todo funcionará correctamente, pero perderá la eficiencia anterior pues siempre se creará el objeto Mem. Para volver a tener la misma eficiencia de antes, ha de modificar el constructor:
MyString::MyString(char* str) { if (!*str) { // Apunta a un string vacío buf = 0; return; } buf = new Mem(strlen(str) + 1); strcpy((char*)buf->pointer(), str); }
Esto significa, en efecto, que el valor por defecto es un caso que ha de tratarse separadamente de un valor que no lo es. Aunque parece algo inocente con un pequeño constructor como éste, en general esta práctica puede causar problemas. Si tiene que tratar por separado el valor por defecto en vez de tratarlo como un valor ordinario, eso debería indicarle que al final tendrá, en la práctica, dos funciones diferentes dentro del cuerpo de una sola función: una versión para el caso normal y otra para el caso por defecto. Más vale que lo divida en dos cuerpos de función diferentes y deje que el compilador elija. Esto resulta en un ligero (pero normalmente invisible) incremento de la eficiencia porque el argumento extra no se pasa y por tanto el código extra debido a la condición no se ejecuta. Más importante es que está manteniendo el código en dos funciones separadas en vez de combinarlas en una utilizando argumentos por defecto, lo que resultará en un mantenimiento más sencillo, sobre todo si las funciones son largas.
Por otro lado, considere la clase Mem. Si mira las definiciones de los dos constructores y las dos funciones pointer(), puede ver que la utilización de argumentos por defecto en ambos casos no causará que los métodos cambien. Así, la clase podría ser fácilmente:
//: C07:Mem2.h #ifndef MEM2_H #define MEM2_H typedef unsigned char byte; class Mem { byte* mem; int size; void ensureMinSize(int minSize); public: Mem(int sz = 0); ~Mem(); int msize(); byte* pointer(int minSize = 0); }; #endif // MEM2_H ///:~
Listado 7.12. C07/Mem2.h
Note que la llamada a ensureMinSize(0) siempre será bastante eficiente.
Aunque ambos casos se basan en decisiones por motivos de eficiencia, debe tener cuidado para no caer en la trampa de pensar sólo en la eficiencia (siempre fascinante). Lo más importante en el diseño de una clase es la interfaz de la clase (sus miembros públicos, que son los que el programador cliente tiene a su disposición). Si se implementa una clase fácil de utilizar y reutilizar, entonces ha tenido éxito; siempre puede realizar ajustes para mejorar la eficiencia en caso necesario, pero el efecto de una clase mal diseñada porque el programador está obsesionado con la eficiencia puede resultar grave. Su primera preocupación debería ser que la interfaz tenga sentido para aquéllos que la utilicen y para los que lean el código. Note que en MemTest.cpp el uso de MyString no cambia independientemente de si se utiliza el constructor por defecto o si la eficiencia es buena o mala.