Останнім часом у мене був досвід роботи з покажчиками функцій у С.
Отже, продовжуючи традицію відповідати на власні запитання, я вирішив скласти невеликий підсумок самих основ, для тих, хто потребує швидкого заглиблення у цю тему.
Останнім часом у мене був досвід роботи з покажчиками функцій у С.
Отже, продовжуючи традицію відповідати на власні запитання, я вирішив скласти невеликий підсумок самих основ, для тих, хто потребує швидкого заглиблення у цю тему.
Відповіді:
Почнемо з основною функцією , яку ми будемо вказуючи на :
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;
}
pshufb
, він повільний, тому попередня реалізація все-таки швидша. x264 / x265 використовують це широко та є відкритим кодом.
Функціональні покажчики на 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
класі чомусь, все, що потрібно було б зробити:
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, тому, ймовірно, є моменти, які я не пояснив добре, або, можливо, просто відзначити, як найкраще реалізувати OOP в C. Але моєю метою було спробувати проілюструвати одне з багатьох застосувань функціональних покажчиків.
Для отримання додаткової інформації про те, як виконувати об'єктно-орієнтоване програмування на C, зверніться до наступних питань:
ClassName_methodName
функції конвенції іменування. Тільки тоді ви отримуєте ті ж самі витрати на виконання та зберігання, що й у C ++ та Pascal.
Посібник із звільнення: Як зловживати функціональними покажчиками в GCC на машинах x86, склавши код вручну:
Ці рядкові літерали є байтами 32-розрядного машинного коду x86. 0xC3
- це ret
інструкція x86 .
Як правило, ви не пишете це вручну, ви пишете мовою асемблера, а потім використовуєте асемблер, як nasm
щоб зібрати його у плоскій двійковий код, який ви введені шістнадцять у буквальний рядок C.
Повертає поточне значення в регістрі EAX
int eax = ((int(*)())("\xc3 <- This returns the value of the EAX register"))();
Напишіть функцію 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);
Напишіть лічильник 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
Можна навіть записати рекурсивну функцію, яка нараховує до 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 зберігається у всіх звичайних режимах викликів, тому використання його як реєстру скриптів без збереження / відновлення може легко призвести до аварії абонента.
Одне з моїх улюблених напрямків використання функціональних покажчиків - це як дешеві, так і прості ітератори -
#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);
}
int (*cb)(void *arg, ...)
. Повернене значення ітератора також дозволяє мені зупинятися рано (якщо це не нульове значення).
Показники функцій стають легко оголосити, коли у вас є основні декларатори:
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: Декларація стверджує, що якщо ці оператори використовуються в виразі за допомогою ідентифікатора, то він видає тип зліва. Це теж для масивів.
Сподіваюся, вам сподобався цей маленький підручник! Тепер ми можемо пов’язати це, коли люди задаються питанням про дивний декларації синтаксис функцій. Я намагався поставити якомога менше внутрішніх матеріалів С. Не соромтесь редагувати / виправляти речі в ньому.
Їх дуже зручно використовувати, коли вам потрібні різні функції в різний час або на різних фазах розвитку. Наприклад, я розробляю додаток на хост-комп'ютері, який має консоль, але остаточний реліз програмного забезпечення буде розміщений на Avnet ZedBoard (який має порти для дисплеїв та консолей, але вони не потрібні / потрібні для остаточний реліз). Тому під час розробки я використовую printf
для перегляду повідомлень про стан та помилки, але коли я закінчую, я не хочу нічого надрукованого. Ось що я зробив:
// 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
#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 просто повертається і не запускає зайвий код
Запуск коду буде виглядати приблизно так:
#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
і код зробить все інше!
Покажчик функції, як правило, визначається 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 ∑
}
// test - use function pointer as variable,
void test_pointer_as_variable() {
// create a pointer to function,
two_num_operation sum_p = ∑
// 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;
}
Одне з найбільших застосувань функціональних покажчиків на 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
Починаючи з нуля, є деяка адреса пам’яті, звідки вони починають виконувати. У мові складання вони називаються як (зателефонуйте "адреса пам'яті функції"). Тепер поверніться до 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 містить бажану адресу функції.
Покажчик функції - це змінна, яка містить адресу функції. Оскільки це змінна вказівник, хоча з деякими обмеженими властивостями, ви можете використовувати її майже так само, як і будь-яку іншу змінну вказівника в структурах даних.
Єдиним винятком, про який я можу подумати, є трактування функції покажчика як вказівки на щось інше, ніж на одне значення. Виконання арифметики вказівника шляхом збільшення або зменшення покажчика функції або додавання / віднімання зміщення до вказівника функції насправді не є корисною, оскільки покажчик функції вказує лише на одну річ, точку входу функції.
Розмір змінної вказівника функції, кількість байтів, зайнятих змінною, може змінюватися в залежності від базової архітектури, наприклад 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, які доставляються у вікно або потік.
Асинхронні функції, які потребують зворотного дзвінка, схожі на обробник подій. Користувач асинхронної функції викликає асинхронну функцію для початку якоїсь дії та надає вказівник функції, який асинхронна функція буде викликати після завершення дії. У цьому випадку подія є асинхронною функцією, що виконує своє завдання.
Оскільки вказівники функцій часто набираються зворотними дзвінками, ви можете ознайомитись із типом безпечних зворотних викликів . Те саме стосується точок входу тощо, функцій, які не є зворотними.
С є досить непостійним і прощаючим одночасно :)