اسم آخر للشيء ذاته
في صفحة معاملات الدوال، كان كل وسيط يُنسخ إلى داخل الدالة. تلك النسخة هي السبب في أن الدالة لا تستطيع تغيير متغير المُستدعي، فهي لا ترى سوى نسختها الخاصة. لكن المرجع يكسر هذا الجدار. إنه اسم بديل: اسم ثانٍ مرتبط بمتغير موجود، يتقاسم معه الموقع ذاته في الذاكرة تماماً.
تُنشئ المرجع باستخدام & في التصريح. وبمجرد ربطه، يصبح المرجع والأصل لا يمكن تمييز أحدهما عن الآخر:
قاعدتان تجعلان المراجع آمنة ويمكن التنبؤ بها: يجب تهيئة المرجع في لحظة التصريح به (int& r; خطأ في الترجمة)، ولا يمكن إعادة ربطه أبداً ليشير إلى شيء آخر بعد ذلك. الإسناد إلى مرجع يكتب دائماً إلى ما رُبط به في الأصل.
التمرير بالمرجع: دع الدالة تمتدّ إلى الخلف
الفائدة الحقيقية في الدوال. ضع & على المعامل، فتتلقى الدالة اسماً بديلاً لوسيط المُستدعي بدلاً من نسخة. عندئذٍ تظهر التغييرات داخل الدالة في خارجها أيضاً:
احذف & وستزيد addBonus نسخةً عابرة، فيبقى total عند 100. هذا الحرف الوحيد هو الفرق كله. إنها الطريقة المعيارية لكتابة دالة تُرجع أكثر من نتيجة واحدة أو تحرّر مدخلاتها في مكانها. والمثال الكلاسيكي هو تبديل قيمتي متغيّرين:
من دون المراجع، ستبدّل swapValues نسخاً محلية فقط ويبقى x/y على 1 2. (تحتوي المكتبة القياسية بالفعل على std::swap، لكن كتابتها بنفسك تُظهر بدقة ما يمنحه إياك المعامل المرجعي.)
مراجع const: اقرأ بسرعة، وتعهّد بألّا تمسّ
يتجنب التمرير بالمرجع النسخ أيضاً، وقد تكون تلك النسخة مكلفة للكائن الكبير. لكن المعامل T& البسيط يشير إلى "قد أعدّل هذا"، وهو ما يضلّل عندما تريد القراءة فقط. الحل هو const T&: تحصل على سرعة المرجع دون نسخ إضافةً إلى تعهّد يفرضه المُترجم بأن الدالة لن تغيّر الوسيط.
المرجع غير الثابت يمكنه الارتباط فقط بمتغير قابل للتعديل، لكن المرجع الثابت يمكنه أيضاً الارتباط بالقيم الحرفية والكائنات المؤقتة، ولهذا تُترجَم greet("literal works too"). وإليك قاعدة عملية مفيدة لاختيار نوع المعامل:
void f(int x) // نوع رخيص، للقراءة فقط -> انسخه ببساطة
void f(const string& s) // نوع ثقيل، للقراءة فقط -> مرجع ثابت
void f(string& s) // تنوي تعديل كائن المُستدعي
اجعل const T& الخيار الافتراضي لأي نوع صنفي تقرؤه فقط (string، vector، بُناك الخاصة)، واحتفظ بالمرجع غير الثابت للحالات التي تقصد فيها فعلاً الكتابة من جديد.
إرجاع مرجع
يمكن للدالة أيضاً أن تُرجع مرجعاً، فتسلّم المُستدعي اسماً بديلاً لشيء موجود مسبقاً. وهذا شائع في الشيفرة الشبيهة بالحاويات، فهو ما يجعل v[i] = 5 يعمل، وهو ما تفعله operator[] خلف الكواليس:
بما أن at تُرجع int&، فإن تعبير الاستدعاء at(data, 1) نفسه قيمةٌ يُسرى (lvalue) يمكنك الإسناد إليها. أرجِع int بسيطاً بدلاً من ذلك ولن تُترجَم at(data, 1) = 42، إذ ستكون تُسند إلى نسخة مؤقتة.
الفخّ الكبير: المراجع المعلّقة
المرجع لا يملك شيئاً؛ إنه يشير فقط إلى ذاكرة تعيش في مكان آخر. فإن ماتت تلك الذاكرة بينما المرجع لا يزال قيد الاستخدام، صار لديك مرجع معلّق، وتكون القراءة من خلاله سلوكاً غير محدّد، فقد يطبع قمامة، أو ينهار، أو يبدو كأنه يعمل إلى أن يفسد يومك في بيئة الإنتاج. والخطأ الكلاسيكي هو إرجاع مرجع لمتغير محلي:
int& broken() {
int local = 42;
return local; // خطأ: local يُدمَّر عندما تعود broken()
} // المرجع المُرجَع يصبح معلّقاً
int main() {
int& r = broken();
cout << r << "\n"; // سلوك غير محدّد - يقرأ ذاكرة ميتة
}
يختفي المتغير local في اللحظة التي تعود فيها broken، فيشير المرجع إلى مساحة في المكدّس استُعيدت بالفعل. لا تُرجِع إلا مرجعاً لشيء يبقى حياً بعد الاستدعاء: معامل مُرَّر بالمرجع، أو عضو بيانات، أو كائن static. وإذا كانت القيمة تُحسب داخل الدالة، فـ أرجِعها بالقيمة بدلاً من ذلك ودع المُترجم يحذف النسخة بالتحسين. ويصيب الفخّ نفسه الحلقات القائمة على المدى وأي مرجع مرتبط بكائن مؤقت: لا تحتفظ أبداً بمرجع يتجاوز عمر الشيء الذي يسمّيه.
التالي: تحميل الدوال الزائد
تمنحك المراجع مقبضاً ثانياً على كل معامل، نسخة مقابل اسم بديل، وقابل للتعديل مقابل const، وهذا المقبض يتفاعل مباشرة مع الموضوع التالي. تالياً، يتيح لك تحميل الدوال الزائد تعريف عدة دوال بالاسم نفسه لكن بقوائم معاملات مختلفة، ويختار المُترجم الدالة الصحيحة بمطابقة أنواع الوسطاء، بما في ذلك ما إذا كانت مُمرَّرة بالقيمة أو بالمرجع أو بمرجع const.
الأسئلة الشائعة
ما هو المرجع في C++؟
المرجع هو اسم بديل لمتغير موجود مسبقاً، أي اسم آخر للموقع نفسه في الذاكرة. تُنشئه باستخدام & في التصريح: int& r = x;. بعد ذلك يصبح r و x قابلين للتبادل؛ تغيير أحدهما يغيّر الآخر. يجب تهيئة المراجع عند التصريح بها، ولا يمكن إعادة ربطها أبداً لتشير إلى متغير مختلف.
ما الفرق بين التمرير بالقيمة والتمرير بالمرجع في C++؟
التمرير بالقيمة (void f(int x)) ينسخ الوسيط، فتعمل الدالة على نسختها الخاصة ويبقى متغير المُستدعي سليماً. أما التمرير بالمرجع (void f(int& x)) فيمنح الدالة وصولاً مباشراً إلى متغير المُستدعي، فتظهر التغييرات بعد الاستدعاء، ولا تُجرى أي نسخة، وهو أمر مهم للكائنات الكبيرة.
متى ينبغي استخدام معاملات المرجع الثابت في C++؟
استخدم const T& عندما تحتاج الدالة إلى قراءة المعامل فقط لكن نسخ النوع مكلف (string، vector، البُنى الكبيرة). تحصل على سرعة المرجع دون نسخ إضافةً إلى ضمان من المُترجم بأن الدالة لن تعدّل قيمة المُستدعي. أما للأنواع الرخيصة مثل int أو double، فالتمرير بالقيمة البسيط أسهل وبالسرعة نفسها.