Introducción a C++

Los elementos presentes dentro de C++ permiten concretar varios conceptos que dentro de la Ingeniería de Software se han considerado importantes, como son: En esta introducción se trata de hacer énfasis en los elementos del lenguaje que son importantes para el desarrollo de software reutilizable y extensible; no disponibles en lenguajes tradicionales.
  Otros elementos del lenguaje importantes en el desarrollo de una aplicación son:

Tipos definidos por el usuario

El lenguaje permite agregar tipos definidos por el usuario que pueden ser tratados de la misma manera que los predefinidos. Esto da  trasparencia y claridad al código.

Comparando el código en C y C++ para un nuevo tipo String sería:
 
struct tag_String
{
  char* rep;
};
typedef tag_String* String;
void InitVacio( String );
void InitCopia( String, String );
void Igualar( String, String );
void Liberar( String );
void InitCadena( String, const char*);
int Length( String );
String SumarCadena( String, const char*);
String SumarString( String, String);
void Mostrar( String ); // No equivalente
// Definición de las funciones
void main( )
{
  String uno = malloc( sizeof(tag_String) );
  String dos = malloc( sizeof(tag_String) );  
  String tres = malloc( sizeof(tag_String) );  
  InitCadena( uno, "Hola Mundo" );
  InitCopia( dos, uno );
  InitVacio( tres );
  Igualar( tres, SumarString(uno,
             SumarCadena(dos," cierto? ") ));
  Mostrar( tres );
}
class String
{
     public:
       String( ); // Constructor sin argumentos
       String( const String& ); // Constructor por copia
       String& operator=( const String& ); // asignación
       ~String( ); // Destructor

       // ... Otras operaciones del tipo
       String( const char* value );
       int length( ) const;
       String& operator+ (const char* );
       String& operator+ (String& );
       friend String& operator<<( ostream&, String);

     private:
       char* rep;  // Implementación del tipo.
};

// ... Definición de los métodos

void main( )
{
  String uno = "Hola Mundo";
  String dos = uno;
  String tres;
  tres = uno + dos + " cierto? ";
  cout << tres;
}
 
 

Un Tipo definido por el usuario utiliza los siguientes mecanismos de C++

Para utilizar un objeto de un tipo dado puede utilizarse la notación nombreObjeto.nombreMensaje. Esta notación se conoce como inversión de objeto. El cambio puede verse en el siguiente ejemplo
 
 
...
num= Length( tres );
...
...
num = tres.length( );
...
Dentro de la implementación del método está implícito el objeto que recibe el mensaje mediante un apuntador denominado this. Este apuntador permite referirse a la implementación del objeto (es decir, su parte privada). Este método puede verse como el siguiente código:
 
int Length( String s )
{
  return strlen(s->rep);
}
int String::length( )
{
  return ::strlen(rep);  
  // Equivalente a return ::strlen(this->rep);
}
Como se vé internamente se pueden referir atributos dentro del objeto directamente, sin utilizar el apuntador this.
 


Representación del mundo real

Mediante el uso de los tipos definidos por el usuario pueden modelarse conceptos existentes en el mundo real. Esta correspondencia entre conceptos del dominio del problema y módulos (clases) dentro del programa permite dar una claridad y arquitectura al código muy adecuada para su evolución. Un ejemplo de esto puede darse en el contexto de un hospital y el manejo de pacientes. En este dominio puede llegarse a modelar clases como las siguientes
 
 
class Habitacion
{
public:
  void AgregarPaciente( Paciente* );
  Paciente* SacarPaciente( const char* nombre );
  bool EstaPaciente( const char* nombre );
private:
  int camas;
  Set<Paciente*> pacientes;
};
class Hospital
{
public:
  void IngresarPaciente( const char* nombre, const char* enfermedad, const char* numISS );
  void DarAltaPaciente( const char* nombre );
  Habitacion* BuscarHabitacion( const char* enfermedad );
private: 
  Set<Paciente> pacientes;
  Set<Habitacion> habitaciones;
};
 


Mecanismos de Reutilización

Mediante mecanismos como herencia, polimorfismo, clases abstractas, y funciones virtuales pueden crearse programas reutilizables, extensibles y modulares.  A continuación se presentan algunos de estos mecanismos.
 

1. Extensión mediante herencia y funciones virtuales

El mecanismo de herencia permite extender el funcionamiento de un sistema, agregando o cambiando la funcionalidad sin tocar el código existente. Supongamos la función de calcular los honorarios de un médico dentro del problema del hospital. Si en la primera definición de requerimientos solo se trabajaba con médicos por horas, el código podría ser:
 
 
float Medico::CalcularPago( )
{
  float valor=0;
  Servicio* serv;
  valor = numHoras*valorHora;
  while( servicios.nextElement( ) )
  {
    serv = (Servicio*) servicios.get( );
    valor += serv->getValor( );
  }
}
Contando con este método, la operación que paga al médico en el hospital podría ser
 
