اذهب إلى المحتوى

الدوال في لغة C


Naser Dakhel

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

ما التغييرات التي طرأت على لغة سي المعيارية بخصوص الدوال؟

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

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

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

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

أنواع الدوال

تمتلك جميع الدوال نوعًا ما، والذي يكون هو نوع القيمة التي تُعيدها عند استخدامها. السبب في عدم احتواء لغة سي على "إجراءات procedures"، والتي هي في معظم اللغات دوال بدون قيمة، هو أنها تسمح بصورةٍ إجبارية في بعض الأحيان بتجاهل القيمة النهائية لمعظم التعابير، وإن فاجئك ذلك تذكر تعبير الإسناد التالي:

a = 1;

الإسناد السابق صالح ويعيد قيمةً ما، لكن القيمة تُهمل. إذا أردت مفاجأةُ أكبر من السابقة، جرِّب التعبير التالي:

1;

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

f(argument);

التعبير السابق هو تعبير ذو قيمةٍ مُهملة.

من السهل فهم نقطة أن القيمة المُعادة من الدالة يمكن إهمالها، لكن هذا يعني أن عدم استخدام القيمة المُعادة هو خطأ برمجي، وعلى العكس تمامًا إن لم يكن هناك أي قيمة مفيدة مُعادة من الدالة، إذًا من الأفضل أن يكون لدينا القدرة على مراقبة فيما إذا كانت القيمة مُستخدمةً عن طريق الخطأ، وللسببين السابقين، يجب التصريح عن أي دالة بكونها لا تعيد أي قيمة مفيدة بالنوع void.

يمكن أن تُعيد الدوال أي نوع مدعوم من لغة سي C عدا المصفوفات arrays والدوال بحد ذاتها، وهذا يتضمن المؤشرات pointers والبُنى structures والاتحادات unions، وسنتكلم عنهم لاحقًا. يمكننا التحايل على الأنواع التي لا يُمكن إعادتها من الدوال باستخدام المؤشرات بدلًا منها، كما يمكن استدعاء جميع الدوال تعاوديًّا recursively.

التصريح عن الدوال

علينا الآن للأسف تقديم بعض المصطلحات بهدف التقليل من النص الوصفي المتكرر بعد ذكر كل مصطلح برمجي للوصول إلى نتيجة أقصر وأكثر دقة دون أي تشويش للقارئ، إليك المصطلحات:

  • التصريح declaration: نذكر فيه النوع type الذي يرتبط باسم ما.
  • التعريف definition: يماثل التصريح، لكنه يحجز أيضًا مساحة تخزينية للكائن المذكور، وقد تكون القواعد التي تفصل بين التصريح والتعريف معقدة، لكن الأمر بسيط بالنسبة للدوال؛ إذ يصبح التصريح تعريفًا عندما يُضاف محتوى الدالة على أنه تعليمةٌ مُركّبة compound statement.
  • المُعاملات parameters والمعاملات الصوريّة formal parameters: الأسماء التي تُشير إلى الوسطاء بداخل الدالة.
  • الوسطاء arguments والوسطاء الفعلية actual arguments: القيم المُستخدمة مثل وسطاء في دالة ما، أي قيم المُعاملات الصوريّة عند تنفيذ الدالة.

يُستخدم المصطلحان "مُعامل" و"وسيط" على نحوٍ تبادلي، لذا لا تتساءل عن سبب استخدامنا لمصطلح عن الآخر في الفقرات القادمة.

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

double aax1(void);

وإليك مثالًا عن استخدامها الخاطئ:

main(){
      double return_v, aax1(void);
      return_v = aax1();
      exit(EXIT_SUCCESS);
}

[مثال 1]

التصريح في المثال السابق مثير للاهتمام، إذ عرّفنا return_v مما تسبب بإنشاء متغيرٍ جديد، كما صرّحنا عن aax1 دون تعريفها، إذ أن الدوال تُعرّف فقط في حالة وجود متن الدالة، كما ذكرنا سابقًا، وفي هذه الحالة يُفترض أن تُعيد الدالة aax1 النوع int ضمنيًّا مع أنها تعيد النوع double، مما يعني أن ذلك سيتسبب بتصرف غير محدد، وهو أمر كارثي دائمًا.

يوضِّح وجود النوع void ضمن لائحة الوسطاء عند التصريح بأن الدالة لا تقبل أي وسيط، وإن كانت مفقودةً من لائحة الوسطاء فلن يترك التصريح أي معلومات عن وسطاء الدالة، وبهذه الطريقة نحافظ على التوافقية مع لغة سي C القديمة على حساب قدرة المصرّف على التحقق.

ينبغي كتابة متن للدالة مثل تعليمةٍ مركّبة حتى تعرّفها، إذ لا يمكن لتعريف دالةٍ ما أن يكون محتوًى بتعريف دالةٍ أخرى. ونتيجةً لذلك، جميع الدوال مستقلةٌ عن بعضها بعضًا وموجودةٌ في المستوى الخارجي لهيكل البرنامج. يوضح التعريف التالي طريقةً ممكنةً للتعريف عن الدالة aax1:

 double
  aax1(void) {
        /*متن الدالة هنا*/
        return (1.0);
  }

من غير الاعتيادي أن تمنعك لغةٌ تعتمد على الهيكلية الكُتلية عن تعريف الدوال داخل دوالٍ أخرى، ولكن هذه سمةٌ من سمات لغة سي C، ويساعد ذلك على تحسين الأداء وقت التنفيذ run-time للغة سي، لأنه يقلل المهام المطلوبة المتعلقة بتنظيم استدعاء الدوال.

تعليمة الإعادة return

تُعد تعليمة return مهمةً للغاية، إذ تستخدمها جميع الدوال -عدا التي تُعيد void- مرةً واحدةً على الأقل، وتوضِّح التعليمة return عند ذكرها القيمة التي يجب أن تُعيدها. من الممكن أن نعيد قيمةً من دالةٍ ما عن طريق وضع return في نهاية الدالة قبل القوس المعقوص الأخير "{"؛ إلا أن هذا الأمر سيتسبب بتصرف غير محدد عند استخدامها في دالة تُعيد void، إذ ستُعاد قيمةٌ غير معروفة.

إليك مثالًا عن دالة أخرى تستخدم getchar لقراءة المحارف من دخل البرنامج ومن ثمّ تعيدها باستثناء المسافة space ومسافة الجدولة tab والأسطر الجديدة newline.

#include <stdio.h>

int
non_space(void){
      int c;
      while ( (c=getchar ())=='\t' || c== '\n' || c==' ')
              ; /*تعليمة فارغة*/
      return (c);
}

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

while (something);

فمن السهل ألّا تلاحظ الفاصلة المنقوطة في نهاية السطر عند قراءة البرنامج، وتفترض أن التعليمات أسفلها تتبع لتعليمة while.

يجب أن يكافئ نوع التعبير المُعاد نوع الدالة ضمن تعليمة return، أو على الأقل أن يكون بالإمكان تحويله ضمن تعليمة إسناد. على سبيل المثال، يمكن أن تحتوي دالة مُصرح عنها أنها تُعيد النوع double التعليمة:

return (1);

سيحوَّل بذلك العدد الصحيح إلى نوع double، ومن الممكن أيضًا كتابة return دون أي تعبير مصاحب لها، لكن ذلك سيتسبب بخطأ برمجي إن استخدمت هذه الطريقة ما لم تُعيد الدالة النوع void. من غير المسموح إلحاق تعبير بتعليمة return إذا كانت الدالة تعيد النوع void.

وسطاء الدوال

لم يكن بالإمكان قبل مجيء لغة سي C المعيارية إضافة أي معلومات عن وسطاء الدالة عدا داخل تعريف الدالة نفسها، وكانت هذه المعلومات تُستخدم داخل متن الدالة فقط وتُنسى في نهاية المطاف. كان من الممكن -في تلك الأيام القديمة البائسة- تعريف دالةٌ بثلاث وسطاء من نوع double وتمرير وسيط من نوع int لها عند استدعائها، وسيُصرِّف البرنامج بصورةٍ طبيعية دون إظهار أي أخطاء ولكنه لن يعمل بالنحو الصحيح، إذ كان من واجب المبرمج التحقق من صحة عدد وأنواع الوسطاء للدالة. كما ستتوقع، كان هذا المسبب الأساسي لكثيرٍ من الأخطاء الأولية في البرنامج ومشكلات قابلية التنقل. إليك مثالًا عن تعريف دالة واستخدامها مع وسطائها لكن دون التصريح عن الدالة كاملًا.

#include <stdio.h>
#include <stdlib.h>
main(){
      void pmax();                    /* التصريح */
      int i,j;
      for(i = -10; i <= 10; i++){
              for(j = -10; j <= 10; j++){
                      pmax(i,j);
              }
      }
      exit(EXIT_SUCCESS);
}
/*
* Function pmax.
* Returns:      void
* Prints larger of its two arguments.
*/
void
pmax(int a1, int a2){                   /* التعريف*/
      int biggest;

      if(a1 > a2){
              biggest = a1;
      }else{
              biggest = a2;
      }

      printf("larger of %d and %d is %d\n",
              a1, a2, biggest);
}

[مثال 2]

ما الذي يمكنك تعلُّمه من المثال السابق؟ بدايةً، لاحظ بحذر أن التصريح هو للدالة pmax التي تُعيد void، إذ يقع النوع void الموافق للدالة عند تعريفها السطر الذي يسبق اسمها، وهذه الطريقة في الكتابة أسلوبٌ وتحبيذٌ شخصي، إذ من السهل إيجاد التصريح عن الدالة إذا كان اسم الدالة يقع في بداية السطر.

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

دعنا ننتقل الآن إلى تعريف الدالة حيث يقع متنها، ولاحظ أنه يدل على أن الدالة تأخذ وسيطين باسم a1 وa2، كما أن نوع الوسطاء محدد بالنوع int.

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

/* مثال سيء على التصريح عن الدالة */

void
pmax(a1, a2){
        /* and so on */

الطريقة المثالية للتصريح والتعريف عن الدوال هي باستخدام ما يُدعى النماذج الأولية prototypes.

نماذج الدوال الأولية

كان تقديم نماذج الدوال الأولية function prototypes من أكبر التغييرات في لغة سي C المعيارية؛ إذ أن نموذج الدالة الأولية هو تصريح أو تعريف يتضمن معلومات عن عدد وأنواع الوسطاء التي تستخدمها الدالة.

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

#include <stdio.h>
#include <stdlib.h>

main(){
      void pmax(int first, int second);       /*التصريح*/
      int i,j;
      for(i = -10; i <= 10; i++){
              for(j = -10; j <= 10; j++){
                      pmax(i,j);
              }
      }
      exit(EXIT_SUCCESS);
}

void
pmax(int a1, int a2){                           /*التعريف*/
      int biggest;

      if(a1 > a2){
              biggest = a1;
      }
      else{
              biggest = a2;
      }

      printf("largest of %d and %d is %d\n",
              a1, a2, biggest);
}

[مثال 3]

احتوى التصريح هذه المرة على معلومات بخصوص وسطاء الدالة، لذا يصنّف على أنه نموذج أوّلي. لا يُعد الاسمان first و second جزءًا ضروريًا من التصريح، لكنهما موجودان لتصبح عملية الإشارة إلى الوسطاء عند توثيق عمل الدالة عمليّةً أسهل، إذ نستطيع وصف عمل الدالة ببساطة عن طريق استخدامهما عن طريق التصريح التالي:

void pmax (int xx, int yy );

وهنا نستطيع القول أن الدالة pmax تطبع القيمة الأكبر ضمن الوسيطين xx و yy بدلًا عن الإشارة إلى الوسطاء بتموضعها، أي الوسيط الثاني واﻷول وهكذا، لأنها طريقةٌ معرضةٌ لسوء الفهم أو الخطأ في عدّ الترتيب.

عدا عن ذلك، تستطيع التخلص من أسماء الوسطاء في تصريح الدالة ويكون التصريح التالي مساويًا للتصريح السابق:

void pmax (int,int);

الفرق بين التصريحين هو فقط أسماء الوسطاء.

يكون التصريح عن دالة لا تأخذ أي وسطاء على النحو التالي:

void f_name (void);

وللتصريح عن دالة تأخذ وسيطًا من نوع int وآخرًا من نوع double وعددًا غير محدّد من الوسطاء الآخرين، نكتب:

void f_name (int,double,...);

توضّح هنا النقاط الثلاث ... أن هناك المزيد من الوسطاء، وهذا مفيدٌ في حال كانت الدالة تسمح بوجود عددٍ غير محدد من الوسطاء، مثل دالة printf، إذ أن تصريح الدالة هذه يكون على النحو التالي:

int printf (const char *format_string,...)

الوسيط الأول هو "مؤشر pointer للقيمة من النوع const char"، وسنناقش معنى ذلك لاحقًا.

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

إليك مثالًا توضيحيًّا: دالةٌ تحسب الجذر التربيعي لقيمةٍ ما باستخدام طريقة نيوتن للتقريبات المتعاقبة Newton's method of successive approximations.

#include <stdio.h>
#include <stdlib.h>
#define DELTA 0.0001
main(){
      double sq_root(double); /* النموذج الأولي*/
      int i;

      for(i = 1; i < 100; i++){
              printf("root of %d is %f\n", i, sq_root(i));
      }
      exit(EXIT_SUCCESS);
}

double
sq_root(double x){      /* التعريف*/
      double curr_appx, last_appx, diff;

      last_appx = x;
      diff = DELTA+1;

      while(diff > DELTA){
              curr_appx = 0.5*(last_appx
                      + x/last_appx);
              diff = curr_appx - last_appx;
              if(diff < 0)
                      diff = -diff;
              last_appx = curr_appx;
      }
      return(curr_appx);
}

[مثال 4]

يوضّح النموذج الأولي للدالة أن sq_root تأخذ وسيطًا واحدًا من نوع double، وفي حقيقة الأمر، تُمرّر قيمة الوسيط ضمن الدالة main بنوع int لذا يجب تحويلها إلى double أوّلًا؛ لكن يجدر الانتباه هنا إلى أن سي ستفترض أن المبرمج تقصَّد تمرير قيمة int، إذا لم يكُن هناك أي نموذج أولي وفي هذه الحالة ستُعامَل القيمة على أنها int دون تحويل.

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

السبب في التحويل من int إلى double في هذه الحالة هو بسبب رؤية المصرّف للنموذج الأولي مما دلّه على ما يجب فعله، وكما توقعت، هناك العديد من القواعد المستخدمة لتحديد التحويل المناسب في كل حالة، وسنتكلم عنها.

تحويلات الوسطاء

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

تتضمن هذه القواعد ترقيات الوسطاء الافتراضية default argument promotions والأنواع المتوافقة compatible types، وفيما يخص ترقية الوسطاء الافتراضية، فهي:

  • تُطبَّق الترقية العددية الصحيحة على كل قيمة وسيط.
  • يُحوَّل الوسيط إلى نوع double إذا كان نوعه float.

أبرزَ ظهور النماذج الأولية -إضافةً إلى أشياء أخرى- الحاجة للدقة بخصوص "الأنواع المتوافقة"، التي لم تكن مشكلةً في سي القديمة. سنستعرض لائحة قوانين الأنواع المتوافقة كاملةً لاحقًا، لأننا نعتقد أن معظم مبرمجي لغة سي لن يكونوا محتاجين لتعلمها كاملةً، وعوضًا عن ذلك سنكتفي في الوقت الحالي بمعرفة أن الأنواع المتماثلة متوافقة فيما بينها.

تُطبّق التحويلات بالاعتماد على القوانين التالية (ليست اقتباسًا مباشرًا من المعيار، بل ستدلك على كيفية تطبيق قوانين سي المعيارية):

أولًا، تُرقّى وسطاء الدالة وفق ترقية الوسطاء الاعتيادية، إذا لم يكن هناك أي نموذج أولي يسبق نقطة استدعاء الدالة، وذلك وفق التفاصيل:

  • نحصل على سلوكٍ غير محدد إذا كان عدد الوسطاء المزوَّد لا يكافئ عدد الوسطاء الفعلي للدالة.
  • يجب أن تكون أنواع الوسطاء المزوّدة للدالة متوافقة مع أنواع الوسطاء الفعلية وفق تعريف الدالة بعد تطبيق الترقية عليهم وذلك إذا لم يكن لتعريف الدالة نموذجٌ أولي، ونحصل على سلوك غير محدد فيما عدا ذلك.
  • نحصل على سلوك غير محدد إذا لم تحتوي الدالة على نموذج أولي في تعريفها وكانت أنواع الوسطاء المزوّدة غير متوافقة مع أنواع الوسطاء الفعلية، كما يكون السلوك غير محدد أيضًا إذا تضمّن النموذج الأولي للدالة علامة الاختصار "…".

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

من الممكن كتابة برنامج يحتوي على نموذج أولي ضمن النطاق عند استدعاء الدالة لكن دون وجود نموذج أولي داخل تعريف الدالة، وهذا طبعًا أسلوبٌ سيءٌ جدًا، وفي هذه الحالة يجب على نوع الدالة المُستدعاة أن يكون متوافقًا مع النوع المُستخدم عند استدعاء هذه الدالة.

لا يحدّد المعيار صراحةً ترتيب تقييم وسطاء الدالة عند استدعائها.

تعريف الدوال

تسمح النماذج الأولية للدوال باستخدام النص ذاته للتصريح عن الدالة والتعريف عنها.

لتحويل تصريح الدالة التالي إلى تعريف:

double
some_func(int a1, float a2, long double a3);

نضيف متنًا إلى الدالة:

double
some_func(int a1, float a2, long double a3){
      /* متن الدالة */
      return(1.0);
}

وذلك باستبدال الفاصلة المنقوطة في نهاية التصريح بتعليمة مركّبة.

يعمل تعريف الدالة أو التصريح عنها مثل نموذج أولي شرط تحديد أنواع المُعاملات parameters، ويُعدّ المثالين السابقين نموذجين أوليين.

ما تزال لغة C المعيارية تدعم طريقة سي القديمة في التصريح عن دالة باستخدام وسائطها، ولكن يجب تجنُّب استخدامها. يمكننا التصريح عن الدالة بالطريقة المذكورة على النحو التالي:

double
some_func(a1, a2, a3)
      int a1;
      float a2;
      long double a3;
{

      /* متن الدالة */
      return(1.0);
}

لا يمثّل التعريف السابق نموذجًا أوليًا، لعدم وجود أي معلومات بخصوص المُعاملات عند تسميتها، ويقدِّم التعريف السابق معلومات حول النوع الذي تُعيده الدالة، وبهذا لا يتذكر المصرّف Compiler أي معلومات تخص أنواع الوسطاء بنهاية التعريف.

يحذِّر المعيار بخصوص هذه الطريقة بقوله أنها ستختفي غالبًأ في إصدارات قادمة، ولذلك لن نذكرها مرةً أخرى.

دعنا نلخّص ما تكلمنا عنه سابقًا بإيجاز:

  • يمكن استدعاء الدوال على نحوٍ تعاودي.
  • يمكن للدوال إعادة أي قيمة تصرح عنها عدا المصفوفات والدوال (إلا أنه يمكنك التحايل على هذا القيد باستخدام المؤشرات)، ويجب أن تكون الدوال من النوع void إذا لم تُعد أي قيمة.
  • استخدم نماذج الدوال الأولية دائمًا.
  • نحصل على سلوك غير محدد، إذا استُدعيت دالة أو تعريفها إلا إذا:
  • كان النموذج الأولي دائمًا ضمن النطاق في كل مرة تُستدعى فيها الدالة أو تُعرَّف.
  • كنت حريصًا جداً بهذا الخصوص.
  • تُحوّل قيم الوسطاء عند استدعاء الدالة إلى أنواع المُعاملات الفعلية للدالة (المعرّفة وفقها)، بصورةٍ مشابهة للتحويل عند عملية الإسناد باستخدام العامل operator "="، وذلك بفرض أنك تستخدم نموذجًا أوّليًّا.
  • يجب أن يشير النموذج الأولي إلى النوع void، إذا لم تأخذ الدالة أي وسطاء.
  • يجب تحديد اسم وسيط واحد على الأقل في دالة تقبل عددًا متغيرًا من الوسطاء، ومن ثم الإشارة إلى العدد المتغير من الوسطاء بالعلامة "…"، كما هو موضح:
int
vfunc(int x, float y, ...);

سنناقش لاحقًا استخدام هذا النوع من الدوال.

التعليمات المركبة والتصريحات

يتألف متن الدالة من تعليمة مركبة Compound statement، ومن الممكن التصريح عن متغيرات جديدة داخل هذه التعليمة المركبة. تغطّي أسماء المتغيرات الجديدة على أسماء المتغيرات الموجودة مسبقًا، إذا تشابهت أسماؤهم ضمن التعليمة المركبة، وهذا مماثلٌ لأي لغة تعتمد تنسيقً كتليًا مشابهًا للغة سي. تقيد لغة سي C التصريحات لتكون ضمن بداية التعليمة المركبة أو "الكتلة البرمجية"، ويُمنع استخدام التصريحات داخل الكتلة حالما يُكتب أي نوع من التعليمات statements داخلها.

كيف يمكن التغطية على أسماء المتغيرات؟ يوضح المثال التالي ما نقصد بذلك:

int a;                  /* يمكن رؤيته من هذه النقطة فصاعدًا */

void func(void){
      float a;        /* مختلف a يمثّل متغير*/
      {
              char a; /* مختلف أيضًا a متغير */
      }
                      /* يمكن رؤية المتغير ذو النوع قيمة حقيقية */
}
                      /* يمكن رؤية المتغير ذو النوع قيمة صحيحة */

[مثال 5]

عند التصريح عن اسم ما داخل كتلة فهذا يتسبب بإهمال أي اسم مشابه خارج الكتلة حتى الوصول لنهايتها، كما يمكنك التصريح عن الاسم ذاته في كتلة داخلية، وتكرار هذه العملية إلى ما لانهاية.

نطاق Scope الاسم هو المجال الذي يكون فيه لهذا الاسم معنًى، ويبدأ نطاق الاسم من نقطة ذكر الاسم إلى نهاية الكتلة التي ذُكر فيها، ويستمر إلى نهاية الملف إذا كان الاسم خارجي External (خارج أي دالة)، ويختفي في نهاية الدالة إذا كان الاسم داخلي Internal (داخل دالة)، ويمكن إعادة تعيين النطاق عند إعادة التصريح عن الاسم داخل الكتلة ذاتها.

يمكنك تنفيذ حيل طريفة مثل المثال التالي باستخدام قوانين النطاق:

main () {}
int i;
f () {}
f2 () {}

يمكن للدالتين f و f2 استخدام المتغير i، لكن main لا تستطيع لأن التصريح عن الدالة أتى بعد الدالة main، ولا تُستخدم كثيرًا هذه الطريقة بالضرورة ولكنها تستفيد من طريقة لغة سي C الضمنية في معالجة التصريحات. قد تتسبب هذه الطريقة ببعض من الحيرة لمن يقرأ ملف الشيفرة البرمجية، ويجب تجنُّبها عن طريق التصريح عن المتغيرات الخارجية قبل تعريف أي دالة في الملف.

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

كان خطأ إعادة التعريف غير المقصود في لغة سي C القديمة خطأً صعب التتبع والحل، إليك ما قد يبدو عليه الخطأ:

/* إعادة تصريح خاطئة للوسطاء */

func(a, b, c){
      int a;  /* AAAAgh! */
}

الجزء المسبب للمتاعب هنا هو التصريح الجديد للمتغير a في متن الدالة، الذي سيغطّي على المعامل a، ولن نتكلم بالمزيد عن هذه المشكلة بما أنها غير ممكنة الحدوث بعد الآن.

ترجمة -وبتصرف- لقسم من الفصل Functions من كتاب The C Book.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...