POO en C++

| 2013-04-27 | No hay comentarios »

¿QUE ES LA PROGRAMACIÓN ORIENTADA A OBJETOS?

En la programación estructurada todos los programas tienen las estructuras secuencial, repetitiva o condicional. También se utilizan los TAD (Tipos Abstractos de Datos) para por ejemplo una pila o un árbol.

 

typdef struct{

int x,y;

int color;

}punto;

struct punto a,b;

 

luego se implementan las funciones de este TAD (pila_vacia, pila_llena).

En C++ se definen los TAD y las funciones o procedimientos y datos dentro de un mismo conjunto llamado class (clase).En el ejemplo, el typedef struct punto sería el equivalente en C de la class de C++ y las variables a y b de los objetos en C++

 

CLASES (CLASS)

Antes de poder definir un objeto debemos definir la clase a la que pertenece (igual que en el ejemplo anterior debemos definir antes la estructura punto para luego poder definir las variables a y b). La forma general de describir una clase sería más o menos:

 

class nombre_clase {

datos y funciones privados;

public:

datos y funciones públicos;

función constructora;

función destructora;

};

 

Los datos y funciones privados son los que no se puede acceder a ellas desde las funciones que son miembros de la clase (que están definidas en ella), se comportan igual que las variables definidas localmente en una función en C normal.

En cambio, los datos y las funciones públicas son accesibles por todas las funciones del programa (igual que si estuviesen definidas las variables globalmente en C normal).

Por defecto, en una clase se define todos los datos y funciones privados, a menos de que le especifiquemos las que son publicas con la instrucción public.

Para saber si una función debe ser definida publica o privada, debemos ver si el resto del programa necesita “conocer como funciona” dicha función. Si la respuesta es “si” entonces la función debe ser pública, en caso contrario debe ser privada.

Veamos un ejemplo:

 

-Vamos a crear la clase CRONOMETRO:

 

class CRONOMETRO{

            struct time tm; // Variable que coge la hora del sistema

            int andando;

            void dif_tm(time tr, time ts, time *dif);

            public:

            void Arranca(void);

            void Muestra(void);

            void Para(void);

            };

CRONOMETRO p; (p será un objeto de la clase cronometro);

La función dif_tm es privada porque al resto del programa no le interesa acceder a ella, en cambio las funciones Arranca, Muestra y Para si pueden acceder a ella porque necesitan saber la diferencia entre dos tiempos (sobre todo la función Muestra, que es la que muestra dicha diferencia). Para llamar a una función de la clase desde una parte del programa que no pertenece a dicha clase, se utiliza:

 

nombre_objeto.funcion;

 

FORMATOS DE CIN Y COUT

En C++ se empieza a utilizar el concepto de flujos de E/S y no son mas que una clase (son dos clases: CIN y COUT). Las salidas con formatos de COUT:

 

cout <<formato<<variable; // equivale a printf(«formato»,variable);

dec = %d (enteros)

hex = hexadecimal

oct = octal

endl = «\n» (en CIN y COUT se puede seguir poniendo «\n»)

ends = ‘\0’ (inserta el fin de cadena, un NULL, no el carácter cero)

setw (num) fija la anchura de un campo en n bytes;

cout<<setw(6)<<variable

setprecision(n) = fija el numero de decimales que queremos (n).

todas estas cosas están en la librería <iomanip.h>

 

*Sacar un numero en tres formatos: hexadecimal, octal y decimal:

#include <iostream.h>

#include <iomanip.h>

int num;

cin<<num;

cout<<» el numero decimal es: «<<dec<<num<<endl; (endl=»\n»)

cout<<«el numero octal es: «<<dec<<oct<<endl; (endl=»\n»)

cout<<» el numero decimal es: «<<dec<<hex<<endl; (endl=»\n»)

 

CONSTRUCTOR Y DESTRUCTOR

Lo mas normal es que un objeto tenga que reinicializarse, para hacerlo usamos el constructor (tiene el mismo nombre de la clase); no es necesario que se creen (en este caso el compilador utiliza el constructor explicito que inicializa los dato en cero o NULL, según sea el tipo de dato); cuando se encuentra una declaración de un objeto, el compilador llama al constructor. No se debe llamar explícitamente al constructor. El formato de la función constructora es:

 

