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.2.4a  Declaración y definición de punteros a función

§1  Sintaxis

Como se ha visto en los ejemplos ( 4.2.4), el núcleo de todas las declaraciones de punteros a función tiene la siguiente sintaxis:

<tipo_devuelto> ... (* nombre_puntero) (<parametros>) ...

  <tipo_devuelto> es el tipo de vuelto por la función señalada por el puntero, podríamos decir que es el tipo de variable señalada por el puntero en último extremo [2].

  <parametros> es la lista de los argumentos aceptados por la función señalada por el puntero.


Observe que el paréntesis (* nombre_puntero) es fundamental.  Esta parte es conocida como núcleo de la declaración, a la derecha del núcleo se incluye la lista de parámetros (que puede estar vacía) y a su izquierda el valor devuelto por la función, que debe ser único (puede ser void). Sin el núcleo la declaración cambia drásticamente su sentido, ya que entonces no es considerada por el compilador como la declaración de una variable de tipo puntero-a-función. Por ejemplo:

int (*fptr)(int); // puntero-a-función que recibe un int y devuelve un int
int *fptr (int);  // función que recibe un int y devuelve puntero-a-int

En consecuencia:

void func(int (*fptr)(int));    // Ok declaración correcta de una función
void func(int *fptr(int));      // Error!! expresión ilegal en C/C++

Nota: a pesar de lo indicado en esta última sentencia, puede comprobarse que, al menos con los compiladores Borland C++ 5.5 y MS Visual C++  6.0, una expresión como la anterior puede ser compilada sin que se presente ningún problema. En la página adjunta ( Nota-1) se ofrece una explicación detallada del motivo, que no está en contradicción con el hecho repetidamente enunciado de que C/C++ no permiten utilizar una función como argumento de otra función [3].

Aunque la sintaxis utilizada resulta difícil de interpretar en algunos casos, la notación de punteros a función sigue cierta regla que está relacionada con la sintaxis de la función a la que apuntan. Si consideramos que la definición de una función tiene tres partes: valor devuelto; argumentos utilizados, y nombre. La representación de un puntero a tal función se obtiene sustituyendo el nombre por la indicación de un puntero y separándolo del resto por un paréntesis.

Ejemplos:
Declaración de función Declaración del puntero-a-función

char* func (char*);

Función que acepta un puntero-a-char y devuelve un puntero-a-char

char* (*func) (char*);

Puntero-a-función que acepta un puntero-a-char y devuelve un puntero-a-char

char const * func ();

Función que no acepta argumento y devuelve un puntero-a-char constante.

char const* (*func) ();

Puntero-a-función que no acepta argumentos y devuelve un puntero-a-char constante

struct S func (char*);

Función que acepta un puntero-a-char y devuelve una estructura tipo S.

struct S (*func) (char*);

Puntero-a-función que acepta un puntero-a-char y devuelve una estructura tipo S.

int* func (struct S* Sptr);

Función que acepta un puntero-a-estructura tipo S y devuelve un puntero-a-int

int* (*func) (struct S* Sptr);

Puntero-a-función que acepta un puntero-a-estructura tipo S y devuelve un punter-a-int

int* (C::* func())(char);

Función que no recibe argumentos, devuelve un puntero a función miembro de la clase C que recibe un char y devuelve un puntero-a-int.

int* (C::* (*func)())(char);

Puntero-a-función que no recibe argumentos, devuelve un puntero-a-función miembro de la clase C que recibe un char y devuelve un punter-a-int.

int (*(*f())[10])();

Función que no acepta argumentos, devuelve un puntero a una matriz de diez punteros-a-función que no aceptan argumentos y devuelven un int.

int (*(*(*f)())[10])();

Puntero-a-función que no acepta argumentos, devuelve un puntero a una matriz de diez punteros-a-función que no aceptan argumentos y devuelven un int.

§1.1  Convención de llamada

Técnicamente, la convención de llamada (  4.4.6a) no forma parte del "tipo" de la función, de forma que, por ejemplo, las funciones que siguen son del mismo tipo y el compilador muestra el correspondiente error si se intentan compilar en la misma unidad de compilación:

float __stdcall  foo (float x) { return 2 * x;  }  // L.1

float foo (float x) { return 2 * x;  }             // L.2 

En concreto, los errores obtenidos en ambas líneas con Borland C++ son los siguientes (otros compiladores dan errores similares):

L.1:  Error ...: Earlier declaration of '__stdcall foo(float)'
L.2:  Error ...: Type mismatch in redeclaration of '__stdcall foo(float)'

Sin embargo, en lo que se refiere a la definición de punteros-a-función, sí es necesario tener en cuenta este detalle en la declaración, ya que los punteros a funciones con distintas convenciones de llamada no son equivalentes entre sí.

La convención de llamada en la declaración de punteros-a-función se utilizan dos sintaxis distintas, según sean los compiladores MSVC++ y BC++ o GNU gcc.  En los dos primeros, el especificador de llamada se sitúa entre e valor devuelto y el paréntesis que contiene el nombre del puntero.  En el compilador GNU esto se hace incluyendo la palabra  __attribute__ seguida del nombre del especificador entre dobles paréntesis al final de la declaración.  Por ejemplo, las declaraciones de punteros para las funciones anteriores serían:

float __stdcall (* fooPtr) (float);                 // para foo de L.1 MSVC++ y BC++
float (* fooPtr) (float) __atribute__((stdcall));   // para foo de L.1 GNU g++
float (* fooPtr) (float);          // para foo de L.2 todos los compiladores estándar

§2  Definición:

Como ocurre con los demás tipos de variables, antes de usar los punteros a función es necesario definirlos, es decir, asignarles un valor; en este caso la dirección de una función concreta. Esto puede hacerse en el mismo momento de la declaración o después.

Nota: recuerde que el puntero-a-void no puede usarse para señalar a una función ( 4.2.1d)

void* fptr = &func;    // Error!!


  Una observación importante a tener en cuenta desde el punto de vista de la sintaxis, es que la dirección de una función func() puede indicarse con el operador de referencia & ( 4.9.11) &func [1], o solo con el nombre func. C++ presenta aquí la misma situación sintáctica que cuando en 4.3.2 decimos que el identificador de una matriz puede ser interpretado como la dirección de su primer elemento. Es decir:

char func(int);       // declara una función
char (*fptr)(int);    // declara fptr puntero-a-función
fptr = &func;         // inicia fptr

las dos últimas sentencias son equivalentes a:

char (*fptr)(int);    
fptr = func;          // inicia fptr

también son equivalentes a:

char (*fptr)(int) = func;  // define ptr (declara e inicia)


En el siguiente epígrafe ( 4.2.4b) veremos que esta equivalencia punterofunción (fptr == func) es utilizada también para la invocación de la función a través de su puntero [4].

Nota: la Librería Estándar C++, que contiene una amplia colección de técnicas de programación avanzadas, incluye múltiples ejemplos de punteros-a-función que utilizan la dualidad punterofunción antes señalada. Por ejemplo, para extender el mecanismo de sobrecarga de operadores en las operaciones de E/S.


§2.1    Por supuesto, en este tipo de asignaciones es imprescindible que la función asignada al puntero responda a la definición de este. Es decir: tanto el tipo de dato devuelto como los parámetros deben ser adecuados, -en cambio, el posible especificador de excepción ( 1.6.4) que pudiera tener la función no se considera parte de su tipo y por consiguiente no afecta al tipo de puntero-. Por ejemplo:

char func(int);
int (*fptr)(int) = func;   // Error: desacuerdo en valor devuelto
char(*fptr)(char) = func;  // Error: desacuerdo en parámetro
char(*fptr)(int) = func;   // Ok: todo correcto


§2.2  Si se trata de definir una matriz de punteros, también se pueden declarar e iniciar en la misma sentencia:

void func1(char letra);    // prototipo de func1
void func2(char letra);    // prototipo de func2
void (* afptr[2])(char) = {func1, func2}; // definición de afptr

en la última línea el tamaño de la matriz está implícito en la inicialización, de forma que también se podría haber puesto:

void (* afptr[])(char) = {func1, func2}; 


También puede iniciarse mediante subíndices (en este caso no puede ahorrarse el tamaño de la matriz en la declaración):

