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

مفهوم التعاود Recursion وتمرير الوسطاء إلى الدوال في لغة سي C


Naser Dakhel

نظرنا سابقًا إلى كيفية تعيين نوع للدالة Funtion type (كيفية التصريح عن القيمة المُعادة ونوع أيّ وسيط argument تأخذه الدالة)، وأن تعريف definition الدالة يمثّل متنها أو جسمها body، ولننظر الآن إلى استخدامات الوسطاء.

استدعاء الوسيط بقيمته call by value

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

void called_func(int, float);

main(){
      called_func(1, 2*3.5);
      exit(EXIT_SUCCESS);
}

void
called_func(int iarg, float farg){
      float tmp;

      tmp = iarg * farg;
}

[مثال 1]

يوجد للدالة called_func تعبيران يحلّان محل الوسطاء في دالة main، وتُقيّم قيمتهما ويُستخدمان في إسناد قيمة مبدئية للمعاملين irag و frag في الدالة called_func، ويملك المعاملان الخصائص ذاتها التي يملكها أي متغير داخلي مصرّحٌ عنه في الدالة called_func دون أي تفريق، مثل tmp.

تُعد عملية إسناد القيمة المبدئية للمعاملات الفعلية التواصل الأخير بين مستدعي الدالة والدالة المُستدعاة، إذا استثنينا القيمة المُعادة.

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

#include <stdio.h>
#include <stdlib.h>
main(){
      void changer(int);
      int i;

      i = 5;
      printf("before i=%d\n", i);
      changer(i);
      printf("after i=%d\n", i);
      exit(EXIT_SUCCESS);
}

void
changer(int x){
      while(x){
              printf("changer: x=%d\n", x);
              x--;
      }
}

[مثال 2]

ستكون نتيجة المثال السابق على النحو التالي:

before i=5
changer: x=5
changer: x=4
changer: x=3
changer: x=2
changer: x=1
after i=5

تَستخدم الدالة changer معاملها الفعلي x بمثابة متغير اعتيادي (وهو فعلًا متغير اعتيادي)، ورغم أن قيمة x تغيرت إلا أن المتغير i في الدالة main لم يتأثر بالتغيير، وهذه هي النقطة التي نريد توضيحها لك بمثالنا، إذ تُمرَّر الوسطاء في سي C إلى الدالة باستخدام قيمها فقط ولا تُمرر أي تغييرات من الدالة بالمقابل.

استدعاء الوسيط بمرجعه call by reference

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

التعاود Recursion

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

يمكن لأي دالة أن تستدعي نفسها من داخلها أو من داخل أي دالة أخرى في لغة سي، ويتسبب كل استدعاء للدالة بحجز متغيراتٍ جديدة مصرّح عنها داخل الدالة، وفي الحقيقة، كانت تفتقر التصريحات التي استخدمناها حتى اللحظة إلى شيءٍ ما، ألا وهو الكلمة المفتاحية auto التي تعني "الحجز التلقائي".

/* مثال عن الحجز التلقائي */
main(){
      auto int var_name;
      .
      .
      .
}

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

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

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

يوضح المثال التالي برنامجًا يحتوي دالةً تعاوديةً تتحقق من التعابير المُدخلة إليها بما فيها الأرقام (0-9) والعوامل "*" و "%" و "/" و "+" و "-"، إضافةً إلى الأقواس، بالطريقة نفسها التي تستخدمها لغة سي، كما استخدم ستروستروب Stroustrup في كتابه عن لغة ++C المثال ذاته لتوضيح مفهوم التعاود، وهذا من قبيل الصدفة لا غير.

يُقيّم التعبير في المثال التالي، ثُم تُطبع قيمته إن صادف محرفًا غير موجودًا في لغته (المحارف التي ذكرناها سابقًا)، ولغرض البساطة لن يكون في المثال أي طريقة للتحقق من الأخطاء. يعتمد المثال كثيرًا على الدالة ungetc التي تسمح للمحرف الأخير الذي قُرأ بواسطة الدالة getchar أن يُعيّن على أنه "غير مقروء" للسماح بقراءته مرةً أخرى، والمُعامل الثاني المُستخدم في المثال مُصرّحٌ عنه في stdio.h.

سيرغب من يفهم صيغة باكوس نور BNF بمعرفة أن التعبير سيُفهم عن طريق استخدام الصيغة التالية:

<primary> ::= digit | (<exp>)
<unary>   ::= <primary> | -<unary> | +<unary>
<mult>    ::= <unary> | <mult> * <unary> |
              <mult> / <unary> | <mult> % <unary>
<exp>     ::= <exp> + <mult> | <exp> - <mult> | <mult>

يكمن التعاود في مثالنا ضمن مكانين أساسيين، هما: الدالة unary_exp التي تستدعي نفسها، والدالة primary التي تستدعي الدالة الموجودة على المستوى العلوي للبرنامج (نقصد دالة expr) لتقييم التعابير المكتوبة بين قوسين.

حاول تشغيل البرنامج باستخدام كل من الأمثلة التالية إذا لم تفهم عمله، وتتبَّع عمله يدويًّا على المُدخلات، كما يلي:

1
1+2
1+2 * 3+4
1+--4
1+(2*3)+4

سيستغرق هذا بعض الوقت منك.

/*
* برنامج يتحقق من تعابير لغة سي على نحوٍ تعاودي
* لم يُشدّد على حالات الإدخال الخاطئة من المستخدم
*/

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

int expr(void);
int mul_exp(void);
int unary_exp(void);
int primary(void);

main(){
      int val;

      for(;;){
              printf("expression: ");
              val = expr();
              if(getchar() != '\n'){
                      printf("error\n");
                      while(getchar() != '\n')
                              /* فارغ */;
              } else{
                      printf("result is %d\n", val);
              }
      }
      exit(EXIT_SUCCESS);
}

int
expr(void){
      int val, ch_in;

      val = mul_exp();
      for(;;){
              switch(ch_in = getchar()){
              default:
                      ungetc(ch_in,stdin);
                      return(val);
              case '+':
                      val = val + mul_exp();
                      break;
              case '-':
                      val = val - mul_exp();
                      break;
              }
      }
}

int
mul_exp(void){
      int val, ch_in;

      val = unary_exp();
      for(;;){
              switch(ch_in = getchar()){
              default:
                      ungetc(ch_in, stdin);
                      return(val);
              case '*':
                      val = val * unary_exp();
                      break;
              case '/':
                      val = val / unary_exp();
                      break;
              case '%':
                      val = val % unary_exp();
                      break;
              }
      }
}

int
unary_exp(void){
      int val, ch_in;

      switch(ch_in = getchar()){
      default:
              ungetc(ch_in, stdin);
              val = primary();
              break;
      case '+':
              val = unary_exp();
              break;
      case '-':
              val = -unary_exp();
              break;
      }
      return(val);
}

int
primary(void){
      int val, ch_in;

      ch_in = getchar();
      if(ch_in >= '0' && ch_in <= '9'){
              val = ch_in - '0';
              goto out;
      }
      if(ch_in == '('){
              val = expr();
              getchar();      /* “')” تخطي قوس الإغلاق */
              goto out;
      }
      printf("error: primary read %d\n", ch_in);
      exit(EXIT_FAILURE);
out:
      return(val);
}

[مثال 3]

ترجمة -وبتصرف- لقسم من الفصل 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.


×
×
  • أضف...