Как работают указатели на функции в C?

1235

В последнее время у меня был некоторый опыт работы с указателями на функции в C.

Продолжая традицию отвечать на ваши собственные вопросы, я решил сделать небольшое краткое изложение самых основ, для тех, кто нуждается в быстром погружении в предмет.

Ювал Адам
источник
35
Также: Для небольшого углубленного анализа указателей C, см. Blogs.oracle.com/ksplice/entry/the_ksplice_pointer_challenge . Кроме того, программирование с нуля показывает, как они работают на уровне машины. Понимание «модели памяти» Си очень полезно для понимания работы указателей Си.
Аббафей
8
Отличная информация Хотя по названию я бы ожидал увидеть объяснение того, как «работают указатели функций», а не как они кодируются :)
Богдан Александру

Ответы:

1478

Функциональные указатели в C

Давайте начнем с базовой функции, на которую мы будем указывать :

int addInt(int n, int m) {
    return n+m;
}

Прежде всего, давайте определим указатель на функцию, которая получает 2 intс и возвращает int:

int (*functionPtr)(int,int);

Теперь мы можем смело указывать на нашу функцию:

functionPtr = &addInt;

Теперь, когда у нас есть указатель на функцию, давайте использовать ее:

int sum = (*functionPtr)(2, 3); // sum == 5

Передача указателя на другую функцию в основном такая же:

int add2to3(int (*functionPtr)(int, int)) {
    return (*functionPtr)(2, 3);
}

Мы также можем использовать указатели на функции в возвращаемых значениях (старайтесь не отставать, это становится грязным):

// this is a function called functionFactory which receives parameter n
// and returns a pointer to another function which receives two ints
// and it returns another int
int (*functionFactory(int n))(int, int) {
    printf("Got parameter %d", n);
    int (*functionPtr)(int,int) = &addInt;
    return functionPtr;
}

Но гораздо приятнее использовать typedef:

typedef int (*myFuncDef)(int, int);
// note that the typedef name is indeed myFuncDef

myFuncDef functionFactory(int n) {
    printf("Got parameter %d", n);
    myFuncDef functionPtr = &addInt;
    return functionPtr;
}
Ювал Адам
источник
19
Спасибо за отличную информацию. Не могли бы вы немного рассказать о том, где используются указатели на функции или они особенно полезны?
Rich.Carpenter
326
"functionPtr = & addInt;" также может быть написано (и часто так) как "functionPtr = addInt;" что также справедливо, так как стандарт говорит, что имя функции в этом контексте преобразуется в адрес функции.
Хловдал
22
хловдал, в этом контексте интересно объяснить, что это то, что позволяет писать functionPtr = ****************** addInt;
Йоханнес Шауб -
105
@ Rich.Carpenter Я знаю, что это на 4 года позже, но я полагаю, что другие люди могут извлечь из этого пользу: указатели на функции полезны для передачи функций в качестве параметров другим функциям . Мне понадобилось много времени, чтобы найти этот ответ по какой-то странной причине. Таким образом, в основном, это дает C псевдо-первоклассную функциональность.
Гигант91
22
@ Rich.Carpenter: функциональные указатели хороши для обнаружения ЦП во время выполнения. Имейте несколько версий некоторых функций, чтобы использовать преимущества SSE, popcnt, AVX и т. Д. При запуске установите указатели функций на лучшую версию каждой функции для текущего процессора. В другом коде просто вызовите указатель на функцию вместо того, чтобы везде иметь условные ветви функций ЦП. Затем вы можете сделать сложную логику для правильного решения, хотя этот процессор поддерживает pshufb, он медленный, поэтому более ранняя реализация все еще быстрее. x264 / x265 используют это широко и с открытым исходным кодом.
Питер Кордес
304

Указатели на функции в C могут использоваться для выполнения объектно-ориентированного программирования в C.

Например, следующие строки написаны на C:

String s1 = newString();
s1->set(s1, "hello");

Да, ->и отсутствие newоператора - это полная отдача, но, похоже, это подразумевает, что мы устанавливаем текст некоторого Stringкласса "hello".

Используя указатели на функции, можно эмулировать методы в Си .

Как это достигается?

StringКласс фактически structс кучей указателей на функции , которые действуют как способ для имитации методов. Ниже приведено частичное объявление Stringкласса:

typedef struct String_Struct* String;

struct String_Struct
{
    char* (*get)(const void* self);
    void (*set)(const void* self, char* value);
    int (*length)(const void* self);
};

char* getString(const void* self);
void setString(const void* self, char* value);
int lengthString(const void* self);

String newString();

Как видно, методы Stringкласса на самом деле являются указателями на функции для объявленной функции. При подготовке экземпляра String, то newStringфункция вызывается для того , чтобы настроить функции указатели на соответствующие функции:

String newString()
{
    String self = (String)malloc(sizeof(struct String_Struct));

    self->get = &getString;
    self->set = &setString;
    self->length = &lengthString;

    self->set(self, "");

    return self;
}

Например, getStringфункция, которая вызывается путем вызова getметода, определяется следующим образом:

char* getString(const void* self_obj)
{
    return ((String)self_obj)->internal->value;
}

Одна вещь, которую можно заметить, заключается в том, что не существует понятия экземпляра объекта и наличия методов, которые на самом деле являются частью объекта, поэтому «собственный объект» должен передаваться при каждом вызове. (И internalэто просто скрытое, structкоторое было опущено в листинге кода ранее - это способ выполнения скрытия информации, но это не относится к указателям на функции.)

