Як функціонують покажчики на C?


1232

Останнім часом у мене був досвід роботи з покажчиками функцій у С.

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


35
Також: Для детального аналізу покажчиків С див. Blogs.oracle.com/ksplice/entry/the_ksplice_pointer_challenge . Також програмування з Ground Up показує, як вони працюють на машинному рівні. Розуміння "моделі пам'яті" C дуже корисно для розуміння того, як працюють покажчики C.
Аббафей

8
Чудова інформація. За назвою, однак, я б очікував дійсно побачити пояснення того, як "функціонують покажчики", а не як їх кодують :)
Богдан Александру

Відповіді:


1477

Функціональні покажчики на С

Почнемо з основною функцією , яку ми будемо вказуючи на :

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;" що також справедливо, оскільки стандарт говорить, що ім'я функції в цьому контексті перетворюється на адресу функції.
hlovdal

22
hlovdal, в цьому контексті цікаво пояснити, що саме це дозволяє записати функціюPtr = ****************** addInt;
Йоханнес Шауб - ліб

105
@ Rich.Carpenter Я знаю, що це на 4 роки занадто пізно, але я думаю, що від цього можуть скористатися інші люди: функціональні вказівники корисні для передачі функцій як параметрів іншим функціям . Мені знадобилось багато пошуків, щоб знайти цю відповідь з якоїсь дивної причини. Таким чином, це дає функціональність першокласного псевдосистеми C.
гігант91

22
@ Rich.Carpenter: функціональні покажчики приємні для виявлення процесора під час виконання процесора. Майте кілька версій деяких функцій, щоб скористатися SSE, popcnt, AVX тощо. При запуску встановіть свої покажчики функцій на найкращу версію кожної функції для поточного процесора. У іншому коді просто зателефонуйте через вказівник функції замість того, щоб усюди було встановлено умовні гілки функцій процесора. Тоді ви можете скористатися складною логікою щодо того, щоб вирішити це добре, навіть якщо цей процесор підтримує pshufb, він повільний, тому попередня реалізація все-таки швидша. x264 / x265 використовують це широко та є відкритим кодом.
Пітер Кордес

303

Функціональні покажчики на 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, скажімо, an 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методу немає, тому його не можна викликати в a 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, зверніться до наступних питань:


22
Ця відповідь жахлива! Це не тільки означає, що OO якось залежить від позначення крапок, але також заохочує вводити мотлох у ваші предмети!
Олексій Аверченко

27
Це OO гаразд, але ніде поблизу O-стилю С-стилю. Що ви зламано реалізували, це OO, заснований на прототипі Javascript. Для отримання ОО + у стилі C ++ / Pascal вам знадобиться: 1. Мати структуру const для віртуальної таблиці кожного класу з віртуальними членами. 2. Показуйте на цю структуру в поліморфних об'єктах. 3. Викличте віртуальні методи за допомогою віртуальної таблиці та всіх інших методів безпосередньо - зазвичай, дотримуючись певної ClassName_methodNameфункції конвенції іменування. Тільки тоді ви отримуєте ті ж самі витрати на виконання та зберігання, що й у C ++ та Pascal.
Відновіть Моніку

19
Робота з ОО з мовою, яка не призначена бути ОО - це завжди погана ідея. Якщо ви хочете OO і все ще маєте C, просто працюйте з C ++.
rbaleksandar

20
@rbaleksandar Скажіть це розробникам ядра Linux. "завжди погана ідея" - це суто ваша думка, з якою я категорично не згоден.
Джонатан Райнхарт

6
Мені подобається ця відповідь, але не кидайте мальків
кіт

227

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

Ці рядкові літерали є байтами 32-розрядного машинного коду x86. 0xC3- це retінструкція x86 .

