Disponible la nueva versión "donationware" 7.3 de OrganiZATOR
Descubre un nuevo concepto en el manejo de la información.
La mejor ayuda para sobrevivir en la moderna jungla de datos la tienes aquí.

Curso C++

[Home]  [Inicio]  [Índice]


4.4.5  Parámetros y argumentos

§1  Sinopsis

Las palabras parámetro y argumento, aunque de significado similar, tiene distintas connotaciones semánticas: Se denominan parámetros los tipos declarados en el prototipo 4.4.1 (que deben corresponder con los declarados en la definición 4.4.2). Cuando se realiza una llamada a la función, los "valores" pasados se denominan argumentos. A veces se utilizan también las expresiones argumentos formales, para los parámetros y argumentos actuales para los valores pasados.

Parámetros (en prototipo o definición)    argumentos formales

Valores pasados (en tiempo de ejecución)    argumentos actuales


§2  La sintaxis utilizada para la declaración de la lista de parámetros formales es similar a la utilizada en la declaración de cualquier identificador.  A continuación se exponen varios ejemplos:

int func(void) {...}                  // sin parámetros
inf func() {...}                      // ídem.
int func(T1 t1, T2 t2, T3 t3=1) {...} // tres parámetros simples,
                                      // uno con argumento por defecto
int func(T1* ptr1, T2& tref) {...}    // los argumentos son un puntero y
                                      // una referencia.
int func(register int i) {...}        // Petición de uso de registro para
                                      // argumento (entero)
int func(char* str,...) {...}         /* Una cadena y cierto número de otros
             argumentos, o un número fijo de argumentos de tipos variables */


§3  Los argumentos son siempre objetos. Sus tipos pueden ser: escalares; estructuras; uniones, o enumeraciones; clases definidas por el usuario; punteros o referencias a estructuras y uniones, o punteros a funciones, a clases o a matrices. El tipo void está permitido como único parámetro formal. Significa que la función no recibe ningún argumento.

Nota: recuerde que cuando coloquialmente se dice que se pasa una matriz como argumento de una función, en realidad se está pasando un puntero a su primer elemento ( 4.3.8). Recuerde también que que las funciones no pueden ser utilizadas como argumentos, pero sí pueden serlo los punteros-a-función, lo que a la postre, viene a ser equivalente.


§3.1
  Es un error realizar una declaración en la lista de parámetros [2]:

int func (int x, class C{...} c) { ... }  // Error!!


§4  Todos los parámetros de una función tienen ámbito del bloque de la propia función y la misma duración automática que la función ( 4.1.5).


§5  El único especificador de almacenamiento que se permite es register ( 4.1.8b). En la declaración de parámetros también pueden utilizarse los modificadores volatile ( 4.1.9) y const ( 3.2.1c). Este último se utiliza cuando se pasan argumentos por referencia y queremos garantizar que la función no modificará el valor recibido. Ejemplo:

int dimension(X x1, const X& x2)      // x2 NO se puede modificar!!

§6  Argumentos por defecto

C++ permite tener valores por defecto para los parámetros. Esto supone que, si no se pasa el parámetro correspondiente, se asume un valor predefinido [1]. La forma de indicarlo es declararlo en el prototipo de la función, como se muestra en el ejemplo (ambas expresiones son equivalentes).

float mod (float x, float y = 0);
float mod (float, float = 0);

Más tarde no es necesario, ni posible (§6.1 ), indicarlo de nuevo en la definición. Por ejemplo:

float mod (float, float = 0);    // prototipo
...
float mod (float x, float y = 0) { return x + y; } // Error!!
float mod (float x, float y) { return x + y; }     // definición Ok

Si declaración y definición están en una sola sentencia entonces si es necesario indicar los valores por defecto. Ejemplo, en la sentencia:

float mod (float x, float y = 0) { return pow(x*x + y*y, 0.5); }

la ausencia de un segundo argumento en la invocación hace que se adopte para él un valor 0 (podría haber sido cualquier otro valor). En este contexto, la función mod aceptaría estas dos llamadas como correctas y de resultados equivalentes:

m1 = mod (2.0 , 0);
m2 = mod (2.0);


§6.1  Un argumento por defecto no puede ser repetido o cambiado en una siguiente declaración dentro del mismo ámbito. Por ejemplo:

void func (int x = 5);
...
void func (int x = 5);    // Error: repetición de argumento por defecto
{                         // nuevo ámbito
    void func (x = 7);    // L.4 Correcto: esta función oculta a la anterior
}                         // el ámbito anterior vuelve a ser visible
void func (x = 7);        // Error: cambiar argumento por defecto

Nota: a pesar de que la expresión de la línea 4 es correcta, tenga en cuenta que las declaraciones en ámbitos anidados que ocultan declaraciones del mismo nombre en ámbitos externos, suele ser fuente de errores y confusiones.


  Es muy de tener en cuenta esta regla en la definición de clases, ya que en ellas es frecuente que la declaración de métodos se realice en un punto (el cuerpo de la clase), y la definición se realice "off-line" (fuera del cuerpo de la clase). En caso que el método tenga argumentos por defecto recuerde no repetirlos más tarde en la definición.


§6.2  La gramática de C++ exige que los parámetros con valores por defecto deben ser los últimos en la lista de parámetros, y que si en una ocasión falta algún argumento, los que le siguen también deben faltar (adoptar también los valores por defecto).

Nota: como puede verse, C++ no admite la posibilidad de otros lenguajes de saltarse parámetros por defecto incluyendo una coma sin ningún valor en la posición correspondiente, por ejemplo:

int somefunc (int, int = 1, char, long);  // Incorrecto!!
....
x = somefunc ( 33, , 'c',  3.14);         // Error! No aceptable en C++
x = somefunc (int, char*, char* = 0);

  Observe que en este último caso, el espacio de separación entre char* y = es importante, ya que *= es un operador de asignación ( 4.9.2). Así pues:

x = somefunc (int, char*, char*= 0);    // Error!


§6.3
  Los argumentos por defecto de métodos (funciones-miembro de clases 4.11) no pueden ser otros miembros a no ser que sean estáticos ( 4.11.7). Ejemplo:

class C {

  int v1;

  void foo(char, int = v1);    // Error!!

};

 

class B {

  static int v1;

  void foo(char, int = v1);    // Ok.

}

Un posible diseño de la clase C podría ser:

class C {

  int v1;

  void foo(char, int = -1);    // Ok

};

más tarde, en la definición del método hacer:

void C::foo(char a, int x) {

  if (x == -1) x = v1;

  ...

}


§6.4
  Los argumentos por defecto no pueden ser otros argumentos:

x = somefunc (int x, int y = x);    // Error!

§6.5  Los argumentos pasados por referencia (§7.3 ) solo pueden adoptar valores por defecto estáticos, globales, o de un subespacio cualificado. Ejemplo:

namespace ALPHA {
  long n = 20.10L;
}
long n = 10.10L;
void f1 (long& lg = ALPHA::n);   // Ok.

void f2 (long& lg = n);          // Ok.
void f3 (long lg = 10.2L);       // Ok.
void f4 (long& lg = 10.2L );     // Error!!

§7  Argumentos: por valor y por referencia

Existen dos formas de pasar argumentos a las funciones: por valor y por referencia. El primero es utilizado por defecto con la declaración usual de parámetros. En el paso "por valor", se crean copias de los argumentos pasados a la función, los cuales, junto a las variables locales (incluyendo el posible valor devuelto), y la dirección de vuelta a la rutina que efectúa la invocación, son pasados a la pila en la secuencia de llamada. Más tarde, cuando termina su ejecución definitivamente, es decir, cuando el control vuelve a la función que la invocó, toda esta información es sacada de la pila mediante la secuencia de retorno (y se pierde). Estos procesos suponen un consumo de tiempo y espacio (memoria), a veces considerable.

§7.1  Paso por valor

Hemos visto que el paso de parámetros por valor significa que existen copias de los argumentos formales (estas copias son variables locales de la función llamada), y que una función no puede alterar ninguna variable de la función que la invocó.

  La única excepción es el caso de las matrices. Cuando se utiliza una matriz como argumento en la llamada a una función, el valor pasado es un puntero a la dirección de memoria del principio de la matriz ( 4.3.2).


