مقدمة: عندما تتوقف C++ عن كونها "مجرد لغة"
لقد كتبت بالفعل عشرات البرامج النصية، وتعلمت استخدام std::vector وحتى فهمت القوالب. لكن C++ هي لغة لا تغفر أبدًا النهج السطحي. ينتقل المطور المبتدئ إلى المستوى التالي تمامًا في اللحظة التي يواجه فيها السلوك غير المحدد (UB) ويبدأ في فهم أن المترجم ليس صديقًا جيدًا، بل هو طرف متعاقد صارم.
في هذه المقالة، سنقوم بتحليل ليس الأخطاء التافهة مثل نسيان ; أو الخلط بين = و ==. سنتحدث عن الفخاخ التي تنتظر أولئك الذين يعرفون بالفعل بناء الجملة، لكنهم لم يتقنوا بعد فلسفة اللغة. هذه الأخطاء هي سبب 90٪ من الأخطاء التي يصعب اكتشافها في الكود الصناعي.
1. تجاهل قاعدة الخمسة وقاعدة الصفر
المشكلة: دوال الأعضاء الخاصة المولدة ضمنيًا
يعتقد العديد من المطورين الذين انتقلوا من Java أو Python أن المترجم سيقوم "بعمل كل شيء بشكل جميل" فيما يتعلق بنسخ ونقل الكائنات. هذا مفهوم خاطئ خطير.
لنفكر في فئة بمورد ديناميكي:
class Buffer {public: Buffer(size_t size) : data(new char[size]), size(size) {} ~Buffer() { delete[] data; } // كل شيء على ما يرام؟ لا! لا يوجد مُنشئ نسخ وعامل إسناد!private: char* data; size_t size;};
void process() { Buffer a(100); Buffer b = a; // نسخ سطحي! مؤشران يشيران إلى نفس الذاكرة.} // تحرير مزدوج — سلوك غير محدد!تنص قاعدة الخمسة على: إذا قمت بتعريف واحد على الأقل من الطرق الخمسة الخاصة (المدمر، مُنشئ النسخ، عامل إسناد النسخ، مُنشئ النقل، عامل إسناد النقل)، فيجب عليك تعريفها جميعًا.
الحل: استخدم std::unique_ptr أو std::vector لإدارة الموارد. هذا هو نهج قاعدة الصفر: إذا كانت فصلك لا تدير الموارد بشكل مباشر، فسيقوم المترجم نفسه بتوليد دوال خاصة صحيحة.
class Buffer {public: Buffer(size_t size) : data(std::make_unique<char[]>(size)), size(size) {} // قاعدة الصفر — المدمر، النسخ والنقل يعملان بشكل صحيح!private: std::unique_ptr<char[]> data; size_t size;};2. الاستخدام غير الصحيح لدلالات النقل
المشكلة: النقل لا يجعل الكائن "فارغًا" تلقائيًا
أحد أكثر الأخطاء غدرًا هو الاعتقاد أنه بعد std::move يصبح الكائن الأصلي "آمنًا" للاستخدام. في الواقع، std::move هو مجرد تحويل إلى مرجع rvalue. حالة الكائن المنقول تعتمد على التنفيذ.
std::string a = "Hello, World!";std::string b = std::move(a);std::cout << a; // تقنيًا — سلوك غير محدد؟ لا، لكن محتوى a غير محدد!// قد يكون سلسلة فارغة، وقد يكون "Hello, World!"يضمن المعيار فقط أن الكائن المنقول في حالة صحيحة ولكن غير محددة. يمكنك استدعاء a.clear() أو a = "new value"، لكن لا يجب أن تعتمد على محتواه.
نصيحة: بعد النقل، إما أعد تعيين الكائن إلى حالة معروفة، أو توقف عن استخدامه. بالنسبة للأنواع المخصصة، قم دائمًا بتوثيق شروط ما بعد مُنشئ النقل.
3. تسرب الذاكرة عبر المؤشرات الخام والاستثناءات
المشكلة: الاستثناءات "تقطع" سلسلة تحرير الموارد
حتى المطورون ذوو الخبرة يكتبون أحيانًا كودًا بإدارة يدوية للذاكرة:
void process() { int* ptr1 = new int(42); int* ptr2 = new int(100); // إذا حدث استثناء هنا — تسرب ptr1! dangerous_function(); delete ptr2; delete ptr1;}في لغة C++ لا يوجد finally