كيف تعمل مؤشرات الوظيفة في C؟
-
20-08-2019 - |
سؤال
كان لدي بعض الخبرة في الآونة الأخيرة مع مؤشرات الوظيفة في C.
لذا ، على تقليد الإجابة على أسئلتك الخاصة ، قررت أن أقوم بإجراء ملخص صغير للأساسيات ذاتها ، لأولئك الذين يحتاجون إلى الغوص السريع للموضوع.
المحلول
مؤشرات الوظيفة في ج
لنبدأ بوظيفة أساسية سنكون يشير الى:
int addInt(int n, int m) {
return n+m;
}
أول شيء ، دعنا نحدد مؤشرًا إلى وظيفة تتلقى 2 int
s ويعيد 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;
}
نصائح أخرى
يمكن استخدام مؤشرات الوظائف في C لأداء البرمجة الموجهة للكائنات في C.
على سبيل المثال ، تتم كتابة الأسطر التالية في ج:
String s1 = newString();
s1->set(s1, "hello");
نعم ->
وعدم وجود أ new
المشغل هو ميت ، لكن يبدو أنه من المؤكد أننا نضع نص بعض String
الفصل ليكون "hello"
.
باستخدام مؤشرات الوظيفة ، من الممكن محاكاة الطرق في ج.
كيف يتم إنجاز هذا؟
ال String
الفصل هو في الواقع أ struct
مع مجموعة من مؤشرات الوظائف التي تعمل كوسيلة لمحاكاة الأساليب. فيما يلي إعلان جزئي لـ String
صف دراسي:
typedef struct String_Struct* String;
struct String_Struct
{
char* (*get)(const void* self);
void (*set)(const void* self, char* value);
int (*length)(const void* self);
};
char* getString(const void* self);
void setString(const void* self, char* value);
int lengthString(const void* self);
String newString();
كما يمكن أن نرى ، طرق String
الفئة هي في الواقع مؤشرات الوظيفة للوظيفة المعلنة. في إعداد مثيل String
, ، ال newString
تتم استدعاء الوظيفة من أجل إعداد مؤشرات الوظيفة لوظائفها:
String newString()
{
String self = (String)malloc(sizeof(struct String_Struct));
self->get = &getString;
self->set = &setString;
self->length = &lengthString;
self->set(self, "");
return self;
}
على سبيل المثال ، getString
الوظيفة التي يسمى من خلال التذرع get
يتم تعريف الطريقة على النحو التالي:
char* getString(const void* self_obj)
{
return ((String)self_obj)->internal->value;
}
شيء واحد يمكن ملاحظة أنه لا يوجد مفهوم لمثيل كائن ما ولديه أساليب هي في الواقع جزء من كائن ، لذلك يجب تمرير "كائن ذاتي" في كل الاحتجاج. (و ال internal
هو مجرد مخفي struct
التي تم حذفها من قائمة الكود في وقت سابق - إنها طريقة لإجراء إخفاء المعلومات ، ولكن هذا غير وثيق الصلة بمؤشرات الوظيفة.)
لذلك ، بدلاً من القدرة على القيام به s1->set("hello");
, ، يجب على المرء أن يمر في الكائن لأداء الإجراء على s1->set(s1, "hello")
.
مع هذا التفسير البسيط الذي يجب أن يمر في إشارة إلى نفسك بعيدًا ، سننتقل إلى الجزء التالي ، وهو الميراث في ج.
دعنا نقول أننا نريد أن نصنع فئة فرعية 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
هدف.
يمكن أن يحقق استخدام مؤشر الوظيفة ميراث طريقة من الطبقة الفائقة.
يمكننا الاستمرار في ذلك تعدد الأشكال في ج.
على سبيل المثال ، أردنا تغيير سلوك 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 ، يرجى الرجوع إلى الأسئلة التالية:
دليل إطلاق النار: كيفية إساءة استخدام مؤشرات الوظائف في GCC على أجهزة X86 عن طريق تجميع الكود الخاص بك باليد:
هذه الحرفية سلسلة هي بايت من رمز آلة X86 32 بت. 0xC3
هو x86 ret
تعليمات.
لن تكتبها عادةً باليد ، فأنت تكتب بلغة التجميع ثم تستخدم مجمعًا مثل nasm
لتجميعها في ثنائي مسطح ، يمكنك من خلاله في سلسلة C حرفية.
إرجاع القيمة الحالية في سجل 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);
اكتب عدادًا من أجل 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) ، والذي يرتبط كجزء من قطاع النص (جنبا إلى جنب مع رمز للوظائف).
لقد قرأ قطاع النص+إذن ، لذا فإن الأبطال الحرفية للوظيفة تعمل على عمل المؤشرات دون الحاجة mprotect()
أو VirtualProtect()
مكالمات النظام كما تحتاج للذاكرة المخصصة ديناميكيا. (أو gcc -z execstack
يربط البرنامج مع مكدس + قطاع البيانات + الكومة قابلة للتنفيذ ، كاختراق سريع.)
لتفكيكها ، يمكنك تجميع هذا لوضع علامة على البايتات ، واستخدام Disassembler.
// 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 عن طريق clobbering 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);
}
تصبح مؤشرات الوظيفة سهلة الإعلان بمجرد حصولك على المعلنين الأساسيين:
- هوية شخصية:
ID
: المعرف هو - مؤشر:
*D
: د مؤشر ل - دور:
D(<parameters>)
: د وظيفة أخذ<
المعلمات>
عودة
في حين أن 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)
. وإلا فإنه سوف يقرأ أخذ وظيفة <
المعلمات>
وظيفة العودة .... NOES ، وظائف عودة الوظائف غير مسموح بها.
الآن نحتاج فقط إلى وضعه <
المعلمات>
. سأعرض إصدارًا قصيرًا من Deriveration ، لأنني أعتقد أن لديك بالفعل فكرة عن كيفية القيام بذلك.
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. لا تتردد في تحرير/إصلاح الأشياء فيه.
استخدام جيد آخر لمؤشرات الوظيفة:
التبديل بين الإصدارات غير مؤلمة
إنها مفيدة للغاية لاستخدامها عندما تريد وظائف مختلفة في أوقات مختلفة ، أو مراحل مختلفة من التطوير. على سبيل المثال ، أقوم بتطوير تطبيق على جهاز كمبيوتر مضيف يحتوي على وحدة تحكم ، ولكن سيتم وضع الإصدار النهائي للبرنامج على لوحة AVNET (التي تحتوي على منافذ للعروض وأجهزة المفاتيح ، لكنها ليست مطلوبة/مطلوبة ل الإصدار النهائي). لذلك أثناء التطوير ، سأستخدم 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 يعود فقط ولن يتم تشغيل رمز غير ضروري
سيبدو تشغيل الرمز هكذا:
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
وسوف يفعل الكود الباقي!
عادة ما يتم تعريف مؤشر الوظيفة بواسطة 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 هو استدعاء وظيفة محددة في وقت التشغيل. على سبيل المثال ، تحتوي مكتبة وقت التشغيل 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
مؤشر الوظيفة هو متغير يحتوي على عنوان الوظيفة. نظرًا لأنه متغير مؤشر على الرغم من بعض الخصائص المقيدة ، يمكنك استخدامه تمامًا كما لو كنت متغيرًا مؤشرًا آخر في هياكل البيانات.
الاستثناء الوحيد الذي يمكنني التفكير فيه هو التعامل مع مؤشر الوظيفة على أنه يشير إلى شيء آخر غير قيمة واحدة. إن القيام بحساب مؤشر عن طريق زيادة أو تقليل مؤشر الوظيفة أو إضافة/طرح إزاحة إلى مؤشر الوظيفة ليس حقًا أي فائدة حيث يشير مؤشر الوظيفة فقط إلى شيء واحد ، وهي نقطة دخول الوظيفة.
قد يختلف حجم متغير مؤشر الوظيفة ، وعدد البايتات التي يشغلها المتغير ، اعتمادًا على البنية الأساسية ، على سبيل المثال X32 أو X64 أو أي شيء آخر.
يحتاج الإعلان عن متغير مؤشر الوظيفة إلى تحديد نفس النوع من المعلومات كإعلان دالة من أجل أن يقوم برنامج التحويل البرمجي C بعمل أنواع الشيكات التي تقوم بها عادةً. إذا لم تحدد قائمة معلمات في إعلان/تعريف مؤشر الوظيفة ، فلن يتمكن برنامج التحويل البرمجي C من التحقق من استخدام المعلمات. هناك حالات يمكن أن يكون فيها هذا الافتقار إلى الفحص مفيدًا ، ولكن فقط تذكر أنه تمت إزالة شبكة الأمان.
بعض الأمثلة:
int func (int a, char *pStr); // declares a function
int (*pFunc)(int a, char *pStr); // declares or defines a function pointer
int (*pFunc2) (); // declares or defines a function pointer, no parameter list specified.
int (*pFunc3) (void); // declares or defines a function pointer, no arguments.
أول إعلانين متشابهان إلى حد ما في ذلك:
func
هي وظيفة تأخذint
وchar *
ويعودint
pFunc
هو مؤشر الوظيفة الذي يتم تعيين عنوان وظيفة تأخذint
وchar *
ويعودint
لذلك من ما سبق يمكن أن يكون لدينا سطر مصدر فيه عنوان الوظيفة func()
تم تعيينه لمؤشر الوظيفة المتغير pFunc
كما في pFunc = func;
.
لاحظ أن بناء الجملة المستخدمة مع إعلان/تعريف مؤشر الوظيفة يتم فيه استخدام الأقواس للتغلب على قواعد أسبقية المشغل الطبيعي.
int *pfunc(int a, char *pStr); // declares a function that returns int pointer
int (*pFunc)(int a, char *pStr); // declares a function pointer that returns an int
العديد من أمثلة الاستخدام المختلفة
بعض الأمثلة على استخدام مؤشر الوظيفة:
int (*pFunc) (int a, char *pStr); // declare a simple function pointer variable
int (*pFunc[55])(int a, char *pStr); // declare an array of 55 function pointers
int (**pFunc)(int a, char *pStr); // declare a pointer to a function pointer variable
struct { // declare a struct that contains a function pointer
int x22;
int (*pFunc)(int a, char *pStr);
} thing = {0, func}; // assign values to the struct variable
char * xF (int x, int (*p)(int a, char *pStr)); // declare a function that has a function pointer as an argument
char * (*pxF) (int x, int (*p)(int a, char *pStr)); // declare a function pointer that points to a function that has a function pointer as an argument
يمكنك استخدام قوائم المعلمة ذات الطول المتغير في تعريف مؤشر الوظيفة.
int sum (int a, int b, ...);
int (*psum)(int a, int b, ...);
أو لا يمكنك تحديد قائمة المعلمات على الإطلاق. يمكن أن يكون هذا مفيدًا ولكنه يلغي الفرصة لمرجم C لإجراء عمليات التحقق من قائمة الوسيطة المقدمة.
int sum (); // nothing specified in the argument list so could be anything or nothing
int (*psum)();
int sum2(void); // void specified in the argument list so no parameters when calling this function
int (*psum2)(void);
C يلقي النمط
يمكنك استخدام CATPS CATS مع مؤشرات الوظائف. ومع ذلك ، كن على دراية بأن برنامج التحويل البرمجي 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 أن يفعل شيئًا مشابهًا لنمط C namespace
النهج الذي يتم فيه طلب واجهة مكتبة معينة من طريقة المصنع في واجهة مكتبة تدعم إنشاء أ 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 هو مؤشر وظيفة مندوب المقارنة المستخدمة مع وظائف مكتبة 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);
مثال آخر هو مع رمز مصدر واجهة المستخدم الرسومية التي يتم فيها تسجيل معالج لحدث معين من خلال توفير مؤشر وظيفة يسمى فعليًا عند حدوث الحدث. يستخدم إطار Microsoft MFC مع خرائط رسائله شيئًا مشابهًا للتعامل مع رسائل Windows التي يتم تسليمها إلى نافذة أو مؤشر ترابط.
تشبه الوظائف غير المتزامنة التي تتطلب رد اتصال معالج الأحداث. يستدعي مستخدم الدالة غير المتزامنة الوظيفة غير المتزامنة لبدء بعض الإجراءات ويوفر مؤشر وظيفة ستدعوها الوظيفة غير المتزامنة بمجرد اكتمال الإجراء. في هذه الحالة ، يكون الحدث هو الوظيفة غير المتزامنة التي تكمل مهمتها.
البدء من وظيفة الصفر لديه بعض عنوان الذاكرة من حيث يبدأون في التنفيذ. في لغة التجميع ، يطلق عليهم اسم (استدعاء "عنوان ذاكرة الوظيفة"). عد الآن إلى 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");
}
بعد ذلك ، يتيح أن يرى كيف يفهمها الماكينة.
تُظهر منطقة Red Mark كيف يتم تبادل العنوان وتخزينه في EAX. ثم هم تعليمات مكالمة على EAX. يحتوي EAX على العنوان المطلوب للوظيفة.
نظرًا لأن مؤشرات الوظائف غالبًا ما يتم كتابة عمليات الاسترجاعات ، فقد ترغب في إلقاء نظرة على اكتب عمليات الاسترجاعات الآمنة. وينطبق الشيء نفسه على نقاط الدخول ، وما إلى ذلك من الوظائف التي لا تراجع.
C متقلبة تمامًا ومتسامحة في نفس الوقت :)