Nuestro primer paso será exactamente ese. Meter las funciones C++
dentro de las estructuras como «funciones
miembro». Éste es el aspecto que tiene la estructura una
vez realizados estos cambios de la versión C de la CStash
a la versión en C++, a la que llamaremos Stash
:
//: C04:CppLib.h // C-like library converted to C++ struct Stash { int size; // Size of each space int quantity; // Number of storage spaces int next; // Next empty space // Dynamically allocated array of bytes: unsigned char* storage; // Functions! void initialize(int size); void cleanup(); int add(const void* element); void* fetch(int index); int count(); void inflate(int increase); }; ///:~
Listado 4.4. C04/CppLib.h
La primera diferencia que puede notarse es que no se usa
typedef
. A diferencia de C que requiere el uso de
typedef
para crear nuevos tipos de datos, el compilador
de C++ hará que el nombre de la estructura sea un nuevo tipo de
dato automáticamente en el programa (tal como los nombres de
tipos de datos int
, char
,
float
y double
).
Todos los datos miembros de la estructura están declarados igual
que antes; sin embargo, ahora las funciones están declaradas
dentro del cuerpo de la struct
. Más aún, fíjese que el primer
argumento de todas las funciones ha sido eliminado. En C++,
en lugar de forzar al usuario a que pase la dirección de la
estructura sobre la que trabaja una función como primer argumento,
el compilador hará este trabajo, secretamente. Ahora sólo
debe preocuparse por los argumentos que le dan sentido a lo
que la función hace y no de los mecanismos
internos de la función.
Es importante darse cuenta de que el código generado por estas funciones es el mismo que el de las funciones de la librería al estilo C. El número de argumentos es el mismo (aunque no se le pase la dirección de la estructura como primer argumento, en realidad sí se hace) y sigue existiendo un único cuerpo (definición) de cada función. Esto último quiere decir que, aunque declare múltiples variables
Stash A, B, C;
no existirán múltiples definiciones de, por ejemplo, la
función add()
, una para cada variable.
De modo que el código generado es casi idéntico al que hubiese
escrito para una versión en C de la librería, incluyendo la
«decoración de nombres» ya mencionada para evitar los
conflictos de nombres, nombrando a las funciones
Stash_initialize()
,
Stash_cleanup()
y demás. Cuando una función
está dentro de una estructura, el compilador C++ hace lo mismo y
por eso, una función llamada initialize()
dentro de una estructura no estará en conflicto con otra función
initialize()
dentro de otra estructura o con
una función initialize()
global. De este
modo, en general no tendrá que preocuparse por los conflictos de
nombres de funciones - use el nombre sin decoración. Sin embargo,
habrá situaciones en las que deseará especificar, por ejemplo,
esta initialize()
pertenece a la estructura
Stash
y no a ninguna otra. En particular, cuando
defina la función, necesita especificar a qué estructura pertenece
para lo cual, en C++ cuenta con el operador ::
llamado operador de resolución de ámbito (ya que ahora un nombre
puede estar en diferentes ámbitos: el del ámbito global o dentro
del ámbito de una estructura. Por ejemplo, si quiere referirse
a una función initialize()
que se encuentra
dentro de la estructura Stash
lo podrá hacer con
la expresión Stash::initialize(int size)
. A
continuación podrá ver cómo se usa el operador de resolución de
ámbito para definir funciones:
//: C04:CppLib.cpp {O} // C library converted to C++ // Declare structure and functions: #include "CppLib.h" #include <iostream> #include <cassert> using namespace std; // Quantity of elements to add // when increasing storage: const int increment = 100; void Stash::initialize(int sz) { size = sz; quantity = 0; storage = 0; next = 0; } int Stash::add(const void* element) { if(next >= quantity) // Enough space left? inflate(increment); // Copy element into storage, // starting at next empty space: int startBytes = next * size; unsigned char* e = (unsigned char*)element; for(int i = 0; i < size; i++) storage[startBytes + i] = e[i]; next++; return(next - 1); // Index number } void* Stash::fetch(int index) { // Check index boundaries: assert(0 <= index); if(index >= next) return 0; // To indicate the end // Produce pointer to desired element: return &(storage[index * size]); } int Stash::count() { return next; // Number of elements in CStash } void Stash::inflate(int increase) { assert(increase > 0); int newQuantity = quantity + increase; int newBytes = newQuantity * size; int oldBytes = quantity * size; unsigned char* b = new unsigned char[newBytes]; for(int i = 0; i < oldBytes; i++) b[i] = storage[i]; // Copy old to new delete []storage; // Old storage storage = b; // Point to new memory quantity = newQuantity; } void Stash::cleanup() { if(storage != 0) { cout << "freeing storage" << endl; delete []storage; } } ///:~
Listado 4.5. C04/CppLib.cpp
Hay muchas otras cosas que difieres entre C y C++. Para empezar, el compilador requiere que declare las funciones en los archivos de cabecera: en C++ no podrá llamar a una función sin haberla declarado antes y si no se cumple esta regla el compilador dará un error. Esta es una forma importante de asegurar que las llamadas a una función son consistentes entre el punto en que se llama y el punto en que se define. Al forzar a declarar una función antes de usarla, el compilador de C++ prácticamente se asegura de que realizará esa declaración por medio de la inclusión de un fichero de cabecera. Además, si también incluye el mismo fichero de cabecera en el mismo lugar donde se defines las funciones, el compilador verificará que las declaraciones del archivo cabecera y las definiciones coinciden. Puede decirse entonces que, de algún modo, los ficheros de cabecera se vuelven un repositorio de validación de funciones y permiten asegurar que las funciones se usan de modo consistente en todas las unidades de traducción del proyecto.
Obviamente, las funciones globales se pueden seguir declarando a mano en aquellos lugares en las que se definen y usan (Sin embargo, esta práctica es tan tediosa que está en desuso.) De cualquier modo, las estructuras siempre se deben declarar antes de ser usadas y el mejor lugar para esto es un fichero de cabecera, exceptuando aquellas que queremos esconder intencionalmente en otro fichero.
Se puede ver que todas las funciones miembro (métodos) tienen casi
la misma forma que sus versiones respectivas en C. Las únicas
diferencias son su ámbito de resolución y el hecho de que el
primer argumento ya no aparece explícito en el prototipo de la
función. Por supuesto que sigue ahí ya que la función debe ser
capaz de trabajar sobre una variable struct
en
particular. Sin embargo, fíjese también que, dentro del método, la
selección de esta estructura en particular también ha
desaparecido! Así, en lugar de decir s->size = sz;
ahora dice size = sz;
eliminando el tedioso
s->
que en realidad no aportaba nada al significado
semántico de lo que estaba escribiendo. Aparentemente, el
compilador de C++ está realizando estas tareas por el
programador. De hecho, está tomando el primer argumento
«secreto» (la dirección de la estructura que antes
tenía que pasar a mano) y aplicándole el selector de miembro (->)
siempre que escribe el nombre de uno de los datos miembro. Eso
significa que, siempre y cuando esté dentro de la definición de
una método de una estructura puede hacer referencia a cualquier
otro miembro (incluyendo otro método) simplemente dando su
nombre. El compilador buscará primero en los nombres locales de la
estructura antes de buscar en versiones más globales de dichos
nombres. El lector podrá descubrir que esta característica no sólo
agiliza la escritura del código, sino que también hace la lectura
del mismo mucho más sencilla.
Pero qué pasaría si, por alguna razón,
quisiera hacer referencia a la dirección de
memoria de la estructura. En la versión en C de la librería ésta
se podía obtener fácilmente del primer argumento de cualquier
función. En C++ la cosa es más consistente: existe la palabra
reservada this
que produce la dirección de la
variable struct
actual. Es el equivalente a la expresión
s
de la versión en C de la librería. De modo
que, podremos volver al estilo de C escribiendo
this->size = Size;
El código generado por el compilador será exactamente el mismo por
lo que no es necesario usar this
en estos
casos. Ocasionalmente, podrá ver por ahí código dónde la gente usa
this
en todos sitios sin agregar nada al
significado del código (esta práctica es indicio de programadores
inexpertos). Por lo general, this
no se usa muy
a menudo pero, cuando se necesite siempre estará allí (en
ejemplos posteriores del libro verá más sobre su uso).
Queda aún un último tema que tocar. En C, se puede asignar un
void *
a cualquier otro puntero, algo como esto:
int i = 10; void* vp = &i; // OK tanto en C como en C++ int* ip = vp; // Sólo aceptable en C
y no habrá ningún tipo de queja por parte de compilador. Sin
embargo, en C++, lo anterior no está permitido. ¿Por qué? Porque C
no es tan estricto con los tipos de datos y permite asignar un
puntero sin un tipo específico a un puntero de un tipo bien
determinado. No así C++, en el cual la verificación de tipos es
crítica y el compilador se detendrá quejándose en cualquier
conflicto de tipos. Esto siempre ha sido importante, pero es
especialmente importante en C++ ya que dentro de las estructuras
puede hacer métodos. Si en C++ estuviera permitido pasar punteros
a estructuras con impunidad en cuanto a conflicto de tipos,
¡podría terminar llamando a un método de una estructura en la cual
no existiera dicha función miembro! Una verdadera fórmula para el
desastre. Así, mientras C++ sí deja asignar cualquier puntero a un
void *
(en realidad este es el propósito original del
puntero a void
: que sea suficientemente largo como
para apuntar a cualquier tipo) no permite asignar un void
*
a cualquier otro tipo de puntero. Para ello se requiere
un molde que le indique tanto al lector como al compilador que
realmente quiere tratarlo como el puntero destino.
Y esto nos lleva a discutir un asunto interesante. Uno de los objetivos importantes de C++ es poder compilar la mayor cantidad posible de código C para así, permitir una fácil transición al nuevo lenguaje. Sin embargo, eso no significa, como se ha visto que cualquier segmento de código que sea válido en C, será permitido automáticamente en C++. Hay varias cosas que un compilador de C permite hacer que son potencialmente peligrosas y propensas a generar errores (verá ejemplos de a lo largo de libro). El compilador de C++ genera errores y avisos en este tipo de situaciones y como verá eso es más una ventaja que un obstáculo a pesar de su naturaleza restrictiva. ¡De hecho, existen muchas situaciones en las cuales tratará de detectar sin éxito un error en C y cuando recompiles el programa con un compilador de C++ éste avisa exactamente de la causa del problema!. En C, muy a menudo ocurre que para que un programa funcione correctamente, además de compilarlo, luego debe hacer que ande. ¡En C++, por el contrario, verá que muchas veces si un programa compila correctamente es probable que funcione bien! Esto se debe a que este último lenguaje es mucho más estricto respecto a la comprobación de tipos.
En el siguiente programa de prueba podrá apreciar cosas nuevas con
respecto a cómo se utiliza la nueva versión de la
Stash
:
//: C04:CppLibTest.cpp //{L} CppLib // Test of C++ library #include "CppLib.h" #include "../require.h" #include <fstream> #include <iostream> #include <string> using namespace std; int main() { Stash intStash; intStash.initialize(sizeof(int)); for(int i = 0; i < 100; i++) intStash.add(&i); for(int j = 0; j < intStash.count(); j++) cout << "intStash.fetch(" << j << ") = " << *(int*)intStash.fetch(j) << endl; // Holds 80-character strings: Stash stringStash; const int bufsize = 80; stringStash.initialize(sizeof(char) * bufsize); ifstream in("CppLibTest.cpp"); assure(in, "CppLibTest.cpp"); string line; while(getline(in, line)) stringStash.add(line.c_str()); int k = 0; char* cp; while((cp =(char*)stringStash.fetch(k++)) != 0) cout << "stringStash.fetch(" << k << ") = " << cp << endl; intStash.cleanup(); stringStash.cleanup(); } ///:~
Listado 4.6. C04/CppLibTest.cpp
Una de las cosas que el lector habrá podido observar en el código anterior es que las variables se definen «al vuelo», o sea (como se introdujo en el capítulo anterior) en cualquier parte de un bloque y no necesariamente -como en C- al comienzo de los mismos.
El código es bastante similar al visto en
CLibTest.cpp
con la diferencia de que, cuando
se llama a un método, se utiliza el operador de selección de
miembro '.
' precedido por el nombre de la
variable. Esta es una síntaxis conveniente ya que imita a la
selección o acceso de un dato miembro de una estructura. La única
diferencia es que, al ser un método, su llamada implica una lista
de argumentos.
Tal y cómo se dijo antes, la llamada que el compilador hace genera
realmente es mucho más parecida a la llamada
a la función de la librería en C. Considere la decoración de
nombres y el paso del puntero this
: la llamada
en C++ de intStash.initialize(sizeof(int),
100)
se transformará en algo parecido a
Stash_initialize(&intStash, sizeof(int),
100)
. Si el lector se pregunta qué es lo que sucede
realmente debajo del envoltorio, debería recordar que el
compilador original de C++ cfront de
AT&T producía código C como salida que luego debía ser
compilada con un compilador de C para generar el ejecutable. Este
método permitía a cfront ser
rápidamente portable a cualquier máquina que soportara un
compilador estándar de C y ayudó a la rápida difusión de C++. Dado
que los compiladores antiguos de C++ tenían que generar código C,
sabemos que existe una manera de representar síntaxis C++ en C
(algunos compiladores de hoy en día aún permiten generar código
C).
Comparando con CLibTest.cpp
observará un
cambio: la introducción del fichero de cabecera
require.h
. He creado este fichero de cabecera
para realizar una comprobación de errores más sofisticada que la
que proporciona assert()
. Contiene varias
funciones incluyendo la llamada en este último ejemplo,
assure()
que se usa sobre ficheros. Esta
función verifica que un fichero se ha abierto exitosamente y en
caso contrario reporta un aviso a la salida de error estándar (por
lo que también necesita el nombre del fichero como segundo
argumento) y sale del programa. Las funciones de
require.h
se usan a lo largo de este libro
especialmente para asegurar que se ha indicado la cantidad
correcta de argumentos en la línea de comandos y para verificar
que los ficheros se abren correctamente. Las funciones de
require.h
reemplazan el código de detección
de errores repetitivo y que muchas veces es causa de distracciones
y más aún, proporcionan mensajes útiles para la detección de
posibles errores. Estas funciones se explican detalladamente más
adelante.