sábado, 6 de septiembre de 2014

C++, funciones sobrecargadas, referencias

Funciones sobrecargadasPaso de parámetros por referenciaReferencia como valor retornadoClases con miembros que son punterosArrays de objetos y de punteros a objetosPunteros a miembros de una clasePunteros como argumentos de funciones


Funciones sobrecargadas

Se dice que una función está sobrecargada cuando se declara una función previamente declarada con distinto número y/o tipo de parámetros pero con el mismo nombre. Es un concepto de programación orientada a objetos llamado polimorfismo. (Muchas declaraciones de una misma función).

Cada función suele tener un nombre que la distingue de las demás. Pero se pueden presentar casos en los que varias funciones ejecuten la misma tarea sobre objetos de diferentes tipos, y puede ser interesante que dichas funciones tengan el mismo nombre. Por ejemplo podemos definir una función Mover para cada tipo de movimiento de una pieza distinta de ajedrez y llamar a todas las funciones con el nombre mover pero distinguirlas por el número y/o el tipo de los parámetros sean diferentes.

C++, funciones sobrecargadas, referencias
Otro ejemplo: una función pot que calcule xy siendo x e y enteros o reales podemos hacer.

int pot (int, int);
double pot (double, double);
double pot (int, double);
double pot (double, int);

El compilador buscará la función adecuada en función del tipo de parámetros que le enviamos, en caso de no encontrar una función con los mismos tipos de argumentos, realizaría las conversiones permitidas sobre los parámetros actuales, buscando así una función. No se pueden declarar dos funciones que difieran solamente en el resultado de salida.
En el siguiente código se utilizan una u otra de estas funciones para calcular xy en función de los argumentos pasados en la llamada.

#include <iostream.h>
#include "pot.h"
void main( )
{
double ad = 2.5 , bd = 1.5;
int ai = 3, bi = 2;
cout << pot ( ai, bi ) << endl;
cout << pot ( ad, bd ) << endl;
cout << pot (ai, bd ) << endl;
cout << pot (ad, bi) << endl;
 }


Paso de parámetros por referencia


Cuando pasamos parámetros por valor se hace una copia de los parámetros en sus correspondientes parámetros formales. Esta operación se hace automáticamente cuando se llama a una función, esto impide que se  modifiquen los parámetros actuales.
Pasar parámetros por referencia, significa que no se transfieren los valores sino las direcciones de las variables donde están contenidos esos valores, de este modo los parámetros actuales se verán modificados en el mismo valor que lo hagan sus correspondientes parámetros formales.
Cuando se llama a una función, los argumentos especificados en la llamada son pasados por valor, excepto si se trata de arrays, donde se pasan por referencia, ya que el nombre del array es un puntero a dicho array.
Por valor se pueden traspasar constantes, variables y expresiones, y utilizando la forma de pasar parámetros por referencia solamente se permite transferir las direcciones de variables de cualquier tipo,  arrays y funciones.
Para pasar una variable por referencia, se pueden utilizar las dos formas explicadas a continuación:
1. Pasar la dirección del parámetro actual a su correspondiente parámetro el cual necesariamente tiene que ser un puntero.
void permutar(int *, int *);
void main( )
{
int a = 10, b = 15;
permutar(&a, &b);// se pasan las direcciones de //a y b
cout << "a = "<< a << "b =  " << b << endl;
}
// utilizando punteros
void permutar( int *x, int *y)
{
int z = *x;
*x = *y;
*y = z;
}

2. Definir el parámetro como una referencia al parámetro actual que se quiere pasar por referencia. Para ello, es necesario anteponer el operador & al nombre del parámetro.
#include <iostream.h>
void permutar (int &, int &);
void main( )
{
int a = 10, b = 15;
permutar (a, b);
cout << " a =  " << a << " b= " << b << endl;
}
// Utilizando referencias x e y son referencias a sus
// correspondientes parámetros actuales a y b.
void permutar(int &x, int &y)
{
int z = x;
x = y;
y = z;
}
Los resultados son los mismos que en el programa anterior. En el programa con punteros, cualquier asignación que se haga a *x afecta a la variable a. En el programa con referencias, la referencia x también tiene esa propiedad, pero sin necesitar el operador de indirección *.
Hay que tener en cuenta que las referencias no pueden manipularse igual que los punteros. Con un puntero se puede distinguir el puntero de la variable apuntada utilizando el operador de indirección *. Es decir, x describe el puntero, mientras que *x describe el dato apuntado. Pero con una referencia sólo podemos referirnos al dato. Cualquier operación sobre la propia referencia se realizará sobre el dato referenciado pero nunca sobre la referencia.

Referencia como valor retornado


También es posible definir una función en la que el valor devuelto venga dado por una referencia a ese valor.
#include <iostream.h>
int &fnx( );
int n;

void main( )
{
int c;
fnx( ) = 25; // n = 15
c = fnx( ); // c = 15
fnx( )++ ; //n = 16
fnx( ) = fnx( ) + 3; // n =19
}
//El tipo del resultado es una referencia a un int
int &fnx( )
{
// otro código
return n;
}

En este código, el valor retornado por la función fnx es una referencia inicializada con la variable global n. Lo que sucede es que fnx actúa como un nombre alternativo para n. Por tanto una llamada a la función puede aparecer a la izquierda o a la derecha de un operador de asignación.

Clases con miembros que son punteros


Los operadores new y delete se pueden utilizar en la definición de las funciones miembro de una clase. En el código mostrado debajo, nuestro objetivo es crear un array de enteros con un número cualquiera de elementos. En este caso es inapropiado definir como miembro privado de la clase CVector un array con un número fijo de elementos. Para ello es mejor definir un puntero, pVector, que apunte a un entero para después reservar dinámicamente la cantidad de memoria necesaria para el array.
class CVector
{
public:
CVector( ); //crea un array con un nº de elementos //por defecto
CVector(int ne); // crea un array de ne elementos
CVector(CVector &); // inicialización con un //vector v

CVector(int [ ], int ); // inicialización con un //array a
// otras funciones miembro
private:
int *pVector;  // puntero base
int n_elementos; // numero de elementos
};

Una clase cuyos miembros sean de tipo puntero como es CVector puede tener problemas. En el caso de que en el programa anterior no se defina un constructor copia y que la función main sea como esta:

void main( )
{
CVector vector1(x,7);
fnEscribir(vector1); // escribe 1 2 3 4 5 6 7
// el siguiente bloque define vector2
{
CVector vector2 = vector1;
fnEscribir(vector2); //escribe1 2 3 4 5 6 7
}
// vector2 ha sido destruido
int *pi = new int[5];
for (int i = 0; i < 5; i++) pi[i] = i * 10;
// pi apunta a un array de valores 0 10 20 30 40
fnEscribir(vector1); //escribe 0 10 20 30 40 50
}

En el caso de que en el programa anterior no definamos un constructor copia, el constructor copia definido por defecto será de la forma.

CVector::CVector(CVector &v) // constructor copia
{
n_elementos = v.n_elementos;
pVector = v.pVector;
}


Ahora v.pVector es un puntero. El resultado de esta asignación es que pVector y v.pVector apuntan a la misma dirección de memoria. Por tanto cuando ejecutemos el código:
CVector vector2 = vector1; // llama al constructor copia

El miembro pVector de vector1 y el miembro pVector de vector2 apuntarán al mismo array de enteros. Por lo que cualquier modificación en uno de los objetos afecta a ambos.
Si salimos fuera del ámbito de vector2 se llama al destructor de la clase CVector y se liberan los dos bloques de memoria correspondientes a ese objeto ( su estructura interna y el array de objetos ). De este modo cualquier operación posterior con el objeto vector1 podría dar resultados inesperados, ya que pVector de vector1 apuntaba a la misma localización de memoria y ésta  ha sido liberada. Por otra parte cuando se sale fuera del ámbito de vector1 se invoca de nuevo al destructor, y este intentará liberar de nuevo el bloque de memoria ocupado por el array de enteros, esto provocará resultados inesperados.
En la función main anterior al destruir el vector2, la siguiente instrucción asigna memoria para un array de enteros irreferenciado por pi. Se da la circunstancia de que la memoria asignada ha coincidido con la liberada. Pero en el vector1, además de no tener memoria asignada para su array de enteros, tampoco devuelve los resultados esperados. Esto pone de manifiesto que la memoria se libera, pero los punteros conservan sus valores.
Si nuestra intención no fuera esta, sino crear un nuevo array para cada objeto, el constructor debería ser como el que sigue a continuación.

CVector::CVector(CVector &v) //constructor copia
{
n_elementos = v.n_elementos;
pVector = new int [n_elementos];
for ( int i = 0; i<n_elementos; i++)
pVector[i] = v.pVector[i];
}

Arrays de objetos y de punteros a objetos

