لماذا الدوال
عاش كل شيء حتى الآن داخل main. وهذا يفي بالغرض حتى يفعل البرنامج أكثر من شيء واحد، فتصل ثلاث مشكلات دفعة واحدة: تُنسَخ الكتلة نفسها من الشيفرة إلى ثلاثة مواضع، وتنمو main إلى ما يتجاوز ما يستوعبه أحد في ذهنه، ولا توجد طريقة لتسمية ما يُقصد من قطعة شيفرة.
الدالة تحلّ الثلاث. فهي تمنح كتلة من الشيفرة اسمًا، ومتغيّرات محلّية خاصة بها، وقائمة مدخلات، وقيمة واحدة تسلّمها.
كُتبت square مرة واحدة واستُدعيت ثلاث مرات، بينها مرة على نتيجتها هي. وأنت تستدعي دوالّ سلفًا - فـ printf واحدة منها، وكذلك main.
تشريح دالة
returnType name(parameterList) {
// الجسم
return value;
}
أربعة أجزاء:
- نوع الإرجاع - نوع القيمة التي تسلّمها الدالة.
intأوdoubleأوcharأو نوع مؤشّر، أوvoidبمعنى "لا شيء". - الاسم - يتبع قواعد اسم المتغيّر نفسها، وينبغي أن يقول ما تفعله الدالة أو ما تنتجه.
- قائمة الوسائط - المدخلات، لكلٍّ منها نوعه، مفصولة بفواصل. و
(void)تعني أنها لا تأخذ شيئًا. - الجسم - الجمل، بين أقواس معقوفة، منتهيةً بـ
returnما لم يكن نوع الإرجاعvoid.
لاحظ أن pi() ما تزال تحتاج إلى القوسين الفارغين عند موضع الاستدعاء - فهذا ما يجعلها استدعاءً لا مجرّد ذكر للاسم. وprintBanner(20); جملة قائمة بذاتها، لأنها لا تنتج قيمة تُستخدم.
اكتب (void) لا () للدالة التي لا وسائط لها. فهما في C تعنيان شيئين مختلفين، و() هي المتساهلة - وسيأتي المزيد عن ذلك بعد قليل.
الاستدعاء والإرجاع
ينقل الاستدعاء التحكّم إلى داخل الدالة، وينفّذ جسمها، ويعود بقيمة تحلّ محلّ الاستدعاء في التعبير المحيط.
تفعل return شيئين في آن: توفّر القيمة وتنهي الدالة في الحال. ولا يُنفَّذ أي شيء بعد return على المسار نفسه إطلاقًا، وهذا ما يجعل أسلوب الإرجاع المبكر في larger ناجحًا - فلا حاجة إلى else، لأن بلوغ السطر الأخير يعني سلفًا أن a > b لم تكن محقّقة.
ويمكن أن تحتوي دالة على عدة جمل return. لكنها لا تنفّذ إلا واحدة منها في كل استدعاء.
دوال void
الدالة الموجودة من أجل أثرها لا من أجل قيمتها يكون نوع إرجاعها void: الطباعة، وتحديث شيء عبر مؤشّر، والرسم.
داخل دالة void، تخرج return; بلا قيمة مبكرًا. أما return someValue; فخطأ ترجمة هناك، وكذلك استخدام الاستدعاء داخل تعبير - فـ int x = printTable(7, 5); لن تُترجَم، وهذا نظام الأنواع يؤدّي عمله.
التفكيك: المقصد الحقيقي
الدوال ليست في الأساس عن تفادي التكرار. بل عن تحويل إجراء طويل واحد إلى بضع خطوات مسمّاة، بحيث تُقرأ main كوصف للبرنامج لا كتنفيذه.
صارت main الآن أربعة أسطر من الإخراج وعملية حسابية واحدة. كل دالة مساعِدة تؤدّي شيئًا واحدًا، ولها اسم يقول ما هو ذلك الشيء، ويمكن اختبارها وحدها. ولاحظ أن طول المصفوفة يسافر كوسيط ثانٍ: فبمجرّد تمرير مصفوفة إلى دالة تتوقّف حيلة sizeof عن العمل هناك، لأن المصفوفة تتحوّل إلى مؤشّر.
ثلاث قواعد إرشادية لتقسيم العمل إلى دوال:
- مهمّة واحدة لكل دالة. إن احتاج الاسم إلى "و" بداخله، فالأرجح أنهما دالّتان.
- خُذ ما تحتاجه، وأرجِع ما تنتجه. فالدالة التي تقرأ حالة عامة يصعب التفكير فيها ويستحيل اختبارها بمعزل.
- قصيرة بما يكفي لرؤيتها دفعة واحدة. لا يوجد عدد أسطر سحري، لكن الدالة التي لا تتّسع في شاشة تخفي عادةً دالة أصغر بداخلها.
دوال تستدعي دوالّ
تستطيع الدالة استدعاء أي دالة صُرِّح عنها سلفًا، بما في ذلك ما كتبته أنت:
تستدعي main الدالةَ report، التي تستدعي sumEvens، التي تستدعي isEven داخل حلقة. ويحصل كل استدعاء على مجموعته الطازجة من المتغيّرات المحلّية، مكدّسة فوق مجموعة المستدعي، وتُهمَل حين يعود.
والتعريفات أعلاه مرتّبة بحسب التبعية - isEven قبل sumEvens قبل report - لأن C تتطلّب أن تكون الدالة معلَنة قبل استدعائها. فاستدعِ دالة تظهر أسفل الملف فيشتكي المترجم من تصريح ضمني. والحلّ العام هو نموذج أوّلي قرب رأس الملف، يتيح لك ترتيب التعريفات كما تشاء. واستدعاء الدالة نفسها مشروع ومفيد أيضًا؛ وذلك هو الاستدعاء الذاتي.
main دالة
main دالة عادية تستدعيها لك بيئة التشغيل. ونوع إرجاعها int هو حالة خروج البرنامج: 0 تعني النجاح، وغير الصفر يعني الفشل، وتقرؤها نصوص الصدفة وأدوات البناء.
int main(void) {
/* ... */
return 0;
}
ومنذ C99، فإن بلوغ نهاية main دون return يُرجِع 0 ضمنًا - وهي قاعدة خاصة تنطبق على main وحدها. أما كل دالة أخرى ليست void وتنتهي دون إرجاع قيمة فتُنتج سلوكًا غير معرَّف حين يستخدم المستدعي النتيجة.
والصيغة المعيارية الأخرى هي int main(int argc, char *argv[])، التي تستقبل وسائط سطر الأوامر. وكلتاهما صحيحة؛ أما void main() فليست كذلك، مهما قالت الدروس القديمة.
أخطاء شائعة
- نسيان
returnفي دالة ليست void. فيقرأ المستدعي عندئذٍ قيمة مهملة. وgcc -Wallيحذّر. - تعريف متغيّر بالاسم نفسه لوسيط داخل الجسم. فهو يحجب الوسيط، ويذهب الإسناد الذي قصدته للمدخل سدى.
- توقّع أن تغيّر الدالة وسائطها. تمرّر C كل شيء بالقيمة، فتستقبل الدالة نسخًا. فـ
void reset(int x) { x = 0; }لا تغيّر شيئًا عند موضع الاستدعاء - وهو موضوع الصفحة التالية. - حذف القوسين في الاستدعاء.
printBanner;تعبير مشروع يقيّم عنوان الدالة ويهمله. يُترجَم، ولا يفعل شيئًا، و-Wallينبّه إليه. - كتابة
()بدلًا من(void). مشروعة، لكنها تعطّل فحص الوسائط في لهجات C القديمة.
الأسئلة الشائعة
كيف تعرّف دالة في لغة C؟
اكتب نوع الإرجاع، ثم الاسم، ثم قائمة وسائط بين قوسين، ثم الجسم بين أقواس معقوفة: int add(int a, int b) { return a + b; }. والدالة التي لا تأخذ شيئًا تستخدم (void)، والتي لا تُرجِع شيئًا يكون نوع إرجاعها void.
ماذا تفعل return في دالة C؟
تُنهي الدالة فورًا وتسلّم قيمة إلى المستدعي. ويجب أن يطابق نوع القيمة نوع الإرجاع المعلَن للدالة. وفي دالة void يمكنك كتابة return; مجرّدة للخروج مبكرًا، أو تركها ودع الدالة تنتهي عند القوس المعقوف المغلق.
لماذا تُرجِع main نوع int في لغة C؟
القيمة المُرجَعة من main هي حالة خروج البرنامج، التي يستطيع نظام التشغيل ونصوص الصدفة قراءتها. فـ return 0; تعني النجاح وأي قيمة غير صفرية تعني الفشل. ومنذ C99، فإن بلوغ نهاية main دون جملة return يُرجِع 0 ضمنًا.