Таким образом, вместо того, чтобы делать это s1->set("hello");, нужно передать объект для выполнения действия s1->set(s1, "hello").

С этим незначительным объяснением , имеющим передать в ссылке на себя из пути, мы перейдем к следующей части, которая является наследование в C .

Допустим, мы хотим сделать подкласс String, скажем, ImmutableString. Для того чтобы сделать строку неизменной, setметод не будет доступен, сохраняя доступ к getи length, и вынуждая «конструктор» принять char*:

typedef struct ImmutableString_Struct* ImmutableString;

struct ImmutableString_Struct
{
    String base;

    char* (*get)(const void* self);
    int (*length)(const void* self);
};

ImmutableString newImmutableString(const char* value);

По сути, для всех подклассов доступные методы снова являются указателями на функции. На этот раз объявление для setметода отсутствует, поэтому его нельзя вызвать в ImmutableString.

Что касается реализации ImmutableString, единственным соответствующим кодом является функция "конструктор", то есть newImmutableString:

ImmutableString newImmutableString(const char* value)
{
    ImmutableString self = (ImmutableString)malloc(sizeof(struct ImmutableString_Struct));

    self->base = newString();

    self->get = self->base->get;
    self->length = self->base->length;

    self->base->set(self->base, (char*)value);

    return self;
}

В инстанцирования ImmutableString, функциональные указатели на getи lengthметоды на самом деле относятся к String.getи String.lengthметоде, пройдя через baseпеременный , которая внутренне хранится Stringобъект.

Использование указателя на функцию может обеспечить наследование метода от суперкласса.

Далее мы можем продолжать полиморфизм в C .

Например, если мы хотим изменить поведение lengthметода, чтобы он по какой-то причине возвращал 0все время в ImmutableStringклассе, все, что нужно сделать, это:

  1. Добавьте функцию, которая будет служить переопределяющим lengthметодом.
  2. Перейдите в «конструктор» и установите указатель функции на lengthметод переопределения .

Добавление переопределяющего lengthметода в ImmutableStringможет быть выполнено путем добавления lengthOverrideMethod:

int lengthOverrideMethod(const void* self)
{
    return 0;
}

Затем указатель функции для lengthметода в конструкторе подключается к lengthOverrideMethod:

ImmutableString newImmutableString(const char* value)
{
    ImmutableString self = (ImmutableString)malloc(sizeof(struct ImmutableString_Struct));

    self->base = newString();

    self->get = self->base->get;
    self->length = &lengthOverrideMethod;

    self->base->set(self->base, (char*)value);

    return self;
}

Теперь вместо того, чтобы вести себя как lengthметод в ImmutableStringклассе с Stringклассом, теперь lengthметод будет ссылаться на поведение, определенное в lengthOverrideMethodфункции.

Я должен добавить заявление об отказе от ответственности, которое я все еще изучаю, как писать в объектно-ориентированном стиле программирования на C, так что, вероятно, есть моменты, которые я не очень хорошо объяснил, или они могут быть просто неуместны с точки зрения того, как лучше реализовать OOP в C. Но моя цель состояла в том, чтобы попытаться проиллюстрировать одно из многих применений указателей функций.

Для получения дополнительной информации о том, как выполнить объектно-ориентированное программирование на C, пожалуйста, обратитесь к следующим вопросам:

coobird
источник
22
Этот ответ ужасен! Это не только подразумевает, что ОО как-то зависит от точечной нотации, но и поощряет добавление мусора в ваши объекты!
Алексей Аверченко
27
Это ОО, хорошо, но не где-нибудь рядом с О-стилем. То, что вы безуспешно реализовали, - это OO на основе прототипов в стиле Javascript. Чтобы получить ОО в стиле C ++ / Pascal, вам необходимо: 1. Иметь структуру const для виртуальной таблицы каждого класса с виртуальными членами. 2. Иметь указатель на эту структуру в полиморфных объектах. 3. Вызывайте виртуальные методы через виртуальную таблицу и все другие методы напрямую - обычно придерживаясь ClassName_methodNameсоглашения об именовании некоторых функций. Только тогда вы получите те же затраты времени выполнения и хранилища, что и в C ++ и Pascal.
Восстановить Монику
19
Работа ОО с языком, который не предназначен для ОО, всегда плохая идея. Если вы хотите OO и все еще имеете C, просто работайте с C ++.
rbaleksandar
20
@rbaleksandar Расскажите об этом разработчикам ядра Linux. «всегда плохая идея» - это строго ваше мнение, с которым я решительно не согласен.
Джонатон Рейнхарт
6
Мне нравится этот ответ, но я не разыгрываю malloc
cat
227

Руководство по увольнению: как злоупотреблять указателями функций в GCC на компьютерах с архитектурой x86 путем компиляции кода вручную:

Эти строковые литералы являются байтами 32-битного машинного кода x86. 0xC3является x86 - retинструкций .