Del mismo modo que se crea a un array de elementos de cualquier tipo es posible crear un array de objetos. Para ello definimos el array Clientes de la clase CCuentasClientes de la forma siguiente:
CCuentasClientes Clientes[150]; // array de 150 elementos
En el caso de que la clase no tenga definido un constructor, el compilador generará uno por defecto y asignará a los datos miembro de cada objeto del array, un valor indefinido si el array es local, o un valor cero si el array es global.
En el caso de que la clase tenga definido un constructor, que es lo óptimo, este no debe tener argumentos o si los tiene que sean los de por defecto; en caso  contrario, una declaración como CCuentasClientes Clientes[150] dará lugar a un error.
class CCuentasClientees
{
public:
// Constructor
CCuentasClientees(char *= 0, float = 0);
// Otras funciones miembro
private:
char *Cliente; //nombre cliente
float Importe; // importe total
static float TpcIRPF; // %IRPF
};
//...
CCuentasClientees Clientes[150]; // array de 150 elementos

Al crear el array Clientes, para cada elemento del array se llama al constructor CCuentasClientes y se le asignan los valores '\0' y 0 a los datos miembro Cliente e Importe, respectivamente de cada objeto.
Llamando explícitamente al constructor con los argumentos se puede inicializar cada elemento del array en el caso de que no provea bastantes inicializadores para todo el array, para el resto de los elementos se llamará al constructor, que utilizará los valores por defecto.
Si se desea inicializar cada elemento del array llamando explícitamente al constructor y la clase tiene un constructor con un sólo argumento, se puede especificar sólo dicho argumento.
Por tanto, para declarar un array de objetos de una clase, es necesario que dicha  clase tenga un constructor que se pueda llamar sin argumentos.

Punteros a miembros de una clase

Para poder asignar la dirección de una función miembro de una clase a un puntero a una función, el puntero tiene que ser de la misma clase que la función miembro. Lo mismo diríamos para un dato miembro de una clase. Esto debe ser así porque en C++ a un puntero que apunta a una función no es posible  asignarle la dirección de una función miembro de una clase, aunque coincidan en número y en tipo sus listas de parámetros, además del tipo retornado.
Por tanto para poder obtener un puntero a un miembro debemos aplicar el operador dirección_de & al nombre completo del miembro, es decir.
&nombre_clase::nombre_miembro
La sintaxis para definir una variable de tipo puntero a un miembro de la clase CEjemplo se obtiene de acuerdo con la siguiente sintaxis: CEjemplo::*

class CCalificacion
{
public:
//...
void SetCalificacion(float n){Calificacion = n;}
private:
float Calificacion;
};
//...
// Definir el puntero pmCalificacion
float CCalificacion::*pmCalificacion = &CCalificacion::Calificacion // puntero a Calificacion
//...
El identificador pmCalificacion es un puntero al dato miembro Calificacion de la clase CCalificacion.
Al ser Calificacion un miembro privado, sólo se tiene acceso al mismo desde el interior de una función miembro.
// Definir el tipo derivado pmfn
typedef void (CCalificacion::*pmfn){float};
// Definir el puntero pmfnSetCalificacion

pmfnSetCalificacion = &CCalificacion::SetCalificacion; // puntero SetCalificacion

El identificador pmfnSetCalificacion es un puntero a la función miembro SetCalificacion de la clase CCalificacion.
Para acceder a un miembro de una clase C referenciado por un puntero, disponemos de los operadores binarios:  .* y ->*.
El operador .* liga su segundo operando a su primer operando, el cual debe ser un puntero a un miembro, el cual que debe ser un objeto. En este caso tenemos:
                            objeto.*puntero_a_miembro

Es decir.

                   CCalificacion Alumno;
float n = Alumno.*pmCalificacion;
Si el primer operando es un puntero a un objeto, entonces utilizaremos el operador ->*. La sintaxis será:
puntero_a_objeto ->* puntero_a_miembro
De este modo temenos.

CCalificacion *pAlumno;
//...
(pAlumno ->*pmfnSetCalificacion)(n);

La precedencia de los paréntesis ( ) es mayor que la de  .* y ->*, por eso son necesarios.
No es posible definir un puntero a un miembro declarado como static, ya que este pertenece a la clase misma y no a un objeto en particular de la clase.

Punteros como argumentos de funciones

Un buen motivo para pasar un puntero a una función en vez de pasarle una copia del valor de la variable es ahorrar tiempo y espacio. Al pasar una copia de una variable grande, como un array o una estructura de datos grande, requiere una gran cantidad de espacio en la pila y de tiempo. Se puede mejorar el rendimiento de un programa pasando la dirección de esa variable y permitir que la función acceda a sus elementos a través de un puntero.
Otro motivo para usar punteros como parámetros es para que una función devuelva más de un sólo valor. Pues una función sólo puede devolver un valor por medio de su sentencia return. Pero si se utilizan parámetros de tipo puntero, se puede dar a la función acceso (y de este modo poder cambiar) los valores de una variable. Para ello, la función llamada tendrá que parar argumentos como una dirección en lugar de como un valor.

No hay comentarios:

Publicar un comentario