§7.1.1  Cuando los argumentos pasan por valor pero no hay concordancia entre el tipo de los argumentos actuales y los argumentos formales utilizados en la declaración de la función, entonces se produce un modelado de tipo antes de la asignación. Supongamos el ejemplo:

void func(int x) {   // definición de func.  Acepta un entero

   x = x * 2;

}

float f = 3.14;

func(f);       // f es promovido a int antes de asignación a 'x'

               // x == 6


Lo que sucede en estos casos es que la copia local f en func(x) es modificada para hacerla coincidir con el tipo esperado por la función, mientras que el valor original (f) permanece inalterado.

§7.2  Pasar un puntero

En C clásico, cuando se desea que la función llamada pueda alterar el valor de variables de la función que la invoca, o ahorrar el espacio que supone la copia local de los argumentos (que pueden ser estructuras de datos muy grandes), la solución consistía en utilizar punteros a las variables respectivas como argumentos para la función (en vez de pasar las variables en sí mismas). A su vez, la función llamada debía declarar el parámetro como puntero, y acceder a la variable indirectamente a través de él. En otras palabras: cuando en C se desea que un valor X pase a una función F y que esta pueda alterar el valor de X en la función que la invocó, el argumento utilizado es &X (la dirección de X). De esta forma, aunque F recibe una copia de &X, puede alterar el valor original a través de esta dirección. Esta técnica puede tener sus ventajas.  Por ejemplo, si X es una estructura muy grande, pero puede tener efectos colaterales peligrosísimos y ser una fuente de errores difíciles de detectar.

§7.3  Paso por referencia

Por supuesto, C++ permite utilizar la técnica del C clásico descrita arriba, pero también utilizar el paso de argumentos por referencia (en realidad es una variante semántica del proceso anteriormente descrito). Para ello se utiliza el declarador de referencia & ( 4.2.3).

Las referencias presentan las ventajas de los punteros, en el sentido que permiten modificar los valores de los objetos pasados como argumento, y de que permiten ahorrar espacio si hay que pasar objetos muy grandes, pero no presentan los peligros potenciales de aquellos. En caso necesario las referencias pueden declararse constantes, indicando así que la función invocada no modificará estos valores. En estos casos, la utilización de referencias obedece casi exclusivamente a razones de eficacia en el mecanismo de llamada ( 4.2.3).

Nota: en ocasiones el paso por referencia tiene una justificación de tipo físico. Es el caso en que los objetos utilizados como argumento representan dispositivos físicos. Por ejemplo, ficheros externos o dispositivos de comunicación. En estas circunstancias, el objeto no puede ser copiado alegremente por el mecanismo de invocación de funciones (que utilizaría el constructor-copia de la clase) si se utilizaran pasos por valor, y es necesario recurrir al paso por referencia.

Otro uso muy común de las referencias es cuando la función debería devolver distintos valores.  Por ejemplo, supongamos que nos interesa que una función foo, devuelva cuatro valores: dos int posX y posY; una cadena de caracteres char* nombre, y un float altura.  Como una función no puede devolver más de un valor (al menos, no de forma directa), recurrimos a construir una estructura con los valores adecuados.  Esta estructura será pasada a la función por referencia, de forma que sus miembros podrán ser modificados desde el cuerpo de la función.  Una vez realizada la invocación, puede considerarse que la mencionada estructura contiene los valores "devueltos" [3], aunque en realidad la función no devuelva nada (en el ejemplo que sigue devuelve void).

El esquema de funcionamiento sería el siguiente:

struct ST {
  int posX; 
  int posY;
  char* nombre;
  float altura;
};

void foo (ST& st) {
  ...
  st.posX = value1;
  st.posY = value2;
  st.nombre = value3;
  st.altura = value4;
}
 
int main () { // =========
  ...
  ST st;
  foo(st);

  cout << "Las coordenadas son: x = " << st.posX << "; y = " << st.posY << endl;

  cout << "es el punto << st.nombre << " de altitud << st.altura << " metros" << endl;
  ...
}

§7.4  Comparativa

A continuación se muestran tres implementaciones de una misma función; cada una con una forma distinta para paso del argumento.

Implementación-1:    Sistema clásico, paso "por valor"

int pru1(int n) {   // n entero; pasa "por valor"

   return 3 * n;

}