Обычно вы не пишете их вручную, вы пишете на ассемблере, а затем используете ассемблер, например, nasmчтобы собрать его в плоский двоичный файл, который вы зашифруете в строковый литерал C.

  1. Возвращает текущее значение в регистре EAX

    int eax = ((int(*)())("\xc3 <- This returns the value of the EAX register"))();
  2. Написать функцию подкачки

    int a = 10, b = 20;
    ((void(*)(int*,int*))"\x8b\x44\x24\x04\x8b\x5c\x24\x08\x8b\x00\x8b\x1b\x31\xc3\x31\xd8\x31\xc3\x8b\x4c\x24\x04\x89\x01\x8b\x4c\x24\x08\x89\x19\xc3 <- This swaps the values of a and b")(&a,&b);
  3. Запишите счетчик цикла для 1000, каждый раз вызывая некоторую функцию

    ((int(*)())"\x66\x31\xc0\x8b\x5c\x24\x04\x66\x40\x50\xff\xd3\x58\x66\x3d\xe8\x03\x75\xf4\xc3")(&function); // calls function with 1->1000
  4. Вы даже можете написать рекурсивную функцию, которая считает до 100

    const char* lol = "\x8b\x5c\x24\x4\x3d\xe8\x3\x0\x0\x7e\x2\x31\xc0\x83\xf8\x64\x7d\x6\x40\x53\xff\xd3\x5b\xc3\xc3 <- Recursively calls the function at address lol.";
    i = ((int(*)())(lol))(lol);

Обратите внимание, что компиляторы размещают строковые литералы в .rodataразделе (или .rdataв Windows), который связан как часть текстового сегмента (вместе с кодом для функций).

Текстовый сегмент имеет разрешения Read + Exec, поэтому литье строковых литералов указателей на функцию работы без необходимости mprotect()или VirtualProtect()системных вызовов , как вам нужно динамически распределяемой памяти. (Или gcc -z execstackсвязывает программу со стеком + сегмент данных + исполняемый файл кучи, как быстрый взлом.)


Чтобы разобрать их, вы можете скомпилировать их, чтобы поместить метку в байты, и использовать дизассемблер.

// at global scope
const char swap[] = "\x8b\x44\x24\x04\x8b\x5c\x24\x08\x8b\x00\x8b\x1b\x31\xc3\x31\xd8\x31\xc3\x8b\x4c\x24\x04\x89\x01\x8b\x4c\x24\x08\x89\x19\xc3 <- This swaps the values of a and b";

Компилируя gcc -c -m32 foo.cи разбирая с помощью objdump -D -rwC -Mintel, мы можем получить сборку и обнаружить, что этот код нарушает ABI, забивая EBX (регистр, сохраняющий вызов), и, как правило, неэффективен.

00000000 <swap>:
   0:   8b 44 24 04             mov    eax,DWORD PTR [esp+0x4]   # load int *a arg from the stack
   4:   8b 5c 24 08             mov    ebx,DWORD PTR [esp+0x8]   # ebx = b
   8:   8b 00                   mov    eax,DWORD PTR [eax]       # dereference: eax = *a
   a:   8b 1b                   mov    ebx,DWORD PTR [ebx]
   c:   31 c3                   xor    ebx,eax                # pointless xor-swap
   e:   31 d8                   xor    eax,ebx                # instead of just storing with opposite registers
  10:   31 c3                   xor    ebx,eax
  12:   8b 4c 24 04             mov    ecx,DWORD PTR [esp+0x4]  # reload a from the stack
  16:   89 01                   mov    DWORD PTR [ecx],eax     # store to *a
  18:   8b 4c 24 08             mov    ecx,DWORD PTR [esp+0x8]
  1c:   89 19                   mov    DWORD PTR [ecx],ebx
  1e:   c3                      ret    

  not shown: the later bytes are ASCII text documentation
  they're not executed by the CPU because the ret instruction sends execution back to the caller

Этот машинный код (вероятно) будет работать в 32-битном коде в Windows, Linux, OS X и т. Д.: Соглашения о вызовах по умолчанию во всех этих ОС передают аргументы в стеке, а не более эффективно в регистрах. Но EBX сохраняется во всех обычных соглашениях о вызовах, поэтому использование его в качестве рабочего регистра без сохранения / восстановления может легко вызвать сбой вызывающего абонента.

Lee
источник
8
Примечание: это не работает, если включена функция предотвращения выполнения данных (например, в Windows XP SP2 +), поскольку строки C обычно не помечаются как исполняемые.
SecurityMatt
5
Привет Мэтт! В зависимости от уровня оптимизации, GCC часто вставляет строковые константы в сегмент TEXT, так что это будет работать даже на более новых версиях окон при условии, что вы не запретите этот тип оптимизации. (IIRC, версия MINGW на момент моего поста более двух лет назад, содержит строковые литералы на уровне оптимизации по умолчанию)
Lee
10
Может кто-нибудь объяснить, что здесь происходит? Что это за странно выглядящие строковые литералы?
Ajay
56
@ajay Похоже, он пишет необработанные шестнадцатеричные значения (например, \ x00 - это то же самое, что и / 0, они оба равны 0), затем преобразует строку в указатель на функцию C, затем выполняет указатель на функцию C, потому что он дьявол.
ejk314
3
Привет FUZxxl, я думаю, что это может варьироваться в зависимости от компилятора и версии операционной системы. Вышеупомянутый код, кажется, работает нормально на codepad.org; codepad.org/FMSDQ3ME
Ли
115

Одно из моих любимых применений для указателей на функции - это дешевые и простые итераторы -

#include <stdio.h>
#define MAX_COLORS  256

typedef struct {
    char* name;
    int red;
    int green;
    int blue;
} Color;

