انتقل إلى المحتوى
Topos Uranos

ملخص

يدرس هذا الدرس الطرق التي تدخل منها البيانات إلى الدالة وتخرج منها نتائجها. فلغة C++ تمرّر القيم الممرَّرة افتراضيًا بالقيمة: الوسيط نسخة، ولا يبلغ الأصلَ شيءٌ مما تفعله الدالة به. أما المرجع فاسم ثانٍ لخانة موجودة من قبل، ويتيح للدالة أن تعدّل متغير من يستدعيها؛ والمرجع الثابت يتيح قراءة بيانات كبيرة دون نسخها ودون القدرة على تغييرها. ولإعادة عدة نتائج تقدّم المكتبة std::pair وstd::tuple، وتُستقبَل بالربط المهيكل. ثم يأتي نطاق رؤية الاسم (أين يُرى) ومدة حياة المتغير (متى يوجد)، مع حجب الاسم والمتغيرات المحلية static، ويُختم الدرس بالمرجع الذي يبقى بعد زوال متغيره. والمفهوم الذي يوحّد ذلك كله هو تدفق البيانات بين أجزاء البرنامج: النسخ في مقابل المشاركة، ورؤية الأسماء بوصفها آلية للعزل.

أهداف التعلم

  1. التنبؤ بأثر الدالة في القيم الممرَّرة إليها بحسب تمريرها بالقيمة أو بالمرجع.
  2. اختيار طريقة تمرير كل وسيط (بالقيمة للأنواع الصغيرة، وبالمرجع الثابت للبيانات الكبيرة، وبالمرجع لما يُعدَّل) وتعليل الاختيار.
  3. إعادة عدة نتائج بـstd::pair أو std::tuple واستقبالها بالربط المهيكل.
  4. تحديد نطاق الرؤية ومدة الحياة لكل متغير في البرنامج، بما في ذلك المتغيرات المحلية static والمتغيرات التي يحجبها متغير آخر.
  5. التعرّف على المرجع إلى متغير محلي أو إلى قيمة مؤقتة لم تعد موجودة، مع تحذير المترجم الذي ينبّه إليه، وتصحيحه.

التمرير بالقيمة

الوسيط نسخة

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

برنامج بلغة 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، بخانات خاصة به.

البرهان

  1. قبل الاستدعاء، في إطار main خانتان: x وفيها 1، وy وفيها 2.

  2. ينشئ الاستدعاء إطار swapValues. ووسيطاه a وb خانتان جديدتان، تُنسخ فيهما قيم القيم الممرَّرة: 1 و2. ومن هذه اللحظة لا تبقى أي صلة بين a وx، سوى أنهما بدأا بالقيمة نفسها.

  3. يُنشأ المتغير المحلي saved في الإطار نفسه ويحفظ نسخة من a.

  4. يبدّل الإسنادان a وb. والخانات التي تتغير كلها في الإطار العلوي؛ أما خانات main فلا تُمسّ.

  5. عند الرجوع يزول إطار swapValues كله، ومعه التبديل الذي كان يحويه. ويبقى x وy مساويين لـ 1 و2.

ما تحميه النسخة

ليس التمرير بالقيمة عيبًا، بل هو القاعدة التي تجعل الدوال معقولة. أولًا، يمكن أن تكون القيمة الممرَّرة أي تعبير، لا متغيرًا فحسب: فـswapValues(x + 1, 7) صحيحة، لأن الوسيط يُهيَّأ بقيمة، أيًّا كان مصدرها. ثانيًا، يعرف من يستدعي من التصريح، دون قراءة الجسم، أن متغيراته لن تتغير. ثالثًا، الوسيط داخل الدالة متغير محلي آخر، يمكن أن يُستعمل متغيرَ عمل دون عواقب في الخارج.

برنامج بلغة C++
#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. ويلزم عن ذلك قاعدة قراءة تصحّ في الدورة كلها: إذا لم يحمل الوسيط العلامة & التي يدرسها القسم الآتي، فلا تستطيع الدالة أن تعدّل متغير من يستدعيها، مهما فعلت به.