مؤشرات الوظائف في برمجة C مع أمثلة

⚡ ملخص ذكي

تخزن مؤشرات الدوال في لغة C عنوان التعليمات البرمجية القابلة للتنفيذ، مما يسمح للبرامج باستدعاء الدوال بشكل غير مباشر، وتمريرها كوسائط، وبناء جداول الإرسال من المصفوفات، وتنفيذ عمليات رد الاتصال مثل روتين مقارنة qsort لتصميمات مرنة وقابلة لإعادة الاستخدام.

  • 🔗 إعلان: يستخدم مؤشر الدالة الشكل return_type (*name)(arguments)؛ الأقواس المحيطة بالاسم إلزامية.
  • 🔄 التبديل حسب العنوان: يسمح تمرير عناوين المتغيرات إلى معلمات المؤشر للدالة بتغيير القيم الأصلية للمستدعي، كما هو الحال في مثال التبديل.
  • 📦 المصفوفات: اسم المصفوفة هو بالفعل مؤشر، لذا يمكن للدوال أن تستقبل مصفوفة أو تعيد مؤشرًا إلى مصفوفة ثابتة.
  • 🎛️ جداول الإرسال: تستبدل مصفوفة مؤشرات الدوال سلاسل switch أو if الطويلة عن طريق الفهرسة مباشرة للعملية المختارة.
  • 🧊 مؤشرات فارغة: يقبل المعامل void * أو نوع الإرجاع أي نوع، والذي تقوم الدالة بتحويله مرة أخرى قبل الاستخدام.
  • 📞 عمليات الاسترجاعات: يؤدي تمرير مؤشر دالة كوسيط إلى تشغيل وظائف الاستدعاء مثل دالة المقارنة qsort.
  • 🤖 مساعدة الذكاء الاصطناعي: يقوم برنامج GitHub Copilot والمساعدون المدعومون بالذكاء الاصطناعي بإنشاء بنية مؤشر الدالة، وتعريفات الأنواع، وتوقيعات الاستدعاء، ثم يقومون بتحديد المعلمات غير المتطابقة.

مؤشرات الدوال في لغة C

توفر المؤشرات إمكانيات كبيرة لوظائف "C" والتي تقتصر على إرجاع قيمة واحدة. باستخدام معلمات المؤشر، يمكن لوظائفنا الآن معالجة البيانات الفعلية بدلاً من نسخة من البيانات.

من أجل تعديل القيم الفعلية للمتغيرات، تقوم عبارة الاستدعاء بتمرير العناوين إلى معلمات المؤشر في الوظيفة.

مثال على مؤشرات الوظائف

على سبيل المثال، يقوم البرنامج التالي بتبديل قيمتين من اثنين:

void swap (int *a, int *b);
int main() {
  int m = 25;
  int n = 100;
  printf("m is %d, n is %d\n", m, n);
  swap(&m, &n);
  printf("m is %d, n is %d\n", m, n);
  return 0;}
void swap (int *a, int *b) {
  int temp;
  temp = *a;
  *a = *b;
  *b = temp;}
}

الإخراج:

m is 25, n is 100
m is 100, n is 25

مثال على مؤشرات الوظائف

يقوم البرنامج بتبديل قيم المتغيرات الفعلية لأن الوظيفة تصل إليها عن طريق العنوان باستخدام مؤشر. سنناقش هنا عملية البرنامج:

  1. نُعلن عن الدالة المسؤولة عن عملية التبديلping القيم المتغيرة، والتي تأخذ مؤشرين صحيحين كمعاملات وتعيد أي قيمة عند استدعائها.
  2. في الوظيفة الرئيسية، نعلن ونقوم بتهيئة متغيرين صحيحين ('m' و'n') ثم نطبع قيمهما على التوالي.
  3. نحن نطلق على وظيفة المبادلة () عن طريق تمرير عنوان المتغيرين كوسائط باستخدام رمز علامة الضم. بعد ذلك، نقوم بطباعة القيم الجديدة للمتغيرات.
  4. هنا نقوم بتعريف محتوى دالة swap() والتي تأخذ عنوانين لمتغيرين صحيحين كمعلمات ونعلن عن متغير صحيح مؤقت يستخدم كصندوق تخزين ثالث لحفظ أحد متغيرات القيمة والذي سيتم وضعه في المتغير الثاني.
  5. احفظ محتوى المتغير الأول المشار إليه بالحرف "a" في المتغير المؤقت.
  6. قم بتخزين المتغير الثاني الذي يشير إليه b في المتغير الأول الذي يشير إليه a.
  7. تحديث المتغير الثاني (المشار إليه بـ b) بقيمة المتغير الأول المحفوظ في المتغير المؤقت.