nombre_clase (parámetros);

 

Podemos definir la función constructora para que inicialice ciertas variables, pedir memoria dinámica, etc. El destructor no se debe llamar explícitamente, el compilador lo llama antes de terminar el programa (cuando llega a la última llave del main). Si no existe el destructor, el compilador llama a uno por defecto. Su formato es:

 

~nombre_clase (parámetros);

 

Si hemos reservado memoria dinámica en la función constructora, podemos aprovechar y liberarla en la destructora.

 

OPERADOR ::

Sirve para especificar a que clase pertenece la función que estamos tratando, es decir:

 

class fichero{

            char *fich1,*fich2;

            FILE *entrada,*salida; datos y prototipos de funciones

            void abrir_ficheros(void); privados

            char encriptar_desencriptar(char);

            public:

            void ordenar(void);

            fichero(void); prototipos de las funciones publicas

            ~fichero(void)

            }

 

fichero a; // Definimos un objeto a de la clase fichero

void fichero::operar(void) // Tenemos que especificar a que clase pertenece

            {

            ….

            ….

            }

 

fichero::fichero(void) // Declaración de la función constructora

            {

            … // Aquí va lo que hace la función constructora: inicializar variables

            … // (solo si es a un valor <> 0, reservar memoria, etc

            }

 

fichero::~fichero(void) // Declaración de la función destructora

            {

            …

            …

            }

 

Para llamar a las funciones es igual que en C normal, no hace falta poner “nombre_clase:: “, pero si en las definiciones de las funciones que están fuera de la clase.

 

MANEJO DE FICHEROS

El manejo de ficheros en C++ es diferente que en C normal. Igual que C normal solo hay un tipo de flujo (definido como FILE *), en C++ hay tres tipo de flujo:

 

ifstream fich_ent; // Definimos fich_ent como flujo de entrada

ofstream fich_sal; // Definimos fich_sal como flujo de salida

fstream fich_entrada_salida; // Definimos fich_entrada_salida como flujo de// entrada y salida.

 

Igual que en C normal una vez definido el flujo (con la instrucción FILE *), tenemos varias funciones para manejar archivos:

 

void open(char *nombrearchivo, int modo, int acceso);

 

Esta función abre un fichero. Hay que pasarle unos parámetros:

 

nombrearchivo: nombre del archivo a abrir.

modo: dice como se abre el archivo, puede ser uno o varios de estos:

 

ios::app todo lo que escribimos en el archivo se añade al final (pero hay que poner el ptro al final del archivo con fseek)

ios::ate añadiendo esto a ios::app ya nos pone automáticamente el ptro al final del archivo

ios::binary abre el fichero para operaciones binarias, pero no convierte el valor leído a carácter (si lee el numero 65 no nos devuelve ‘A’ sino 65)

ios::in abre el fichero para operaciones de entrada (es decir para leer datos del fichero). Solo se puede utilizar con un flujo de entrada (ifstream)

ios::out abre el fichero para operaciones de escritura (para escribir datos en el fichero). Solo se puede utilizar con un flujo de salida (ofstream)

ios::nocreate hace que se produzca un fallo en la función open() si el archivo especificado no existe

ios::noreplace igual que la anterior provoca un fallo en open() si el archivo ya existía

ios::trunc destruye el contenido del archivo, si existía previamente, y deja la longitud del archivo en cero

 

Para unir varios modos se utiliza la “o lógica” (||).

acceso: indica de que tipo es el archivo al cual vamos a acceder.

 

Hay cinco tipos de acceso::

0 Archivo normal

1 Archivo de solo lectura

2 Archivo oculto

4 Archivo del sistema

8 Bit de archivo activo (solo se usa en entornos multitarea para saber el archivo activo)

Todo esto se ve mejor con un ejemplo:

 

#include <iostream.h>

#include <fstream.h>

