Ir al contenido

Programación en C++/Objetos y Clases

De Wikilibros, la colección de libros de texto de contenido libre.
Desarrollo Orientado a Objetos Sobrecarga de Operadores

Clases y Objetos

[editar]

Este capítulo introduce a las clases en C++. La clase es la fundación de C++ para el soporte de la programación orientada a objetos, y se encuentra en el núcleo de muchas de sus más avanzadas características. La clase es la unidad básica de C++ de la encapsulación y esta provee el mecanismo por el cual los objetos son creados.

Fundamentos de Clases

[editar]

Vamos a comenzar definiendo los términos de clase y objeto. Una clase define un nuevo tipo de dato que se especifica en la forma de un objeto. Una clase incluye los datos y el código que operará sobre esos datos. Además, una clase enlaza datos y código. C++ usa una especificación de una clase para construir objetos. Los objetos son instancias de una clase. Además, una clase es esencialmente una serie de planes que especifican cómo construir un objeto. Es importante tener claro esto: Una clase es una abstracción lógica.

No es sino hasta que un objeto de esa clase sea creado que la representación física de la clase existe en la memoria. Cuando se define una clase, se declaran los datos que ésta contiene y el código que opera en esos datos. Aunque clases muy simples pueden contener sólo código o sólo datos, la mayoría de las clases contienen ambos. En conjunto, los datos se almacenan en las variables y el código en las funciones. Colectivamente, las funciones y variables que constituyen una clase son llamados 'miembros' de la clase. Una variable declarada dentro de una clase es llamada 'variable miembro', y una función declarada en una clase es llamada 'función miembro'. En ocasiones el término 'variable de instancia' es usado en lugar de variable miembro.

Una clase es creada con la palabra clave class. La declaración de una clase es similar sintácticamente a una estructura (y tienen muchísimo que ver). Aquí tenemos un ejemplo. La siguiente clase define un tipo llamado CRender, el cual es usado para implementar operaciones de renderizado en este caso.

// Esto define la clase CRender
class CRender {
	char buffer[256];
public:
	void m_Renderizar();
};


Veamos más de cerca esta declaración de la clase.

Todos los miembros de CRender son declarados dentro de la declaración 'class'. La variables miembro de CRender es buffer. La función miembro es m_Renderizar.

NOTA: Por defecto los miembros de una clase son privados.

Una clase puede contener tanto miembros privados como públicos. Por defecto, todos los elementos definidos en una clase son privados. Por ejemplo la variable buffer es privada. Esto significa que sólo pueden acceder a ella otros miembros de la clase CRender, cosa que no podrá hacer ninguna otra parte del programa. Es una forma de lograr la encapsulación: se puede controlar el acceso a ciertos elementos de datos manteniéndolos privados. Aunque no hay ninguna en este ejemplo, se pueden definir funciones privadas, las cuales pueden ser llamadas solamente por otros miembros de la clase.

Para hacer pública una parte de la clase (accesible a otras partes del programa), se debe declarar con la palabra clave public. Todas las variables o funciones definidas después de la declaración pública son accesibles por todas las demás funciones en el programa. En nuestra clase CRender, la función m_Renderizar() es pública. Típicamente, su programa accederá a los miembros privados de una clase a través de sus funciones públicas. Note que la palabra clave public es seguida con : . Mantenga en mente que un objeto forma una relación entre código y datos. Una función miembro tiene acceso a los elementos privados de su clase. Esto significa que m_Renderizar tiene acceso a buffer en nuestro ejemplo. Para añadir una función miembro a la clase, debe especificar su prototipo en la definición de la misma.

Una vez que se ha definido una clase, se puede crear un objeto de ese tipo usando el nombre de la clase. El nombre de la clase se convierte en un especificador del nuevo tipo. Por ejemplo la siguiente declaración crea 2 objetos llamados render1 y render2 del tipo CRender.

CRender render1, render2;

Cuando un objeto de la clase es creado, éste tendrá su propia copia de las variables miembros que contiene la clase. Esto significa que render1 y render2 tendrán su propia e independiente copia de buffer. Los datos asociados con render1 son distintos y separados de los datos asociados con render2.

Recordemos: En C++, una clase es un nuevo tipo de dato que puede ser usado para crear objetos. Específicamente, una clase crea una consistencia lógica que define una relación entre sus miembros. Cuando se declara una variable de una clase, se está creando un objeto. Un objeto tiene existencia física, y es una instancia específica de una clase. ( Esto es, un objeto ocupa espacio de memoria, pero una definición de tipo no ). Además, cada objeto de una clase tiene su propia copia de los datos definidos dentro de esa clase.

Dentro de la declaración de CRender, el prototipo de una función es especificado. Ya que las funciones miembros son prototipadas dentro de la definición de la clase, no necesitan ser prototipadas en otro lugar cualquiera.

Para implementar una función que es un miembro de una clase, debe indicarle al compilador a cual clase pertenece la función calificando el nombre de la función con el nombre de la clase. Por ejemplo, esta es una manera de codificar la función m_Renderizar().

void CRender::m_Renderizar()
{
	strcpy(buffer, "C++ en wikibooks");
	return;
}

Resolución de ámbito

[editar]

El :: es llamado el operador de resolución de ámbito. Esencialmente le dice al compilador que esta versión de m_Renderizar pertenece a la clase CRender. Dicho de otra forma, :: declara que m_Renderizar se encuentra en el ámbito de CRender. Varias clases diferentes pueden usar los mismos nombres de función. El compilador sabe cuál función pertenece a cuál clase y esto es posible por el operador de resolución de ámbito y el nombre de la clase.

Acceso a las funciones

[editar]

Las funciones miembros de una clase sólo pueden ser llamadas relativas a un objeto específico. Para llamar a una función miembro desde alguna parte del programa que se encuentre fuera de la clase, se debe usar el nombre del objeto y el operador de direccionamiento '.' ( punto ). Por ejemplo, lo siguiente llama a m_Renderizar() en el objeto objeto1.

CRender objeto1, objeto2;

objeto1.m_Renderizar();

La invocación de objeto1.m_Renderizar() causa a m_Renderizar() operar en los datos de la copia de objeto1. Mantenga en mente que objeto1 y objeto2 son 2 objetos separados. Esto significa, por ejemplo, que inicializar objeto1 no causa que objeto2 sea inicializado, La única relación que objeto1 tiene con objeto2 es que es un objeto del mismo tipo.

Cuando una función miembro llama a otra función miembro de la misma clase, puede hacerlo directamente, sin usar un objeto y el operador '.' En este caso, el compilador ya conoce en cuál objeto se está operando. Solamente cuando una función miembro es llamada por código que se encuentra fuera de la clase es cuando debe utilizarse el nombre del objeto y el operador '.' Por la misma razón, una función miembro puede referirse directamente a una variable miembro, pero código fuera de la clase debe referenciarse a la variable a través de un objeto y el operador '.'

El programa siguiente muestra aquí todas las piezas juntas y detalles perdidos, e ilustra la clase CRender.

Ejemplo

[editar]
// Programa OPP01.CPP
#include <iostream>
#include <cstring>

using std::cout;
using std::endl;


// Esto define la clase CRender
class CRender {
public:
    char buffer[255];
    void m_Renderizar(const char *cadena);
};


/* implementar m_Renderizar() para la c;*/
void CRender::m_Renderizar(const char *cadena){
    strcpy(buffer, cadena);//copia la cadena
    return;
}


int main (int argc, char **argv){
    // crear 2 objetos CRender
    CRender render1, render2;

    render1.m_Renderizar("Inicializando el objeto render1");
    render2.m_Renderizar("Inicializando el objeto render2");	
   
    cout << "buffer en render1: ";
    cout << render1.buffer << endl;   // tenemos acceso a buffer ya que es publico.

    cout << "buffer en render2: ";
    cout << render2.buffer << endl;

return (0);
}

Este programa imprime:

buffer en render1: Inicializando el objeto render1

buffer en render2: Inicializando el objeto render2

Miembros de una clase (métodos y atributos)

[editar]

En el lenguaje coloquial de la programación orientada al objeto es común escuchar términos tales como: métodos, atributos, herencia, polimorfismo, etc. En esta sección nos encargaremos de hablar de los dos primeros.

Métodos:

En comparación con la programación tradicional, un método es lo mismo que una función cualquiera, salvo que como los métodos se declaran para pertenecer a una clase específica, se dice que todos los métodos de dicha clase son miembros de la misma. Por lo demás, la declaración y definición de los métodos es exactamente igual que declarar y definir cualquier otra función.

Atributos:

En comparación con la programación tradicional, un atributo es lo mismo que una variable cualquiera, salvo que como los atributos se declaran para pertenecer a una clase específica, se dice que todos los atributos de dicha clase son miembros de la misma. Por lo demás, la declaración de los atributos es exactamente igual que declarar cualquier otra variable.


Miembros:

A partir de este momento usaremos la palabra miembro para referirnos al hecho de que un método o un atributo pertenece a tal o cual clase.

Por Ejemplo, en el programa OOP01.CPP (visto anteriormente) la Clase CRender posee dos miembros, buffer que es un atributo; y m_Renderizar que es un método.

class CRender {
public:
    char buffer[256]; // atributo
    void m_Renderizar(const char *cadena); // método
};

Visibilidad de los miembros de una clase

[editar]

Por visibilidad se entiende al acto de acceder a los miembros de una clase. En este sentido, los miembros de una clase pueden ser: públicos, privados y protegidos.

  • Un miembro público significa que el acceso al mismo puede darse dentro del interior de la clase, dentro de una subclase, y desde un objeto instanciado de cualquiera de estas. Por ejemplo, los miembros de la clase CRender son accesibles dentro de la misma y podrán accederse desde cualquier otra clase que se derive de CRender, así como desde cualquier objeto instanciado de estas.
  • Un miembro privado significa que el acceso al mismo puede darse solamente dentro del interior de la clase que lo posee. Normalmente, el programador creador de una clase declara a los atributos de la clase como privados y a los métodos como públicos, esto con la idea de que el usuario de la clase no pueda tener acceso a los atributos sino es a través de los métodos definidos para el caso.
  • Un miembro protegido se comporta de manera parecida a un miembro privado, salvo que estos son accesibles dentro de la clase que lo posee y desde las clases derivadas, pero no desde los objetos instanciados a raíz de dichas clases.

Nota: por defecto, los miembros de una clase son privados.


En la clase Pareja que se verá en seguida, se declaran dos atributos y cuatro métodos para la manipulación de dichos atributos. Observe que los atributos son privados(por defecto), mientras que los métodos se declaran públicos.

class Pareja
{
    // atributos
    double a, b;

public:
    // métodos
    double getA();
    double getB();    
    void   setA(double n);
    void   setB(double n);
};

// implementación de los métodos de la clase Pareja
//
double Pareja::getA() { return a; }
double Pareja::getB() { return b; }
void Pareja::setA(double n) { a = n; }
void Pareja::setB(double n) { b = n; }

Subclases

[editar]

Una subclase es una clase que se deriva de otra. La clase que sirve de base suele conocerse como parent (padre), y a la subclase se le llama child (hija). En C++ cada clase que es creada se convierte en candidata para servir de base de donde se deriven otras. Por ejemplo, la clase Pareja es candidata para convertirse en la base para las subclases Suma, Resta, Multiplica, Divide, y otras posibles subclases en donde se utilice un par de valores numéricos.

Para poner un ejemplo, pensemos en que deseamos crear la clase Suma, misma que será utilizada para obtener la suma de dos números. Puesto que la clase Pareja posee dos atributos numéricos puede ser usada como base para la clase que estamos proyectando. Así, el siguiente ejemplo se constituye en un caso de clases derivadas.

Nota: Observe que la sintaxis para crear una subclase es:
class hija : [public | private] padre // ¿Qué significan public y private aquí?
{
   ...
};