وظائف مع معلمات الصفيف

في لغة C، لا يمكننا تمرير مصفوفة من حيث القيمة إلى دالة. حيث أن اسم المصفوفة هو مؤشر (عنوان)، لذلك نقوم فقط بتمرير اسم مصفوفة إلى دالة مما يعني تمرير مؤشر إلى المصفوفة.

على سبيل المثال، نأخذ في الاعتبار البرنامج التالي:

int add_array (int *a, int num_elements);
int main() {
  int Tab[5] = {100, 220, 37, 16, 98};
  printf("Total summation is %d\n", add_array(Tab, 5)); 
  return 0;}
int add_array (int *p, int size) {
  int total = 0;
  int k;
  for (k = 0; k < size; k++) {
    total += p[k];  /* it is equivalent to total +=*p ;p++; */}
 return (total);}

الإخراج:

 Total summation is 471

هنا سوف نقوم بشرح كود البرنامج مع تفاصيله

وظائف مع معلمات الصفيف

  1. نعلن ونحدد وظيفة add_array() التي تأخذ عنوان المصفوفة (المؤشر) برقم عناصرها كمعلمات وترجع المجموع الإجمالي المتراكم لهذه العناصر. يتم استخدام المؤشر لتكرار عناصر المصفوفة (باستخدام تدوين p[k])، ونقوم بتجميع المجموع في متغير محلي سيتم إرجاعه بعد تكرار مصفوفة العناصر بأكملها.
  2. نعلن ونهيئ مصفوفة عددية تحتوي على خمسة عناصر صحيحة. نقوم بطباعة المجموع الإجمالي عن طريق تمرير اسم المصفوفة (الذي يعمل كعنوان) وحجم المصفوفة إلى ملف add_array()تسمى الوظيفة كوسيطات.

الوظائف التي تقوم بإرجاع صفيف

في لغة C، يمكننا إرجاع مؤشر إلى مصفوفة، كما في البرنامج التالي:

#include <stdio.h>
int * build_array();
int main() {
  int *a;
  a = build_array(); /* get first 5 even numbers */
  for (k = 0; k < 5; k++)
    printf("%d\n", a[k]);
  return 0;}
int * build_array() {
  static int Tab[5]={1,2,3,4,5};
   return (Tab);}

الإخراج:

1
2
3
4
5

وهنا سوف نناقش تفاصيل البرنامج

الوظائف التي تقوم بإرجاع صفيف

  1. نقوم بتعريف وإعلان دالة تقوم بإرجاع عنوان مصفوفة تحتوي على قيمة عددية ولا تأخذ أي وسيطات.
  2. نعلن عن مؤشر عدد صحيح يستقبل المصفوفة الكاملة التي تم إنشاؤها بعد استدعاء الوظيفة ونطبع محتوياتها عن طريق تكرار مصفوفة العناصر الخمسة بالكامل.

لاحظ أنه تم تعريف مؤشر، وليس مصفوفة، لتخزين عنوان المصفوفة التي أرجعتها الدالة. لاحظ أيضًا أنه عند إرجاع متغير محلي من دالة، علينا أن نعلن أنه ثابت في الدالة.

مؤشرات الوظيفة

كما نعلم بحكم التعريف أن المؤشرات تشير إلى عنوان في أي موقع بالذاكرة، ويمكنها أيضًا الإشارة إلى بداية التعليمات البرمجية القابلة للتنفيذ كوظائف في الذاكرة.
يتم الإعلان عن مؤشر الدالة باستخدام العلامة *، والبيان العام لإعلانه هو:

return_type (*function_name)(arguments)

عليك أن تتذكر أن الأقواس الموجودة حول (*function_name) مهمة لأنه بدونها، سيعتقد المترجم أن function_name يُرجع مؤشر return_type.
بعد تعريف مؤشر الدالة، علينا أن نعينه لدالة. على سبيل المثال، يقوم البرنامج التالي بإعلان دالة عادية، ثم تعريف مؤشر دالة، ثم تعيين مؤشر الدالة للدالة العادية، ثم بعد ذلك استدعاء الدالة من خلال المؤشر:

#include <stdio.h>
void Hi_function (int times); /* function */
int main() {
  void (*function_ptr)(int);  /* function pointer Declaration */
  function_ptr = Hi_function;  /* pointer assignment */
  function_ptr (3);  /* function call */
 return 0;}
void Hi_function (int times) {
  int k;
  for (k = 0; k < times; k++) printf("Hi\n");}

الإخراج:

Hi
Hi
Hi

مؤشرات الوظائف في C

  1. نحن نحدد ونعلن عن وظيفة قياسية تطبع نص Hi k مرات تشير إليها المعلمة مرات عند استدعاء الوظيفة
  2. نحدد دالة المؤشر (مع إعلانها الخاص) والتي تأخذ معلمة عددية ولا تُرجع أي شيء.
  3. نقوم بتهيئة وظيفة المؤشر باستخدام وظيفة Hi_function مما يعني أن المؤشر يشير إلى وظيفة Hi_function().
  4. بدلاً من استدعاء الدالة القياسي بواسطة taping اسم الدالة مع الوسائط، نستدعي فقط دالة المؤشر بتمرير الرقم 3 كوسيط، وهذا كل شيء!

ضع في اعتبارك أن اسم الوظيفة يشير إلى عنوان البداية للتعليمة البرمجية القابلة للتنفيذ مثل اسم المصفوفة الذي يشير إلى العنصر الأول الخاص بها. ولذلك، فإن التعليمات مثل function_ptr = &Hi_function و(*funptr)(3) صحيحة.
ملاحظة: ليس من المهم إدراج عامل العنوان & وعامل الإحالة غير المباشرة * أثناء تعيين الوظيفة واستدعاء الوظيفة.

مجموعة من المؤشرات الوظيفية

يمكن لمجموعة من المؤشرات الوظيفية أن تلعب دور التبديل أو عبارة if لاتخاذ القرار، كما في البرنامج التالي:

#include <stdio.h>
int sum(int num1, int num2);
int sub(int num1, int num2);
int mult(int num1, int num2);
int div(int num1, int num2);

int main() 
{  int x, y, choice, result;
  int (*ope[4])(int, int);
  ope[0] = sum;
  ope[1] = sub;
  ope[2] = mult;
  ope[3] = div;
  printf("Enter two integer numbers: ");
  scanf("%d%d", &x, &y);
  printf("Enter 0 to sum, 1 to subtract, 2 to multiply, or 3 to divide: ");
  scanf("%d", &choice);
  result = ope[choice](x, y);
  printf("%d", result);
return 0;}

int sum(int x, int y) {return(x + y);}
int sub(int x, int y) {return(x - y);}
int mult(int x, int y) {return(x * y);}
int div(int x, int y) {if (y != 0) return (x / y); else  return 0;}
Enter two integer numbers: 13 48
Enter 0 to sum, 1 to subtract, 2 to multiply, or 3 to divide: 2
624

هنا نناقش تفاصيل البرنامج:

مجموعة من المؤشرات الوظيفية

  1. نعلن ونحدد أربعة وظائف تأخذ هذه الدوال وسيطين من نوع عدد صحيح وتعيد قيمة عددية صحيحة. وتشمل هذه الدوال الجمع والطرح.tract، اضرب واقسم الوسيطين المتعلقين بالدالة التي يستدعيها المستخدم.
  2. نعلن عن 4 أعداد صحيحة للتعامل مع المتغيرات ونوع العملية والنتيجة على التوالي. كما نعلن عن مصفوفة من أربعة مؤشرات دالة. يأخذ كل مؤشر دالة لعنصر المصفوفة معاملين صحيحين ويعيد قيمة عددية صحيحة.
  3. نقوم بتعيين كل عنصر من عناصر المصفوفة وتهيئته بالدالة المعلنة بالفعل. على سبيل المثال، سيشير العنصر الثالث الذي يمثل مؤشر الدالة الثالثة إلى دالة عملية الضرب.
  4. نحن نبحث عن المتغيرات ونوع العملية من المستخدم الذي يكتب باستخدام لوحة المفاتيح.
  5. قمنا بتسمية عنصر المصفوفة المناسب (مؤشر الدالة) باستخدام الوسائط، وقمنا بتخزين النتيجة التي تم إنشاؤها بواسطة الدالة المناسبة.

