تعيش الشيفرة المترجَمة في الذاكرة تمامًا كما تعيش البيانات، فللدالة عنوان. ومؤشّر الدالة متغيّر يخزّن واحدًا - وبمجرّد أن تستطيع تخزين دالة، تستطيع تمرير واحدة كوسيط، أو الاحتفاظ بجدول منها، أو الاختيار بينها أثناء التشغيل.
وتلك هي القدرة وراء الترتيب بترتيب مخصّص، ومعالجات الأحداث، وواجهات الإضافات، وآلات الحالة. وهي أيضًا قطعة صياغة C الأكثر احتمالًا لجعل القارئ يتوقّف ويحدّق، فنبدأ من هناك.
صياغة التعريف
خذ دالة عادية:
int add(int a, int b);
ولتعريف مؤشّر يستطيع حمل عنوانها، استبدل الاسم بـ (*name):
int (*op)(int, int);
اقرأها من الداخل: op مؤشّر، إلى دالة، تأخذ (int, int)، وتُعيد int.
والقوسان ليسا زينة. فبدونهما ترتبط * بنوع الإرجاع بدلًا من ذلك:
int (*f)(int); // مؤشّر إلى دالة تأخذ int وتُعيد int
int *g(int); // دالة تأخذ int وتُعيد int* - مختلفة تمامًا
وإليها وهي تعمل من البداية إلى النهاية:
تعمل صيغتا الاستدعاء كلتاهما لأن مُعيِّن الدالة يتحوّل إلى مؤشّر تلقائيًا. اكتب op(10, 3)؛ أما تهجئة (*op)(10, 3) فبقية من الماضي.
وملاحظة على printf الأخيرة: %p تتوقّع مؤشّر كائن، ومؤشّرات الدوال عائلة منفصلة رسميًا، فطباعة واحد ليست محمولة بدقّة. والتحويل يُسكت المترجمات على المنصّات التي يعمل عليها أصلًا؛ ونادرًا ما ستحتاج إلى طباعة واحد.
typedef يجعلها مقروءة
تصبح صياغة التعريف قبيحة بسرعة، وtypedef هو العلاج القياسي:
قارن int apply(BinaryOp op, int x, int y) بالصيغة الخام int apply(int (*op)(int, int), int x, int y). المعنى نفسه؛ وإحداهما تستطيع قراءتها بنظرة. انظر typedef للقواعد العامة.
ردود النداء: ترك السلوك للمستدعي
ردّ النداء دالة تسلّمها لدالة أخرى كي تستطيع مناداتك في اللحظة المناسبة. وهو يتيح لقطعة شيفرة أن تتولّى بنية المهمّة بينما يوفّر المستدعي القرار.
بدون مؤشّرات الدوال كنت ستكتب count_even وcount_positive وcount_big - ثلاث نسخ من الحلقة نفسها. ومعها تُكتب الحلقة مرة واحدة ويكون الاختبار هو الوسيط.
المردود الحقيقي: qsort
دالة qsort في المكتبة القياسية هي مثال مؤشّرات الدوال الذي ستستخدمه فعلًا. فهي ترتّب مصفوفة من أي شيء، وتدبّر ذلك بألّا تعرف شيئًا عن العناصر سوى حجمها ومقارن تزوّدها به.
void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *));
يتلقّى المقارن const void * اثنين - عنواني عنصرين - ويُعيد عددًا سالبًا إن كان الأول يسبق الثاني في الترتيب، وصفرًا إن تعادلا، وموجبًا إن كان يليه.
وعادتان تحملهما من ذلك المقارن. لا تكتب أبدًا return x - y; - فهي تبدو ذكية وتفيض مع القيم الكبيرة أو السالبة، فتُنتج إشارة خاطئة ومصفوفة مرتّبة خطأً. وحوّل قبل فكّ الإشارة، لا بعدها: *(const int *)a، لأن *a على void * لا شيء لديها لتقرأه.
والآلية نفسها ترتّب البنى بأي حقل تشاء:
صيغة (q->score > p->score) - (q->score < p->score) تُعيد -1 أو 0 أو 1 بالضبط بلا طرح وبلا فيضان. وstrcmp لديها أصلًا عُرف الإرجاع الصحيح، فيكون مقارن السلاسل النصية سطرًا واحدًا.
جداول مؤشّرات الدوال
مصفوفة من مؤشّرات الدوال تحوّل سلسلة if/else إلى بحث. وهكذا توزّع المفسّرات رموز العمليات وتشغّل أنظمة القوائم أوامرها.
فإضافة عملية صارت إضافة صفّ، لا تحرير switch. اقرن ذلك بـ البنى فتحصل على شكل الإضافات القياسي في C: بنية من مؤشّرات الدوال هي ما تعنيه "الواجهة" في هذه اللغة.
أمور تعضّ
يجب أن تتطابق البصمات تمامًا. فنوع مؤشّر الدالة يشمل أنواع وسائطه ونوع إرجاعه. وإسناد دالة غير متطابقة انتهاك قيد، والاستدعاء عبر مؤشّر بنوع خاطئ سلوك غير معرَّف حتى حين "ينجح". لا تحوّل بصمة مقارن لإسكات تحذير - أصلِح المقارن.
ومؤشّر الدالة الفارغ يبقى مؤشّرًا فارغًا. هيّئ بـ NULL وافحص قبل الاستدعاء، تمامًا كما مع مؤشّرات البيانات:
if (handler != NULL) {
handler(event);
}
انظر المؤشّرات الفارغة لمعرفة لماذا يستحقّ ذلك الفحص وجوده.
ومؤشّرات void تُفقد أمان الأنواع، وتصبح أنت نظام الأنواع. فداخل مقارن qsort لا شيء يمنعك من التحويل إلى النوع الخاطئ. وإن رتّبت مصفوفة double بـ compare_ints، فإنها تُترجَم بنظافة وتُنتج هراءً.
وصياغة المعرِّف تتداخل. فدالة تُعيد مؤشّر دالة هي int (*get_op(char c))(int, int);. وحين تجد نفسك تكتب ذلك، فالجأ إلى typedef:
typedef int (*BinaryOp)(int, int);
BinaryOp get_op(char c); // الشيء نفسه، لكن مقروءًا
الأسئلة الشائعة
كيف تعرّف مؤشّر دالة في لغة C؟
اكتب بصمة الدالة مع (*name) مكان اسم الدالة: int (*op)(int, int); يعرّف op بأنه مؤشّر إلى دالة تأخذ عددين صحيحين وتُعيد int. والقوسان حول *op مطلوبان - فبدونهما تعرّف دالة تُعيد مؤشّرًا.
كيف تستدعي دالة عبر مؤشّر؟
إما op(3, 4) أو (*op)(3, 4) - كلتاهما مسموحتان وتفعلان الشيء نفسه، لأن مُعيِّن الدالة يتحوّل إلى مؤشّر تلقائيًا. وشيفرة C الحديثة تستخدم الصيغة البسيطة op(3, 4).
ما ردّ النداء في لغة C؟
دالة تسلّمها لدالة أخرى كي تستطيع مناداتك. وqsort هي المثال الكلاسيكي: تمرّر مقارنًا، وتستدعيه qsort كلما احتاجت معرفة أيّ العنصرين يأتي أولًا. وذلك ما يتيح لروتين ترتيب واحد أن يعمل على أي نوع.
هل تحتاج إلى & عند أخذ عنوان دالة؟
لا. فاسم الدالة يتحوّل أصلًا إلى مؤشّر إلى تلك الدالة، فـ op = add; وop = &add; متكافئتان. ومعظم شيفرات C تحذف &.