ifstream entrada; // Definimos un flujo de entrada

ofstream salida; //Definimos un flujo de salida

entrada.open(“pepe.dat”, ios::in, 0); // Abrimos el fichero en modo lectura

salida.open(“pepito.dat”, ios::out,0); // Abrimos el fichero en modo escritura

entrada.close(); // Cerramos pepe.dat

salida.close(); //Cerramos pepito.dat

 

Todas estas definiciones y funciones están definidas en <fstream.h>

 

CONSTRUCTORES PARAMETRIZADOS

Son los constructores de siempre, pero a los que le pasamos un parámetro. Pueden ser útiles en algunos casos (por ejemplo para reservar la memoria justa para una cola, en vez de definir un numero fijo de elementos). Vamos a ver como se define una función constructora parametrizada:

 

class punto{

            int *coordenadas // Aquí guardamos las coordenadas del punto

            public:

            punto(int); // Constructor parametrizado al que le pasamos el

            // numero de coordenadas del punto (2 o 3)

            ~punto(void); // Función destructora

            };

 

punto::punto(int num_coord)

            {

            coordenadas=new int [num_coord]; // Reservamos memoria para num_coord

            } // coordenadas (que serán 2 o 3)

 

punto::~punto(void)

            {

            delete (coordenadas); // Liberamos la memoria que reservamos en la función

            } // constructora

 

Para llamar a las funciones constructoras parametrizadas, hay dos formas:

 

clase nombre_objeto=clase (parámetros); punto inicio=punto (3);

clase nombre_objeto (parámetros); punto inicio (3);

 

La forma más cómoda de hacer la llamada, es la segunda. En el listado anterior, podemos definir un objeto (un punto) de dos o tres coordenadas.

Con los constructores parametrizados, le decimos de que tipo va a ser el punto (bidimensional o tridimensional), de otra manera tendríamos que definir el punto tridimensional, ahorrando así 1 byte. Pues, ahorrar 1 byte de memoria!. Pero si con nuestros grandes conocimientos de C hacemos un programa que calcule figuras, de unos 20.000 puntos cada una, si la figura es tridimensional, no ahorraremos nada; pero si es bidimensional ahorraremos 20.000 bytes (19.5 K). No esta mal, verdad?.

 

SOBRECARGA DE OPERADORES

Esto se utiliza para hacer que un operador (+, -, *, /, etc.) haga mas cosas que sumar números enteros, decimales, etc. Podemos hacer que nos sume matrices, vectores, etc. Si definimos el operador + para que sume matrices, no dejará de hacer lo que hacia antes (sumar enteros, decimales, etc.).

La forma para definir la función sobrecargada es:

 

tipo_a_devolver nombre_clase::operator(parámetros);

 

Vamos a ver un ejemplo para sumar dos vectores:

class vector{

            int x, y, z;

            public:

            vector vector::operator+(vector);

            };

 

vector vector::operator+(vector p)

            {

            x=p.x+x;

            y=p.y+y;

            z=p.z+z;

            return(*this);

            }

 

void main(void)

            {

            vector a, b, c;

            c=a+b;

            }

 

Este ejemplo tiene mas “cosillas” que los anteriores. Cuando en el main hacemos la llamada a+b, el compilador lo transforma en a.operator+(b), cuando vamos a la función, el vector p es una copia del b (o sea p.x=b.x, p.y=b.y, p.z=b.z) y las coordenadas x, y, z son las del vector a, que es el objeto con el cual llamamos a la función. El objeto por defecto es el que queda a la izquierda del operador y el objeto que normalmente se suele pasar es el de la derecha. Lo de this es muy sencillo, this es un puntero al objeto por defecto, es decir el objeto que llama a la función. En el ejemplo anterior, al llamar a la función +, el objeto que llama a la función es a, por lo que this apuntará a “a”. Como la función devuelve un vector, y this es un puntero a un objeto tipo vector tendremos que devolver el contenido de this (*this=a). Si la llamada a la función es del tipo d=a+b+c, el resultado de la suma de los vectores b y c, quedaría guardado en b, pudiendo afectar a cálculos posteriores que se hagan con este vector.