...

int x, i = 4;

x = pru1(i);        // ahora: x = 12, i = 4

int& ry = i;

x = pru (ry);       // ahora: x = 12, i = 4

Observe que la última sentencia no es un paso por referencia, sino por valor (a pesar de que el argumento actual sea una referencia).


Implementación-2:  
Sistema clásico, paso de "punteros por valor" (seudo-referencia)

void pru2(int* np) {  // np puntero-a-entero; pasa "por valor"
   *np = (*np) * 3;
}
  . . .
int x = 4;
pru2(&x);             // ahora x = 12

Observe que en este caso, pasar el valor &x (dirección de x) como argumento, es equivalente a pasar un puntero a dicha variable (que es lo exigido en la definición de pru2). Es decir, la última línea se puede sustituir por las siguientes:

int* ptr = &x         // define puntero-a-x
pru2(ptr);            // pasa el puntero como argumento


Implementación-3:   
Sistema C++,  paso "por referencia"

void pru3(int& n) { // n tipo "referencia-a-int"; pasa "por referencia"
   n = 3 * n;
}
  . . .
int x = 4;
pru3(x);            // ahora x = 12

  Atención a la sintaxis: aquí la invocación a pru3 tiene la forma:  pru3(x), no pru3(&x) como en el caso anterior. Es decir, la notificación de que el argumento pasa por referencia hay que hacerla en la definición de la función, y no es necesario indicarlo en el momento de la invocación.

En este último caso, la declaración int& n como parámetro de la función pru3, establece que este n sea declarado como "referencia-a-entero", de forma que cuando se pasa el argumento x, la función crea un valor n que es una especie de alias o espejo de x, de forma que la expresión n = 3*n tiene el mismo efecto que x = 3*x.


§7.4.1  Ya hemos visto (Referencias 4.2.3) que cuando, en la declaración de una referencia, el iniciador es una constante, o un objeto de tipo diferente que el referenciado, se crea un objeto temporal para el que la referencia actúa como un alias. Esta creación de objetos temporales es lo que permite la conversión de tipos referencia-a-tipoX cuando se utilizan como parámetros de funciones y no hay concordancia entre el valor recibido y el esperado (suponiendo que exista posibilidad de conversión). Este sería el mecanismo utilizado en el siguiente caso:

void pru(int& n) { // n tipo "referencia-a-int" (pasa "por referencia")
   n = 3 * n;
}
  . . .
float f = 4.1;
pru(f);            // ahora f = 12

§7.4.2  Ejemplo

En el programa que sigue se muestra un caso de paso por referencia y acceso a subespacios.

#include <iostream.h>

namespace ALPHA {
   class CAlpha {
     int x;
     public:
     int getx(void) { return x; }
     void putx(int i) { x = i; }
   };
   CAlpha CA1;           // instancia de CAlpha (en ALPHA)
}
int func (ALPHA::CAlpha& ca, int i);  /* prototipo: ca es la referencia
                          a un objeto de la clase CAlpha en ALPHA */

int main (void) {        // ========================
  int x = 0;
  cout << "x = " << x << endl;
  ALPHA::CA1.putx(10);   // invocación al método putx de CA1
  x = func(ALPHA::CA1, 3);
  cout << "x = " << x << endl;
}

int func (ALPHA::CAlpha& ca, int i) {  // definición
  return (i + ca.getx());
}

Salida:

x = 0
x = 13

  Tema relacionado
  • Paso de funciones como argumento ( 4.2.4c)

  Inicio.


[1]  Esta posibilidad de omitir alguno, o algunos, de los parámetros en la invocación de una función, se conoce como de "parámetros opcionales". En C++ los parámetros omitidos deben ser los últimos; otros lenguajes (por ejemplo Java) no los permiten en absoluto, mientras que otros compiladores permiten omitirlos en cualquier posición.

[2]  La razón es que un argumento como c solo podría ser referenciado desde el cuerpo de la propia función, por lo que la función solo podría ser invocada desde ella misma.

[3]  Esta técnica es profusamente utilizada en la programación Windows, donde muchas funciones de la API del Sistema devuelven conjuntos de valores en estructuras que son pasadas por referencia.