Color Colors[MAX_COLORS];


void eachColor (void (*fp)(Color *c)) {
    int i;
    for (i=0; i<MAX_COLORS; i++)
        (*fp)(&Colors[i]);
}

void printColor(Color* c) {
    if (c->name)
        printf("%s = %i,%i,%i\n", c->name, c->red, c->green, c->blue);
}

int main() {
    Colors[0].name="red";
    Colors[0].red=255;
    Colors[1].name="blue";
    Colors[1].blue=255;
    Colors[2].name="black";

    eachColor(printColor);
}
Ник
источник
7
Вам также следует передать указатель на указанные пользователем данные, если вы хотите каким-либо образом извлечь любой вывод из итераций (например, замыкания).
Алексей Аверченко
1
Согласовано. Все мои итераторы выглядеть следующим образом : int (*cb)(void *arg, ...). Возвращаемое значение итератора также позволяет мне остановиться рано (если не ноль).
Джонатон Рейнхарт
24

Указатели на функции легко объявляются, когда у вас есть основные деклараторы:

  • ID: ID: ID является
  • Указатель: *D: D указатель
  • Функция: D(<parameters>): D функция , принимающие <параметры >возвращаются

В то время как D - еще один декларатор, созданный по тем же правилам. В конце концов, где-то это заканчивается ID(см. Пример ниже), которое является именем объявленной сущности. Давайте попробуем построить функцию, которая берет указатель на функцию, которая ничего не берет и возвращает int, и возвращает указатель на функцию, которая принимает char и возвращает int. С type-def это так

typedef int ReturnFunction(char);
typedef int ParameterFunction(void);
ReturnFunction *f(ParameterFunction *p);

Как видите, его довольно легко собрать с помощью typedefs. Без typedefs это не сложно и с указанными выше правилами декларатора, применяемыми последовательно. Как видите, я пропустил ту часть, на которую указывает указатель, и функцию, которую возвращает функция. Это то, что появляется в самом левом углу объявления, и не представляет интереса: оно добавляется в конце, если уже создан декларатор. Давайте сделаем это. Построить его последовательно, в первую очередь, - показать структуру, используя [и ]:

function taking 
    [pointer to [function taking [void] returning [int]]] 
returning
    [pointer to [function taking [char] returning [int]]]

Как видите, можно полностью описать тип, добавив деклараторы один за другим. Строительство может быть сделано двумя способами. Один - снизу вверх, начиная с самой правильной вещи (уходит) и проходя путь до идентификатора. Другой способ - сверху вниз, начиная с идентификатора, работая вниз до листьев. Я покажу оба пути.

Вверх дном

Построение начинается с правильной вещи: возвращенная вещь, то есть функция, принимающая символ. Чтобы отличить деклараторов, я собираюсь перечислить их:

D1(char);

Вставил параметр char напрямую, так как он тривиален. Добавление указателя на декларатор путем замены D1на *D2. Обратите внимание, что мы должны обернуть круглые скобки *D2. Это можно узнать, посмотрев приоритет *-operatorоператора и вызова функции (). Без наших скобок компилятор прочитал бы это как *(D2(char p)). Но это *D2, конечно , больше не будет простой заменой D1 . Круглые скобки всегда разрешены вокруг деклараторов. Таким образом, вы не сделаете ничего плохого, если добавите слишком много из них, на самом деле.

(*D2)(char);

Тип возврата завершен! Теперь давайте заменим D2на функцию объявления функции, принимающую <parameters>возврат , D3(<parameters>)что мы и делаем сейчас.

(*D3(<parameters>))(char)

Обратите внимание, что скобки не нужны, так как на этот раз мы хотим D3 быть декларатором функции, а не декларатором указателя. Отлично, осталось только параметры для него. Параметр выполняется точно так же, как мы сделали тип возвращаемого значения, только с charзаменой на void. Поэтому я скопирую это:

(*D3(   (*ID1)(void)))(char)

Я заменил D2на ID1, так как мы закончили с этим параметром (это уже указатель на функцию - нет необходимости в другом деклараторе). ID1будет именем параметра. Теперь, как я сказал выше, в конце добавляется тип, который модифицируют все эти деклараторы - тот, который появляется слева от каждого объявления. Для функций это становится типом возврата. Для указателей, указывающих на тип и т. Д. Интересно, что когда вы записываете тип, он будет отображаться в обратном порядке, в самом праве :) В любом случае, его замена приводит к полному объявлению. Оба раза intконечно.

int (*ID0(int (*ID1)(void)))(char)

Я назвал идентификатор функции ID0в этом примере.

Сверху вниз

Это начинается с идентификатора слева от описания типа, оборачивая этот декларатор, пока мы идем по правому пути. Начните с функции, возвращающей <параметры>

ID0(<parameters>)

Следующим в описании (после «возврата») был указатель на . Давайте включим это:

*ID0(<parameters>)

Тогда следующая вещь была functon с <параметрами >возвращения . Параметр представляет собой простой символ, поэтому мы сразу добавляем его, поскольку он действительно тривиален.

(*ID0(<parameters>))(char)

Обратите внимание на круглые скобки мы добавили, так как мы еще раз хотим, чтобы *связывает первый, а затем(char) . В противном случае это читало бы функцию принимает <параметры >функции , возвращающие ... . Нет, функции, возвращающие функции, даже не разрешены.