Para evitar esto, habría que declarar un vector auxiliar local a la función, quedando de la forma:

 

vector vector::operator+(vector p)

            {

            vector h;

            h.x=p.x+x;

            h.y=p.y+y;

            h.z=p.z+z;

            return(h);

            }

 

FUNCIONES AMIGAS

Son funciones que no pertenecen a la clase (están definidas en la parte publica de la clase), pero que tienen acceso a la parte privada de la clase. La forma de definir una función amiga es:

 

friend tipo_objeto_devuelve nombre_funcion (parámetros)

 

La utilidad de estas funciones, son por ejemplo, en el caso de que queramos sumar un entero con un vector, siendo la suma de la forma entero+vector. Si no utilizamos las funciones amigas, si sumamos 1 al vector a (1+a), el compilador la transforma en 1.operator+(a), pero esto no funcionaría porque 1 es un entero y no un objeto. Aparte de esto, la función debe poder acceder a la parte privada de la clase vector, que son las componentes x, y, z de cada vector (si no, no podríamos sumar). Con todo esto, la declaración de las funciones amigas seria:

 

class vector{

            int x,y,z;

            public:

            friend vector operator+(int, vector);

            };

 

vector operator+(int num, vector p)

            {

            vector h;

            h.x=p.x+num;

            h.y=p.y+num;

            h.z=p.z+num;

            return(h);

            }

 

Como no pertenece a la clase al hacer la llamada en el main, se llama como una función normal en C, ni en la definición de la función poner vector:: .

CONSTRUCTORES DE COPIA

Cuando inicializamos un vector utilizando otro y ese objeto trabaja con punteros (asigna memoria dinámicamente, abre ficheros, etc.) el compilador lo que hace es copiar bit a bit el objeto. Si estamos utilizando punteros nos encontramos con que hay dos punteros que apuntan a la misma zona de memoria, y esto puede dar problemas (o sea puede el segundo objeto sobrescribir el contenido del puntero del primer objeto). Como siempre, viene el ejemplo:

 

class libro{

            char *titulo;

            int año;

            public:

            void Introduce_libro(libro);

            libro(void);

            };

 

void libro::libro(void)

            {

            titulo=new char [100];

            }

 

void main(void)

            {

            ejemplo libro1;

            ejemplo libro2=libro1;

            }

 

En este ejemplo nos encontramos con que libro1.titulo y libro2.titulo apuntan a la misma zona de memoria, con lo que al pedir el titulo del segundo libro, lo vamos a guardar en el mismo sitio que el primero, perdiendo el titulo del primer libro. Para evitar esto utilizamos los constructores de copia, la forma de definirlos es:

 

nombre_clase (const nombre_clase &objeto); libro (const libro &p);

 

añadiendo esto el código quedaría igual en a parte publica de la clase, pero habría que definir la función constructora de copia:

 

void libro::libro(const libro &p)

            {

            titulo=new char [100];

            año=p.año;

            }

 

Con esta solución el titulo de libro1.titulo no comparte el mismo sitio de memoria que libro2.titulo. Las llamadas a este constructor las hace ya el compilador cuando encuentra una instrucción del tipo libro libro2=libro1.

 

SOBRECARGA DE INSERTORES Y EXTRACTORES

 

Cuando definimos una clase (por ejemplo un vector de tres componentes) y queremos mostrar las componentes de cada vector, tenemos que definir una función de lectura y otra de escritura para leer y mostrar datos de este tipo. Pero ahora se acabo el definirlas !. Para no tener que definirlas

usamos la sobrecarga de insertores y extractores. Los extractores son los operadores >> y los insertores son los << . La forma de definir la sobrecarga de insertor es:

 

friend ostream &operator(ostream &stream, tipo_clase objeto)

 

Se debe definir como función amiga porque si no, no podría acceder a las partes privadas de la clase (es decir a los componentes del vector) y no podría mostrar ni guardar dichas componentes. Las dos funciones devuelve un flujo con los datos para que pueda ser utilizado por cin o cout.

 