Donde padre es la clase base e hija es la subclase.
class Suma : public Pareja
{
    // atributos de Suma
    double resultado;

public:
    // métodos de Suma
    double calcular();
};

// implementación de Suma
//
double Suma::calcular() { return getA() + getB(); }


Probando las clases Pareja y Suma

// programa OOP02.CPP
// Este programa pone a prueba el uso de las clase Suma,
// misma que es una subclase de la clase Pareja (ambas definidas anteriormente).

#include <iostream.h>

int main()
{
    Suma s;
    s.setA(80);
    s.setB(100);
    cout << s.getA() << " + " << s.getB() << " = " << s.calcular() << endl;
    cin.get();
    return 0;
}

Herencia

[editar]

La herencia es uno de los mecanismos más útiles de la programación orientada al objeto, ya que por medio de la misma se puede llevar a cabo la reutilización de código. Es decir, puesto que toda clase definida se convierte en candidata para ser usada como base de donde se deriven otras, esto da como resultado que las clases derivadas hereden todos los miembros de la clase base. Por ejemplo, la clase Suma vista en la sección anterior, hereda todos los miembros de la clase Pareja puesto que Suma es una extensión de Pareja. En ese sentido, podemos decir que existen dos tipos de herencia, por extensión y por agregación o composición. En el caso de las clases Pareja y Suma, se dice que Suma es una extensión de Pareja. Vista gráficamente, la herencia por extensión se puede representar así:

Herencia por extensión

[editar]
Herencia

Al tipo de diagrama mostrado arriba (Herencia por extensión) se le conoce como UML [1] y es utilizado para mostrar de forma grafica la relación existente entre una clase hija con la clase padre. En el caso del ejemplo, se muestra que la clase Suma es una extensión de la clase Pareja y, en consecuencia, Suma posee a los miembros { a, b, getA(), getB(), setA(), setB() } heredados de la clase Pareja. Observe como la clase Suma posee otros dos miembros no heredados, { resultado, y calcular() }, y es precisamente a este tipo de situación por lo que se dice que Suma es una extensión de Pareja, ya que Suma, además de poseer a todos los miembros de Pareja, se extiende para poseer o adquirir otros dos miembros.

Agregación o composición

[editar]

La composición se da en los casos en donde una clase posee un objeto que es una instancia de otra clase. Por ejemplo, la clase Suma podría escribirse de la siguiente forma:

class Suma
{
    // atributo privado     
    double resultado;

public:
    // método público
    double calcular();

    // atributo público
    Pareja  p;
};

// implementación del método calcular de la clase Suma.
double Suma::calcular() { return p.getA() + p.getB(); }

Luego, si usted presta atención, notará que el miembro p de la clase Suma es un objeto o instancia de la clase Pareja, en consecuencia, la clase Suma puede acceder a los miembros de la clase Pareja a través de la variable p. También se debe observar que la implementación del método calcular() es diferente que el mismo de la clase Suma original. Si usted desea poner a prueba a la nueva clase Suma, compile y ejecute el siguiente programa.

 
  
// programa herencia_por_composicion.CPP

#include <iostream>
using namespace std;

class Pareja
{
    // atributos
    double a, b;

public:
    // métodos
    double getA();
    double getB();    
    void   setA(double n);
    void   setB(double n);
};
 
// implementación de los métodos de la clase Pareja
double Pareja::getA() { return a; }
double Pareja::getB() { return b; }
void Pareja::setA(double n) { a = n; }
void Pareja::setB(double n) { b = n; }

class Suma
{
    // atributo privado     
    double resultado;

public:
    // método público
    double calcular();
    // atributo público
    Pareja  p;
};
// implementación del método calcular de la clase Suma.
double Suma::calcular() { return p.getA() + p.getB(); }

int main()
{
    Suma s;
    s.p.setA(80);
    s.p.setB(100);
    cout << s.p.getA() << " + " << s.p.getB() << " = " << s.calcular() << endl;
    cin.get();
    return 0;
}

////SALIDA////

80 + 100 = 180

Constructores

[editar]

Un constructor es un método que pertenece a una clase y el cual (en C++) debe tener el mismo nombre de la clase a la que pertenece. A diferencia de los otros métodos de la clase, un constructor deberá ser del tipo void, es decir, el mismo no regresará valor alguno. Una clase puede tener más de un método constructor. Cada clase debe tener al menos un constructor, tanto así que si el programador creador de una clase no define un método constructor para la misma, el sistema, o sea el compilador, creará de manera automática un constructor nulo.

El objetivo principal del constructor es establecer las condiciones necesarias dentro de la memoria y crear una copia del objeto mismo dentro de la memoria.

Los constructores suelen usarse para la inicialización de los atributos de los objetos instanciados. Por ejemplo, con las instrucciones:

    Suma s;
    s.setA(80);
    s.setB(100);

del programa OOP02.CPP, se declara el objeto s de la clase Suma y luego se inicializan los atributos del objeto por medio de los métodos setA() y setB(). En este caso, es necesario hacer uso de dichos métodos debido al hecho de que no se ha definido un constructor para la clase Suma. Ahora bien, para evitar este tipo de situaciones podemos definir un método constructor para Suma y que este se encargue de inicializar los atributos. Veamos.

Notas: ya que Suma es una extensión de Pareja, se ha definido el método constructor de Pareja y éste a la vez es invocado por el constructor de Suma. Otro punto de interés es el hecho de la definición de un constructor base para Pareja, ya que de acuerdo con las reglas de programación en C++ toda clase en donde se defina un constructor parametrizado deberá definir un constructor base; esta regla se aplica en los casos en donde la clase proyectada servirá de base para otras.

// programa OOP04.CPP
// Ejemplo: clases Pareja y Suma, ambas con constructor

#include <iostream>
using namespace std;

//------------------------
class Pareja
{
    // atributos
    double a, b;

public:
    // constructor de base (null)
    Pareja() {}

    // constructor parametrizado
    Pareja(double x, double y) : a(x), b(y) {}

    // métodos
    double getA();
    double getB();
    void   setA(double n);
    void   setB(double n);
};

// implementación de los métodos de la clase Pareja
//
double Pareja::getA() { return a; }
double Pareja::getB() { return b; }
void Pareja::setA(double n) { a = n; }
void Pareja::setB(double n) { b = n; }

//------------------------
class Suma : public Pareja
{
    // atributos de Suma
    double resultado;

public:
    // constructor
    Suma(double a, double b) : Pareja(a, b) {}

    // métodos de Suma
    double calcular();
};

// implementación de Suma
//
double Suma::calcular() { return getA() + getB(); }


//------------------------
int main()
{
    Suma s(80, 100);
    cout << s.getA() << " + " << s.getB() << " = " << s.calcular() << endl;
    cin.get();
    return 0;
}

Destructores

[editar]

Un destructor es un método que pertenece a una clase y el cual (en C++) debe tener el mismo nombre de la clase a la que pertenece. A diferencia de los otros métodos de la clase, un destructor deberá ser del tipo void, es decir, el mismo no regresará valor alguno. Para diferenciar a un método destructor de un método constructor, al nombre del destructor se le debe anteponer el carácter ~ (Alt + 126).

El objetivo principal del destructor es el de retirar de la memoria al objeto, o sea, el destructor hace todo lo contrario que el constructor.

Los destructores suelen usarse para liberar memoria que haya sido solicitada por el objeto a través de las ordenes malloc(), new, etc. En tales casos se deberá incluir dentro del método destructor la orden free, delete, etc., según sea el caso.

// clase Pareja con constructor y destructor
class Pareja
{
    // atributos
    double a, b;

public:
    // constructor de base (nulo)
    Pareja() {}

    // constructor parametrizado
    Pareja(double x, double y) : a(x), b(y) {}

    // destructor
    ~Pareja() {}

    // métodos
    double getA();
    double getB();
    void   setA(double n);
    void   setB(double n);
};



Desarrollo Orientado a Objetos Arriba Sobrecarga de Operadores