Теперь нам просто нужно поставить <параметры >. Я покажу краткую версию вывода, так как я думаю, что у вас уже есть идея, как это сделать.

pointer to: *ID1
... function taking void returning: (*ID1)(void)

Просто поставьте intперед деклараторами, как мы сделали с восходящим, и мы закончили

int (*ID0(int (*ID1)(void)))(char)

Хорошая вещь

Лучше снизу вверх или сверху вниз? Я привык снизу вверх, но некоторые люди могут чувствовать себя более комфортно с сверху вниз. Это вопрос вкуса, я думаю. Кстати, если вы примените все операторы в этой декларации, вы получите int:

int v = (*ID0(some_function_pointer))(some_char);

Это хорошее свойство объявлений в C: Объявление утверждает, что если эти операторы используются в выражении, использующем идентификатор, то это приводит к типу слева. Так же и для массивов.

Надеюсь, вам понравился этот маленький урок! Теперь мы можем ссылаться на это, когда люди задаются вопросом о странном синтаксисе объявления функций. Я пытался поставить как можно меньше C внутренних органов. Не стесняйтесь редактировать / исправлять вещи в нем.

оборота Йоханнес Шауб - лит
источник
24

Еще одно хорошее применение для указателей функций:
безболезненное переключение между версиями

Их очень удобно использовать, когда вам нужны разные функции в разное время или на разных этапах разработки. Например, я разрабатываю приложение на главном компьютере с консолью, но окончательный выпуск программного обеспечения будет помещен на Avnet ZedBoard (который имеет порты для дисплеев и консолей, но они не нужны / не нужны для Окончательный релиз). Поэтому во время разработки я буду использовать printfдля просмотра сообщений о состоянии и ошибках, но когда я закончу, я не хочу ничего печатать. Вот что я сделал:

version.h

// First, undefine all macros associated with version.h
#undef DEBUG_VERSION
#undef RELEASE_VERSION
#undef INVALID_VERSION


// Define which version we want to use
#define DEBUG_VERSION       // The current version
// #define RELEASE_VERSION  // To be uncommented when finished debugging

#ifndef __VERSION_H_      /* prevent circular inclusions */
    #define __VERSION_H_  /* by using protection macros */
    void board_init();
    void noprintf(const char *c, ...); // mimic the printf prototype
#endif

// Mimics the printf function prototype. This is what I'll actually 
// use to print stuff to the screen
void (* zprintf)(const char*, ...); 

// If debug version, use printf
#ifdef DEBUG_VERSION
    #include <stdio.h>
#endif

// If both debug and release version, error
#ifdef DEBUG_VERSION
#ifdef RELEASE_VERSION
    #define INVALID_VERSION
#endif
#endif

// If neither debug or release version, error
#ifndef DEBUG_VERSION
#ifndef RELEASE_VERSION
    #define INVALID_VERSION
#endif
#endif

#ifdef INVALID_VERSION
    // Won't allow compilation without a valid version define
    #error "Invalid version definition"
#endif

В version.cI определит функцию 2 прототипов , присутствующие вversion.h

version.c

#include "version.h"

/*****************************************************************************/
/**
* @name board_init
*
* Sets up the application based on the version type defined in version.h.
* Includes allowing or prohibiting printing to STDOUT.
*
* MUST BE CALLED FIRST THING IN MAIN
*
* @return    None
*
*****************************************************************************/
void board_init()
{
    // Assign the print function to the correct function pointer
    #ifdef DEBUG_VERSION
        zprintf = &printf;
    #else
        // Defined below this function
        zprintf = &noprintf;
    #endif
}

/*****************************************************************************/
/**
* @name noprintf
*
* simply returns with no actions performed
*
* @return   None
*
*****************************************************************************/
void noprintf(const char* c, ...)
{
    return;
}

Обратите внимание, как указатель на функцию прототипируется version.hкак

void (* zprintf)(const char *, ...);

Когда на него ссылаются в приложении, оно начинает исполняться там, куда указывает, что еще не определено.

В version.c, обратите внимание, в board_init()функции, гдеzprintf назначена уникальная функция (чья сигнатура функции совпадает) в зависимости от версии, определенной вversion.h

zprintf = &printf; zprintf вызывает printf для отладки

или

zprintf = &noprint; zprintf просто возвращает и не будет запускать ненужный код

Запуск кода будет выглядеть так:

mainProg.c

#include "version.h"
#include <stdlib.h>
int main()
{
    // Must run board_init(), which assigns the function
    // pointer to an actual function
    board_init();

    void *ptr = malloc(100); // Allocate 100 bytes of memory
    // malloc returns NULL if unable to allocate the memory.

    if (ptr == NULL)
    {
        zprintf("Unable to allocate memory\n");
        return 1;
    }

    // Other things to do...
    return 0;
}

Приведенный выше код будет использоваться printfв режиме отладки или ничего не делать в режиме выпуска. Это гораздо проще, чем просматривать весь проект, комментировать или удалять код. Все, что мне нужно сделать, это изменить версию, version.hа код сделает все остальное!

Зак Шеффилд
источник
4
Вы потеряете много времени на исполнение. Вместо этого вы можете использовать макрос, который включает и отключает раздел кода, основанный на Debug / Release.
AlphaGoku
19

Указатель на функцию обычно определяется typedefи используется как параметр и возвращаемое значение.

Выше ответы уже многое объяснили, просто приведу полный пример:

#include <stdio.h>

#define NUM_A 1
#define NUM_B 2

// define a function pointer type
typedef int (*two_num_operation)(int, int);

// an actual standalone function
static int sum(int a, int b) {
    return a + b;
}

// use function pointer as param,
static int sum_via_pointer(int a, int b, two_num_operation funp) {
    return (*funp)(a, b);
}

// use function pointer as return value,
static two_num_operation get_sum_fun() {
    return &sum;
}

// test - use function pointer as variable,
void test_pointer_as_variable() {
    // create a pointer to function,
    two_num_operation sum_p = &sum;
    // call function via pointer
    printf("pointer as variable:\t %d + %d = %d\n", NUM_A, NUM_B, (*sum_p)(NUM_A, NUM_B));
}

// test - use function pointer as param,
void test_pointer_as_param() {
    printf("pointer as param:\t %d + %d = %d\n", NUM_A, NUM_B, sum_via_pointer(NUM_A, NUM_B, &sum));
}

// test - use function pointer as return value,
void test_pointer_as_return_value() {
    printf("pointer as return value:\t %d + %d = %d\n", NUM_A, NUM_B, (*get_sum_fun())(NUM_A, NUM_B));
}

int main() {
    test_pointer_as_variable();
    test_pointer_as_param();
    test_pointer_as_return_value();

    return 0;
}
Eric Wang
источник
14

Одним из наиболее важных применений указателей функций в C является вызов функции, выбранной во время выполнения. Например, библиотека C во время выполнения имеет две процедуры, qsortи bsearch, которые принимают указатель на функцию, которая вызывается для сравнения двух элементов сортируется; это позволяет вам сортировать или искать, соответственно, что угодно, основываясь на любых критериях, которые вы хотите использовать.

Очень простой пример, если есть одна вызванная функция, print(int x, int y)которая, в свою очередь, может потребовать вызова функции ( add()илиsub() , того же типа), то, что мы будем делать, мы добавим один аргумент указателя на print()функцию, как показано ниже :

#include <stdio.h>

int add()
{
   return (100+10);
}

int sub()
{
   return (100-10);
}

void print(int x, int y, int (*func)())
{
    printf("value is: %d\n", (x+y+(*func)()));
}

int main()
{
    int x=100, y=200;
    print(x,y,add);
    print(x,y,sub);

    return 0;
}

Выход:

значение: 410
значение: 390

Vamsi
источник
10

Функция запуска с нуля имеет некоторый адрес памяти, с которого они начинают выполняться. В ассемблере они называются как (вызов «адрес памяти функции»). Теперь возвращаемся к C Если функция имеет адрес памяти, то они могут управляться указателями в C. Так по правилам C

1. Сначала вам нужно объявить указатель на функцию 2. Передать адрес желаемой функции

**** Примечание-> функции должны быть одного типа ****

Эта простая программа проиллюстрирует каждую вещь.

#include<stdio.h>
void (*print)() ;//Declare a  Function Pointers
void sayhello();//Declare The Function Whose Address is to be passed
                //The Functions should Be of Same Type
int main()
{
 print=sayhello;//Addressof sayhello is assigned to print
 print();//print Does A call To The Function 
 return 0;
}

void sayhello()
{
 printf("\n Hello World");
}

введите описание изображения здесьПосле этого давайте посмотрим, как машина понимает их. Взгляд на машинную инструкцию вышеупомянутой программы в 32-битной архитектуре.

Область красной метки показывает, как происходит обмен и сохранение адреса в eax. Тогда это инструкция вызова на eax. Eax содержит желаемый адрес функции.

Mohit Dabas
источник
8

Указатель на функцию - это переменная, которая содержит адрес функции. Поскольку это переменная-указатель, хотя и с некоторыми ограниченными свойствами, вы можете использовать ее почти так же, как и любую другую переменную-указатель в структурах данных.

Единственное исключение, о котором я могу думать, - это обращение к указателю на функцию, указывающее на что-то, отличное от одного значения. Делать арифметику указателя, увеличивая или уменьшая указатель на функцию или добавляя / вычитая смещение к указателю на функцию, на самом деле не имеет никакого смысла, так как указатель на функцию указывает только на одну вещь - точку входа в функцию.

Размер переменной указателя функции, количество байтов, занимаемых этой переменной, может варьироваться в зависимости от базовой архитектуры, например, x32 или x64 или чего-либо еще.

Объявление для переменной указателя функции должно указывать ту же информацию, что и объявление функции, чтобы компилятор C мог выполнять те виды проверок, которые он обычно делает. Если вы не укажете список параметров в объявлении / определении указателя функции, компилятор C не сможет проверить использование параметров. Есть случаи, когда это отсутствие проверки может быть полезным, однако просто помните, что сеть безопасности была удалена.

Некоторые примеры:

int func (int a, char *pStr);    // declares a function

int (*pFunc)(int a, char *pStr);  // declares or defines a function pointer

int (*pFunc2) ();                 // declares or defines a function pointer, no parameter list specified.

int (*pFunc3) (void);             // declares or defines a function pointer, no arguments.

Первые два объявления несколько похожи в этом:

  • funcэто функция, которая принимает intи char *и возвращаетint
  • pFuncявляется указателем на функцию , к которой присваивается адрес функции , которая принимает intи char *и возвращаетint