ostream & operator<<(ostream &stream, tipo_clase objeto)

            {

            // Aquí ponemos lo queramos que haga

            return stream;

            }

 

La forma del prototipo del extractor es similar:

 

friend istream &operator>>(istream &stream, tipo_clase &objeto)

 

y la definición de la función también:

 

istream &operator>>(istream &stream, tipo_clase &objeto)

            {

            // Aquí va el código

            return stream;

            }

 

En esta función pasamos objeto por referencia (con el símbolo &) y esto es porque como vamos a guardar los datos en dicho objeto. Mientras ejecutemos la función el objeto va a contener los valores que le metemos nosotros, pero al finalizar como es una copia del objeto original, esta se destruiría y se perderían los valores. Todo esto, aunque parezca mentira tiene una utilidad. Sirve para que cuando nosotros ponemos en el main cout<<b siendo b un vector de tres componentes, nos muestre las tres componentes del vector (igual que hace cuando le ponemos cout << a siendo a un entero). Las funciones sobrecargadas de inserción y extracción no pueden ser miembros de la clase, pueden ser como mucho funciones amigas (para poder acceder a los datos privadas de dicha clase) o funciones independientes.

 

SOBRECARGA DE OPERADORES MONARIOS

 

Son los que actúan sobre un solo dato (no como por ejemplo la suma, que se necesitan dos tipos de datos para sumar, o sea son ++, —, etc.). Los operadores monarios, al igual que los binarios se pueden sobrecargar. Hay dos formas de sobrecargar un operador, con una función miembro de la clase y como una función amiga.

Para sobrecargar un operador con una función miembro de la clase, basta con ir al apartado de SOBRECARGA DE OPERADORES y leerlo. Para definirla con una función amiga, hacemos lo mismo (vamos al apartado de las FUNCIONES AMIGAS). Hay que recordar que si la función sobrecargada es definida como miembro, y es un operador binario, hay que pasarle parámetros; si es un operador monario no se le pasa ningún parámetro. En caso de ser una función amiga, y ser un operador binario, hay que pasarle dos parámetros; si es operador monario, se le pasa uno. Veamos un cuadro explicativo:

o1

 

Una ultima cosa, debemos, siempre que sea posible, usar las funciones amigas, porque lo que el C++ trata de hacer es la encapsulación (tener juntas en la parte privada de la clase los datos y las funciones que manejan, siendo estos inaccesibles desde fuera de la clase).

 

SOBRECARGA DE [ ]

Este operador también se puede sobrecargar (los únicos que no se pueden sobrecargar son ., ::, ?, sizeof).Como ya sabemos cuando escribimos:

 

            x [ i ] el compilador lo convierte en x.operator[ ](i)

 

Hay una serie de restricciones que debemos de tener en cuenta:

 

-No se puede construir operadores propios, solo se pueden sobrecargar los operadores definidos en C

-No se puede cambiar la preferencia o asociatividad de los operadores

-No se puede cambiar un operador binario para funcionar con un único objeto

 

FUNCIONES DE CONVERSIÓN

Si queremos asignar una matriz a un entero, podemos hacer dos cosas: sobrecargar la función =, o declarar una función de conversión. EL problema de definir la función sobrecargada =, es que si en el main hay una instrucción del tipo cout<<(int)b no funcionaria, porque no sabría como asignar una matriz a un entero, debido a que esto lo tenemos definido en la función sobrecargada =. La forma de definir una función de conversión sería:

 

            operator tipo_dato();

 

Todo esto se ve mejor con un ejemplo:

 

class vector{

            int componentes[MAX];

            int orden;

            public:

            …

            …

            operator float();

            };

 

vector::operator float()

            {

            int i;

            float f;

            for(i=0;i<orden;i++)

            f=f+componentes[i];

            return (f/orden);

            }

 

En este caso al hacer cout<<(float)a nos devolvería la media aritmética de la suma de los componentes del vector. Las funciones de conversión deben ser funciones miembro de la clase.

 

HERENCIA

