Herramientas de usuario

Herramientas del sitio


highlevel:c:poo

¡Esta es una revisión vieja del documento!


C\C++ Orientado a objetos

Sintaxis

Definición de una clase

class <nombre> {};
  • Atención! No olvides el ;.

Para indicar los elementos públicos y privados:

	public:
		<elementos>
	private:
		<elementos>

Declarar clases

Podemos declarar una clase mediante un archivo de definiciones (.h) poniendo su cuerpo en un archivo de código (.cpp).

  • Ejemplo de .h:
class Probando
{
public:
	Probando(int i);
	virtual ~Probando();
};
  • Ejemplo del .cpp:
#include "Probando.h"
#include <iostream>
using namespace std;
 
Probando::Probando(int i){
	cout << "hola";
}
 
Probando::~Probando(){
}
  • Cuando vayas a usarlas en tu código no olvides incluir el .h. Ni tampoco olvides incluirlo en el .cpp.
  • A la definición que encontramos en el .h la llamaremos “interface de la clase”.
  • La declaración de los datos de la clase, variables y objetos, los colocaremos en la definición del .h.
  • Cuando vayamos a codificar un método en el .cpp definido en el .h, lo que haremos será, por ejemplo un int Suma (int a, int b); que esté dentro de la clase Mates:
int Mates::Suma (int a, int b) {...}
  • Si definimos una clase en un .cpp podemos codificar dentro del cuerpo de la clase, pero sólo se puede acceder a ellos por punteros:
class Perro 
{
	public: 
	Perro ()
	 {
	 	cout << "One ta namera";
	 }
 
	 void Exec()
	 {
	 	cout << this->Suma(3, 4);
	 }
 
	 int Suma (int a, int b)
	 {
	 	return a + b;
	 }
};

Constructores

  • Si no se ha definido constructor por defecto, la clase no lo tiene.
  • Sigue con la misma nomenclatura, es un método especial que se llama igual que la clase, y no se ha de indicar un tipo devuelto.
  • Existen los constructores que inicializan valores, directamente estos son una forma óptima de inicializar los valores de la clase mediante el constructor, antes de que empiece a ejecutarse el código del cuerpo. Esta forma es, siguiendo a un constructor indicamos: : Var(valor)<, mas variables>

Ejemplo, tenemos la siguiente declaración de clase:

class Probando
{
private:
	int X;
public:
	Probando();
	Probando(int xval);
};

Y su cuerpo:

Probando::Probando() : X(0)
{}
Probando::Probando(int xval) : X(xval)
{}	

Constructores copia

Son constructores que no tienen cuerpo y que únicamente se le pasa una referencia a otro objeto de la misma clase. El objeto construido es una copia de ese:

class pareja {
   public:
	pareja(const pareja &p);
	...

Destructores

  • Igual que los constructores, pero van precedidos por ~, Se llaman para destruir el objeto de memoria.
  • Cuando hacemos un delete de un puntero a un objeto se llama automáticamente a su destructor.

Declarar un objeto

Para usarlo en una expresión (función): <clase>(<pars>)

Perro("Bobby");

Declararlo y contenerlo en un objeto: <clase> <obj> (<pars>)

Perro p ("Bobby");

Para llamar al constructor por defecto NO usaremos los parentesis:

Perro p;	// <- Se está llamando al constructor por defecto!

Podemos volver a declarar un objeto:

p = Perro("Bobby");

Acceder a miembros:

Si los objetos no son punteros podemos acceder mediante <obj>.<miembro>

Propiedades del lenguaje

Usar punteros

Para reservar un espacio de memoria para un objeto y colocar dicho objeto en esa posición usaremos el operador 'new' más el nombre de la clase y los parámetros del constructor, recuerda que si es el constructor por defecto no es ni necesario poner los parentesis:

new Clase;
new Clase("Parametro1");
Clase *obj = new Clase;

Para llamar a métodos de un puntero a un objeto usaremos el operador →

obj->Metodo();

O…

(*obj).Metodo();	// *obj.Metodo(); daría error 
  • Para liberar el espacio de memoria al que apunta un puntero usaremos el comando delete: delete obj;

Métodos estáticos

En nuestras clases podemos crear métodos estáticos. Para usar estos no será necesario crear un objeto de dicha clase. Para usarlos haremos: NombreClase::NombreMetodo.
Ejemplo…

