Какой смысл указателей на функции?
У меня проблемы с видением полезности указателей на функции. Я думаю, что это может быть полезно в некоторых случаях (в конце концов, они существуют), но я не могу вспомнить случай, когда лучше или неизбежно использовать указатель на функцию.
Не могли бы вы привести пример хорошего использования указателей на функции (в C или C++)?
18 ответов
Большинство примеров сводятся к обратным вызовам: вы вызываете функцию f()
передача адреса другой функции g()
, а также f()
звонки g()
для какой-то конкретной задачи. Если вы пройдете f()
адрес h()
вместо этого тогда f()
перезвоню h()
вместо.
По сути, это способ параметризации функции: некоторая часть ее поведения не жестко запрограммирована в f()
, но в функцию обратного вызова. Абоненты могут сделать f()
ведут себя по-разному, передавая различные функции обратного вызова. Классика это qsort()
из стандартной библиотеки C, которая берет свой критерий сортировки как указатель на функцию сравнения.
В C++ это часто делается с использованием функциональных объектов (также называемых функторами). Это объекты, которые перегружают оператор вызова функции, поэтому вы можете вызывать их, как если бы они были функцией. Пример:
class functor {
public:
void operator()(int i) {std::cout << "the answer is: " << i << '\n';}
};
functor f;
f(42);
Идея заключается в том, что, в отличие от указателя функции, функциональный объект может содержать не только алгоритм, но и данные:
class functor {
public:
functor(const std::string& prompt) : prompt_(prompt) {}
void operator()(int i) {std::cout << prompt_ << i << '\n';}
private:
std::string prompt_;
};
functor f("the answer is: ");
f(42);
Другое преимущество заключается в том, что иногда проще встроить вызовы в объекты функций, чем вызовы через указатели функций. Это причина, почему сортировка в C++ иногда быстрее, чем сортировка в C.
Ну, я обычно использую их (профессионально) в таблицах переходов (см. Также этот вопрос Stackru).
Таблицы переходов обычно (но не исключительно) используются в конечных автоматах, чтобы сделать их управляемыми данными. Вместо вложенного переключателя / кейса
switch (state)
case A:
switch (event):
case e1: ....
case e2: ....
case B:
switch (event):
case e3: ....
case e1: ....
Вы можете сделать 2d массив или указатели на функции и просто вызвать handleEvent[state][event]
Примеры:
- Выборочная сортировка / поиск
- Различные модели (например, Стратегия, Наблюдатель)
- Callbacks
"Классическим" примером полезности указателей на функции является библиотека C qsort()
функция, которая реализует быструю сортировку. Чтобы быть универсальным для любой структуры данных, которую может придумать пользователь, требуется пара пустых указателей на сортируемые данные и указатель на функцию, которая знает, как сравнивать два элемента этих структур данных. Это позволяет нам создавать нашу функцию выбора для работы и фактически даже позволяет выбирать функцию сравнения во время выполнения, например, для сортировки по возрастанию или по убыванию.
Согласитесь со всем вышеперечисленным, плюс.... Когда вы динамически загружаете dll во время выполнения, вам понадобятся указатели функций для вызова функций.
Я собираюсь пойти против течения здесь.
В C указатели на функции являются единственным способом реализации настройки, поскольку OO отсутствует.
В C++ вы можете использовать либо указатели на функции, либо функторы (функциональные объекты) для одного и того же результата.
Функторы имеют ряд преимуществ по сравнению с необработанными указателями на функции из-за их природы объекта, а именно:
- Они могут представлять несколько перегрузок
operator()
- Они могут иметь состояние / ссылку на существующие переменные
- Их можно построить на месте (
lambda
а такжеbind
)
Лично я предпочитаю, чтобы функторы указателей на функции (несмотря на шаблонный код), главным образом потому, что синтаксис для указателей на функции может легко стать проблематичным (из учебника по указателям на функции):
typedef float(*pt2Func)(float, float);
// defines a symbol pt2Func, pointer to a (float, float) -> float function
typedef int (TMyClass::*pt2Member)(float, char, char);
// defines a symbol pt2Member, pointer to a (float, char, char) -> int function
// belonging to the class TMyClass
Единственный раз, когда я видел указатели на функции, которые не могли использовать функторы, был в Boost.Spirit. Они совершенно неправильно использовали синтаксис для передачи произвольного количества параметров в качестве одного параметра шаблона.
typedef SpecialClass<float(float,float)> class_type;
Но так как вариационные шаблоны и лямбды не за горами, я не уверен, что мы будем долго использовать указатели на функции в чистом коде C++.
В C классическим использованием является функция qsort, где четвертый параметр - указатель на функцию, используемую для выполнения упорядочения в сортировке. В C++ можно было бы использовать функторы (объекты, похожие на функции) для такого рода вещей.
I used function pointers recently to create an abstraction layer.
I have a program written in pure C that runs on embedded systems. It supports multiple hardware variants. Depending on the hardware I am running on, it needs to call different versions of some functions.
Во время инициализации программа выясняет, на каком оборудовании она работает, и заполняет указатели функций. Все высокоуровневые подпрограммы в программе просто вызывают функции, на которые ссылаются указатели. Я могу добавить поддержку новых вариантов оборудования, не затрагивая процедуры более высокого уровня.
Раньше я использовал операторы switch / case для выбора правильных версий функций, но это стало непрактичным, так как программа стала поддерживать все больше аппаратных вариантов. Я должен был добавить заявления случая повсюду.
Я также попробовал промежуточные функциональные слои, чтобы выяснить, какую функцию использовать, но они не сильно помогли. Мне все еще приходилось обновлять операторы case в нескольких местах всякий раз, когда мы добавляли новый вариант. С указателями на функции мне нужно только изменить функцию инициализации.
Другая точка зрения, в дополнение к другим хорошим ответам здесь:
В C есть только указатели на функции, нет никаких функций.
Я имею в виду, вы пишете функции, но вы не можете манипулировать функциями. Во время выполнения нет представления функции как таковой. Вы даже не можете назвать "функцию". Когда вы пишете:
my_function(my_arg);
то, что вы на самом деле говорите, "выполнить звонок в my_function
Указатель с указанным аргументом ". Вы делаете вызов через указатель на функцию. Этот распад на указатель на функцию означает, что следующие команды эквивалентны предыдущему вызову функции:
(&my_function)(my_arg);
(*my_function)(my_arg);
(**my_function)(my_arg);
(&**my_function)(my_arg);
(***my_function)(my_arg);
и так далее (спасибо @LuuVinhPhuc).
Итак, вы уже используете указатели функций в качестве значений. Очевидно, что вы хотели бы иметь переменные для этих значений - и вот где все другие применения используются: полиморфизм / настройка (как в qsort), обратные вызовы, таблицы переходов и т. Д.
В C++ все немного сложнее, так как у нас есть лямбды и объекты с operator()
и даже std::function
класс, но принцип все еще в основном тот же.
Как сказал выше Рич, указатели на функции в Windows обычно ссылаются на некоторый адрес, который хранит функцию.
Когда вы программируете в C language
на платформе Windows вы в основном загружаете некоторый файл DLL в основную память (используя LoadLibrary
) и для использования функций, хранящихся в DLL, необходимо создать указатели на функции и указать их адрес (используя GetProcAddress
).
Рекомендации:
Я использовал их в основном CALLBACKS: когда вам нужно сохранить информацию о функции для последующего вызова.
Скажем, вы пишете Bomberman. Через 5 секунд после того, как человек сбросит бомбу, она должна взорваться (позвоните explode()
функция).
Теперь есть 2 способа сделать это. Один из способов - "проверить" все бомбы на экране, чтобы увидеть, готовы ли они взорваться в основном цикле.
foreach bomb in game
if bomb.boomtime()
bomb.explode()
Другой способ - прикрепить обратный вызов к вашей системе часов. Когда бомба заложена, вы добавляете обратный вызов, чтобы она вызывала bomb.explode(), когда настало время.
// user placed a bomb
Bomb* bomb = new Bomb()
make callback( function=bomb.explode, time=5 seconds ) ;
// IN the main loop:
foreach callback in callbacks
if callback.timeToRun
callback.function()
Вот callback.function()
может быть любой функцией, потому что это указатель на функцию.
Использование указателя функции
Для динамического вызова функции на основе пользовательского ввода. Создавая карту строки и указатель на функцию в этом случае.
#include<iostream>
#include<map>
using namespace std;
//typedef map<string, int (*)(int x, int y) > funMap;
#define funMap map<string, int (*)(int, int)>
funMap objFunMap;
int Add(int x, int y)
{
return x+y;
}
int Sub(int x, int y)
{
return x-y;
}
int Multi(int x, int y)
{
return x*y;
}
void initializeFunc()
{
objFunMap["Add"]=Add;
objFunMap["Sub"]=Sub;
objFunMap["Multi"]=Multi;
}
int main()
{
initializeFunc();
while(1)
{
string func;
cout<<"Enter your choice( 1. Add 2. Sub 3. Multi) : ";
int no, a, b;
cin>>no;
if(no==1)
func = "Add";
else if(no==2)
func = "Sub";
else if(no==3)
func = "Multi";
else
break;
cout<<"\nEnter 2 no :";
cin>>a>>b;
//function is called using function pointer based on user input
//If user input is 2, and a=10, b=3 then below line will expand as "objFuncMap["Sub"](10, 3)"
int ret = objFunMap[func](a, b);
cout<<ret<<endl;
}
return 0;
}
Таким образом, мы использовали указатель на функцию в нашей реальной балансовой единице. Вы можете написать "n" номер функции и вызвать их, используя этот метод.
ВЫХОД:
Введите ваш выбор ( 1. Добавить 2. Sub 3. Multi): 1 Введите 2 нет:2 4 6 Введите ваш выбор ( 1. Добавить 2. Sub 3. Multi): 2 Введите 2 нет: 10 3 7 Введите ваш выбор ( 1. Добавить 2. Sub 3. Multi): 3 Введите 2 №: 3 6 18
Указатели на функции могут использоваться в C для создания интерфейса для программирования. В зависимости от конкретной функциональности, которая требуется во время выполнения, указателю функции может быть назначена другая реализация.
Для ОО-языков, чтобы выполнить полиморфные вызовы за кулисами (это также верно для C до некоторой степени, я думаю).
Более того, они очень полезны для введения другого поведения в другую функцию (foo) во время выполнения. Это делает функцию foo функцией высшего порядка. Помимо гибкости это делает код foo более читабельным, поскольку он позволяет вам извлечь из этого лишнюю логику "если-еще".
Это позволяет много других полезных вещей в Python, таких как генераторы, замыкания и т. Д.
Я широко использую указатели функций для эмуляции микропроцессоров с 1-байтовыми кодами операций. Массив из 256 функциональных указателей является естественным способом реализации этого.
Я постараюсь дать здесь несколько исчерпывающий список:
Обратные вызовы: настройте некоторые (библиотечные) функции с помощью кода, предоставленного пользователем. Ярким примером является
qsort()
, но также полезно для обработки событий (например, кнопки, вызывающей обратный вызов при нажатии) или необходимости для запуска потока (pthread_create()
).Полиморфизм: vtable в классе C++ - это не что иное, как таблица указателей на функции. И программа на C может также предоставить vtable для некоторых своих объектов:
struct Base; struct Base_vtable { void (*destruct)(struct Base* me); }; struct Base { struct Base_vtable* vtable; }; struct Derived; struct Derived_vtable { struct Base_vtable; void (*frobnicate)(struct Derived* me); }; struct Derived { struct Base; int bar, baz; }
Конструктор
Derived
затем установил быvtable
переменная-член в глобальный объект с реализациями производного классаdestruct
а такжеfrobnicate
, и код, необходимый для разрушенияstruct Base*
просто позвонил быbase->vtable->destruct(base)
, который вызовет правильную версию деструктора, независимо от производного классаbase
на самом деле указывает на.Без указателей на функции полиморфизм нужно было бы закодировать с помощью целой армии конструкций переключателей, таких как
switch(me->type) { case TYPE_BASE: base_implementation(); break; case TYPE_DERIVED1: derived1_implementation(); break; case TYPE_DERIVED2: derived2_implementation(); break; case TYPE_DERIVED3: derived3_implementation(); break; }
Это довольно быстро становится довольно громоздким.
Динамически загружаемый код: когда программа загружает модуль в память и пытается вызвать его код, она должна пройти через указатель функции.
Все виды использования указателей на функции, которые я видел, попадают в один из этих трех широких классов.
Одно использование указателя на функцию может быть в том случае, когда мы можем не захотеть модифицировать код, в котором вызывается функция (то есть вызов может быть условным, и при других условиях нам нужно выполнять различный вид обработки). Здесь указатели на функции очень удобны, поскольку нам не нужно изменять код в том месте, где вызывается функция. Мы просто вызываем функцию, используя указатель на функцию с соответствующими аргументами. Указатель на функцию можно сделать так, чтобы он указывал на разные функции условно. (Это можно сделать где-нибудь на этапе инициализации). Более того, приведенная выше модель очень полезна, если мы не в состоянии изменить код, из которого она вызывается (предположим, что это библиотечный API, который мы не можем изменить). API использует указатель на функцию для вызова соответствующей определенной пользователем функции.
Они улучшают повторное использование и модульность кода, делая код более удобным для сопровождения, читабельным и менее подверженным ошибкам.
С указателями функций:
Обратите внимание, как у нас естьiterator
метод, который получает указатель на функцию. Этот указатель функции говорит нам, что мы должны делать с каждым элементом в списке.
#include <iostream>
#include <vector>
int square(int x) {
return x * x;
}
int root(int x) {
return sqrt(x);
}
int negative(int x) {
return -x;
}
std::vector<int> listIterator(std::vector<int> list, int (*itemOperation)(int)) {
for (int i = 0; i < list.size(); i++) {
list[i] = itemOperation(list[i]);
}
return list;
}
int main() {
std::vector<int> list = { 9, 16, 4, 25 };
for (int i : listIterator(list, square)) {
std::cout << i << ' ';
}
std::cout << std::endl;
for (int i : listIterator(list, root)) {
std::cout << i << ' ';
}
std::cout << std::endl;
for (int i : listIterator(list, negative)) {
std::cout << i << ' ';
}
return 0;
}
Без указателей функций:
Без указателей на функции вам пришлось бы включать итератор в каждый изsquare
,root
иnegative
методы.