La herencia permite la reutilización de código ya escrito. No necesitamos el código fuente, a partir del obj podemos hacer una clase derivada y completarla con mas funciones. Es algo parecido al árbol de directorios del DOS.

 

Clase base (clase de la que partimos)

^

Clase derivada (clase que heredan las características de la clase base)

^

Clase derivada

 

La sintaxis es:

 

class base {

            …

            …

            };

 

class derivada:base {

            …

            …

            };

 

Lo que hereda una clase derivada de una clase base son funciones y datos (miembro) públicos (privados no).Desde la clase derivada se tiene acceso a los datos y funciones publicas de la base. La clase base no conoce a la clase derivada.

 

DATOS PROTEGIDOS (Protected): los datos o funciones que estén definidos así, son accesibles desde las funciones de las clases derivadas de ella

 

class base{ class derivada:base{ void main(void)

            public: … {

            int a; … base y;

            protected: public: derivada z;

            int b; void suma(void);

            private: }; y.a=3;

            int c;

            }; }

 

Desde la clase derivada podremos acceder a “a”, y también a b, pero no a c, porque es privado. Los públicos son accesibles desde cualquier sitio, pero los protegidos solo desde las funciones de la clase derivadas

 

TIPOS DE ACCESO A LA CLASE BASE:

 

class base{

            …

            …

            }; public

 

class derivada: private base{};

            protected

 

PUBLIC : los miembros públicos de la clase base son miembros públicos de la clase derivada los miembros protegidos de la clase base son protegidos de la clase derivada

PRIVATE : todos los miembros públicos y protegidos de la clase base son miembros privados de la clase derivada

PROTECTED : los miembros públicos y protegidos de la clase base son protegidos de la clase derivada

En los tres casos los miembros privados siguen siéndolo, son inaccesibles.

o2

Independientemente de la forma de acceso a la clase, no se heredan:

• Constructores y destructores (cada clase tendrá los propios)

• Funciones amigas

• Sobrecarga del operador =

 

Un objeto de la clase derivada se considera un objeto también de la clase base, pero no al revés. Cuando hay clases derivadas el orden de ejecución de constructores: constructor base, constructores miembro, constructor de propio miembro

Dado clase B derivada de A : constructor de la clase A, constructores de los datos miembros de la clase B, constructor de la clase B

 

class C{ };

 

class A{

            A(void);

            };

 

class B:A{

            C z;

            public:

            B(void)

            };

 

El orden de llamada a los constructores es: constructor de A, constructor de C, constructor de B.

 

#include <iostream.h>

class A{

            public:

            A(){cout<<«Constructor A\n»;}

            };

 

class B{

            public:

            B(){cout<<«Constructor B\n»;}

            };

 

class C{

            public:

            C(){cout<<«Constructor C\n»;}

            };

 

class D:A{

            public:

            B c;

            C d;

            D(){cout<<«Constructor D\n»;}

            };

 

void main(void)

            {

            D d;

            }

 

En este otro caso el orden sería: constructor de A, constructor de B, constructor de C y constructor de D.

 

class padre{

            int valor;

            public:

            padre(){valor=0;}

            padre(int v){valor=v;}

            };

 

class hija:public padre{

            int total;

            public:

            hija(int t){total=t;}

            };

 

void main(void)

            {

            hija a;

            }

 

Da un error, por lo que hay que cambiar en class hija::public cambiar la línea del constructor con parámetros por hija(int t=0):padre(t){total=t}

Esto es porque los constructores no se derivan. Desde la clase derivada hija no tiene acceso al constructor de la clase base (padre). La forma de llamar a un constructor especifico es ponérselo en la llamada del constructor de la clase derivada. Hay que recordar que las llamadas a los constructores se hacen de la siguiente forma: primero al de la clase base, luego a los de las derivadas; mientras que las llamadas a los destructores ocurre justamente lo contrario: primero el de las clases derivadas y luego el de la clase base.

 

LIGADURA DINÁMICA Y ESTÁTICA

Es un concepto que C se refiere a la asociación que hay entre la llamada a una función y el cuerpo de la función que finalmente se ejecuta. En tiempo de compilación se sabe que cuando llama a una función, va a ejecutar ese cuerpo.

