Tanto la sobrecarga de funciones como los argumentos por defecto resultan útiles para ponerle nombre a las 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 a 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
ensureSize()
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 (que es el mismo
caso 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 eficaz). 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
avanzada proporcionado, por ejemplo, por el 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 el
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 cuando se llama a delete
con un puntero a cero, el compilador no hace nada para no
causar problemas.
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 eficacia anterior
pues siempre se creará el objeto Mem
. Para
volver a tener la misma eficacia 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, debería ser una pista para que al final se implementen dos funciones diferentes dentro de una función: una versión para el caso normal y otra para el caso por defecto. Podría partirlo en dos cuerpos de función diferentes y dejar que el compilador elija. Esto resulta en un ligero (pero normalmente invisible) incremento de la eficacia porque el argumento extra no se pasa y por tanto el código extra debido a la condición 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
eficacia, debe tener cuidado para no caer en la trampa de pensar
sólo en la eficacia (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 las 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 eficacia en caso necesario, pero el
efecto de una clase mal diseñada porque el programador está
obsesionado con la eficacia 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 eficacia es
buena o mala.