Таким образом, из вышесказанного может быть строка источника, в которой адрес функции func()назначен переменной указателя функции, pFuncкак в pFunc = func;.

Обратите внимание на синтаксис, используемый с объявлением / определением указателя функции, в котором скобки используются для преодоления естественных правил приоритета операторов.

int *pfunc(int a, char *pStr);    // declares a function that returns int pointer
int (*pFunc)(int a, char *pStr);  // declares a function pointer that returns an int

Несколько различных примеров использования

Некоторые примеры использования указателя на функцию:

int (*pFunc) (int a, char *pStr);    // declare a simple function pointer variable
int (*pFunc[55])(int a, char *pStr); // declare an array of 55 function pointers
int (**pFunc)(int a, char *pStr);    // declare a pointer to a function pointer variable
struct {                             // declare a struct that contains a function pointer
    int x22;
    int (*pFunc)(int a, char *pStr);
} thing = {0, func};                 // assign values to the struct variable
char * xF (int x, int (*p)(int a, char *pStr));  // declare a function that has a function pointer as an argument
char * (*pxF) (int x, int (*p)(int a, char *pStr));  // declare a function pointer that points to a function that has a function pointer as an argument

Вы можете использовать списки параметров переменной длины в определении указателя функции.

int sum (int a, int b, ...);
int (*psum)(int a, int b, ...);

Или вы не можете указать список параметров вообще. Это может быть полезно, но исключает возможность для компилятора C выполнять проверки по предоставленному списку аргументов.

int  sum ();      // nothing specified in the argument list so could be anything or nothing
int (*psum)();
int  sum2(void);  // void specified in the argument list so no parameters when calling this function
int (*psum2)(void);

C стиль бросает

Вы можете использовать приведения в стиле C с указателями на функции. Однако имейте в виду, что компилятор C может быть небрежным в отношении проверок или предоставлять предупреждения, а не ошибки.

int sum (int a, char *b);
int (*psplsum) (int a, int b);
psplsum = sum;               // generates a compiler warning
psplsum = (int (*)(int a, int b)) sum;   // no compiler warning, cast to function pointer
psplsum = (int *(int a, int b)) sum;     // compiler error of bad cast generated, parenthesis are required.

Сравните указатель функции с равенством

Вы можете проверить, что указатель функции равен определенному адресу функции, используя ifинструкцию, хотя я не уверен, насколько это было бы полезно. Другие операторы сравнения могут показаться еще менее полезными.

static int func1(int a, int b) {
    return a + b;
}

static int func2(int a, int b, char *c) {
    return c[0] + a + b;
}

static int func3(int a, int b, char *x) {
    return a + b;
}

static char *func4(int a, int b, char *c, int (*p)())
{
    if (p == func1) {
        p(a, b);
    }
    else if (p == func2) {
        p(a, b, c);      // warning C4047: '==': 'int (__cdecl *)()' differs in levels of indirection from 'char *(__cdecl *)(int,int,char *)'
    } else if (p == func3) {
        p(a, b, c);
    }
    return c;
}

Массив указателей на функции

И если вы хотите иметь массив указателей на функции, каждый из элементов которых имеет список аргументов, то вы можете определить указатель функции с неопределенным списком аргументов (не voidозначающим никаких аргументов, а просто неуказанным), что-то вроде следующего, хотя вы может видеть предупреждения от компилятора C. Это также работает для параметра указателя на функцию:

int(*p[])() = {       // an array of function pointers
    func1, func2, func3
};
int(**pp)();          // a pointer to a function pointer


p[0](a, b);
p[1](a, b, 0);
p[2](a, b);      // oops, left off the last argument but it compiles anyway.

func4(a, b, 0, func1);
func4(a, b, 0, func2);  // warning C4047: 'function': 'int (__cdecl *)()' differs in levels of indirection from 'char *(__cdecl *)(int,int,char *)'
func4(a, b, 0, func3);

    // iterate over the array elements using an array index
for (i = 0; i < sizeof(p) / sizeof(p[0]); i++) {
    func4(a, b, 0, p[i]);
}
    // iterate over the array elements using a pointer
for (pp = p; pp < p + sizeof(p)/sizeof(p[0]); pp++) {
    (*pp)(a, b, 0);          // pointer to a function pointer so must dereference it.
    func4(a, b, 0, *pp);     // pointer to a function pointer so must dereference it.
}

Стиль C namespaceИспользование Global structс указателями на функции

Вы можете использовать staticключевое слово, чтобы указать функцию, имя которой является областью действия файла, а затем назначить ее глобальной переменной, чтобы обеспечить нечто похожее наnamespace функциональность C ++.

В заголовочном файле определите структуру, которая будет нашим пространством имен, вместе с глобальной переменной, которая ее использует.

typedef struct {
   int (*func1) (int a, int b);             // pointer to function that returns an int
   char *(*func2) (int a, int b, char *c);  // pointer to function that returns a pointer
} FuncThings;

extern const FuncThings FuncThingsGlobal;

Затем в исходном файле C:

#include "header.h"

// the function names used with these static functions do not need to be the
// same as the struct member names. It's just helpful if they are when trying
// to search for them.
// the static keyword ensures these names are file scope only and not visible
// outside of the file.
static int func1 (int a, int b)
{
    return a + b;
}

static char *func2 (int a, int b, char *c)
{
    c[0] = a % 100; c[1] = b % 50;
    return c;
}