En C++ como puede haber mas de una función con el mismo nombre (sobrecarga), se produce lo que se llama la ligadura dinámica, lo que hace es que el compilador elige en tiempo de ejecución que función se va a ejecutar finalmente. El compilador sabe a que función debe llamar viendo a que puntero esta referenciada. Ejemplo de ligadura en C normal:

 

switch(a)

            {

            case 1: sumar(); break

            case 2: restar(); break

            case 3: salir();

            }

 

void sumar(void);

 

void restar(void);

 

void salir(void);

 

-Dinámica: se dará en C++, implementándose a través de las funciones virtuales no genera un código de un salto incondicional a una función concreta del programa, sino que deja esa decisión para cuando se ejecute el programa. Será en tiempo de ejecución cuando sepa a que función debe llamar (debido a que puede haber la sobrecarga de funciones). Esto lo sabe mediante el puntero this, si apunta a un objeto de clase A, llamara a la función de la clase A

Hasta ahora la sobrecarga es un ejemplo de ligadura estática (hacíamos la llamada como a.sumar(), el compilador sabe a que función estamos llamando). Pero a partir de ahora vamos a usar punteros a objetos. Vamos a ver un ejemplo con clases derivadas y clases bases

 

class A

            {

            void sumar(void);

            }

 

class B: public A

            {

            void sumar(void);

            }

 

A *a;

B b;

a=&b; // Si se puede hacer, se puede asignar un objeto derivado a uno base. (1)

b=&a; // Incorrecto, no se puede asignar un objeto base a un objeto derivado

 

Cuando hacemos esto (1), suprimimos todo lo de la clase derivada y nos quedamos con lo que tenga la clase base.

 

A c;

A *a;

B b;

a=&b;

a->sumar(); //Llama a la función de la clase derivada

a=&c;

c->sumar(); // Llama a la función de la clase base;

 

FUNCIONES VIRTUALES

Veamos un ejemplo y después pasemos a explicarlas:

#include <iostream.h>

#include <iomanip.h>

class A{

            public:

            A(void){};

            virtual void sumar(void);

            };

 

class B:public A{

            public:

            B(void):A(){};

            void sumar(void);

            };

 

void A::sumar(void)

            {

            cout<<endl<<«Es la suma de la clase A.»;

            }

 

void B::sumar(void)

            {

            cout<<endl<<«Es la suma de la clase B.»;

            }

 

void main(void)

            {

            A c;

            A *a;

            B b;

            a=&b;

            a->sumar(); //Llama a la función de la clase derivada

            a=&c;

            a->sumar(); // Llama a la función de la clase base;

}

 

El resultado de este programa seria:

Esta es la suma de la clase A

Esta es la suma de la clase A

Este programa lo que hace es declarar una clase base (A) y una derivada (B), y un puntero a un objeto de la clase derivada (*a). Como ya vimos en la parte de ligadura, a partir de ahora para llamar a las funciones, ya no utilizaremos el formato de ligadura estática (nombre_objeto.funcion(), por ejemplo a.sumar()); sino que declararemos un puntero de la clase base, e iremos haciendo que dicho puntero apunte a las direcciones de las clases derivadas y llamando a las funciones así:

 

ptro->función() (por ejemplo a->sumar()).

 

Como ya vimos el resultado del programa anterior era la llamada a la función sumar de la clase A dos veces. El problema es que si ahora no sabe distinguir entre dos funciones sobrecargadas ¿para que sirve todo esto?. La solución está en declarar dichas funciones sobrecargadas como virtuales. Como en el programa anterior no están definidas como virtuales, no comprueba los tipos, pero añadiendo virtual a la función sumar de la clase base, lo hace.

Resultado con virtual:

 

Esta es la suma de la clase B

Esta es la suma de la clase A

 

Con solo definir un ptro de tipo base y unos objetos de clase derivada, en función de quien apunte dicho ptro llamara a unas funciones u otras.

La sintaxis de las funciones virtuales es la siguiente:

 

