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.
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