ملخص
يدرس هذا الدرس الطرق التي تدخل منها البيانات إلى الدالة وتخرج منها نتائجها. فلغة C++ تمرّر القيم الممرَّرة افتراضيًا بالقيمة: الوسيط نسخة، ولا يبلغ الأصلَ شيءٌ مما تفعله الدالة به. أما المرجع فاسم ثانٍ لخانة موجودة من قبل، ويتيح للدالة أن تعدّل متغير من يستدعيها؛ والمرجع الثابت يتيح قراءة بيانات كبيرة دون نسخها ودون القدرة على تغييرها. ولإعادة عدة نتائج تقدّم المكتبة std::pair وstd::tuple، وتُستقبَل بالربط المهيكل. ثم يأتي نطاق رؤية الاسم (أين يُرى) ومدة حياة المتغير (متى يوجد)، مع حجب الاسم والمتغيرات المحلية static، ويُختم الدرس بالمرجع الذي يبقى بعد زوال متغيره. والمفهوم الذي يوحّد ذلك كله هو تدفق البيانات بين أجزاء البرنامج: النسخ في مقابل المشاركة، ورؤية الأسماء بوصفها آلية للعزل.
أهداف التعلم
- التنبؤ بأثر الدالة في القيم الممرَّرة إليها بحسب تمريرها بالقيمة أو بالمرجع.
- اختيار طريقة تمرير كل وسيط (بالقيمة للأنواع الصغيرة، وبالمرجع الثابت للبيانات الكبيرة، وبالمرجع لما يُعدَّل) وتعليل الاختيار.
- إعادة عدة نتائج بـ
std::pairأوstd::tupleواستقبالها بالربط المهيكل. - تحديد نطاق الرؤية ومدة الحياة لكل متغير في البرنامج، بما في ذلك المتغيرات المحلية
staticوالمتغيرات التي يحجبها متغير آخر. - التعرّف على المرجع إلى متغير محلي أو إلى قيمة مؤقتة لم تعد موجودة، مع تحذير المترجم الذي ينبّه إليه، وتصحيحه.
التمرير بالقيمة
الوسيط نسخة
بيّن الدرس الدوال أن كل استدعاء ينشئ إطارًا في المكدس، فيه وسائط الدالة ومتغيراتها المحلية، وأن الإطار يزول عند الرجوع. ويبقى أن نحدد العلاقة بين الوسيط والقيمة الممرَّرة التي تهيّئه. وجواب C++ الافتراضي هو أبسط الأجوبة: الوسيط متغير محلي جديد يُهيَّأ بـنسخة من قيمة القيمة الممرَّرة. وإذا كان الأمر كذلك، فالدالة التي تعدّل وسائطها إنما تعدّل نُسَخها، ولا تتغير متغيرات من يستدعيها. والبرنامج الآتي يختبر ذلك بدالة تحاول تبديل عددين صحيحين.
#include <print>// تحاول تبديل a وb؛ ولا تبدّل إلا نسختيهما.void swapValues(int a, int b){ const int saved{a}; a = b; b = saved; std::println("في الداخل: a = {}, b = {}", a, b);}int main(){ int x{1}; int y{2}; swapValues(x, y); std::println("بعد الاستدعاء: x = {}, y = {}", x, y);}في الداخل: a = 2, b = 1بعد الاستدعاء: x = 1, y = 2داخل الدالة يتم التبديل على الوجه الصحيح؛ وبعد الاستدعاء يحتفظ x وy بقيمتيهما. ولا يحذّر المترجم، لأنه لا شيء يستدعي التحذير: فالشيفرة تبدّل متغيرين محليين يزولان عند انتهائها. ويتتبّع الرسم إطارات المكدس أثناء الاستدعاء.
يحوي إطار main المتغيرين x وy؛ وينشئ الاستدعاء فوقه إطار swapValues، بخانات خاصة به.
البرهان
قبل الاستدعاء، في إطار
mainخانتان:xوفيها 1، وyوفيها 2.ينشئ الاستدعاء إطار
swapValues. ووسيطاهaوbخانتان جديدتان، تُنسخ فيهما قيم القيم الممرَّرة: 1 و2. ومن هذه اللحظة لا تبقى أي صلة بينaوx، سوى أنهما بدأا بالقيمة نفسها.يُنشأ المتغير المحلي
savedفي الإطار نفسه ويحفظ نسخة منa.يبدّل الإسنادان
aوb. والخانات التي تتغير كلها في الإطار العلوي؛ أما خاناتmainفلا تُمسّ.عند الرجوع يزول إطار
swapValuesكله، ومعه التبديل الذي كان يحويه. ويبقىxوyمساويين لـ 1 و2.
ما تحميه النسخة
ليس التمرير بالقيمة عيبًا، بل هو القاعدة التي تجعل الدوال معقولة. أولًا، يمكن أن تكون القيمة الممرَّرة أي تعبير، لا متغيرًا فحسب: فـswapValues(x + 1, 7) صحيحة، لأن الوسيط يُهيَّأ بقيمة، أيًّا كان مصدرها. ثانيًا، يعرف من يستدعي من التصريح، دون قراءة الجسم، أن متغيراته لن تتغير. ثالثًا، الوسيط داخل الدالة متغير محلي آخر، يمكن أن يُستعمل متغيرَ عمل دون عواقب في الخارج.
#include <print>// عدد الأرقام العشرية في n.// الشرط المسبق: n >= 0.// الشرط اللاحق: تُعيد عدد أرقام n (1 للعدد 0).int digitCount(int n){ int count{1}; while (n >= 10) { n = n / 10; ++count; } return count;}int main(){ const int population{48'210}; std::println("العدد {} مكوَّن من {} أرقام", population, digitCount(population));}العدد 48210 مكوَّن من 5 أرقامتُتلف الدالة وسيطها بقسمته على 10 حتى لا يبقى فيه إلا رقم واحد؛ ولأنه نسخة يبقى population سليمًا، ويمكن أن يكون const. ويلزم عن ذلك قاعدة قراءة تصحّ في الدورة كلها: إذا لم يحمل الوسيط العلامة & التي يدرسها القسم الآتي، فلا تستطيع الدالة أن تعدّل متغير من يستدعيها، مهما فعلت به.
جارٍ تحميل المحتوى…