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

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

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

1006
задан Yuval Adam 08 мая '09 в 18:49
источник поделиться

11 ответов

Указатели функций в 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;
}
1244
ответ дан Yuval Adam 08 мая '09 в 18:49
источник поделиться

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

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

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

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

С помощью указателей функций можно эмулировать методы в C.

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

Класс 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 по какой-то причине, все, что нужно сделать, это:

  • Добавьте функцию, которая будет служить в качестве переопределяющего метода length.
  • Перейдите к "конструктору" и установите указатель на метод переопределения 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, поэтому, вероятно, есть точки, которые я не очень хорошо объяснил, или просто не могу сказать, насколько лучше для реализации ООП в C. Но моя цель состояла в том, чтобы попытаться проиллюстрировать одно из многих применений указателей на функции.

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

267
ответ дан coobird 08 мая '09 в 19:32
источник поделиться

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

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

    int eax = ((int(*)())("\xc3 <- This returns the value of the EAX register"))();
    
  • Запись функции свопинга

    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);
    
  • Напишите счетчик for-loop 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
    
  • Вы даже можете записать рекурсивную функцию, которая насчитывает 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);
    
187
ответ дан Lee 09 апр. '11 в 3:51
источник поделиться

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

#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);
}
95
ответ дан Nick Van Brunt 08 мая '09 в 19:26
источник поделиться

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

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

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

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

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

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

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

Внизу вверх

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

D1(char);

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

(*D2)(char);

Тип возврата завершен! Теперь замените D2 функцией declarator функции, возвращающей <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>)

Следующее в описании (после "return" ) было указателем на. Позвольте включить его:

*ID0(<parameters>)

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

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

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

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

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. Не стесняйтесь редактировать/исправлять в нем вещи.

23
ответ дан Johannes Schaub - litb 09 мая '09 в 5:05
источник поделиться

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

Они очень удобны в использовании, когда вам нужны разные функции в разное время или на разных этапах разработки. Например, я разрабатываю приложение на главном компьютере с консолью, но окончательная версия программного обеспечения будет размещена на 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.c я определяю прототипы 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, а код сделает все остальное!

21
ответ дан Zack Sheffield 10 июня '13 в 22:56
источник поделиться

Указатель функций обычно определяется 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;
}
13
ответ дан Eric Wang 10 нояб. '14 в 11:50
источник поделиться

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

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

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

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

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

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

    return 0;
}
7
ответ дан Vamsi 17 нояб. '14 в 21:24
источник поделиться

Начиная с нуля, функция имеет некоторый адрес памяти от того места, где они начинают выполняться. В языке сборки они называются (вызывают "адрес памяти функции" ). Теперь вернитесь к функции C Если у функции есть адрес памяти, тогда с ними можно управлять указателями в C.So По правилам 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");
}

enter image description here После этого давайте посмотрим, как машина подхватывает Them.Glimpse машинной инструкции вышеуказанной программы в 32-битной архитектуре.

В области красных отметок показано, как происходит обмен и хранение адреса в eax.Then их команда вызова на eax. eax содержит желаемый адрес функции

3
ответ дан Mohit Dabas 26 сент. '14 в 11:09
источник поделиться

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

C довольно изменчив и прощает одновременно:)

0
ответ дан Tim Post 09 мая '09 в 16:56
источник поделиться

указатели функций полезны во многих ситуациях, например:

  • Члены COM-объектов являются указателями на функцию ag: This->lpVtbl->AddRef(This); AddRef - это указатель на функцию.
  • обратный вызов функции, например, пользовательская функция для сравнения две переменные, которые должны быть переданы как обратный вызов специальной функции сортировки.
  • очень полезно для реализации плагина и SDK приложения.
-8
ответ дан milevyo 02 янв. '16 в 16:15
источник поделиться

Другие вопросы по меткам