void Hospital::Pagar( Medico& med )
{
  pagosPendientes.add( med, med.CalcularPago( ) );
  med.BorrarCuenta( );
}
Suponiendo que el hospital comienza a tener médicos residentes, es necesario replantear el método de pago. Se puede incluir este nuevo método agregando una nueva subclase, sin tocar el código pre-existente. Se utiliza de esta manera el mecanismo de herencia junto con el de funciones virtuales, para redefinir lo que significa llamad CalcularPago de un médico. El código es como el siguiente
 
class Medico
{
  public:
  //... 
  virtual float CalcularPago( );
}
class MedicoResidente : public Medico
{
  public:
  //... 
  virtual float CalcularPago( );
}
float MedicoResidente::CalcularPago( )
{
  float valor=0;
  valor = Medico::CalcularPago( );
  valor += sueldoMensual;
  return valor;
}
Mediante esta nueva operación es posible reutilizar el método de Pagar en el hospital sin ningún cambio, enviando médicos residentes en vez de médicos.

2. Extensión de tipos por polimorfismo de contenedoras, funciones y variables

Supongamos que un algoritmo de refresco de pantalla es como el que sigue
 
void Ventana::pintar( )
{
  Grafico* g;
  elems.first( );
  while( elems.nextElement( ) )
  {
    g = elems.get( );
    g->pintar( ); 
  }
}
y supongamos una estructura de clases como la que sigue
 
Ejemplo de polimorfismo de contenedora
 

Este algoritmo se considera genérico, independiente del tipo de subclases de Gráfico que se modelen. En este caso se tiene un conjunto polimorfo (elems), una variable polimorfa (g) y un mensaje polimorfo (pintar) que se interpreta según sea el tipo exacto de g.

3. Frameworks

El objetivo en este esquema es plantear inicialmente un modelo de interacción entre clases abstractas que luego pueda concretarse en las subclases correspondientes. Supongamos un framework de interfaz, donde hay una clase Application que representa el control de la aplicación y una clase Window que representa a la ventana principal, entre otras. El mecanismo abstracto que permite comenzar la ejecución entre aplicaciones puede ser:
 
 
class Application
{
public:
  //...
  virtual void InitWindow( ) =0;
  Application( )
  {
    mainWindow = InitWindow( );
    mainWindow->run( );
  }
private:
  Window* mainWindow;
};
class Window
{
public:
  // ...
  virtual void run( )
  {
    // ... Implementacion ... 
  }
};
Los usuarios del framework deben En este tipo de esquemas, existe un gran número de clases predefinidas con un comportamiento por defecto, el cual puede extenderse mediante los mecanismos ya planteados dentro del framework.

4. Reutilización de Templates

El objetivo es reutilizar una clase genérica utilizando como parámetro una clase desarrollada por el usuario. Supongamos una clase paramétrica Graph<T> pre-existente y una clase Persona hecha por el programador. Lograr que se puedan combinar estas clases y crear Graph<Persona> implica agregar los métodos que permitan cumplir con la interfaz pedida por la clase paramétrica en el nuevo tipo.

Modularidad y separación entre modelos

En los lenguajes tradicionales, uno de los problemas que existe al reutilizar software son los conflictos de nombres con otros módulos del sistema. C++ permite definir el concepto de módulo o componente abstracto por medio de espacios de nombres o namespaces. De esta manera pueden tenerse módulos como los siguientes:
 
 
namespace Modulo1;
class A
{
  //... declaración de Modulo1::A
}
namespace Modulo2;
class A
{
  //... declaración de Modulo2::A
}
void main( )
{
  Modulo1::A obj1;
  Modulo1::A obj2;
  // ... 
}
Asi pueden reutilizarse librerías aunque utilicen los mismos nombres para sus clases.

Otro concepto importante en términos de modularidad es el acoplamiento débil entre los módulos. Supongamos un módulo de interfaz que debe reflejar el estado de un modelo del mundo. Se desearía que el modelo del mundo no dependa en ninguna medida del módulo de interfaz, aunque también se desea que los posibles cambios y errores puedan mostrarse al usuario final. Una forma de lograr esta cohesión débil es por medio del mecanismo de excepciones. Un ejemplo de su uso puede ser:
 
class Hostpital
{
public:
  // ...
  void IngresarPaciente( const char* nombre, const char*, const char* ) throws SinCupoException
  {
    if( NumCamasDisponibles( ) == 0 )
      throw new SinCupoException( String( "no hay cama para " ) + nombre );
    else
      // Otros casos de excepcion y caso por defecto...
  }
};


class Window
{
public:
  // ...
  virtual void run( )
  {
    // ... Otros eventos
    try
    {
      hospital.IngresarPaciente( "Pedro Perez", "hepatitis", "6523773" );
    }
    catch( SinCupoException e )
    {
      MessageBox( this, "No hay cupo para el paciente... Intente ingreso de urgencia" );
    }
  }
};
 
Su busca en este caso una reacción del mundo (el hecho de no haber cupo) con el resultado en interfaz. El modelo del mundo no conoce cómo va a ser usada esta información y el mecanismo permite tratarla aparte del "caso por defecto".



Pablo Figueroa
1997