void func1(char letra);
void func2(char letra);
void (* afptr[2])(char);    // declaracion de afptr
afptr[0] = func1;           // inicia afptr[1]
afptr[1] = func2;           // inicia afptr[2]


Observe que los elementos de una matriz (de punteros o de cualquier otro tipo de objetos) son del mismo tipo. En el caso de una matriz de punteros, significa que estos deben apuntar a funciones recibiendo el mismo tipo de argumentos y devolviendo lo mismo, lo que significa a su vez que sus prototipos ( 4.4.1) deben ser idénticos. Las diferencias solo pueden estar en el nombre y en el cuerpo de las funciones referenciadas.

§3  typedef y la declaración de punteros a función

Como hemos visto, la notación utilizada por C++ en la declaración de punteros a función puede llegar a ser bastante compleja, por lo que es muy común la costumbre de utilizar typedef auxiliares, en especial cuando se trata de definiciones ( 3.2.1a). Ejemplo:

#include <iostream>
#include <cstdlib>
using namespace std;


typedef void (*PFV)();        // L.5

void fun1() { cout << "Primera" << endl; }
void fun2() { cout << "Segunda" << endl; }
void fun3() { cout << "Tercera" << endl; }
void fun4() { cout << "Argumento No valido" << endl; }

int main( int argc, char * argv[] ) {           // ==================
  PFV apf[] = { &fun1, &fun2, &fun3, &fun4 };   // M.1
  if (argc > 1 && *argv[1] > '0' && *argv[1] <= '4')
    (*apf[atoi( argv[1] ) - 1])();              // M.3
  else
    (*apf[3])();                                // M.5

  return 0;
}

Comentario:

El ejemplo comprueba el argumento pasado con el programa ( 4.4.4). Este argumento puede ser un entero de 1 a 3, invocándose la función correspondiente al número utilizado. En los demás casos se invoca la función cuarta.

En la segunda línea se ha utilizado un typedef para definir que PFV es un tipo puntero a función que devuelve void y no recibe ningún argumento. A continuación se definen cuatro funciones que corresponden con esta definición.

El programa comienza definiendo apf, un array de punteros a funciones que devuelven void y no reciben ningún argumento (precisamente utilizando el tipo PFV). En la misma línea se inicializa con las direcciones de las cuatro funciones fun1 a fun4.

Observe que la sentencia M.1 podría haberse sustituido por otra equivalente más elegante:

PFV apf[] = { fun1, fun2, fun3, fun4 };   // M.1bis

Las sentencias M.3 y M.5 son la forma canónica de invocación de las funciones mediante sus punteros ( 4.2.4b)


Otra posibilidad sintáctica es utilizar el typedef auxiliar para definir un tipo de función en sí en lugar del puntero-a-función.  En este caso, la definición del puntero-a-función debe ser modificada convenientemente.  En el ejemplo anterior, las sentencias L.5 y M.1 podrían tener el siguiente aspecto:

typedef void PFV();                          // L.5

PFV* apf[] = { &fun1, &fun2, &fun3, &fun4 }; // M.1

Observe que en este caso, PFV define una función que no recibe argumentos y devuelve void, mientras que apf sigue siendo una matriz de 4 punteros a funciones que no reciben argumento y devuelven void.

 

  Inicio.


[1]  Observe que la sintaxis es &func y no &func(), o &func(x, y, ... z).  La segunda forma da error de compilación, pues en principio se interpreta como una llamada a función a la que le faltan parámetros. Si a pesar de todo, la llamada es correcta porque func() no acepta ningún argumento, o se ponen valores compatibles (tercer caso), el error es que se pretende aplicar el operador de referencia (&) al valor devuelto por func(), que no es direccionable ( 4.9.11b).

[2]  Al tratar de los objetos-valor ( 4.1.1) indicamos que las funciones son objetos cuyo "valor" es el que devuelven, si bien con la característica adicional de realizar cierta computación.

[3]  "There are only two things one can do to a function: call it and take its address". Stroustrup  TC++PL §7.7.

[4]  En realidad en estos casos se realiza una conversión estándar, efectuada automáticamente por el compilador ( 2.2.5).