Programación orientada a objetos
Clases e instancias
El objetivo de Bjarne Stroustrup era implementar las clases descritas en el lenguaje Simula, en un compilador C. Como estos dos lenguajes son radicalmente opuestos en su planteamiento, fue necesario identificar una doble continuidad, sobre todo en el lado C.
Era fácil ver que la programación se simplificaría enormemente si ciertas funciones pudieran migrar dentro de estructuras C. Como resultado, no habría ninguna estructura que pasar a estas funciones, ya que obviamente se aplicarían a los campos de la estructura.
Sin embargo, era necesario conservar una forma de distinguir entre dos instancias, por lo que se ha modificado la sintaxis del operador punto:
Programación funcional |
Programación orientada a objetos |
|
|
|
|
Esta diferencia de enfoque tiene varias consecuencias positivas para la programación. En primer lugar, el programador ya no tendrá que elegir entre las distintas formas de pasar de la estructura a la función visualizar(). En segundo lugar, podremos distinguir entre elementos en primer y segundo plano (campos, funciones). Los que estén en primer plano serán visibles, accesibles desde fuera de la estructura. Los otros estarán ocultos y serán inaccesibles.
Este proceso garantiza un alto grado de independencia en la aplicación de un concepto, lo que también conduce a una buena estabilidad de desarrollo.
1. Definición de clase
Por tanto, una clase es una estructura con campos y funciones. Cuando las funciones se consideran dentro de una clase, se denominan métodos.
Todos los campos y métodos se denominan miembros. No recomendamos utilizar el término atributo para referirse a los campos, ya que puede adoptar un significado muy específico en C++ gestionado o en C#.
El lector que esté cambiando de Java a C++, debe tener cuidado de terminar la declaración de una clase con un punto y coma, ya que una clase es la continuación del concepto de estructura:
class Punto
{
int x,y;...
Herencia
1. Derivación de clases (herencia)
Ahora que estamos familiarizados con la estructura y el funcionamiento de una clase, podemos hacer que nuestros programas sean más genéricos. Es práctica común describir un problema general con los algoritmos apropiados, y luego hacer pequeñas modificaciones cuando se presente un caso similar.
La filosofía de orientación a objetos consiste en reducir al mínimo las macros, las inclusiones y los módulos. Este enfoque presenta una serie de riesgos cuando los problemas se hacen cada vez más complejos. La programación orientada a objetos adopta un enfoque genérico/específico, que se adapta mucho mejor a las pequeñas variaciones de los datos implicados en un problema. Los métodos de modelización basados en UML pueden guiarle en la construcción de redes de clases adaptadas a las circunstancias de un proyecto, sobre todo teniendo en cuenta que C++ es uno de los lenguajes que admiten este enfoque.
2. Ejemplo de derivación de clase
Imaginemos una clase Cuenta compuesta por los siguientes elementos:
class Cuenta
{
protected:
int numero; // número de la cuenta
double saldo; // saldo de la cuenta
static int num; // variable utilizada para calcular el siguiente
número
static int siguiente_numero();
public:
char*titular; // titular de la cuenta
Cuenta(char*titular);
~Cuenta(void);
void credito(double cantidad);
bool debito(double cantidad);
void aumentar();
};
Ahora podemos imaginar una clase CuentaRemunerada, que especialice el funcionamiento de la clase Cuenta. Es fácil ver que una cuenta remunerada puede realizar las mismas operaciones que una cuenta convencional, pero su comportamiento es ligeramente diferente cuando se trata de operaciones de crédito, ya que el banco paga intereses. En consecuencia, sería tedioso reescribir completamente el programa que funciona para la clase Cuenta. En su lugar, vamos a derivar esta clase para obtener la clase CuentaRemunerada.
La clase CuentaRemunerada adopta todas las características...
Otros aspectos de la programación orientada a objetos
1. Conversión dinámica
a. Conversiones desde otro tipo
Los constructores se utilizan para convertir objetos a partir de instancias (valores) expresadas en otro tipo.
Tomemos el caso de nuestra clase String. Sería interesante "convertir" un char* o un char en una cadena:
#include <string.h>
class Cadena
{
private:
char*buffer;
int t_buf;
int longitud;
public:
// un constructor por defecto
Cadena()
{
t_buf=100;
buffer=new char[t_buf];
longitud=0;
}
Cadena (int t_buf)
{
this->t_buf=t_buf;
buffer=new char[t_buf];
longitud=0;
}
Cadena(char c)
{
t_buf=1;
longitud=1;
buffer=new char[t_buf];
buffer[0]=c;
}
Cadena(char*s)
{
t_buf=strlen(s)+1;
buffer=new char[t_buf];
longitud=strlen(s);
strcpy(buffer,s);
}
void visualizar()
{
for(int i=0; i<longitud; i++)
printf("%c",buffer[i]);
printf("\n");
}
} ;
int main(int argc, char* argv[])
{
// Escritura 1
// Conversión mediante el uso explícito del constructor
Cadena x;
x=Cadena("hola"); // conversión
x.visualizar();
// Escritura 2
// Transtipado (cast) par coerción
Cadena y=(char*) "hola"; // conversión (cast)
y.visualizar();
// Escritura 3
//...
Las excepciones más seguras que los errores
La enorme ventaja de las excepciones es que son intrínsecas al lenguaje, e incluso al entorno de ejecución (runtime o incluso framework). Por tanto, son mucho más seguras para controlar la ejecución de un programa multithread (como es el caso de muchas librerías). Además, las excepciones guían al programador y facilitan la estructuración del código. En otras palabras, es difícil prescindir de ellas una vez que se domina su uso.
Algunos lenguajes van incluso más allá y exigen que se tengan en cuenta a la hora de escribir el programa. Es el caso, por ejemplo, de Java.
El lenguaje C++ ofrece excepciones estructuradas, basadas, por supuesto, en clases y una gestión avanzada de la pila. La idea es supervisar una secuencia de instrucciones -que pueden contener llamadas a funciones-. Cuando se produce un problema, el programador puede interceptar la excepción que describe el problema a medida que se propaga por la pila.
El principio de las excepciones en C++ consiste en separar la detección de un problema de su tratamiento. Es probable que una función de cálculo no disponga de medios para decidir qué estrategia adoptar en caso de fallo. Las distintas alternativas son continuar con un resultado falso, integrar nuevos valores introducidos por el usuario, suspender el cálculo, etc.
Cuando una función provoca una excepción, ésta se propaga por la pila de llamadas hasta que es interceptada.

Esta organización elimina las restricciones impuestas por la librería del lenguaje C; ya no hay acceso concurrente a una variable global responsable de describir el estado del error, puesto que el contexto del error lo define una instancia por derecho propio. Cuando se desencadena una excepción, provoca una salida inmediata del procedimiento o la función y empieza a buscar un bloque de procesamiento.
1. Propagación explícita
La palabra clave throw se utiliza para lanzar una excepción. Va seguida de una instancia, cuya semántica se deja al programador. A veces, el tipo utilizado para instanciar la excepción es suficiente para especificar las circunstancias del error.
Cuando se produce una excepción, se busca en la pila de llamadas un bloque de intercepción correspondiente a este tipo...
Programación genérica
El lenguaje C está demasiado cerca de la máquina para ofrecer una verdadera genericidad. Como mucho, el uso de punteros void* (introducidos por C++) permite trabajar con distintos tipos. Pero esta imprecisión tiene un alto precio, en términos de legibilidad, robustez y rendimiento del programa.
Las macros tampoco son una solución viable para implementar un algoritmo independientemente de la tipificación. Las macros desarrollan una filosofía opuesta a las técnicas de compilación. Pueden ser apropiadas en ciertos casos sencillos, pero su uso es, la mayoría de las veces, un ejercicio de bricolaje.
El hecho es que no todos los algoritmos están diseñados para ser implementados para la universalidad de los tipos de datos de C++. Para un cierto número de tipos, empezamos a pensar en términos de funciones polimórficas.
Por último, los modelos C++ son una solución mucho más elegante, muy sencilla y al mismo tiempo muy segura de utilizar. El tipo de datos lo elige el programador, que instanciará el modelo bajo demanda. El lenguaje C++ ofrece modelos de funciones y modelos de clases y trataremos estos dos aspectos sucesivamente.
1. Modelos de funciones
No todos los algoritmos se prestan a la construcción de un modelo. Identificar un algoritmo es una simple precaución que hay que tomar, porque un modelo construido con el propósito equivocado puede reducir la calidad de una aplicación.
Para el algoritmo estándar, la librería STL ha optado por implementarse en forma de modelos. Los contenedores son estructuras cuyo funcionamiento es, por supuesto, independiente del tipo de objeto a almacenar.
En el caso de las cadenas, la elección es más discutible. Por ello, la librería STL ofrece un modelo de clase para cadenas que es independiente de la codificación de caracteres, lo que abre la puerta a una gran variedad de formatos. Para la codificación más común, ASCII, la clase string es una implementación específica.
El ámbito digital es especialmente exigente en cuanto a modelos de funciones y clases. Los programadores pueden elegir entre precisión (long double o double) y velocidad (float) sin tener que cuestionar todo su programa. Algunos algoritmos pueden incluso trabajar...
Trabajo práctico
1. Uso de la herencia de clases en el intérprete tiny-lisp
La herramienta tiny-lisp es un intérprete de LISP escrito en C++, cuyo código fuente está disponible con este libro.
En el intérprete tiny-lisp, las clases derivadas de ScriptBox se encargan de ejecutar el analizador en tantos entornos como sea posible:
-
La Sandbox es un entorno aislado donde la E/S está neutralizada (no activada).
-
El entorno ConsoleBox proporciona entrada/salida en la consola del sistema.
La clase base se llama ScriptBox y define un método virtual init_events():
/*
Entorno genérico de ejecución
*/
class ScriptBox
{
public:
LexScan*lexScan;
Evaluator*parser;
ScriptBox();
~ScriptBox();
virtual void init_events();
void new_text();
void set_text(string text);
void parse();
bool has_errors();
string get_errors();
string get_text();
bool set_debug();
};
En la versión neutral, Sandbox, este método no está sobrecargado.
/*
Entorno de ejecución neutro
*/
class Sandbox :
public ScriptBox
{
public:
Sandbox();
~Sandbox();
};
En el entorno Consolebox, en cambio, el método se sobrecarga para declarar eventos de entrada/salida en la consola (pantalla, teclado).
/*
Entorno...