نظرنا سابقًا إلى كيفية تعيين نوع للدالة 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
اقرأ أيضًا
- المقال التالي: مفهوم النطاق Scope والربط Linkage على مستوى الدوال في لغة C
- المقال السابق: الدوال في لغة C
- البنية النصية لبرامج سي C
- العوامل في لغة سي C
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.