كيف تحذف عنصرًا من std::vector حسب الفهرس: نظرة تحت الغطاء
\n\nمرحبًا يا زميل! إذا كنت قد كتبت يومًا بلغة C++ وتعاملت مع المصفوفات الديناميكية، فبالتأكيد واجهت السؤال: "How do I erase an element from std::vector<> by index?" — وهو أحد أكثر الأسئلة شيوعًا على Stack Overflow، وليس من دون سبب. للوهلة الأولى، تبدو المهمة بسيطة: استدعِ erase وانتهى الأمر. لكن ماذا يحدث حقًا في الذاكرة؟ لماذا يعتبر الحذف من منتصف المتجه مكلفًا؟ وكيف تتجنب المشاكل مع المكررات (iterators)؟
في هذه المقالة، لن نكتفي بشرح كيفية كتابة v.erase(v.begin() + index). بل سنفهم كيف تعمل هذه العملية تحت الغطاء، وما هي هياكل البيانات المستخدمة، وكيف تختار الطريقة المثلى للحذف. هيا بنا!
1. std::vector: ما هو هذا الكائن؟
\n\nقبل أن نحذف، دعنا نتذكر كيف يعمل std::vector. إنها مصفوفة ديناميكية تخزن العناصر في مساحة ذاكرة متصلة. لديها ثلاثة مؤشرات (أو مكررات) رئيسية:
- \n
begin— بداية الذاكرة المخصصة؛ \n end— نهاية الذاكرة المشغولة (بعد آخر عنصر)؛ \n capacity— نهاية الذاكرة المخصصة (الحد الذي يمكن النمو إليه دون إعادة تخصيص). \n
بصريًا، يبدو الأمر هكذا:
\n[0] [1] [2] [3] [4] ... [capacity-1]\n ^ ^\n begin end\n\n\nعندما تحذف عنصرًا من المنتصف، لا يقوم المتجه ببساطة "برميه" — بل يقوم جسديًا بإزاحة جميع العناصر التالية إلى اليسار. هذا هو "الألم" الرئيسي لعملية erase.
2. الحذف حسب الفهرس: الصيغة الأساسية
\n\nأبسط طريقة لحذف عنصر حسب الفهرس هي استخدام دالة erase مع مكرر:
#include <vector>\n#include <iostream>\n\nint main() {\n std::vector<int> v = {10, 20, 30, 40, 50};\n int index = 2; // نريد حذف العنصر الثالث (30)\n\n // نتحقق من أن الفهرس ضمن النطاق\n if (index < v.size()) {\n v.erase(v.begin() + index);\n }\n\n for (int x : v) std::cout << x << " ";\n // الإخراج: 10 20 40 50\n return 0;\n}\n\n\nالأمر بسيط، أليس كذلك؟ لكن ماذا لو كنت بحاجة لحذف عدة عناصر متتالية؟ لهذا يوجد التحميل الزائد erase(first, last):
v.erase(v.begin() + 1, v.begin() + 3); // سيحذف العناصر ذات الفهارس 1 و 2\n\n\n3. ماذا تحت الغطاء؟ تحليل مفصل
\n\nالآن الجزء الممتع. عندما تستدعي v.erase(iterator)، يحدث التالي داخليًا:
- \n
- التحقق من المكرر (في وضع التصحيح) — نتأكد من أنه صالح ويشير إلى عنصر في المتجه. \n
- إزاحة العناصر — يتم نسخ (أو نقل، إذا كان النوع يدعم move semantics) جميع العناصر التي تلي العنصر المحذوف موضعًا واحدًا إلى اليسار. \n
- تقليل الحجم — يتم تقليل العداد الداخلي
sizeبمقدار 1. \n - المدمر (Destructor) — بالنسبة للعنصر "الذيل" (الذي أصبح مكررًا الآن)، يتم استدعاء المدمر (إذا لم يكن النوع بسيطًا). \n
دعنا نلقي نظرة على تنفيذ مبسط (كود زائف):
\n\ntemplate<typename T>\ntypename std::vector<T>::iterator\nvector<T>::erase(iterator pos) {\n // 1. إزاحة جميع العناصر بعد pos موضعًا واحدًا إلى اليسار\n for (auto it = pos; it != end() - 1; ++it) {\n *it = std::move(*(it + 1));\n }\n // 2. تقليل الحجم\n --size;\n // 3. استدعاء المدمر للعنصر الأخير\n allocator.destroy(end()); // end() الآن يشير إلى العنصر الأخير السابق\n return pos; // إرجاع مكرر للعنصر التالي بعد المحذوف\n}\n\n\nلاحظ استخدام std::move — لغة C++ الحديثة تحاول تجنب النسخ غير الضروري.