Як правило, ви не пишете це вручну, ви пишете мовою асемблера, а потім використовуєте асемблер, як nasmщоб зібрати його у плоскій двійковий код, який ви введені шістнадцять у буквальний рядок C.

  1. Повертає поточне значення в регістрі EAX

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

    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. Напишіть лічильник for-циклу до 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 зберігається у всіх звичайних режимах викликів, тому використання його як реєстру скриптів без збереження / відновлення може легко призвести до аварії абонента.


8
Примітка: це не працює, якщо ввімкнено запобігання виконанню даних (наприклад, у Windows XP SP2 +), оскільки рядки C зазвичай не позначаються як виконувані.
SecurityMatt

5
Привіт Метт! Залежно від рівня оптимізації, GCC часто вбудовує рядкові константи в сегмент TEXT, тому це буде працювати навіть у більш новій версії Windows за умови, що ви не забороните цей тип оптимізації. (IIRC, версія MINGW під час моєї посади понад два роки тому вказує рядкові літерали на рівні оптимізації за замовчуванням)
Лі

10
хтось, будь ласка, пояснить, що тут відбувається? Що це за дивні буквені букви?
Адже

56
@ajay Схоже, що він записує сировинні дванадцятимісні значення (наприклад, '\ x00' - це те саме, що '/ 0', вони обоє дорівнюють 0) у рядок, потім вводить рядок у покажчик функції 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. З типами defs це так

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)). Але це, звичайно, вже не буде простою заміною D1 *D2. Навколо деклараторів завжди допускаються дужки. Тож ви нічого не зробите, якщо насправді додаєте їх занадто багато.

(*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>)

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

(*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: Декларація стверджує, що якщо ці оператори використовуються в виразі за допомогою ідентифікатора, то він видає тип зліва. Це теж для масивів.

Сподіваюся, вам сподобався цей маленький підручник! Тепер ми можемо пов’язати це, коли люди задаються питанням про дивний декларації синтаксис функцій. Я намагався поставити якомога менше внутрішніх матеріалів С. Не соромтесь редагувати / виправляти речі в ньому.


24

Ще одне корисне використання функціональних покажчиків:
безболісне перемикання між версіями

Їх дуже зручно використовувати, коли вам потрібні різні функції в різний час або на різних фазах розвитку. Наприклад, я розробляю додаток на хост-комп'ютері, який має консоль, але остаточний реліз програмного забезпечення буде розміщений на Avnet ZedBoard (який має порти для дисплеїв та консолей, але вони не потрібні / потрібні для остаточний реліз). Тому під час розробки я використовую printfдля перегляду повідомлень про стан та помилки, але коли я закінчую, я не хочу нічого надрукованого. Ось що я зробив:

версія.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я визначу два прототипи функції, присутні вversion.h

версія.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
Ви готові втратити багато часу на продуктивність. Натомість ви можете використовувати макрос, який дозволяє та вимикає розділ коду на основі налагодження / випуску.
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;
}

14

Одне з найбільших застосувань функціональних покажчиків на 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


10

Починаючи з нуля, є деяка адреса пам’яті, звідки вони починають виконувати. У мові складання вони називаються як (зателефонуйте "адреса пам'яті функції"). Тепер поверніться до 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");
}

введіть тут опис зображенняПісля цього давайте подивіться, як машина розуміє Them.Glimpse машинного навчання вищевказаної програми в 32-бітовій архітектурі.

Область червоної позначки показує, як обмінюються та зберігаються адреси в eax. Тоді їхня інструкція по виклику на eax. eax містить бажану адресу функції.


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- покажчик функції, якому присвоюється адреса функції, яка приймає a intі a char *і повертає anint

Отже, з вищесказаного ми могли б мати вихідний рядок, у якому адреса функції 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Використання глобального 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, які доставляються у вікно або потік.

Асинхронні функції, які потребують зворотного дзвінка, схожі на обробник подій. Користувач асинхронної функції викликає асинхронну функцію для початку якоїсь дії та надає вказівник функції, який асинхронна функція буде викликати після завершення дії. У цьому випадку подія є асинхронною функцією, що виконує своє завдання.


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.