Por favor, activa JavaScript y desactiva tu adblock para este sitio

El Javatar

Blog dedicado a la Programación en Java, C, PHP, Phyton, HTML, SQL y Mucho Más

miércoles, 23 de noviembre de 2016

Programación Orientada a Objetos en C++ | Clases y Objetos

Clases

La base de la Programación Orientada a Objetos es la abstracción de los datos o los Tipos de Datos Abstractos, y dicha abstracción de los datos es posible gracias a las clases y a los objetos.

Programación Orientada a Objetos en C++: Clases y Objetos

Podemos decir entonces que una clase es aquella que nos proporciona una estructura de datos con sus respectivas operaciones. Los datos son conocidos como atributos y las operaciones se conocen como métodos. Podemos concluir entonces que una clase es el molde que nos sirve para la creación de un objeto.

Así pues, la unión entre los atributos y los métodos es la que define el comportamiento de un objeto o grupo de objetos según sea el caso.

Algunos ejemplos de clases pueden ser: Auto, Empresa, Libro, Persona, etc.

Objetos e Instancias

La instanciación es una de las principales características de los lenguajes de programación orientados a objetos. Recordemos que la instanciación es la capacidad que tienen los nuevos tipos de datos, en este caso, las clases, de ser "instanciadas" en cualquier momento.

El instanciar una clase produce un objeto o instancia de la clase que asigna un determinado espacio en la memoria para ser gestionada o destruida posteriormente según sea el caso.

Un objeto es por tanto y a grandes rasgos, una instancia de una clase. Todo objeto puede ser identificado en forma única a través de su nombre; además, define un estado el cuál es definido por los valores de sus atributos en un momento dado.

El estado de un objeto cambia de acuerdo a los métodos que le son aplicados. A esta posible secuencia de cambios de estado es la que conocemos como el comportamiento del objeto.

Instanciación: Los objetos pueden ser creados de dos formas al igual que una estructura de datos: Estáticamente y Dinámicamente.

1) Estáticamente: En tiempo de compilación se le asigna un área de memoria al objeto.

2) Dinámicamente: En tiempo de ejecución se le asigna un área de memoria al objeto y su existencia es temporal. Es necesario liberar el espacio usado por el objeto en memoria cuando éste ya no es útil. En algunos lenguajes como Java, el lenguaje nos proporciona mecanismos de recolección de basura y por tanto no debemos preocuparnos por liberar el espacio de memoria usada por los objetos, sin embargo en C++ sí debemos hacerlo explícitamente.

Clases en C++

La definición de una clase define nuevos Tipos de Datos Abstractos. La definición en C++ se realiza mediante la palabra reservada class, seguida del nombre de la clase y finalmente el cuerpo de la clase encerrado entre llaves {} y finalizando con un punto y coma (;).

El cuerpo de la clase contiene la declaración de los atributos (variables) y la declaración de los métodos (funciones). Tanto los atributos como los métodos pertenecen exclusivamente a la clase y sólo pueden ser usados a través de un objeto de dicha clase.

Sintaxis:

class <nombre_clase> {
    <cuerpo de la clase>
};  // Finaliza declaración de la clase

Ejemplo:

class Triangulo {
    int base;
    int altura;
    int area;

    void calcularArea() {
        area = (base * altura) / 2;
    }
};

Miembros de una clase

Miembros de una Clase - Programación Orientada a Objetos en C++

Una clase está formada por un conjunto de miembros que pueden ser datos, funciones, clases anidadas, enumeraciones, tipos de dato, etc. Primeramente nos enfocaremos en los datos y las funciones (atributos y métodos).

Hay que tener en cuenta que un miembro no puede ser declarado más de una vez. Tampoco es posible añadir miembros después de la declaración de la clase, es decir, después de la llave y el punto y coma (};).

Ejemplo:

class ClaseEjemplo {
    int a;
    int b;
    int a;    // error
    int metodo(int, int);
};

Atributos miembro: Todos los atributos que forman parte de una clase deben ser declarados dentro de la misma.

Métodos miembro: Al igual que los atributos, los métodos deben ser definidos dentro de la declaración de la clase; sin embargo, el cuerpo de la función puede ir dentro o fuera de la clase. Si un método se declara completo dentro de la clase, éste se considera como inline.

La declaración dentro de la clase no cambia con respecto a la declaración de una función, salvo que se hace dentro de la clase. Retomemos la clase del ejemplo inicial, pero ahora la modificaremos agregando un método con el cuerpo del mismo fuera del cuerpo de la clase:

Ejemplo:

class Triangulo {
    public:
        int base;
        int altura;
        int area;
        void calcularArea() {
            area = (base * altura) / 2;
        }
        int obtenerPerimetro(int, int, int);
};

Podemos apreciar que en la definición de la clase se incluye un método en línea o inline (calcularArea) y un prototipo de otro método (obtenerPerimetro).

Para definir un método miembro de una clase fuera de la misma, se debe escribir antes del nombre del método la clase con la que el método está asociado. Para esto, se hace uso del operador de resolución de alcance ::

Continuación del ejemplo:

int Triangulo::obtenerPerimetro(int lado1, int lado2, int lado3) {
    int perimetro = lado1 + lado2 + lado3;
    return perimetro;
}

Es importante tener muy en cuenta que al declarar los métodos fuera de la clase, No es posible mencionar la declaración de un método que no esté contemplado dentro de la clase, ya que si esto fuera posible, cualquiera podría tener acceso a la clase con sólo declarar un método adicional.

Ejemplo:

// Error en la declaración de un método
class MiClase {
    public:
        int a;
        void m();
};

// Esto es un error, ya que el método algo() no ha sido declarado
// dentro de la clase MiClase
int MiClase::algo() {
    return 0;
}

La declaración de un método miembro es considerada dentro del ámbito de su clase, lo cual significa que puede usar nombres de miembros de la clase directamente sin usar el operador de acceso de miembro de la clase.

Recordemos que por convención, en la programación orientada a objetos, las funciones son llamadas métodos, y la invocación o llamada a dichos métodos es conocida como mensaje.

Acceso a miembros

Una de las características de la Programación Orientada a Objetos es la posibilidad de encapsular datos, ocultándolos de otros objetos si es necesario. En C++, existen tres tipos de calificadores que definen a los datos y métodos como públicos, privados o protegidos, aunque por el momento sólo nos enfocaremos en los miembros públicos y privados.

Miembros públicos: Se utiliza cuando queremos dar a usuarios o clientes de una clase el acceso a miembros de esa clase. Los miembros deben ser declarados públicos a través de la palabra reservada public: La sintaxis es la siguiente:

Sintaxis:

public:
    <definición de miembros>

Miembros privados: Se utiliza cuando queremos ocultar ciertos miembros de una clase de los usuarios o clientes de la misma. De esta manera, nadie más que los miembros de la misma clase pueden usar a los miembros privados. Por omisión, los miembros se consideran privados. Sin embargo, en una estructura de datos se consideran públicos por omisión. Los miembros deben ser declarados públicos a través de la palabra reservada private: La sintaxis es la siguiente:

Sintaxis:

private:
    <definición de miembros>

Normalmente, y por buenas prácticas de programación orientada a objetos, se trata de que los atributos de la clase sean privados, y el acceso a dichos atributos se realiza mediante métodos públicos conocidos como Getters y Setters.

Así mismo, los métodos que no sean necesarios externamente, o que puedan conducir a un estado inconsistente del objeto, también deberían declararse como privados.

Ejemplo:

class Fecha {
    private:
        int dia;
        int mes;
        int anio;

    public:
        void setDia(int);    // Asigna día
        int getDia();    // Obtiene día
        void setMes(int);
        int getMes();
        void setAnio(int);
        int getAnio();
};

Objetos de clase

Ya hemos visto como se define una clase, la forma de declarar sus atributos y sus métodos, los cuales hemos aprendido que pueden ir dentro de la definición de la clase (inline) o fuera de ella. Ahora veremos cómo es posible crear objetos o instancias de esa clase.

Recordemos que una de las características de los objetos es que cada uno guarda un estado particular de acuerdo al valor de sus atributos. La importancia de los lenguajes Orientados a Objetos es precisamente el objeto, el cual no es más que una identidad lógica que contiene datos y código que manipula dichos datos.

En C++, un objeto es una variable de un tipo definido por el usuario. Veamos ahora un ejemplo completo:

#include <iostream>
#include <string>

using namespace std;

class MiClase {
    public:
        string a;
        string b;
};

int main() {
    MiClase c1;
    MiClase c2;
    
    c1.a = "Hola";
    c1.b = "Mundo";
    
    c2.a = "Hello";
    c2.b = "World";
    
    cout << c1.a << " " << c1.b << endl;
    cout << c2.a << " " << c2.b << endl;
    
    return 0;
}

Veamos ahora el mismo ejemplo, pero instanciando objetos de la clase y usando métodos mediante los cuales accedemos a los atributos de la misma:

#include <iostream>
#include <string>

using namespace std;

class MiClase {
    private:
        string a;
        string b;

    public:
        string getA();
        void setA(string);
        string getB();
        void setB(string);
        string concatenar();
};

string MiClase::concatenar() {
    return this->a + " " + this->b;
}

string MiClase::getA() {
    return this->a;
}

void MiClase::setA(string a) {
    this->a = a;
}

string MiClase::getB() {
    return this->b;
}

void MiClase::setB(string b) {
    this->b = b;
}

// Método main
int main() {
    MiClase *c1 = new MiClase;
    MiClase *c2 = new MiClase;
    
    c1->setA("Hola");
    c1->setB("Mundo");
    
    c2->setA("Hello");
    c2->setB("World");
    
    cout << c1->concatenar() << endl;
    cout << c2->concatenar() << endl;
    
    delete c1;
    delete c2;
    
    return 0;
}

NOTA: Es recomendable manejar la declaración de las clases mediante archivos .h y la implementación de los métodos de la misma mediante archivos .cpp; de esta forma, en el proyecto se puede incluir el correspondiente objeto importando solamente el archivo .h y sin proporcionar el código de su implementación.

La forma de hacerlo sería la siguiente:

#ifndef MICLASE_H
#define MICLASE_H

<definición de la clase>

#endif

Alcance de Clase

El nombre de un miembro de una clase es local a la clase y las funciones no miembros se definen en un alcance de archivo.

Dentro de la clase, los miembros pueden ser accedidos directamente por todos los métodos miembros. Fuera del alcance de la clase, los miembros de la clase se pueden utilizar seguidos del operador de selección de miembro de punto (.) ó del operador de selección de miembro de flecha (->), posterior al nombre de un objeto de la clase.

Ejemplo:

class ClaseEjemplo {
    public:
        void metodoPrueba();
}

void main() {
    ClaseEjemplo ce1;
    ce1.metodoPrueba();

    ClaseEjemplo *ce2 = new ClaseEjemplo;
    ce2->metodoPrueba();
}

Cuando usamos el operador de desreferencia u operador de indirección (*) en un objeto, debemos acceder a los campos o métodos de dicha clase usando el operador de flecha (->), o bien podemos hacerlo mediante el operador punto (.) siempre y cuando desreferenciemos dicho puntero.

Ejemplo:

#include <iostream>

using namespace std;

class MiClase {
    public:
        int a;
};

int main() {
    MiClase obj;

    (&obj)->a = 1;   // Referenciamos el objeto
    cout << (&obj)->a << endl;

    MiClase *obj2 = new MiClase;
    
    (*obj2).a = 2;   // Desreferenciamos el objeto
    cout << (*obj2).a << endl;
    
    return 0;
}

Sobrecarga de operaciones

En C++, al igual que otros lenguajes de programación Orientados a Objetos como Java, es posible tener el mismo nombre para un método, con la condición de que su firma sea distinta, es decir, que tenga parámetros diferentes. La diferencia debe ser al menos en el tipo de datos de loas parámetros que recibe el método.

Un método está sobrecargado cuando se tienen dos o más métodos con el mismo nombre, pero diferentes parámetros.

El compilador de C++ sabe qué operación debe ejecutar a través de la firma del método, la cual es la combinación entre el nombre del método y el número y tipo de los parámetros que recibe. Del mismo modo, el tipo de retorno del método puede ser igual o diferente.

La sobrecarga de operaciones de gran importancia, ya que es muy útil para escribir código más legible y modular. La idea de este concepto es utilizar el mismo nombre para operaciones relacionadas; de esta forma, si no tienen nada que ver, entonces es mejor utilizar un nombre distinto.

Ejemplo

class MiClase {
    int num;
    
    public:
        void cambiaValor() {
            num++;
        }
        
        void cambiaValor(int x) {
            num = num + x;
        }
};

En C++ también es posible usar la sobrecarga de operaciones por fuera de las clases:

Ejemplo

#include <iostream>

int multipliacion(int x) {
    return x * x;
}

double multiplicacion(double y) {
    return y * y;
}

int main() {
  std::cout << "4 multiplicado por 4 = " << multiplicacion(4) << std::endl;
  std::cout << "6.8 multiplicado por 6.8 = " << multiplicacion(6.8) << std::endl;
  
  return 0;
}

Constructores y destructores

Cunado trabajamos con datos primitivos en C++, el compilador se encarga directamente de reservar la memoria, y así mismo de liberarla cuando estos datos salen de su ámbito.

En la programación orientada a objetos, se trata de proporcionar mecanismos similares. En C++, cuando se crea un objeto, se llama a un método conocido como Constructor, y al salir, se llama a otro conocido como Destructor. Si no se proporcionan dichos métodos, se asume la acción más simple.

Constructor

Un constructor no es más que un método que posee el mismo nombre de la clase. Este método no puede tener un tipo de dato de retorno, sin embargo, al igual que los métodos, si permite la sobrecarga.

Ejemplo

class EjemploConstructor {
    private:
        string saludo;
    
    public:
        EjemploConstructor();
        void setSaludo(string);
        string getSaludo();
};

// Implementación del constructor
EjemploConstructor::EjemploConstructor() {
    this->saludo = "Hola Mundo";
    cout << "Se ha inicializado la clase" < endl;
}

Hay que tener en cuenta que un constructor si puede ser llamado desde un método de la clase.

Destructor

La contraparte del constructor es el destructor. Este es ejecutado momentos antes de que el objeto sea destruido, ya sea porque salen de su ámbito o por medio de la palabra reservada delete. El uso más común para un destructor es liberar la memoria asignada dinámicamente, aunque puede ser también utilizado para otras tareas de finalización, como cerrar archivos, una conexión de red, etc.

El destructor, al igual que el destructor, tiene el nombre de la clase pero con una tilde de negativo como prefijo (~).

Al igual que el constructor, el destructor tampoco regresa valores, sin embargo, éste último tampoco tiene parámetros.

Ejemplo

class EjemploClase {
    private:
        string saludo;
    
    public:
        EjemploClase();   // Constructor
        ~EjemploClase();  // Destructor
        void setSaludo(string);
        string getSaludo();
};

// Implementación del constructor
EjemploClase::~EjemploClase() {
    cout << "Clase destruida" << endl;
}

Miembros estáticos

En la Programación Orientada a Objetos, cada objeto tiene su propio estado, sin embargo, en muchas ocasiones es pertinente tener atributos por clase que conserven su valor a nivel de aplicación, y no por objeto mediante los cuales se obtiene su valor a nivel de instancia. En esos es necesario tener atributos estáticos, los cuales pueden ser compartidos por todos los objetos de la clase.

De este modo, sólo existe una copia de un miembro estático, y dicho miembro no forma parte de los objetos de la clase.

Ejemplo:

class EjemploStatic {
    private:
        string nombre;
        static int cantidadObjetos;
    
    public:
        EjemploStatic(string texto);
        ~EjemploStatic();
};


EjemploStatic::EjemploStatic(string texto) {
    nombre = texto;
    cantidadObjetos++;
}

EjemploStatic::~EjemploStatic() {
    cantidadObjetos--;
}

Un miembro estático, ya sea un atributo o un método, puede ser accedido desde cualquier objeto de la clase, o mediante el operador de resolución de alcance binario (::) y el nombre de la clase, debido a que un miembro estático existe a nivel de aplicación aunque no haya instancias de la clase.

Sin embargo, hay que tener en cuenta que el acceso a un miembro estático se restringe a las reglas de acceso a miembros que ya hemos visto anteriormente:

- Si se quiere acceder a un miembro estático que es privado, se debe hacer a través de un miembro público.

- Si no existe ninguna instancia de la clase, el acceso al miembro debe hacerse entonces a través de un método público estático.

Por otro lado, hay que tener en cuenta que un método estático SÓLO puede tener acceso a miembros estáticos. Además, los atributos estáticos se deben inicializar al igual que lo hacemos con los atributos constantes, fuera de la declaración de la clase. Por ejemplo:

int Clase::atributoEstatico = 0;
int const Clase::atributoConstante = 30;

Veamos ahora un ejemplo un poco más completo:

#include <iostream>
#include <stdio.h>
#include <string.h>

using namespace std;

class EjemploStatic {
    private:
        static int cantidadObjetos;
        static const int CANTIDAD_MAXIMA;
        char *nombre;
    
    public:
        EjemploStatic(char *texto = NULL);
        ~EjemploStatic();
        static int getCantidadMaxima() {
            return CANTIDAD_MAXIMA;
        };
        static int getCantidadObjetos() {
            return cantidadObjetos;
        };
};

// Inicializamos atributos estáticos
int EjemploStatic::cantidadObjetos = 0;
const int EjemploStatic::CANTIDAD_MAXIMA = 10;

EjemploStatic::EjemploStatic(char *texto) {
    if (texto != NULL) {
        this->nombre = new char[strlen(texto) + 1];
        strcpy(this->nombre, texto);
    } else {
        this->nombre = "NULL";
    }
    cout << "Nombre: " << this->nombre << endl;
    cantidadObjetos++;
}

