Wskaźniki na funkcje w C

sugar_hiccup

Wskaźniki na funkcje w C są użyteczne, a nie takie straszne, jak się wydają.

***

Wstęp

Na przykładzie naszego Układu Słonecznego postaram się napisać coś o wskaźnikach na funkcje. Napiszmy sobie szkielet naszego programu wypisującego planety w układzie.

#include <stdio.h>

typedef struct Planet 
{
  int nthFromSun;   // planet's number relative to the Sun
  float auFromSun; // distance from the Sun in a.u.
  const char *name;
} Planet;

void fillPlanet(Planet *p, int n, float au, const char *name)
{
  p->nthFromSun = n;
  p->auFromSun = au;
  p->name = name;
}

void printPlanets(Planet p[], int size)
{
  for (int i = 0; i < size; i++)
  {
      printf("%4d\t%10s\t%0.2f a.u. from the Sun\n",
             p[i].nthFromSun,
             p[i].name,
             p[i].auFromSun);
  }
}

int main()
{
  Planet p[8];
  fillPlanet(&p[0], 1, 0.3871, "Mercury");
  fillPlanet(&p[1], 2, 0.7233, "Venus");
  fillPlanet(&p[2], 3, 1.0, "Earth");
  fillPlanet(&p[3], 4, 1.5237, "Mars");
  fillPlanet(&p[4], 5, 5.2034, "Jupiter");
  fillPlanet(&p[5], 6, 9.5371, "Saturn");
  fillPlanet(&p[6], 7, 19.1913, "Uranus");
  fillPlanet(&p[7], 8, 30.0690, "Neptune");

  printf("All planets:\n");
  printPlanets(p, 8);

  return 0;
}

Wszystko ładnie. Na wyjściu dostajemy:

All planets:
   1       Mercury      0.39 a.u. from the Sun
   2         Venus      0.72 a.u. from the Sun
   3         Earth      1.00 a.u. from the Sun
   4          Mars      1.52 a.u. from the Sun
   5       Jupiter      5.20 a.u. from the Sun
   6        Saturn      9.54 a.u. from the Sun
   7        Uranus      19.19 a.u. from the Sun
   8       Neptune      30.07 a.u. from the Sun

Problemy abstrakcyjne

Powiedzmy, że chcemy wypisać tylko te planety, które są dalej od Słońca niż Ziemia. Oczywiście możemy zmodyfikować naszą funkcję printPlanets w następujący sposób:

void printPlanets(Planet p[], int size)
{
  for (int i = 0; i < size; i++)
  {
    if (p[i].auFromSun > 1.0)
    {
      printf("%4d\t%10s\t%0.2f a.u. from the Sun\n",
             p[i].nthFromSun,
             p[i].name,
             p[i].auFromSun);
    }
  }
}

Jednak nie jest to rozwiązanie zbyt abstrakcyjne. Później możemy na przykład chcieć wypisać tylko planety na literę M albo takie których liczba porządkowa jest podzielna przez 3. Moglibyśmy pisać funkcje dla każdego takiego przypadku, co spowodowałoby dużą ilość nadmiarowego kodu, ale ciała tych funkcji prawie nie różniłyby się między sobą.

Na szczęście funkcje mogą przyjmować jako argument wskaźnik na jakąś inną funkcję. Nasza printPlanets może przyjąć za argument wskaźnik na funkcję filtrującą to, co chcemy wypisać. Nasza funkcja filtrująca będzie miała następującą sygnaturę:

int filter(Planet p);

Jeżeli dana planeta będzie spełniała nasze wymagania, to zwrócimy 1, w przeciwnym wypadku 0. Wskaźnik na taką funkcję wygląda tak:

int (*filter)(Planet);

Nawiasy są obowiązkowe, bo w przeciwnym razie kompilator potraktowałby taki kod jako deklarację funkcji zwracającej wskaźnik na int:

int *filter(Planet);

Czyli ogólnie:

typ_zwracany (*nazwa_wskaźnika_na_funkcję)(typ_argumentu1, ..., typ_argumentu_n);

Przystąpmy więc do uogólnienia funkcji printPlanets:

// Jako trzeci argument przyjmujemy wskaźnik na funkcję o sygnaturze int filter(Planet)
void printPlanets(Planet p[], int size, int (*filter)(Planet))
{
  for (int i = 0; i < size; i++)
  {
    int doPrint = 1; // decydujemy się czy wypisać daną planetę

    // Jeżeli przekazaliśmy jako filtr coś innego niż NULL,
    // to sprawdzamy, czy dana planeta spełnia wymagania zdefiniowane w filtrze
    if (filter != NULL)
      doPrint = filter(p[i]);

    if (doPrint)
    {
      printf("%4d\t%10s\t%0.2lf a.u. from the Sun\n",
             p[i].nthFromSun,
             p[i].name,
             p[i].auFromSun);
    }
  }
}

Teraz możemy podefiniować poszczególne filtry:

int planetDistanceLargerThanEarth(Planet p)
{
  if (p.auFromSun > 1.0)
    return 1;
  return 0;
}

int planetNameStartingWithM(Planet p)
{
  if (p.name[0] == 'M')
    return 1;
  return 0;
}

Teraz aby wypisać różne rodzaje planet, wystarczy, że napiszemy:

// Wszystkie planety, brak filtra
printPlanets(p, 8, NULL);

// Planety położone dalej od Ziemi
printPlanets(p, 8, &planetDistanceLargerThanEarth);

// Planety na literę M
printPlanets(p, 8, planetNameStartingWithM);

O, a dlaczego napisałem & przy funkcji w drugim przypadku, a w trzecim już nie?

Otóż zapisy te są równonważne. Adres funkcji możemy wyłuskać używając & albo użyć jej nazwy, nie ma to znaczenia.

Zwracanie wskaźnika na funkcję

Takie wskaźniki możemy również zwracać. Przykładowo:

#include <stdio.h>

void hello()
{
  printf("Hello!\n");
}

void (*getFunc(void (*f)(void)))(void)
{
  return f;
}

int main()
{
  (getFunc(hello))();

  return 0;
}

Nawet nie będę się starał wytłumaczyć tego koszmaru, niebezpiecznie zaczynającego przypominać Lispa.

void (*getFunc(void (*f)(void)))(void)

Ale możemy sobie to uprościć:

typedef void (*func)(void);

func getFunc(void (*f)(void))
{
  return f;
}

Albo nawet:

typedef void (*func)(void);

func getFunc(func f)
{
  return f;
}

Albo inaczej:

void *getFunc(void (*f)(void))
{
  return f;
}

int main()
{
  void (*f)(void);
  f = getFunc(hello);
  f();

  return 0;
}

Podsumowanie

Podsumowując i uzupełniając:

  1. w przeciwieństwie do zwykłych wskaźników na dane, wskaźniki na funkcje wskazują na kod wykonywalny w pamięci,
  2. w przypadku wskaźników na funkcje, nie alokujemy i nie zwalniamy pamięci,
  3. przy użyciu takich wskaźników nie musimy wyłuskiwać adresów i wartości, wystarczy nam nazwa funkcji,
  4. wskaźniki na funkcje możemy przekazywać jako argumenty do innej funkcji, jak i je zwracać.

Dziękuję za uwagę i proszę uprzejmie o wytknięcie wszelkich błędów. :)

0 komentarzy