التعليمات int (*ope[4])(int, int); يحدد مجموعة مؤشرات الوظيفة. يجب أن يكون لكل عنصر صفيف نفس المعلمات ونوع الإرجاع.
نتيجة العبارة = ope[choice](x, y); يقوم بتشغيل الوظيفة المناسبة وفقًا لاختيار المستخدم. العددان الصحيحان اللذان تم إدخالهما هما الوسيطات التي تم تمريرها إلى الوظيفة.

وظائف باستخدام مؤشرات باطلة

يتم استخدام المؤشرات الفارغة أثناء إعلانات الوظائف. نحن نستخدم تصاريح نوع الإرجاع الفارغة * لإرجاع أي نوع. إذا افترضنا أن معلماتنا لا تتغير عند المرور إلى دالة، فإننا نعلنها على أنها const.
فمثلا:

 void * cube (const void *);

ضع في اعتبارك البرنامج التالي:

#include <stdio.h>
void* cube (const void* num);
int main() {
  int x, cube_int;
  x = 4;
  cube_int = cube (&x);
  printf("%d cubed is %d\n", x, cube_int);
  return 0;}

void* cube (const void *num) {
  int result;
  result = (*(int *)num) * (*(int *)num) * (*(int *)num);
  return result;}

النتيجة:

 4 cubed is 64

هنا سوف نناقش تفاصيل البرنامج:

وظائف باستخدام مؤشرات باطلة

  1. نقوم بتعريف وإعلان دالة تقوم بإرجاع قيمة عددية وتأخذ عنوان متغير غير قابل للتغيير بدون نوع بيانات محدد. نحسب القيمة المكعبة لمتغير المحتوى (x) الذي يشير إليه مؤشر num، وبما أنه مؤشر فارغ، علينا أن نكتب cast إلى نوع بيانات صحيح باستخدام مؤشر تدوين محدد (* datatype)، ونعود قيمة المكعب.
  2. نعلن عن المتغير المتغير والنتيجة. كما نقوم بتهيئة المتغير المتغير بالقيمة "4".
  3. نقوم باستدعاء دالة المكعب عن طريق تمرير عنوان المتغير، ونقوم بمعالجة القيمة المرتجعة في متغير النتيجة

مؤشرات الوظيفة كوسائط

هناك طريقة أخرى لاستغلال مؤشر دالة عن طريق تمريرها كوسيطة إلى دالة أخرى تسمى أحيانًا "وظيفة رد الاتصال" لأن الوظيفة المتلقية "تستدعيها مرة أخرى".
في ملف الرأس stdlib.h، تستخدم وظيفة Quicksort “qsort()” هذه التقنية وهي خوارزمية مخصصة لفرز مصفوفة.