EjemploStatic::~EjemploStatic() {
    cout << "Eliminando nombre: " << this->nombre << endl;
    if (this->nombre) {
        delete []nombre;
    }
    cantidadObjetos--;
}


// Método main
int main() {
    cout << "Cantidad Maxima de Objetos: " << EjemploStatic::getCantidadMaxima() << endl;
    cout << "Cantidad de Objetos: " << EjemploStatic::getCantidadObjetos() << endl;
    
    EjemploStatic obj1;
    cout << "Cantidad Maxima de Objetos: " << EjemploStatic::getCantidadMaxima() << endl;
    cout << "Cantidad de Objetos: " << EjemploStatic::getCantidadObjetos() << endl;
    
    EjemploStatic obj2("nombre 1");
    cout << "Cantidad Maxima de Objetos: " << EjemploStatic::getCantidadMaxima() << endl;
    cout << "Cantidad de Objetos: " << EjemploStatic::getCantidadObjetos() << endl;
    
    return 0;
}

Objetos constantes

En C++ es posible crear objetos de tipo constante, los cuales no van a poder ser modificados en ningún momento durante la ejecución de una aplicación, lo cual ayuda a cumplir el principio del mínimo privilegio, donde se debe restringir al máximo el acceso a los datos cuando este acceso estaría de sobra.

Si tratamos de modificar un objeto constante, obtendremos un error en tiempo de compilación.

Sintaxis:

const <clase> <lista de objetos>

const cHora h1(9, 30, 20);

Para estos objetos, existen compiladores lo bastante rígidos en el cumplimiento de esta instrucción, que no nos permiten hacer llamadas a métodos sobre estos objetos. Por ejemplo, en el caso de C++ de Borland, el compilador tan sólo manda una advertencia y permite que se ejecute la aplicación, pero advierte que debe ser considerado como un error.

Sin embargo, es posible que queramos consultar el estado del objeto a través de llamadas a métodos get, y para esto lo correcto es declarar métodos con la palabra reservada const, pues de esta forma podemos actuar libremente sobre el objeto sin modificarlo.

La sintaxis consiste en añadir después de la lista de parámetros la palabra reservada const en la declaración y en su definición.

Sintaxis

Declaración:

<tipo> <nombre> (<parámetros>) const;

Definición del método fuera de la declaración de la clase:

<tipo> <clase> :: <nombres> (<parámetros>) const {
    <código>
}

Definición del método dentro de la declaración de la clase:

<tipo> <nombres> (<parámetros>) const {
    <código>
}

El compilador de Borland permite usar los métodos constantes de manera indiferente para objetos constantes y no constantes, siempre y cuando no modifiquen al objeto; sin embargo hay compiladores que incluso restringen el uso de métodos constantes a objetos constantes. Para solucionar esto, es posible sobrecargar el método con la única diferencia de la palabra const, aunque el resto de la firma del método sea la misma.

Los constructores no necesitan la declaración const, puesto que deben poder modificar al objeto. Veamos ahora un ejemplo completo:

Ejemplo:

#include <iostream>
#include <stdlib.h>

using namespace std;

class ObjetoConstante {
    private:
        int a[10];
    public:
        ObjetoConstante(int x = 0) {
            for (int i = 0; i < 10; i++) {
                if (x == 0) {
                    x = random();
                }
                a[i] = x;
            }
        }
        char set(int, int);
        int get(int) const;
        int get(int);
};

// Implementación de los métodos
char ObjetoConstante::set(int pos, int val) {
    if (pos >= 0 && pos < 10) {
        a[pos] = val;
        return 1;
    }
    return 0;
}

int ObjetoConstante::get(int pos) const {
    if (pos >= 0 && pos < 10) {
        return a[pos];
    }
}

int ObjetoConstante::get(int pos) { // En este caso no es necesario sobrecargar
    if (pos >= 0 && pos < 10) {
        return a[pos];
    }
}

// Método main
int main () {
    const ObjetoConstante a(5), b;
    ObjetoConstante c;
    
    //a.set(0, 1);    // Esto nos genera un error de compilación porque llamamos a un
    //b.set(0, 2);    // método que modifica un objeto que ha sido declarado constante
    c.set(0, 3);
    //a.set(1, 11);
    //b.set(1, 22);
    c.set(1, 33);
    
    cout << a.get(0) << endl;
    cout << a.get(1) << endl;
    cout << b.get(0) << endl;
    cout << b.get(1) << endl;
    cout << c.get(0) << endl;
    cout << c.get(1) << endl;
}

No hay comentarios.:

Publicar un comentario