-Se decide cuales son las funciones que van a tener un propósito parecido o el mismo en todas las clases.

-Se declara dicha función como virtual solamente en la clase base, de tal forma que al definir una función como virtual en la clase base, nos permite redefinir dicha función en las clases derivadas.

Una función que se declare virtual en la base, si esa función no es redefinida en alguna de las clases derivadas, da error de compilación (solo si se trata de funciones virtuales puras). La solución si no redefinimos, es declararla como virtual en la clase derivada, para así evitar el error de compilación.

 

El uso de las funciones virtuales tiene varias restricciones:

-El prototipo de la función virtual debe coincidir tanto en la clase base como en la redefinición en la derivada (tanto en el numero y el tipo de parámetros) como en lo que devuelve

-Una función virtual debe ser miembro de la clase en la cual esta definida, pero puede ser amiga de otras clases.

Los constructores no pueden ser virtuales, ni necesitan serlo, pero los destructores pueden y deben ser virtuales. Hay que recordar que si una clase no sobrescribe una función virtual, el compilador usa la primera redefinición de la clase siguiendo el orden inverso de la derivación

 

#include <iostream.h>

#include <iomanip.h>

            class base{

            public:

            base(){cout<<endl<<«base constructora de A»;}

            /*virtual*/ ~base(){cout<<endl<<«base destructora de A»;}

            // Si no ponemos el virtual, no hace comprobación de tipos.

            };

 

class der:public base{

            public:

            der(){cout<<endl<<«constructor derivado de B»;}

 

 

            ~der(){cout<<endl<<«base destructora de B»;}

            };

 

void main(void)

            {

            base *p=new der;

            delete p; // El uso de delete con un objeto, llama al destructor del objeto.

            }

 

CLASES ABSTRACTAS

Son las herramientas mas potentes para representar el mundo real, podremos representar cosas como casas, figuras geométricas. La forma de hacerla, no se diferencian en las normales solo en que debe tener, al menos una función virtual pura.

 

VIRTUAL PURA

Son funciones especiales, son similares a las virtuales. Supongamos que definimos una clase figura_3D; y dos clases derivadas: esfera y cubo. Los datos de la clase base son tres puntos (numero mínimo de coordenadas para tener una figura en 3D sería un punto); y una función hallar_volumen. ¿Pero como podemos definir la función hallar_volumen, si para cada figura en 3D cambia la formula de hallarlo (p.e. cubo es el lado al cubo (l3) y para la esfera es 4/3 ¶ r3)?.La solución es declararlo como una función virtual pura, obligando a que en cada clase derivada tenga que ser redefinida (con la formula de hallar el volumen correspondiente). Las funciones virtuales puras lo que haría seria como guardar sitio, pero teniendo en cuenta que este sitio debe ser llenado en una clase derivada. Sintácticamente es:

 

virtual void nada(void)=0; // es diferente que poner {}

 

Las funciones virtuales puras al definirlas en la base, se definen nula (=0), no damos ninguna especificación hasta que las redefinamos en las clases derivadas. La diferencia mas drástica entre las puras y las normales es que no se pueden llamar.

 

class A

            {

            virtual void nada(void)=0;

            …

            }

 

void main(void)

            {

            A a;

            a.nada(); // Da un error de compilación, no se puede llamar.

            }

 

Si en una derivada no queremos implementar en una clase derivada, la declaramos otra vez como derivada pura, pero debemos declararla en otras clases derivadas de esta ultima

 

CLASES ABSTRACTAS:

Estas clases no se deben utilizar para crear objetos, están hechas para servir de base, lo que hacíamos hasta ahora (ser bases de ptros. A *a;)

Cuando tengamos que hacer un programa, debemos hacer:

-Clase abstracta de figura geométrica (define características comunes a todas las figuras geométricas)

-Definir clases derivadas concretas (punto, línea, circulo)

Acerca del autor: Rodrigo Paszniuk

Ingeniero Informático, amante de la tecnología, la música, el ciclismo y aprender cosas nuevas.




SEGUÍNOS EN FACEBOOK


GITHUB