W lekcji 12.7 -- Wprowadzenie do wskaźników, dowiedziałeś się, że wskaźnik jest zmienną przechowującą adres innej zmiennej. Wskaźniki funkcji działają podobnie, z tą różnicą, że zamiast wskazywać na zmienne, wskazują na funkcje!
Rozważ następującą funkcję:
int foo()
{
return 5;
}Identyfikator foo() to nazwa funkcji. Ale jakiego typu jest ta funkcja? Funkcje mają swój własny typ funkcji — w tym przypadku typ funkcji, który zwraca liczbę całkowitą i nie przyjmuje żadnych parametrów. Podobnie jak zmienne, funkcje żyją pod przypisanym adresem w pamięci (co czyni je lwartościami).
Gdy funkcja jest wywoływana (przez operator()), wykonanie przeskakuje pod adres wywoływanej funkcji:
int foo() // code for foo starts at memory address 0x002717f0
{
return 5;
}
int main()
{
foo(); // jump to address 0x002717f0
return 0;
}W pewnym momencie swojej kariery programistycznej (jeśli jeszcze tego nie zrobiłeś) prawdopodobnie popełnisz prosty błąd:
#include <iostream>
int foo() // code starts at memory address 0x002717f0
{
return 5;
}
int main()
{
std::cout << foo << '\n'; // we meant to call foo(), but instead we're printing foo itself!
return 0;
}Zamiast wywoływania funkcji foo() i drukowania wartość zwracaną, niechcący wysłaliśmy funkcję foo bezpośrednio do std::cout. Co się dzieje w tym przypadku?
Kiedy odwołanie się do funkcji następuje po nazwie (bez nawiasów), C++ konwertuje tę funkcję na wskaźnik funkcji (przechowujący adres funkcji). Następnie operator<< próbuje wydrukować wskaźnik funkcji, ale kończy się to niepowodzeniem, ponieważ operator<< nie wie, jak wydrukować wskaźniki funkcji. Norma mówi, że w tym przypadku foo należy zamienić na bool (który operator<< wie jak wydrukować). A ponieważ wskaźnik funkcji dla foo nie jest wskaźnikiem o wartości null, zawsze powinien mieć wartość Boolean true. Powinno to zatem wyświetlić:
1
Wskazówka
Niektóre kompilatory (np. Visual Studio) mają rozszerzenie kompilatora, które zamiast tego wypisuje adres funkcji:
0x002717f0
Jeśli Twoja platforma nie drukuje adresu funkcji, a chcesz, możesz ją wymusić, konwertując funkcję na wskaźnik pustej przestrzeni i wyświetlając następujący komunikat:
#include <iostream>
int foo() // code starts at memory address 0x002717f0
{
return 5;
}
int main()
{
std::cout << reinterpret_cast<void*>(foo) << '\n'; // Tell C++ to interpret function foo as a void pointer (implementation-defined behavior)
return 0;
}To jest zachowanie zdefiniowane w implementacji, więc może nie działać na wszystkich platformy.
Tak jak możliwe jest zadeklarowanie niestałego wskaźnika do normalnej zmiennej, możliwe jest również zadeklarowanie niestałego wskaźnika do funkcji. W dalszej części tej lekcji przyjrzymy się wskaźnikom funkcji i ich zastosowaniu. Wskaźniki funkcji to dość zaawansowany temat i resztę tej lekcji mogą bezpiecznie pominąć lub prześledzić ci, którzy szukają jedynie podstaw C++.
Wskaźniki do funkcji
Składnia tworzenia niestałego wskaźnika funkcji jest jedną z najbrzydszych rzeczy, jakie kiedykolwiek zobaczysz w C++:
// fcnPtr is a pointer to a function that takes no arguments and returns an integer
int (*fcnPtr)();W powyższym fragmencie fcnPtr jest wskaźnikiem do funkcji, która ma nie ma parametrów i zwraca liczbę całkowitą. fcnPtr może wskazywać dowolną funkcję pasującą do tego typu.
Nawiasy wokół *fcnPtr są konieczne ze względu na pierwszeństwo, ponieważ int* fcnPtr() byłyby interpretowane jako deklaracja forward dla funkcji o nazwie fcnPtr, która nie przyjmuje parametrów i zwraca wskaźnik do liczby całkowitej.
Aby utworzyć wskaźnik funkcji const, const znajduje się po gwiazdka:
int (*const fcnPtr)();Jeśli umieścisz const przed int, oznacza to, że wskazywana funkcja zwróci const int.
Wskazówka
Składnia wskaźnika funkcji może być trudna do zrozumienia. W poniższych artykułach przedstawiono metodę analizowania takich deklaracji:
Przypisywanie funkcji do wskaźnika funkcji
Wskaźniki funkcji można inicjować funkcją (a wskaźniki funkcji niebędące stałymi można przypisać funkcję). Podobnie jak w przypadku wskaźników do zmiennych, możemy również użyć &foo, aby uzyskać wskaźnik funkcji do foo.
int foo()
{
return 5;
}
int goo()
{
return 6;
}
int main()
{
int (*fcnPtr)(){ &foo }; // fcnPtr points to function foo
fcnPtr = &goo; // fcnPtr now points to function goo
return 0;
}Jednym z częstych błędów jest wykonanie następującej czynności:
fcnPtr = goo();To próbuje przypisać wartość zwracaną z wywołania funkcji goo() (która ma typ int) do fcnPtr (który oczekuje wartości typu int(*)()), czego nie chcemy. Chcemy, aby fcnPtr miał przypisany adres funkcji goo, a nie wartość zwracaną przez funkcję goo(). Zatem nie są potrzebne żadne nawiasy.
Zauważ, że typ (parametry i typ zwracany) wskaźnika funkcji musi odpowiadać typowi funkcji. Oto kilka przykładów:
// function prototypes
int foo();
double goo();
int hoo(int x);
// function pointer initializers
int (*fcnPtr1)(){ &foo }; // okay
int (*fcnPtr2)(){ &goo }; // wrong -- return types don't match!
double (*fcnPtr4)(){ &goo }; // okay
fcnPtr1 = &hoo; // wrong -- fcnPtr1 has no parameters, but hoo() does
int (*fcnPtr3)(int){ &hoo }; // okayW przeciwieństwie do typów podstawowych, C++ będziesz w razie potrzeby niejawnie konwertuje funkcję na wskaźnik funkcji (więc nie musisz używać operatora adresu (&), aby uzyskać adres funkcji). Jednakże wskaźniki funkcji nie zostaną skonwertowane na wskaźniki puste i odwrotnie (chociaż niektóre kompilatory, takie jak Visual Studio, i tak mogą na to pozwolić).
// function prototypes
int foo();
// function initializations
int (*fcnPtr5)() { foo }; // okay, foo implicitly converts to function pointer to foo
void* vPtr { foo }; // not okay, though some compilers may allowWskaźniki funkcji można również inicjować lub przypisywać im wartość nullptr:
int (*fcnptr)() { nullptr }; // okayWywoływanie funkcji przy użyciu wskaźnika funkcji
Inną podstawową rzeczą, którą można zrobić ze wskaźnikiem funkcji, jest użycie go do faktycznego wywołania funkcji. Można to zrobić na dwa sposoby. Pierwszy polega na jawnym dereferencji:
int foo(int x)
{
return x;
}
int main()
{
int (*fcnPtr)(int){ &foo }; // Initialize fcnPtr with function foo
(*fcnPtr)(5); // call function foo(5) through fcnPtr.
return 0;
}Drugi sposób polega na niejawnym dereferencji:
int foo(int x)
{
return x;
}
int main()
{
int (*fcnPtr)(int){ &foo }; // Initialize fcnPtr with function foo
fcnPtr(5); // call function foo(5) through fcnPtr.
return 0;
}Jak widzisz, metoda ukrytego dereferencji wygląda jak zwykłe wywołanie funkcji - czego można się spodziewać, ponieważ normalne nazwy funkcji i tak są wskaźnikami do funkcji! Jednak niektóre starsze kompilatory nie obsługują metody ukrytego dereferencji, ale wszystkie nowoczesne kompilatory powinny.
Pamiętaj również, że ponieważ wskaźniki funkcji można ustawić na wartość nullptr, dobrym pomysłem jest sprawdzenie lub warunkowe sprawdzenie, czy wskaźnik funkcji jest wskaźnikiem zerowym przed jego wywołaniem. Podobnie jak w przypadku zwykłych wskaźników, dereferencja wskaźnika funkcji zerowej prowadzi do niezdefiniowanego zachowania.
int foo(int x)
{
return x;
}
int main()
{
int (*fcnPtr)(int){ &foo }; // Initialize fcnPtr with function foo
if (fcnPtr) // make sure fcnPtr isn't a null pointer
fcnPtr(5); // otherwise this will lead to undefined behavior
return 0;
}Domyślne argumenty nie działają w przypadku funkcji wywoływanych poprzez wskaźniki funkcji Zaawansowane
Kiedy kompilator napotyka normalne wywołanie funkcji z jednym lub większą liczbą domyślnych argumentów, przepisuje wywołanie funkcji, aby uwzględnić argumenty domyślne. Proces ten zachodzi w czasie kompilacji i dlatego można go zastosować tylko do funkcji, które można rozwiązać w czasie kompilacji.
Jednak gdy funkcja jest wywoływana poprzez wskaźnik funkcji, jest ona rozpoznawana w czasie wykonywania. W tym przypadku nie ma potrzeby przepisywania wywołania funkcji w celu uwzględnienia argumentów domyślnych.
Kluczowa informacja
Ponieważ rozpoznawanie odbywa się w czasie wykonywania, argumenty domyślne nie są rozpoznawane, gdy funkcja jest wywoływana przez wskaźnik funkcji.
Oznacza to, że możemy użyć wskaźnika funkcji do ujednoznacznienia wywołania funkcji, które w przeciwnym razie byłoby niejednoznaczne ze względu na domyślne argumenty. W poniższym przykładzie pokazujemy dwa sposoby, aby to zrobić:
#include <iostream>
void print(int x)
{
std::cout << "print(int)\n";
}
void print(int x, int y = 10)
{
std::cout << "print(int, int)\n";
}
int main()
{
// print(1); // ambiguous function call
// Deconstructed method
using vnptr = void(*)(int); // define a type alias for a function pointer to a void(int) function
vnptr pi { print }; // initialize our function pointer with function print
pi(1); // call the print(int) function through the function pointer
// Concise method
static_cast<void(*)(int)>(print)(1); // call void(int) version of print with argument 1
return 0;
}Przekazywanie funkcji jako argumentów do innych funkcji
Jedną z najbardziej przydatnych rzeczy związanych ze wskaźnikami funkcji jest przekazywanie funkcji jako argumentu do innej funkcji. Funkcje używane jako argumenty innej funkcji są czasami nazywane funkcjami wywołania zwrotnego.
Rozważmy przypadek, w którym piszesz funkcję w celu wykonania zadania (takiego jak sortowanie tablicy), ale chcesz, aby użytkownik mógł określić, w jaki sposób będzie wykonywana konkretna część tego zadania (np. czy tablica będzie sortowana w kolejności rosnącej, czy malejącej). Przyjrzyjmy się bliżej temu problemowi w zastosowaniu konkretnie do sortowania, jako przykładowi, który można uogólnić na inne podobne problemy.
Wiele algorytmów sortowania opartego na porównaniach działa na podobnej koncepcji: algorytm sortowania iteruje po liście liczb, dokonuje porównań par liczb i zmienia kolejność liczb na podstawie wyników tych porównań. W rezultacie, zmieniając porównanie, możemy zmienić sposób sortowania algorytmu bez wpływu na resztę kodu sortującego.
Oto nasza procedura sortowania przez wybór z poprzedniej lekcji:
#include <utility> // for std::swap
void SelectionSort(int* array, int size)
{
if (!array)
return;
// Step through each element of the array
for (int startIndex{ 0 }; startIndex < (size - 1); ++startIndex)
{
// smallestIndex is the index of the smallest element we've encountered so far.
int smallestIndex{ startIndex };
// Look for smallest element remaining in the array (starting at startIndex+1)
for (int currentIndex{ startIndex + 1 }; currentIndex < size; ++currentIndex)
{
// If the current element is smaller than our previously found smallest
if (array[smallestIndex] > array[currentIndex]) // COMPARISON DONE HERE
{
// This is the new smallest number for this iteration
smallestIndex = currentIndex;
}
}
// Swap our start element with our smallest element
std::swap(array[startIndex], array[smallestIndex]);
}
}Zastąpmy to porównanie funkcją umożliwiającą dokonanie porównania. Ponieważ nasza funkcja porównania porówna dwie liczby całkowite i zwróci wartość logiczną wskazującą, czy elementy powinny zostać zamienione, będzie to wyglądać mniej więcej tak:
bool ascending(int x, int y)
{
return x > y; // swap if the first element is greater than the second
}A oto nasza procedura sortowania przez wybór wykorzystująca funkcję rosnąco() do wykonania porównania:
#include <utility> // for std::swap
void SelectionSort(int* array, int size)
{
if (!array)
return;
// Step through each element of the array
for (int startIndex{ 0 }; startIndex < (size - 1); ++startIndex)
{
// smallestIndex is the index of the smallest element we've encountered so far.
int smallestIndex{ startIndex };
// Look for smallest element remaining in the array (starting at startIndex+1)
for (int currentIndex{ startIndex + 1 }; currentIndex < size; ++currentIndex)
{
// If the current element is smaller than our previously found smallest
if (ascending(array[smallestIndex], array[currentIndex])) // COMPARISON DONE HERE
{
// This is the new smallest number for this iteration
smallestIndex = currentIndex;
}
}
// Swap our start element with our smallest element
std::swap(array[startIndex], array[smallestIndex]);
}
}Teraz, aby pozwolić wywołującemu zdecydować, w jaki sposób sortowanie zostanie wykonane, zamiast korzystać z naszej własnej, zakodowanej na stałe funkcji porównania, pozwolimy wywołującemu zapewnić własne sortowanie funkcja! Odbywa się to poprzez wskaźnik funkcji.
Ponieważ funkcja porównawcza wywołującego porówna dwie liczby całkowite i zwróci wartość logiczną, wskaźnik do takiej funkcji będzie wyglądał mniej więcej tak:
bool (*comparisonFcn)(int, int);Pozwolimy więc wywołującemu przekazać naszej procedurze sortującej wskaźnik do żądanej funkcji porównania jako trzeci parametr, a następnie użyjemy funkcji wywołującej do wykonania porównanie.
Oto pełny przykład sortowania przez zaznaczenie, które wykorzystuje parametr wskaźnika funkcji do wykonania porównania zdefiniowanego przez użytkownika, wraz z przykładem, jak to wywołać:
#include <utility> // for std::swap
#include <iostream>
// Note our user-defined comparison is the third parameter
void selectionSort(int* array, int size, bool (*comparisonFcn)(int, int))
{
if (!array || !comparisonFcn)
return;
// Step through each element of the array
for (int startIndex{ 0 }; startIndex < (size - 1); ++startIndex)
{
// bestIndex is the index of the smallest/largest element we've encountered so far.
int bestIndex{ startIndex };
// Look for smallest/largest element remaining in the array (starting at startIndex+1)
for (int currentIndex{ startIndex + 1 }; currentIndex < size; ++currentIndex)
{
// If the current element is smaller/larger than our previously found smallest
if (comparisonFcn(array[bestIndex], array[currentIndex])) // COMPARISON DONE HERE
{
// This is the new smallest/largest number for this iteration
bestIndex = currentIndex;
}
}
// Swap our start element with our smallest/largest element
std::swap(array[startIndex], array[bestIndex]);
}
}
// Here is a comparison function that sorts in ascending order
// (Note: it's exactly the same as the previous ascending() function)
bool ascending(int x, int y)
{
return x > y; // swap if the first element is greater than the second
}
// Here is a comparison function that sorts in descending order
bool descending(int x, int y)
{
return x < y; // swap if the second element is greater than the first
}
// This function prints out the values in the array
void printArray(int* array, int size)
{
if (!array)
return;
for (int index{ 0 }; index < size; ++index)
{
std::cout << array[index] << ' ';
}
std::cout << '\n';
}
int main()
{
int array[9]{ 3, 7, 9, 5, 6, 1, 8, 2, 4 };
// Sort the array in descending order using the descending() function
selectionSort(array, 9, descending);
printArray(array, 9);
// Sort the array in ascending order using the ascending() function
selectionSort(array, 9, ascending);
printArray(array, 9);
return 0;
}Ten program generuje wynik:
9 8 7 6 5 4 3 2 1 1 2 3 4 5 6 7 8 9
Czy to fajne, czy co? Daliśmy wywołującemu możliwość kontrolowania sposobu, w jaki sortowanie przez wybór wykonuje swoją pracę.
Wywołujący może nawet zdefiniować własne „dziwne” funkcje porównawcze:
bool evensFirst(int x, int y)
{
// if x is even and y is odd, x goes first (no swap needed)
if ((x % 2 == 0) && !(y % 2 == 0))
return false;
// if x is odd and y is even, y goes first (swap needed)
if (!(x % 2 == 0) && (y % 2 == 0))
return true;
// otherwise sort in ascending order
return ascending(x, y);
}
int main()
{
int array[9]{ 3, 7, 9, 5, 6, 1, 8, 2, 4 };
selectionSort(array, 9, evensFirst);
printArray(array, 9);
return 0;
}Powyższy fragment daje następujący wynik:
2 4 6 8 1 3 5 7 9
Jak widzisz, użycie wskaźnika funkcji w tym kontekście to dobry sposób na umożliwienie wywołującemu „podczepienia” własnej funkcjonalności do czegoś, co wcześniej napisałeś i przetestowałeś, co ułatwia ponowne użycie kodu! Poprzednio, jeśli chcieliśmy posortować jedną tablicę w porządku malejącym, a drugą w porządku rosnącym, potrzebowaliśmy wielu wersji procedury sortowania. Teraz możesz mieć jedną wersję, która może sortować w dowolny sposób!
Uwaga: Jeśli parametr funkcji jest typu funkcji, zostanie on przekonwertowany na wskaźnik do typu funkcji. Oznacza to, że:
void selectionSort(int* array, int size, bool (*comparisonFcn)(int, int))można równoważnie zapisać jako:
void selectionSort(int* array, int size, bool comparisonFcn(int, int))Działa to tylko w przypadku parametrów funkcji, a zatem ma nieco ograniczone zastosowanie. W przypadku parametru niefunkcyjnego ten ostatni jest interpretowany jako deklaracja forward:
bool (*ptr)(int, int); // definition of function pointer ptr
bool fcn(int, int); // forward declaration of function fcnUdostępnianie funkcji domyślnych
Jeśli chcesz pozwolić wywołującemu przekazywać funkcję jako parametr, często przydatne może być udostępnienie dzwoniącemu pewnych standardowych funkcji, z których będzie mógł korzystać dla własnej wygody. Na przykład w powyższym przykładzie sortowania przez wybór udostępnienie funkcji rosnąco() i malejąco() wraz z funkcją choiceSort() ułatwiłoby życie dzwoniącemu, ponieważ nie musiałby on przepisywać funkcji rosnąco() lub malejąco() za każdym razem, gdy chce ich użyć.
Możesz nawet ustawić jeden z nich jako parametr domyślny:
// Default the sort to ascending sort
void selectionSort(int* array, int size, bool (*comparisonFcn)(int, int) = ascending);W tym przypadku, o ile użytkownik normalnie wywoła wybórSort (nie przez wskaźnik funkcji), parametr porównaniaFcn będzie domyślnie ustawiony na wartość rosnącą. Będziesz musiał upewnić się, że ascending funkcja została zadeklarowana przed tym punktem, w przeciwnym razie kompilator będzie narzekał, że nie wie co ascending jest.
Ulepszanie wskaźników funkcji za pomocą aliasów typów
Spójrzmy prawdzie w oczy — składnia wskaźników do funkcji jest brzydka. Jednakże aliasów typów można używać, aby wskaźniki do funkcji wyglądały bardziej jak zwykłe zmienne:
using ValidateFunction = bool(*)(int, int);Definiuje to alias typu o nazwie „ValidateFunction”, który jest wskaźnikiem do funkcji, która pobiera dwie wartości typu int i zwraca wartość bool.
Teraz zamiast to robić:
bool validate(int x, int y, bool (*fcnPtr)(int, int)); // uglyMożesz to zrobić:
bool validate(int x, int y, ValidateFunction pfcn) // cleanUżywając std::function
Alternatywną metodą definiowania i przechowywania wskaźników funkcji jest użycie std::function, która jest częścią nagłówka <funkcjonalnej> biblioteki standardowej. Aby zdefiniować wskaźnik funkcji przy użyciu tej metody, zadeklaruj obiekt std::function w następujący sposób:
#include <functional>
bool validate(int x, int y, std::function<bool(int, int)> fcn); // std::function method that returns a bool and takes two int parametersJak widać, zarówno typ zwracany, jak i parametry są podawane w nawiasach kątowych, a parametry w nawiasach. Jeśli nie ma parametrów, nawiasy można pozostawić puste.
Aktualizacja naszego wcześniejszego przykładu za pomocą std::function:
#include <functional>
#include <iostream>
int foo()
{
return 5;
}
int goo()
{
return 6;
}
int main()
{
std::function<int()> fcnPtr{ &foo }; // declare function pointer that returns an int and takes no parameters
fcnPtr = &goo; // fcnPtr now points to function goo
std::cout << fcnPtr() << '\n'; // call the function just like normal
std::function fcnPtr2{ &foo }; // can also use CTAD to infer template arguments
return 0;
}Aliasing typu std::function może być pomocny dla czytelności:
using ValidateFunctionRaw = bool(*)(int, int); // type alias to raw function pointer
using ValidateFunction = std::function<bool(int, int)>; // type alias to std::functionPamiętaj również, że std::function pozwala na wywołanie funkcji tylko poprzez niejawne wyłuskanie (np. fcnPtr()), a nie jawne wyłuskanie (np. (*fcnPtr)()).
Podczas definiowania aliasu typu musimy jawnie określić argumenty szablonu. Nie możemy w tym przypadku użyć CTAD, ponieważ nie ma inicjatora, z którego można by wydedukować argumenty szablonu.
Wnioskowanie o typie dla wskaźników funkcji
Podobnie jak automatyczny słowo kluczowe może być użyte do wywnioskowania typu normalnych zmiennych, automatyczny słowo kluczowe może również wnioskować o typie wskaźnika funkcji.
#include <iostream>
int foo(int x)
{
return x;
}
int main()
{
auto fcnPtr{ &foo };
std::cout << fcnPtr(5) << '\n';
return 0;
}Działa to dokładnie tak, jak można się spodziewać, a składnia jest bardzo przejrzysta. Minusem jest oczywiście to, że wszystkie szczegóły dotyczące typów parametrów funkcji i typu zwracanych wartości są ukryte, więc łatwiej jest popełnić błąd podczas wywoływania funkcji lub używania jej zwracanej wartości.
Wnioski
Wskaźniki funkcji są przydatne przede wszystkim wtedy, gdy chcesz do przechowywania funkcji w tablicy (lub innej strukturze) lub gdy musisz przekazać funkcję do innej funkcji, ponieważ natywna składnia deklarowania wskaźników funkcji jest brzydka i podatna na błędy, zalecamy użycie std::function. W miejscach, gdzie typ wskaźnika funkcji jest używany tylko raz (np. pojedynczy parametr lub zwracana wartość), std::function można użyć bezpośrednio w miejscach, gdzie typ wskaźnika funkcji jest używany wielokrotnie, lepszym wyborem jest alias typu do std::function (aby zapobiec powtarzaniu). siebie).
Czas quizu
- W tym quizie napiszemy wersję naszego podstawowego kalkulatora wykorzystującego wskaźniki funkcji.
1a) Stwórz krótki program proszący użytkownika o podanie dwóch liczb całkowitych i wykonanie operacji matematycznej („+”, „-”, „*”, „/”). Upewnij się, że użytkownik wprowadził poprawną operację.
1b) Napisz funkcje o nazwach add(), subtract(), multiply() i dziel(). Powinny one przyjmować dwa parametry całkowite i zwracać liczbę całkowitą.
1c) Utwórz alias typu o nazwie ArithmeticFunction dla wskaźnika do funkcji, która przyjmuje dwa parametry całkowite i zwraca liczbę całkowitą. Użyj std::function i dołącz odpowiedni nagłówek.
1d) Napisz funkcję o nazwie getArithmeticFunction(), która przyjmuje znak operatora i zwraca odpowiednią funkcję jako wskaźnik funkcji.
1e) Zmodyfikuj funkcję main(), aby wywoływała getArithmeticFunction(). Wywołaj wartość zwracaną przez tę funkcję za pomocą swoich danych wejściowych i wydrukuj wynik.
Oto pełny program:

