В последнее время у меня был некоторый опыт работы с указателями на функции в C.
Продолжая традицию отвечать на ваши собственные вопросы, я решил сделать небольшое краткое изложение самых основ, для тех, кто нуждается в быстром погружении в предмет.
c
function-pointers
Ювал Адам
источник
источник
Ответы:
Функциональные указатели в C
Давайте начнем с базовой функции, на которую мы будем указывать :
Прежде всего, давайте определим указатель на функцию, которая получает 2
int
с и возвращаетint
:Теперь мы можем смело указывать на нашу функцию:
Теперь, когда у нас есть указатель на функцию, давайте использовать ее:
Передача указателя на другую функцию в основном такая же:
Мы также можем использовать указатели на функции в возвращаемых значениях (старайтесь не отставать, это становится грязным):
Но гораздо приятнее использовать
typedef
:источник
pshufb
, он медленный, поэтому более ранняя реализация все еще быстрее. x264 / x265 используют это широко и с открытым исходным кодом.Указатели на функции в C могут использоваться для выполнения объектно-ориентированного программирования в C.
Например, следующие строки написаны на C:
Да,
->
и отсутствиеnew
оператора - это полная отдача, но, похоже, это подразумевает, что мы устанавливаем текст некоторогоString
класса"hello"
.Используя указатели на функции, можно эмулировать методы в Си .
Как это достигается?
String
Класс фактическиstruct
с кучей указателей на функции , которые действуют как способ для имитации методов. Ниже приведено частичное объявлениеString
класса:Как видно, методы
String
класса на самом деле являются указателями на функции для объявленной функции. При подготовке экземпляраString
, тоnewString
функция вызывается для того , чтобы настроить функции указатели на соответствующие функции:Например,
getString
функция, которая вызывается путем вызоваget
метода, определяется следующим образом:Одна вещь, которую можно заметить, заключается в том, что не существует понятия экземпляра объекта и наличия методов, которые на самом деле являются частью объекта, поэтому «собственный объект» должен передаваться при каждом вызове. (И
internal
это просто скрытое,struct
которое было опущено в листинге кода ранее - это способ выполнения скрытия информации, но это не относится к указателям на функции.)Таким образом, вместо того, чтобы делать это
s1->set("hello");
, нужно передать объект для выполнения действияs1->set(s1, "hello")
.С этим незначительным объяснением , имеющим передать в ссылке на себя из пути, мы перейдем к следующей части, которая является наследование в C .
Допустим, мы хотим сделать подкласс
String
, скажем,ImmutableString
. Для того чтобы сделать строку неизменной,set
метод не будет доступен, сохраняя доступ кget
иlength
, и вынуждая «конструктор» принятьchar*
:По сути, для всех подклассов доступные методы снова являются указателями на функции. На этот раз объявление для
set
метода отсутствует, поэтому его нельзя вызвать вImmutableString
.Что касается реализации
ImmutableString
, единственным соответствующим кодом является функция "конструктор", то естьnewImmutableString
:В инстанцирования
ImmutableString
, функциональные указатели наget
иlength
методы на самом деле относятся кString.get
иString.length
методе, пройдя черезbase
переменный , которая внутренне хранитсяString
объект.Использование указателя на функцию может обеспечить наследование метода от суперкласса.
Далее мы можем продолжать полиморфизм в C .
Например, если мы хотим изменить поведение
length
метода, чтобы он по какой-то причине возвращал0
все время вImmutableString
классе, все, что нужно сделать, это:length
методом.length
метод переопределения .Добавление переопределяющего
length
метода вImmutableString
может быть выполнено путем добавленияlengthOverrideMethod
:Затем указатель функции для
length
метода в конструкторе подключается кlengthOverrideMethod
:Теперь вместо того, чтобы вести себя как
length
метод вImmutableString
классе сString
классом, теперьlength
метод будет ссылаться на поведение, определенное вlengthOverrideMethod
функции.Я должен добавить заявление об отказе от ответственности, которое я все еще изучаю, как писать в объектно-ориентированном стиле программирования на C, так что, вероятно, есть моменты, которые я не очень хорошо объяснил, или они могут быть просто неуместны с точки зрения того, как лучше реализовать OOP в C. Но моя цель состояла в том, чтобы попытаться проиллюстрировать одно из многих применений указателей функций.
Для получения дополнительной информации о том, как выполнить объектно-ориентированное программирование на C, пожалуйста, обратитесь к следующим вопросам:
источник
ClassName_methodName
соглашения об именовании некоторых функций. Только тогда вы получите те же затраты времени выполнения и хранилища, что и в C ++ и Pascal.Руководство по увольнению: как злоупотреблять указателями функций в GCC на компьютерах с архитектурой x86 путем компиляции кода вручную:
Эти строковые литералы являются байтами 32-битного машинного кода x86.
0xC3
является x86 -ret
инструкций .Обычно вы не пишете их вручную, вы пишете на ассемблере, а затем используете ассемблер, например,
nasm
чтобы собрать его в плоский двоичный файл, который вы зашифруете в строковый литерал C.Возвращает текущее значение в регистре EAX
Написать функцию подкачки
Запишите счетчик цикла для 1000, каждый раз вызывая некоторую функцию
Вы даже можете написать рекурсивную функцию, которая считает до 100
Обратите внимание, что компиляторы размещают строковые литералы в
.rodata
разделе (или.rdata
в Windows), который связан как часть текстового сегмента (вместе с кодом для функций).Текстовый сегмент имеет разрешения Read + Exec, поэтому литье строковых литералов указателей на функцию работы без необходимости
mprotect()
илиVirtualProtect()
системных вызовов , как вам нужно динамически распределяемой памяти. (Илиgcc -z execstack
связывает программу со стеком + сегмент данных + исполняемый файл кучи, как быстрый взлом.)Чтобы разобрать их, вы можете скомпилировать их, чтобы поместить метку в байты, и использовать дизассемблер.
Компилируя
gcc -c -m32 foo.c
и разбирая с помощьюobjdump -D -rwC -Mintel
, мы можем получить сборку и обнаружить, что этот код нарушает ABI, забивая EBX (регистр, сохраняющий вызов), и, как правило, неэффективен.Этот машинный код (вероятно) будет работать в 32-битном коде в Windows, Linux, OS X и т. Д.: Соглашения о вызовах по умолчанию во всех этих ОС передают аргументы в стеке, а не более эффективно в регистрах. Но EBX сохраняется во всех обычных соглашениях о вызовах, поэтому использование его в качестве рабочего регистра без сохранения / восстановления может легко вызвать сбой вызывающего абонента.
источник
Одно из моих любимых применений для указателей на функции - это дешевые и простые итераторы -
источник
int (*cb)(void *arg, ...)
. Возвращаемое значение итератора также позволяет мне остановиться рано (если не ноль).Указатели на функции легко объявляются, когда у вас есть основные деклараторы:
ID
: ID является*D
: D указательD(<parameters>)
: D функция , принимающие<
параметры>
возвращаютсяВ то время как D - еще один декларатор, созданный по тем же правилам. В конце концов, где-то это заканчивается
ID
(см. Пример ниже), которое является именем объявленной сущности. Давайте попробуем построить функцию, которая берет указатель на функцию, которая ничего не берет и возвращает int, и возвращает указатель на функцию, которая принимает char и возвращает int. С type-def это такКак видите, его довольно легко собрать с помощью typedefs. Без typedefs это не сложно и с указанными выше правилами декларатора, применяемыми последовательно. Как видите, я пропустил ту часть, на которую указывает указатель, и функцию, которую возвращает функция. Это то, что появляется в самом левом углу объявления, и не представляет интереса: оно добавляется в конце, если уже создан декларатор. Давайте сделаем это. Построить его последовательно, в первую очередь, - показать структуру, используя
[
и]
:Как видите, можно полностью описать тип, добавив деклараторы один за другим. Строительство может быть сделано двумя способами. Один - снизу вверх, начиная с самой правильной вещи (уходит) и проходя путь до идентификатора. Другой способ - сверху вниз, начиная с идентификатора, работая вниз до листьев. Я покажу оба пути.
Вверх дном
Построение начинается с правильной вещи: возвращенная вещь, то есть функция, принимающая символ. Чтобы отличить деклараторов, я собираюсь перечислить их:
Вставил параметр char напрямую, так как он тривиален. Добавление указателя на декларатор путем замены
D1
на*D2
. Обратите внимание, что мы должны обернуть круглые скобки*D2
. Это можно узнать, посмотрев приоритет*-operator
оператора и вызова функции()
. Без наших скобок компилятор прочитал бы это как*(D2(char p))
. Но это*D2
, конечно , больше не будет простой заменой D1 . Круглые скобки всегда разрешены вокруг деклараторов. Таким образом, вы не сделаете ничего плохого, если добавите слишком много из них, на самом деле.Тип возврата завершен! Теперь давайте заменим
D2
на функцию объявления функции, принимающую<parameters>
возврат ,D3(<parameters>)
что мы и делаем сейчас.Обратите внимание, что скобки не нужны, так как на этот раз мы хотим
D3
быть декларатором функции, а не декларатором указателя. Отлично, осталось только параметры для него. Параметр выполняется точно так же, как мы сделали тип возвращаемого значения, только сchar
заменой наvoid
. Поэтому я скопирую это:Я заменил
D2
наID1
, так как мы закончили с этим параметром (это уже указатель на функцию - нет необходимости в другом деклараторе).ID1
будет именем параметра. Теперь, как я сказал выше, в конце добавляется тип, который модифицируют все эти деклараторы - тот, который появляется слева от каждого объявления. Для функций это становится типом возврата. Для указателей, указывающих на тип и т. Д. Интересно, что когда вы записываете тип, он будет отображаться в обратном порядке, в самом праве :) В любом случае, его замена приводит к полному объявлению. Оба разаint
конечно.Я назвал идентификатор функции
ID0
в этом примере.Сверху вниз
Это начинается с идентификатора слева от описания типа, оборачивая этот декларатор, пока мы идем по правому пути. Начните с функции, возвращающей
<
параметры>
Следующим в описании (после «возврата») был указатель на . Давайте включим это:
Тогда следующая вещь была functon с
<
параметрами>
возвращения . Параметр представляет собой простой символ, поэтому мы сразу добавляем его, поскольку он действительно тривиален.Обратите внимание на круглые скобки мы добавили, так как мы еще раз хотим, чтобы
*
связывает первый, а затем(char)
. В противном случае это читало бы функцию принимает<
параметры>
функции , возвращающие ... . Нет, функции, возвращающие функции, даже не разрешены.Теперь нам просто нужно поставить
<
параметры>
. Я покажу краткую версию вывода, так как я думаю, что у вас уже есть идея, как это сделать.Просто поставьте
int
перед деклараторами, как мы сделали с восходящим, и мы закончилиХорошая вещь
Лучше снизу вверх или сверху вниз? Я привык снизу вверх, но некоторые люди могут чувствовать себя более комфортно с сверху вниз. Это вопрос вкуса, я думаю. Кстати, если вы примените все операторы в этой декларации, вы получите int:
Это хорошее свойство объявлений в C: Объявление утверждает, что если эти операторы используются в выражении, использующем идентификатор, то это приводит к типу слева. Так же и для массивов.
Надеюсь, вам понравился этот маленький урок! Теперь мы можем ссылаться на это, когда люди задаются вопросом о странном синтаксисе объявления функций. Я пытался поставить как можно меньше C внутренних органов. Не стесняйтесь редактировать / исправлять вещи в нем.
источник
Еще одно хорошее применение для указателей функций:
безболезненное переключение между версиями
Их очень удобно использовать, когда вам нужны разные функции в разное время или на разных этапах разработки. Например, я разрабатываю приложение на главном компьютере с консолью, но окончательный выпуск программного обеспечения будет помещен на Avnet ZedBoard (который имеет порты для дисплеев и консолей, но они не нужны / не нужны для Окончательный релиз). Поэтому во время разработки я буду использовать
printf
для просмотра сообщений о состоянии и ошибках, но когда я закончу, я не хочу ничего печатать. Вот что я сделал:version.h
В
version.c
I определит функцию 2 прототипов , присутствующие вversion.h
version.c
Обратите внимание, как указатель на функцию прототипируется
version.h
какvoid (* zprintf)(const char *, ...);
Когда на него ссылаются в приложении, оно начинает исполняться там, куда указывает, что еще не определено.
В
version.c
, обратите внимание, вboard_init()
функции, гдеzprintf
назначена уникальная функция (чья сигнатура функции совпадает) в зависимости от версии, определенной вversion.h
zprintf = &printf;
zprintf вызывает printf для отладкиили
zprintf = &noprint;
zprintf просто возвращает и не будет запускать ненужный кодЗапуск кода будет выглядеть так:
mainProg.c
Приведенный выше код будет использоваться
printf
в режиме отладки или ничего не делать в режиме выпуска. Это гораздо проще, чем просматривать весь проект, комментировать или удалять код. Все, что мне нужно сделать, это изменить версию,version.h
а код сделает все остальное!источник
Указатель на функцию обычно определяется
typedef
и используется как параметр и возвращаемое значение.Выше ответы уже многое объяснили, просто приведу полный пример:
источник
Одним из наиболее важных применений указателей функций в C является вызов функции, выбранной во время выполнения. Например, библиотека C во время выполнения имеет две процедуры,
qsort
иbsearch
, которые принимают указатель на функцию, которая вызывается для сравнения двух элементов сортируется; это позволяет вам сортировать или искать, соответственно, что угодно, основываясь на любых критериях, которые вы хотите использовать.Очень простой пример, если есть одна вызванная функция,
print(int x, int y)
которая, в свою очередь, может потребовать вызова функции (add()
илиsub()
, того же типа), то, что мы будем делать, мы добавим один аргумент указателя наprint()
функцию, как показано ниже :Выход:
источник
Функция запуска с нуля имеет некоторый адрес памяти, с которого они начинают выполняться. В ассемблере они называются как (вызов «адрес памяти функции»). Теперь возвращаемся к C Если функция имеет адрес памяти, то они могут управляться указателями в C. Так по правилам C
1. Сначала вам нужно объявить указатель на функцию 2. Передать адрес желаемой функции
**** Примечание-> функции должны быть одного типа ****
Эта простая программа проиллюстрирует каждую вещь.
После этого давайте посмотрим, как машина понимает их. Взгляд на машинную инструкцию вышеупомянутой программы в 32-битной архитектуре.
Область красной метки показывает, как происходит обмен и сохранение адреса в eax. Тогда это инструкция вызова на eax. Eax содержит желаемый адрес функции.
источник
Указатель на функцию - это переменная, которая содержит адрес функции. Поскольку это переменная-указатель, хотя и с некоторыми ограниченными свойствами, вы можете использовать ее почти так же, как и любую другую переменную-указатель в структурах данных.
Единственное исключение, о котором я могу думать, - это обращение к указателю на функцию, указывающее на что-то, отличное от одного значения. Делать арифметику указателя, увеличивая или уменьшая указатель на функцию или добавляя / вычитая смещение к указателю на функцию, на самом деле не имеет никакого смысла, так как указатель на функцию указывает только на одну вещь - точку входа в функцию.
Размер переменной указателя функции, количество байтов, занимаемых этой переменной, может варьироваться в зависимости от базовой архитектуры, например, x32 или x64 или чего-либо еще.
Объявление для переменной указателя функции должно указывать ту же информацию, что и объявление функции, чтобы компилятор C мог выполнять те виды проверок, которые он обычно делает. Если вы не укажете список параметров в объявлении / определении указателя функции, компилятор C не сможет проверить использование параметров. Есть случаи, когда это отсутствие проверки может быть полезным, однако просто помните, что сеть безопасности была удалена.
Некоторые примеры:
Первые два объявления несколько похожи в этом:
func
это функция, которая принимаетint
иchar *
и возвращаетint
pFunc
является указателем на функцию , к которой присваивается адрес функции , которая принимаетint
иchar *
и возвращаетint
Таким образом, из вышесказанного может быть строка источника, в которой адрес функции
func()
назначен переменной указателя функции,pFunc
как вpFunc = func;
.Обратите внимание на синтаксис, используемый с объявлением / определением указателя функции, в котором скобки используются для преодоления естественных правил приоритета операторов.
Несколько различных примеров использования
Некоторые примеры использования указателя на функцию:
Вы можете использовать списки параметров переменной длины в определении указателя функции.
Или вы не можете указать список параметров вообще. Это может быть полезно, но исключает возможность для компилятора C выполнять проверки по предоставленному списку аргументов.
C стиль бросает
Вы можете использовать приведения в стиле C с указателями на функции. Однако имейте в виду, что компилятор C может быть небрежным в отношении проверок или предоставлять предупреждения, а не ошибки.
Сравните указатель функции с равенством
Вы можете проверить, что указатель функции равен определенному адресу функции, используя
if
инструкцию, хотя я не уверен, насколько это было бы полезно. Другие операторы сравнения могут показаться еще менее полезными.Массив указателей на функции
И если вы хотите иметь массив указателей на функции, каждый из элементов которых имеет список аргументов, то вы можете определить указатель функции с неопределенным списком аргументов (не
void
означающим никаких аргументов, а просто неуказанным), что-то вроде следующего, хотя вы может видеть предупреждения от компилятора C. Это также работает для параметра указателя на функцию:Стиль C
namespace
Использование Globalstruct
с указателями на функцииВы можете использовать
static
ключевое слово, чтобы указать функцию, имя которой является областью действия файла, а затем назначить ее глобальной переменной, чтобы обеспечить нечто похожее наnamespace
функциональность C ++.В заголовочном файле определите структуру, которая будет нашим пространством имен, вместе с глобальной переменной, которая ее использует.
Затем в исходном файле C:
Затем это можно использовать, указав полное имя глобальной переменной структуры и имя члена для доступа к функции.
const
Модификатор используется на глобальном , так что он не может быть изменен случайно.Области применения указателей функций
Компонент библиотеки DLL может делать что-то похожее на
namespace
подход в стиле C, в котором конкретный интерфейс библиотеки запрашивается из фабричного метода в интерфейсе библиотеки, который поддерживает созданиеstruct
указателей с содержащимися функциями. Этот интерфейс библиотеки загружает запрошенную версию DLL, создает структура с необходимыми указателями на функции, а затем возвращает структуру запрашивающей вызывающей стороне для использования.и это может быть использовано как в:
Тот же подход можно использовать для определения абстрактного аппаратного уровня для кода, который использует конкретную модель базового аппаратного обеспечения. Указатели функций заполняются фабрично-специфическими функциями для обеспечения аппаратно-специфической функциональности, которая реализует функции, указанные в абстрактной аппаратной модели. Это может использоваться для предоставления абстрактного аппаратного уровня, используемого программным обеспечением, которое вызывает заводскую функцию, чтобы получить интерфейс конкретной аппаратной функции, а затем использует предоставленные указатели функций для выполнения действий для базового аппаратного обеспечения без необходимости знать подробности реализации о конкретной цели. ,
Указатели на функции для создания делегатов, обработчиков и обратных вызовов
Вы можете использовать указатели на функции как способ делегировать некоторые задачи или функции. Классическим примером в C является указатель функции делегата сравнения, используемый с функциями библиотеки Standard C
qsort()
иbsearch()
обеспечивающий порядок сопоставления для сортировки списка элементов или выполнения двоичного поиска по отсортированному списку элементов. Делегат функции сравнения определяет алгоритм сортировки, используемый при сортировке или двоичном поиске.Другое использование аналогично применению алгоритма к контейнеру стандартной библиотеки шаблонов C ++.
Другой пример - с исходным кодом GUI, в котором зарегистрирован обработчик для определенного события, предоставляя указатель на функцию, которая фактически вызывается, когда происходит событие. Платформа Microsoft MFC с ее картами сообщений использует нечто подобное для обработки сообщений Windows, которые доставляются в окно или поток.
Асинхронные функции, требующие обратного вызова, аналогичны обработчику событий. Пользователь асинхронной функции вызывает асинхронную функцию для запуска некоторого действия и предоставляет указатель на функцию, которую асинхронная функция будет вызывать после завершения действия. В этом случае событие является асинхронной функцией, выполняющей свою задачу.
источник
Поскольку указатели на функции часто являются типизированными обратными вызовами, вы можете взглянуть на безопасные обратные вызовы типа. . То же самое относится к точкам входа и т. Д. Функций, которые не являются обратными вызовами.
С довольно переменчивый и прощающий одновременно :)
источник