const FuncThings FuncThingsGlobal = {func1, func2};

Затем это можно использовать, указав полное имя глобальной переменной структуры и имя члена для доступа к функции. constМодификатор используется на глобальном , так что он не может быть изменен случайно.

int abcd = FuncThingsGlobal.func1 (a, b);

Области применения указателей функций

Компонент библиотеки DLL может делать что-то похожее на namespaceподход в стиле C, в котором конкретный интерфейс библиотеки запрашивается из фабричного метода в интерфейсе библиотеки, который поддерживает создание structуказателей с содержащимися функциями. Этот интерфейс библиотеки загружает запрошенную версию DLL, создает структура с необходимыми указателями на функции, а затем возвращает структуру запрашивающей вызывающей стороне для использования.

typedef struct {
    HMODULE  hModule;
    int (*Func1)();
    int (*Func2)();
    int(*Func3)(int a, int b);
} LibraryFuncStruct;

int  LoadLibraryFunc LPCTSTR  dllFileName, LibraryFuncStruct *pStruct)
{
    int  retStatus = 0;   // default is an error detected

    pStruct->hModule = LoadLibrary (dllFileName);
    if (pStruct->hModule) {
        pStruct->Func1 = (int (*)()) GetProcAddress (pStruct->hModule, "Func1");
        pStruct->Func2 = (int (*)()) GetProcAddress (pStruct->hModule, "Func2");
        pStruct->Func3 = (int (*)(int a, int b)) GetProcAddress(pStruct->hModule, "Func3");
        retStatus = 1;
    }

    return retStatus;
}

void FreeLibraryFunc (LibraryFuncStruct *pStruct)
{
    if (pStruct->hModule) FreeLibrary (pStruct->hModule);
    pStruct->hModule = 0;
}

и это может быть использовано как в:

LibraryFuncStruct myLib = {0};
LoadLibraryFunc (L"library.dll", &myLib);
//  ....
myLib.Func1();
//  ....
FreeLibraryFunc (&myLib);

Тот же подход можно использовать для определения абстрактного аппаратного уровня для кода, который использует конкретную модель базового аппаратного обеспечения. Указатели функций заполняются фабрично-специфическими функциями для обеспечения аппаратно-специфической функциональности, которая реализует функции, указанные в абстрактной аппаратной модели. Это может использоваться для предоставления абстрактного аппаратного уровня, используемого программным обеспечением, которое вызывает заводскую функцию, чтобы получить интерфейс конкретной аппаратной функции, а затем использует предоставленные указатели функций для выполнения действий для базового аппаратного обеспечения без необходимости знать подробности реализации о конкретной цели. ,

Указатели на функции для создания делегатов, обработчиков и обратных вызовов

Вы можете использовать указатели на функции как способ делегировать некоторые задачи или функции. Классическим примером в C является указатель функции делегата сравнения, используемый с функциями библиотеки Standard C qsort()иbsearch() обеспечивающий порядок сопоставления для сортировки списка элементов или выполнения двоичного поиска по отсортированному списку элементов. Делегат функции сравнения определяет алгоритм сортировки, используемый при сортировке или двоичном поиске.

Другое использование аналогично применению алгоритма к контейнеру стандартной библиотеки шаблонов C ++.

void * ApplyAlgorithm (void *pArray, size_t sizeItem, size_t nItems, int (*p)(void *)) {
    unsigned char *pList = pArray;
    unsigned char *pListEnd = pList + nItems * sizeItem;
    for ( ; pList < pListEnd; pList += sizeItem) {
        p (pList);
    }

    return pArray;
}

int pIncrement(int *pI) {
    (*pI)++;

    return 1;
}

void * ApplyFold(void *pArray, size_t sizeItem, size_t nItems, void * pResult, int(*p)(void *, void *)) {
    unsigned char *pList = pArray;
    unsigned char *pListEnd = pList + nItems * sizeItem;
    for (; pList < pListEnd; pList += sizeItem) {
        p(pList, pResult);
    }

    return pArray;
}

int pSummation(int *pI, int *pSum) {
    (*pSum) += *pI;

    return 1;
}

// source code and then lets use our function.
int intList[30] = { 0 }, iSum = 0;

ApplyAlgorithm(intList, sizeof(int), sizeof(intList) / sizeof(intList[0]), pIncrement);
ApplyFold(intList, sizeof(int), sizeof(intList) / sizeof(intList[0]), &iSum, pSummation);

Другой пример - с исходным кодом GUI, в котором зарегистрирован обработчик для определенного события, предоставляя указатель на функцию, которая фактически вызывается, когда происходит событие. Платформа Microsoft MFC с ее картами сообщений использует нечто подобное для обработки сообщений Windows, которые доставляются в окно или поток.

Асинхронные функции, требующие обратного вызова, аналогичны обработчику событий. Пользователь асинхронной функции вызывает асинхронную функцию для запуска некоторого действия и предоставляет указатель на функцию, которую асинхронная функция будет вызывать после завершения действия. В этом случае событие является асинхронной функцией, выполняющей свою задачу.

Ричард Чемберс
источник
0

Поскольку указатели на функции часто являются типизированными обратными вызовами, вы можете взглянуть на безопасные обратные вызовы типа. . То же самое относится к точкам входа и т. Д. Функций, которые не являются обратными вызовами.

С довольно переменчивый и прощающий одновременно :)

Тим Пост
источник