void qsort(void *base, size_t num, size_t width, int (*compare)(const void *, const void *))
  • void *base : مؤشر فارغ للمصفوفة.
  • size_t num : رقم عنصر المصفوفة.
  • size_t width حجم العنصر.
  • int (*compare (const void *, const void *) : يتكون مؤشر الدالة من وسيطتين ويعيد 0 عندما تكون الوسيطات لها نفس القيمة، و<0 عندما يأتي arg1 قبل arg2، و>0 عندما يأتي arg1 بعد arg2.

يقوم البرنامج التالي بفرز مجموعة الأعداد الصحيحة من رقم صغير إلى رقم كبير باستخدام الدالة qsort():

#include <stdio.h>
#include <stdlib.h>
int compare (const void *, const void *); 
int main() {
  int arr[5] = {52, 14, 50, 48, 13};
  int num, width, i;
  num = sizeof(arr)/sizeof(arr[0]);
  width = sizeof(arr[0]);
  qsort((void *)arr, num, width, compare);
  for (i = 0; i < 5; i++)
    printf("%d ", arr[ i ]);
  return 0;}
int compare (const void *elem1, const void *elem2) {
  if ((*(int *)elem1) == (*(int *)elem2))  return 0;
  else if ((*(int *)elem1) < (*(int *)elem2)) return -1;
  else return 1;}

النتيجة:

 13 14 48 50 52

هنا سوف نناقش تفاصيل البرنامج:

مؤشرات الوظيفة كوسائط

  1. نحدد دالة المقارنة المكونة من وسيطتين ونعيد 0 عندما تكون الوسيطات لها نفس القيمة، و<0 عندما يأتي arg1 قبل arg2، و>0 عندما يأتي arg1 بعد arg2. المعلمات هي نوع مؤشرات فارغة يتم إرسالها إلى نوع بيانات المصفوفة المناسب (عدد صحيح)
  2. نقوم بتعريف وتهيئة مصفوفة عدد صحيح، ويتم تخزين حجم المصفوفة في ملف NUM متغير ويتم تخزين حجم كل عنصر من عناصر المصفوفة في متغير العرض باستخدام sizeof() المحدد مسبقًا مشغل C.
  3. نستدعي دالة qsort ونمرر إليها اسم المصفوفة وحجمها وعرضها ودالة المقارنة التي حددها المستخدم مسبقًا لترتيب المصفوفة تصاعديًا. تتم المقارنة بأخذ عنصرين من المصفوفة في كل تكرار حتى يتم ترتيب المصفوفة بالكامل.
  4. نقوم بطباعة عناصر المصفوفة للتأكد من أن المصفوفة الخاصة بنا مرتبة جيدًا عن طريق تكرار المصفوفة بأكملها باستخدام لحلقة.

الأسئلة الشائعة

نعم. يجب أن يتطابق مؤشر الدالة تمامًا مع نوع القيمة المرجعة وقائمة المعاملات للدالة المستهدفة. يُعدّ تعيين دالة بتوقيع مختلف سلوكًا غير مُعرّف، حتى لو تمّت عملية الترجمة بنجاح، لأنّ الاستدعاء سيُضيف ويقرأ الوسائط بشكل غير صحيح أثناء التشغيل.

يُعطي تعريف النوع typedef اسمًا مختصرًا لنوع مؤشر دالة معقد. كتابة typedef int (*Operaيتيح لك تعريف )(int, int)؛ Operaإضافة tion؛ بدلاً من تكرار بناء جملة المؤشر بالكامل، مما يجعل المصفوفات والمعلمات وأعضاء البنية أسهل بكثير في القراءة.

تُستخدم مؤشرات الوظائف لتشغيل عمليات الاستدعاء، وجداول القفز أو الإرسال، والأنظمة التي تعتمد على المكونات الإضافية والأحداث، وآلات الحالة، وروتينات المكتبة مثل qsort و bsearch. Operaتعتمد أنظمة الاختبار والبرامج الثابتة المدمجة عليها لاختيار السلوك في وقت التشغيل دون الحاجة إلى سلاسل شرطية طويلة.

يُخزّن مؤشر البيانات عنوان متغير في الذاكرة، بينما يُخزّن مؤشر الدالة عنوان دخول الكود القابل للتنفيذ. يتم استدعاء الدالة من خلال مؤشر الدالة لتشغيلها، بدلاً من فك مرجعيتها لقراءة البيانات أو كتابتها.

نعم. يمكن تعيين مؤشر الدالة إلى NULL للدلالة على عدم تعيين أي دالة. استدعاء مؤشر دالة بقيمة NULL يؤدي إلى سلوك غير محدد، لذا يُنصح بحماية الاستدعاء باستخدام الشرط if (ptr != NULL) قبل استدعائه، خاصةً بالنسبة لوظائف الاستدعاء الاختيارية.

نعم. يسمح وضع مؤشرات الدوال كأعضاء في البنية بتجميع البيانات مع العمليات ذات الصلة، محاكاةً للكائنات وجداول الأساليب الافتراضية. هذا النمط هو أساس العديد من مكتبات لغة C وبرامج تشغيل الأجهزة، حيث يختار كل مثيل تطبيقات الدوال الخاصة به.

نعم. يمكن لمساعد البرمجة المدعوم بالذكاء الاصطناعي إنشاء تعريفات مؤشرات الدوال، وأنواع البيانات المُعرّفة، وتوقيعات دوال الاستدعاء من خلال توجيه بلغة بسيطة، وشرح الصيغ المُعقدة، واكتشاف المعاملات أو أنواع الإرجاع غير المتطابقة. يُنصح دائمًا بتجميع واختبار الكود المُقترح قبل اعتماده.

مساعد الطيار جيثب يقترح صيغة مؤشر الدالة، ويكمل نصوص الاستدعاءات من تعليق أو توقيع، ويقترح دوال مقارنة qsort. افتح ملفات الرأس ذات الصلة بحيث تتطابق عمليات الإكمال مع النماذج الأولية المُعلنة، ثم راجع المنطق.

تلخيص هذه التدوينة بـ: