4.3. El objeto básico

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.