المعاملات مقابل الوسائط
معاملات الدالة هي المتغيرات المسماة في تعريفها؛ أما الوسائط فهي القيم الفعلية التي تمررها إليها عند استدعائها. أظهرت الصفحة السابقة كيفية تعريف الدوال واستدعائها - وهذه الصفحة تتناول كيف تصل تلك القيم فعليًا إلى الداخل، لأن ++C يتيح لك عدة طرق، والاختيار يؤثر على الصحة والسرعة معًا.
الوضع الافتراضي في ++C هو التمرير بالقيمة: تستقبل الدالة نسخة.
داخل addTen، يكون n متغيرًا منفصلًا مُهيأً من score. إعادة إسناد n تمسّ تلك النسخة فقط، لذا يبقى score سليمًا عند العودة إلى main. هذا آمن ويمكن التنبؤ به - فالدالة لا تستطيع أن تطمس بياناتك عن طريق الخطأ - وهذا تحديدًا سبب كونه الوضع الافتراضي.
التمرير بالمرجع: السماح للدالة بتغيير المُستدعي
أحيانًا تريد أن تعدّل الدالة متغير المُستدعي. أضف & إلى نوع المعامل فيصبح مرجعًا - اسمًا بديلًا للأصل، لا نسخة:
الفرق الوحيد عن المثال الأول هو &، لكن n و score الآن هما الكائن نفسه. هذه هي الطريقة المعيارية «لإرجاع» أكثر من قيمة أو لتحديث شيء في مكانه. ومن الاستخدامات الكلاسيكية تبديل قيمتي متغيرين:
بدون &، كانت swapValues ستبدّل بين نسختين ولن يرى main أي تغيير على الإطلاق - وهو خطأ شائع جدًا لدى المبتدئين.
مراجع const: وصول رخيص للقراءة فقط
التمرير بالقيمة ينسخ الوسيط. بالنسبة لـ int لا يكلّف ذلك شيئًا، لكن نسخ string أو vector كبير في كل استدعاء عمل حقيقي ومهدور. الحل هو مرجع const (const T&): تحصل على سرعة المرجع (بلا نسخ) إضافة إلى وعد يفرضه المترجِم بعدم تعديل الوسيط.
قاعدة عملية مفيدة: مرّر الأنواع المدمجة الصغيرة (int، double، char، bool، المؤشرات) بالقيمة، ومرّر الكائنات الكبيرة التي تحتاج قراءتها فقط بـ مرجع const. واحتفظ بـ T& العادي غير الـ const للحالات التي تنوي فيها فعلًا تعديل كائن المُستدعي.
مزلق دقيق: لا يمكن لـ int& n العادي أن يرتبط بكائن مؤقت أو بقيمة حرفية. فاستدعاء addTen(5) من المثال الأول لن يُترجَم لو كان المعامل int&، لأن 5 ليست متغيرًا يمكنك إنشاء اسم بديل له. أما const int& فـ يمكنه الارتباط بـ 5، وهذا سبب إضافي لشيوع استخدام مراجع const.
الوسائط الافتراضية
يمكنك إعطاء معامل قيمة احتياطية ليتمكن المُستدعون من إغفاله. إذا غاب الوسيط، تُستخدم القيمة الافتراضية:
ثمة قاعدتان تُربكان الناس. الأولى، يجب أن تكون القيم الافتراضية في الطرف الختامي - فبمجرد أن يكون لمعامل قيمة افتراضية، يجب أن يكون لكل معامل بعده قيمة افتراضية أيضًا. لا يمكنك كتابة void f(int a = 1, int b) لأنه لن تكون هناك طريقة لتمرير b مع تخطّي a. والثانية، عندما تُعلَن الدالة في ملف ترويسة وتُعرَّف في مكان آخر، ضع القيمة الافتراضية في الإعلان فقط، ولا تكررها أبدًا في التعريف - فتكرارها خطأ ترجمة.
تمرير المصفوفات والمتجهات
تتحلل المصفوفة الخام إلى مؤشر عند تمريرها، فتفقد الدالة معرفة حجمها - لذا تمرّر الطول إلى جانبها في كل الأحوال تقريبًا:
لأن المصفوفة صارت مؤشرًا، فإن sizeof(arr) داخل sum سيعطي حجم المؤشر، لا حجم المصفوفة - وهو خطأ سيّئ السمعة. في ++C الحديثة، فضّل std::vector (أو std::span في 20++C)، ممررًا بمرجع const، فهو يحمل حجمه معه:
لاحظ const&: أسقطه فيقوم كل استدعاء بنسخ المتجه بأكمله. بالنسبة لمتجه من أربعة عناصر هذا غير مؤذٍ، لكن لمليون عنصر يصبح استنزافًا صامتًا للأداء.
معاملات المؤشرات
يمكنك أيضًا تمرير مؤشر (T*). كما هو الحال مع المرجع، يتيح هذا للدالة الوصول إلى بيانات المُستدعي، لكن المؤشر يمكن إعادة توجيهه أو يمكن أن يكون فارغًا (null) - لذا فهو الأداة الصحيحة عندما يكون «لا قيمة» خيارًا مشروعًا:
يمرّر المُستدعي &value لمشاركة عنوانه، وتكتب الدالة عبر *out. الفرق الجوهري عن المراجع: قد يكون المؤشر nullptr، لذا ينبغي للدالة التي تستقبل مؤشرًا أن تتحقق قبل إلغاء الإشارة - فتخطّي هذا التحقق وإلغاء الإشارة لمؤشر فارغ سلوك غير مُعرَّف، وعادةً ما يكون انهيارًا. وإذا كان «لا قيمة» لا يحمل معنى أبدًا، فالمرجع أنظف لأنه لا يمكن أن يكون فارغًا أصلًا.
التالي: المراجع
المعاملات هي المكان الذي تُثبت فيه المراجع جدارتها، لكن المراجع ميزة قائمة بذاتها - أسماء بديلة يمكنك إنشاؤها لأي متغير، لا داخل توقيع دالة فحسب. تتعمق الصفحة التالية في كيفية عمل المراجع بمفردها: كيف تُعلِنها، ولماذا يجب تهيئتها فورًا، والفرق بين مرجع lvalue ومرجع const، والطرق الدقيقة التي قد ينتهي بها المرجع معلّقًا (dangling).
الأسئلة الشائعة
ما الفرق بين التمرير بالقيمة والتمرير بالمرجع في ++C؟
التمرير بالقيمة ينسخ الوسيط إلى المعامل، لذا فإن التغييرات داخل الدالة لا تؤثر على المُستدعي. أما التمرير بالمرجع (int&) فيجعل المعامل اسمًا بديلًا لمتغير المُستدعي، لذا تكون التغييرات مرئية من الخارج. استخدم void f(int x) للنسخ و void f(int& x) لتعديل الأصل.
متى ينبغي استخدام معامل من نوع مرجع const في ++C؟
استخدم const T& عندما تريد قراءة كائن كبير دون نسخه ودون السماح للدالة بتعديله - مثل void print(const string& s). يمنحك ذلك سرعة التمرير بالمرجع مع أمان التمرير بالقيمة. أما للأنواع الصغيرة مثل int أو char، فإن التمرير بالقيمة العادي يكون بنفس السرعة.
ما هي الوسائط الافتراضية في ++C؟
تتيح الوسائط الافتراضية للمعامل أن يأخذ قيمة احتياطية عندما يُغفلها المُستدعي، مثل void greet(string name = "there"). يجب أن تكون القيم الافتراضية للمعاملات الأخيرة (الأكثر إلى الطرف الختامي)، وتُحدَّد في الإعلان فقط، لا في التعريف إذا كانا منفصلين.