	class A
	{
	public:
		static void B (string hi)
		{
			cout << hi.c_str();
		}
	};
	void main ()
	{
		A::B("hiii!");
	}

Herencia

  • Al heredar de una clase la nueva contendrá implicitamente los métodos de su base.
  • Sintaxis: class Nueva : public cBase {…};
  • Pero hemos de tener en cuenta unas cosas (Imaginemos una clase A con una clase que hereda de esta, B):
    • Cuando se crea un objeto de la clase B se llama antes al constructor por defecto de la clase A.
    • Si la clase A no tubiese constructor por defecto debemos indicar otro alternativo. O, esto simplemente, porque queramos que se inicialice el objeto con otro constructor:
public: 
	B () : A(2) {...}
  • Si a un método nos pide un elemento de la clase B como parámetro le paasamos un elemento de la clase A peta. Si pasamos un objeto B por un sitio donde nos pida A sí que podremos hacerlo, ya que B es A pero A no es B.
  • Para llamar a métodos sobreescritos de la clase A desde la clase B haremos A::Metodo();. Para los no sobreescritos no es necesario.
  • Si en un método se nos pide como parámetro un elemento de la clase A y le pasamos uno de la B, el método sólo podrá llamar a métodos de la clase A, además, los métodos a los que llame serán los de la clase A interna en B, es decir, sólo serán accesibles a los métodos de la clase A aunque estemos en B.
  • Para que lo anterior pueda ser efectivo, debemos declarar el método de la clase base como virtual. Y no sólo eso, al declarar dicho método debemos hacerlo pidiendo un puntero a un objeto creado con new.
void PruebaA (A* p) {
	p->Prueba();
}

A este podemos pasarle un objeto B*.

  • En el anterior método, podríamos convertir ese A* en B*:
B* b = (B*)p;

Pero debemos tener cuidado, si el parámetro que viene es un objeto B o derivados no ocurrirá nada, pero si es un objeto A podemos tener problemas al acceder a los miembros de B.
Podemos llamar directamente a métodos de B sin tener que hacer la conversión:

((B*)p)->Prueba();
  • Al declarar un miembro como private, este sólo es accesible desde la clase donde está. Al declararlo como protected será accesible desde esa clase o derivadas y public desde cualquier lugar.
  • Si hay un método const en la clase A y lo heredamos en la clase B, al llamarlo en un objeto B aunque esté sobreescrito se llamará al de la clase A, a no ser que esta sobreescritura también esté con const. Recuerda que en el cuerpo de este método no podrá llamarse a otro método de la clase que no sea ese (ya sea en B o en A).

Herencia privada\protegida

Cuando hacemos class nombreclase : public nombreclase {…}; Estamos indicando que los miembros que hereda como públicos siguen siendolo… Imaginas que pasará cuando hagas herencia private o protected ;-)?

Herencia múltiple

Respecto a la herencia múltiple sólo decir que es posible ya que C++ permite hacer: class C : public A, public B { … };
Es posible que A y B tengan elementos que se llamen igual, y que C los herede. Al llamarlos daría error de ambigüedad, para solucionarlo sólo hay que codificar otras funciones que llamen a esas, por ejemplo en C:

void funcA ()
{
	A::func();
}
void funcB ()
{
	B::func();
}

Puede que por herencia dupliques la clase base, no directamente sino indirectamente, esto sería tener C que hereda de A y de B que, a la vez, estas dos heredan de cLetra, C no puede convertirse en cLetra porque no sabría a qué funciones acudir de cLetra, a las de A o a las de B. Para saltar este herror A y B deben heredar virtualmente de cLetra:

class cLetra
{
};
class A : virtual public cLetra
{
};
class B : virtual public cLetra
{
};
class C : public A, public B
{
};

Operador this

  • El operador this, se usa como en C#, hace referencia al objeto en cuestión en el que estamos.
  • Si queremos enviar el objeto en sí: return *this.
this->num = 4;

Métodos constantes

Dentro de la clase podemos declarar métodos constantes, estos métodos aseguran que en su cuerpo no se cambia el contenido del objeto al que pertenecen.
Por ejemplo:

  • Definición: void MetodoConst () const;
  • Cuerpo:
void Probando::MetodoConst() const
{
	// X = 4;
	std::cout << "hola!";
}

La primera línea del cuerpo del método (X = 4;) cambiaría un valor de un dato interno de la clase, si no estubiese comentado esto petaría.

Variables globales desde dentro de la clase

Imaginemos el siguiente código:

int iNum;
class A {
private:
	int iNum;
public:
	A (){
		this->iNum = ::iNum;
	}
};

Es una declaración de una variable global y de una clase que tiene una variable interna que se llama igual a la global. Para acceder a la interna usaremos this-> y a la global: ::.

Clases internas

Podemos declarar clases internas en otras. Desde esta podremos crear objetos de la contenedora, pero no viceversa, además podremos acceder a los elementos internos y privados de ellas.
Para poder crear un objeto de una clase anidada (imaginemos que B está dentro de A), haremos:

A::B b;

También podemos definir una clase dentro de otra colocando en la definición “class nombreClase;”. Por ejemplo:

class A {
	class B;
}

Luego podemos definir class B así:

class A::B {
}

Y los métodos:

void A::B::Metodo () { }

Friends

  • Declarar en una clase un elemento como amigo significa dar a ese elemento la posivilidad de acceder a los métodos privados de la clase.
  • No es necesario que las declaraciones de amigos se hagan entre private, public o protected.

Ejemplos:

  • Declarar una clase amiga:
friend class clase;
  • Declarar un método amigo:
friend void metodo();
  • Declarar un operador amigo:
friend int operator+ (A, B);
class A {
private:
	int i;
	friend class B;
public:
	A (int d) {
		this->i = d;
	}
	void show () {
		printf("%d\n", this->i);
	}
};
 
class B {
	public:
	B (A* a) {
		a->i = 444;
	}
};
 
 
void main () {
	A a(1);
	B b(&a);
	a.show();
}

Notas sobre los amigos

  • Las declaraciones de amigos no hacen que la función o la clase sean miembros de donde se han declarado como amigos.
  • Los amigos de los amigos no son amigos de la clase.
  • Una clase derivada de una amiga de una clase no es amiga de la amiga.

Clases abstractas

  • Las clases abstractas no pueden ser instanciadas. En C++ son aquellas que tienen algún método virtual sin cuerpo.
  • Se usan sólo para herencia.
  • Obligan codificar sus miembros abstractos en sus clases derivadas.
  • Una clase es abstracta cuando tiene una función abstracta.
  • Si se hereda de ella y no se sobreescriben los métodos, la abstracción sigue vigente en la clase derivada, por lo que seguirá sin poder ser instanciada.
  • Los métodos abstractos han de ser virtuales y no podrán tener código. Declaración de un método virtual:
virtual void func () = 0;

Ejemplo:

class cLetra {
public:
	virtual void func () = 0;
	void fanc() {
		cout << "hola";
	}
};
class A : public cLetra {
public:
	void func (){
		this->fanc();
		cout << "hola" << endl;
	}
};

Conversiones dinámicas

La conversión de toda la vida: double d = (double)i; es llamada conversión estática, y existe otra forma de realizarla, usando la sentencia static_cast:

double d = static_cast<double>(i);

Pero también existe la conversión dinámica, equivalente al as de c#, es intentar convertir una clase base en otra derivada, si el objeto que hay en la clase base no es del tipo de la derivada pone NULL en el objeto destino. Para hacer la conversión dinámica usaremos la sentencia dynamic_cast de la misma forma que usamos la conversión estática.

  • Por ejemplo, tenemos tres clases: A, B y C. B y C heredan de A. Una función te pide como parámetro un objeto A y le enviamos uno B, esto puede hacerse ya que es derivada, pero en un futuro ese objeto A queremos que sea B o C para usar sus funciones más avanzadas, si hacemos una conversión dinámica a B la hará bien, si la hacemos a C colocará un NULL.

Para poder hacer alguna conversión de estas los elementos usados deben de ser punteros o referencias. Además, si lo que hacemos es una conversión dinámica, la clase base ha de tener algún elemento virtual.
Cuando se realiza una conversión dinámica con referencias, no podemos colocar un NULL, pero para indicar el error se lanza la excepción bad_cast.

A* a = new B;
try {
	C& c = dynamic_cast<C&>(*a);
}
catch (bad_cast) {
	cout << "error!" << endl;
}

Sacar información del tipo

  • typeid(obj).name() nos devuelve un string con el nombre de la clase a la que obj pertenece.
  • También podemos saber si un objeto es del tipo tal de usando type_id:
B* b = new B;
if (typeid(b) == typeid(B))

Ten cuidado, aquí el objeto b no es un B, sino un B*, es decir, devuelve false.

Plantillas

  • Las plantillas permiten crear un tipo de datos genérico dentro de una clase o función.
  • Indicando template<typnename|class alias> antes de la función o de la clase, estamos indicando que dentro del cuerpo de esta existe un tipo(int, float…) o clase (es indiferente usar typename o class), los que sean, y para hacer referencia a ellos usamos el alias.

Por ejemplo:

template<class T>
void func (T t)
{
	cout << typeid(t).name() << endl;
}

Si viene un objeto de la clase A, T valdrá 'clase A', por lo que ahora podemos hacer referencia a los objetos de esta. Hasta definirlos haciendo: T i; Estaríamos haciendo A i;
Para llamarla (con, por ejemplo, un integer) sólo tendríamos que hacer: func<int>(43);
Si hiciesemos lo siguiente:

template<class T>
void func (T t, T d)
{
	int i = t + d;
	cout << i << endl;
}

La clase que pasasemos a func debe de tener definido el operador '+' con otro miembro de la misma clase.

  • De la misma forma, podemos también pasar dos parámetros que no se tenga claro que clase es, por ejemplo un int y un double: func(3, 3.7);, de esta forma no sabe si coger los valores 3.0 y 3.7 o 3 y 3. Para definir el tipo genérico, en la llamada haríamos: func<int>(3, 3.7); y los cogería como ints.
  • Para crear un objeto de una clase plantilla debemos indicar el tipo al crearlo, por ejemplo: vector<int> vctr; La clase vector es una plantilla.
  • También podemos definir dos tipos:
template<typename F, typename S>
class Pair
{
public:
   Pair(const F& a, const S& b);
   F get_first() const;
   S get_second() const;
private:
   F first;
   S second;
};

A la hora de crear un objeto de esta clase haríamos:

Pair<int, string> (3, "hola");
  • No debemos olvidar, al declarar los métodos, indicar que es una plantilla de la siguiente forma:
template<typename F, typename S>
Painr<F,S>::Pair(const F& a, const S& b)
{
	first = a;
	second = b;
}
F Pair<F,S>::get_first() const
{
	return first;
}

Podemos añadir argumentos a una plantilla:

template<typename T, int ROWS, int COLUMNS>
class Matrix
{
   . . .
private:
   T data[ROWS][COLUMNS];
};

Pero hemos de tener claro que a la hora de declarar un objeto de esta plantilla, los valores que irán en ROWS o COLUMNS deben de ser constantes.
Lo siguiente es correcto:

Matrix<double, 3, 4> a;

Lo siguiente no:

int i = 3;
Matrix<double, i, 4> a;

Además, al hacer:

 Matrix<int, 3, 4> a;
 Matrix<int, 5, 7> c;
 Matrix<int, 3, 4> d;

Podremos hacer a = d; ya que a y d son <int, 3, 4> en cambio a = c daría error, no son del mismo tipo.
Si queremos pasar un tipo plantilla a una función podemos hacerla de dos formas:

  1. Indicando qué tipo de plantilla es
  2. Pasar una plantilla genérica
// Ejemplo modo 1
void Incrementa (Plantilla<int> t) {...}
Aquí sólo podremos enviar variables creadas así: Plantilla<int> var; y no así, por ejemplo Plantilla<char> var;
// Ejemplo modo 2
template <class T> void Incrementa (Plantilla<T> t{ ... }
Ahora podemos pasar cualquier objeeto Plantilla.

Para declarar el método anterior como amigo haríamos:

friend void Incremena<>(Plantilla<T>);
highlevel/c/poo.1209229886.txt.gz · Última modificación: 2020/05/09 09:24 (editor externo)