ما هو الباني
في الصفحة السابقة عرّفت أصنافًا وأعطيتها متغيرات أعضاء. لكن أعضاء كائن أُنشئ للتو تحمل أي قمامة كانت موجودة في تلك الذاكرة ما لم تضبطها بنفسك. الباني يحلّ ذلك: فهو دالة عضو خاصة تُنفَّذ تلقائيًا لحظة إنشاء الكائن، ومهمتها الوحيدة هي ترك الكائن في حالة صحيحة ومهيّأة بالكامل.
يحمل الباني الاسم نفسه للصنف وليس له نوع إرجاع — ولا حتى void. أنت لا تستدعيه مباشرة أبدًا؛ بل المترجم يستدعيه نيابة عنك كلما وُجد كائن.
يُسمّى Counter() الذي لا معاملات له الباني الافتراضي — وهو الذي يُستخدم عندما تنشئ كائنًا دون تمرير أي معاملات.
الباني ذو المعاملات
الباني الذي لا يأخذ معاملات لا بأس به، لكنك تريد عادة إنشاء كائن بقيم محددة. يقبل الباني ذو المعاملات معاملاتٍ ويستخدمها لتهيئة الأعضاء.
يمكن للصنف أن يملك أكثر من بانٍ واحد، ما دامت قوائم معاملاتها مختلفة — وهذا هو التحميل الزائد للدوال المعتاد مُطبَّقًا على الباني. هنا يمكن إنشاء Point بإحداثيات أو بدونها:
مزلق شائع: العبارة Point p(); لا تنشئ كائنًا — إذ يقرؤها المترجم على أنها تصريح لدالة اسمها p تُرجع Point. لاستدعاء الباني الافتراضي، اكتب Point p; (بلا أقواس) أو Point p{}; بأقواس معقوفة.
قوائم تهيئة الأعضاء
حتى الآن كانت الأمثلة تُسنِد إلى الأعضاء داخل جسم الباني. هذا يعمل مع الأنواع البسيطة، لكنه الأداة الخاطئة. فبحلول وقت تنفيذ الجسم، يكون كل عضو قد بُني افتراضيًا بالفعل؛ ثم يتخلّى الجسم عن ذلك ويُسنِد فوقه. أما قائمة تهيئة الأعضاء فتهيّئ كل عضو مباشرة، قبل الجسم، في خطوة واحدة.
الصياغة هي نقطتان رأسيتان بعد قائمة المعاملات، يتبعهما أزواج member(value):
بالنسبة لعضو من النوع string، يتجنّب هذا أيضًا بناء سلسلة نصية فارغة ثم الإسناد إليها — إذ تبنيها قائمة التهيئة على نحوٍ صحيح من المحاولة الأولى.
قائمة التهيئة ليست مجرد تحسين للأداء؛ بل هي إلزامية في ثلاث حالات، لأن الجسم لا يستطيع إلا الإسناد، لا التهيئة:
- الأعضاء من النوع
const— لا يمكنك الإسناد إلىconstبعد وجوده. - الأعضاء المراجع — يجب ربط المرجع لحظة ميلاده.
- الأعضاء التي ليس لنوعها بانٍ افتراضي.
class Sensor {
const int id; // عضو const
int& slot; // عضو مرجع
public:
Sensor(int sensorId, int& s) : id(sensorId), slot(s) {}
// محاولة ضبط id أو slot في الجسم لن تُترجَم.
};
دقيقة ينبغي معرفتها: تُهيَّأ الأعضاء بالترتيب الذي صُرِّحت به في الصنف، لا بالترتيب الذي تسرده في قائمة التهيئة. فإذا كانت تهيئة أحد الأعضاء تقرأ عضوًا آخر، فالمهم هو ترتيب التصريح — والخلط بين الاثنين مصدر كلاسيكي لاستخدام قيمة لم تُهيَّأ بعد.
المعاملات الافتراضية والباني المُفوِّض
لست بحاجة دائمًا إلى تحميلات زائدة منفصلة. تتيح المعاملات الافتراضية لبانٍ واحد أن يغطّي عدة حالات: أَهمِل معاملًا فتملأ القيمة الافتراضية مكانه:
كن حذرًا عند الجمع بين بانٍ ذي معاملات بقيم افتراضية وبانٍ افتراضي Point() منفصل — إذ لا يستطيع المترجم تحديد أيّهما يستدعي للعبارة Point p; فيُبلّغ عن التباس. اختر أسلوبًا واحدًا.
عندما يكون لديك عدة بانين يتشاركون تهيئةً مشتركة، يتيح الباني المُفوِّض (delegating constructor، في C++11) لبانٍ أن يستدعي آخر بدلًا من تكرار المنطق. أنت "تُفوِّض" بوضع الباني الآخر في قائمة التهيئة:
باني النسخ
عندما تنشئ كائنًا كنسخة من آخر — بتمريره بالقيمة، أو بإرجاعه، أو بكتابة Foo b = a; — يُنفَّذ باني النسخ. توقيعه يأخذ مرجعًا من نوع const إلى النوع نفسه:
ClassName(const ClassName& other);
إن لم تكتب واحدًا، يولّد المترجم باني نسخ افتراضيًا ينسخ كل عضو. وبالنسبة للأصناف التي تحمل قيمًا فقط (أعداد صحيحة، سلاسل نصية، vectors)، فهذا هو الصحيح تمامًا، ولا ينبغي أن تكتب باني نسخ خاصًا بك.
المزلق الكبير يقبع في الفصل التالي عن الذاكرة: إذا كان صنفك يملك مؤشرًا خامًا إلى ذاكرة في الكومة (heap)، فإن باني النسخ الافتراضي ينسخ المؤشر لا البيانات — فينتهي الأمر بكائنين يشيران إلى الذاكرة نفسها، وكلاهما سيحاول تحريرها. هذا هو خطأ التحرير المزدوج (double-free). القاعدة الإرشادية هي قاعدة الثلاثة/الخمسة: إن كتبت هادمًا (destructor) مخصّصًا، فأنت بحاجة على الأرجح إلى باني نسخ مخصّص (وإسناد نسخ) أيضًا. في C++ الحديثة، الحل الأنظف هو الاحتفاظ بـ std::vector أو مؤشر ذكي، حتى يعمل النسخ الذي يولّده المترجم على نحوٍ سليم تلقائيًا.
لاحظ أيضًا أن أخذ المعامل بالمرجع أمر إلزامي لا اختياري: فباني نسخ يأخذ معامله بالقيمة سيحتاج إلى نسخ المعامل كي يستدعي نفسه — وهذا استدعاء ذاتي لا نهائي لن يُترجَم أصلًا.
التالي: الهوادم (Destructors)
الباني يُنشئ الكائن؛ والهادم (destructor) يُفكّكه. عندما يخرج كائن من النطاق أو يُحذف، يُنفَّذ هادمه تلقائيًا — المكان المثالي لتحرير الملفات أو اتصالات الشبكة أو ذاكرة الكومة التي كان الكائن يحتفظ بها. تتناول الصفحة التالية كيف تعمل الهوادم، ومتى تُنفَّذ بالضبط، وكيف تقترن بالباني لتمنح C++ نمط RAII القوي.
الأسئلة الشائعة
ما هو الباني (constructor) في C++؟
الباني دالة عضو خاصة تحمل الاسم نفسه للصنف وليس لها نوع إرجاع. تُنفَّذ تلقائيًا عند إنشاء الكائن، ومهمتها هي جعل الكائن في حالة صحيحة ومهيّأة بالكامل قبل أن تستخدمه أي شيفرة أخرى.
ما الفرق بين الباني الافتراضي والباني ذي المعاملات؟
الباني الافتراضي لا يأخذ أي معاملات ويُستخدم عندما تنشئ كائنًا دون تمرير قيم (Point p;). أما الباني ذو المعاملات فيأخذ معاملات حتى يتمكّن المُستدعي من تهيئة الكائن بقيم محددة (Point p(3, 4);). يمكن للصنف أن يملك كليهما لأن الباني يُحمَّل تحميلًا زائدًا حسب قائمة معاملاته.
لماذا ينبغي أن أستخدم قائمة تهيئة الأعضاء في C++؟
قائمة تهيئة الأعضاء (: name(n), age(a)) تهيّئ الأعضاء مباشرة، قبل تنفيذ جسم الباني. وهي ضرورية للأعضاء من النوع const وللمراجع وللأعضاء التي ليس لنوعها بانٍ افتراضي، كما أنها تتجنّب الهدر الناتج عن البناء الافتراضي ثم الإسناد الذي يحدث عندما تُسنِد داخل الجسم.