Menu
flag Ar iconالعربيةdown icon

vector في ++C: شرح المصفوفات الديناميكية باستخدام std::vector

std::vector هو مصفوفة ++C القابلة لتغيير الحجم - الحاوية التي ينبغي اللجوء إليها افتراضيًا. تعلّم كيف تنشئ vector وتصل إلى عناصره وتوسّعه وتمر عليه، إضافةً إلى مزالق إبطال المُكرِّرات والوصول خارج الحدود.

تحتوي هذه الصفحة على محررات قابلة للتشغيل - حرّر، شغّل، وشاهد النتيجة فوراً.

لماذا vector بدلًا من مصفوفة خام

تمتلك المصفوفة الخام حجمًا ثابتًا مُضمَّنًا في وقت الترجمة، وتنسى طولها لحظة تمريرها إلى دالة. أما std::vector فيحلّ المشكلتين معًا: فهو مصفوفة قابلة لتغيير الحجم تتتبّع طولها بنفسها، وتكبر عند الطلب، وتنظّف ذاكرتها بنفسها. في ++C الحديثة، يُعدّ vector الحاوية الافتراضية - لا تلجأ إلى مصفوفة خام إلا حين يكون لديك سبب محدد لعدم استخدامه.

ضمّن <vector>، ثم صرّح عن واحد مع وضع نوع العنصر بين قوسين زاويّين:

يخبرك scores.size() دائمًا بالطول الحالي - دون الحاجة إلى int n منفصل تُبقيه متزامنًا، ودون حيل sizeof. والقيمة {90, 75, 100, 60} هي مُهيّئ بأقواس معقوفة؛ ويستنتج الـ vector بنفسه أنه يحتاج إلى أربع خانات.

إنشاء vector وتهيئته

هناك عدة طرق لبنائه، حسب ما تعرفه مسبقًا:

انتبه لمزلق الأقواس الدائرية مقابل المعقوفة: فـ vector<int> tens(5, 10) ينشئ خمس نسخ من 10، بينما vector<int> tens{5, 10} ينشئ vector من عنصرين يحتويان على 5 و10. الأقواس الدائرية تعني «الحجم وقيمة التعبئة»؛ والأقواس المعقوفة تعني «هذه العناصر الحرفية».

إضافة العناصر وإزالتها

جوهر الـ vector هو أنه يكبر. يُلحق push_back في النهاية، ويزيل pop_back من النهاية:

يُعيد back() العنصر الأخير ويُعيد front() الأول - وهذا أنظف من v[v.size() - 1] وv[0]. ومنذ ++C11 يمكنك أيضًا استخدام emplace_back(args...) لبناء عنصر في مكانه، ما يتجنّب نسخة مؤقتة للأنواع الأثقل.

من الأخطاء الشائعة لدى المبتدئين استدعاء front() أو back() على vector فارغ. هذا سلوك غير معرَّف، وليس خطأً - احرص دائمًا على الحماية أولًا بـ if (!v.empty()).

قراءة العناصر: [] مقابل ()at

تُفهرس الـ vector تمامًا كما تفهرس مصفوفة باستخدام []. لكن [] لا يجري أي فحص للحدود - فالفهرس خارج النطاق سلوك غير معرَّف، قد يقرأ بيانات غير صالحة بصمت أو ينهار لاحقًا في موضع مربك:

vector<int> v = {1, 2, 3};
cout << v[10];   // سلوك غير معرَّف - لا فحص، لا خطأ

عندما تريد الأمان، استخدم at(). فهو يفحص الفهرس ويُطلق std::out_of_range عند وصول خاطئ، فتحصل على فشل واضح بدلًا من الفساد:

قاعدة عملية: استخدم [] في الحلقات المُحكمة حيث أثبتّ بالفعل أن الفهرس صالح، واستخدم at() عند الحدود حيث قد يتسلّل إدخال خاطئ.

المرور على vector

أنظف طريقة للمرور على vector هي حلقة for القائمة على النطاق. خذ العناصر بـ const auto& للقراءة دون نسخ، أو بـ auto& لتعديلها في مكانها:

إن كنت تحتاج فعلًا إلى الفهرس (لمقارنة العناصر المتجاورة مثلًا)، فاستخدم حلقة عدّ تقليدية - لكن لاحظ أن size() يُعيد نوعًا بلا إشارة (size_t). إن مقارنة int i ذي إشارة به قد تطلق تحذيرات من المترجم والتفافًا غير متوقع، لذا فضّل size_t i أو حلقة قائمة على النطاق متى أمكن:

for (size_t i = 0; i < v.size(); i++) {   // size_t، وليس int
    cout << v[i];
}

size وcapacity وreserve

يحتفظ الـ vector برقمين: size() (عدد العناصر التي يحملها) وcapacity() (عدد العناصر التي يمكنه حملها قبل أن يضطر إلى الكبر). عندما يتجاوز push_back السعة، يخصّص الـ vector كتلة أكبر، وينسخ كل عنصر إليها، ويحرّر الكتلة القديمة. لهذا فإن push_back المتكرر رخيص على نحو مُستهلَك، لكن كل عملية إعادة تخصيص منفردة ليست مجانية:

إن كنت تعرف تقريبًا عدد العناصر التي ستضيفها، فاستدعِ reserve() أولًا لتخطّي عمليات إعادة التخصيص المتكررة. لاحظ أن reserve() يغيّر السعة لا الحجم - فيبقى عدد عناصر الـ vector صفرًا حتى تدفعها إليه.

إعادة التخصيص هذه هي أيضًا مصدر أخبث علّة في الـ vector. ولأن الكبر ينقل المخزن، فإن أي مؤشر أو مرجع أو مُكرِّر حفظته إلى داخل الـ vector يصبح مُعلَّقًا بعد push_back يُعيد التخصيص:

vector<int> v = {1, 2, 3};
int& first = v[0];     // مرجع إلى داخل الـ vector
v.push_back(4);        // قد يُعيد التخصيص...
cout << first;         // مُعلَّق - قد يشير إلى ذاكرة محرَّرة

وينطبق الأمر نفسه على المُكرِّرات: لا تستدعِ push_back ولا erase أثناء المرور بمُكرِّر محفوظ. وإن كان لا بدّ من إزالة عناصر أثناء الحلقة، فاستخدم القيمة المُعادة من erase، أو نمط erase-remove مع std::remove.

التالي: map

يكون الـ vector مثاليًا حين تبحث عن الأشياء حسب الموضع - العنصر 0، العنصر 1، وهكذا. لكنك كثيرًا ما تريد البحث عن الأشياء بـ مفتاح بدلًا من ذلك: اسم مستخدم، أو معرّف منتج، أو كلمة. ولهذا الغرض وُجد std::map. سنتناول تاليًا map، حاوية المفتاح-القيمة في ++C، بما في ذلك كيفية الإدراج والبحث والمرور على المدخلات - ومزلق أن [] يُنشئ قيمة افتراضية، الذي يوقع كل الناس تقريبًا في الخطأ.

الأسئلة الشائعة

ما هو vector في ++C؟

إن std::vector هو مصفوفة ديناميكية (قابلة لتغيير الحجم) من المكتبة القياسية في ++C. على عكس المصفوفة الخام، فهو يعرف حجمه بنفسه، ويكبر تلقائيًا عند إضافة عناصر باستخدام push_back، ويحرّر ذاكرته نيابةً عنك. ضمّن <vector> واكتب vector<int> v; لإنشاء واحد.

ما الفرق بين [] و()at على vector في ++C؟

إن v[i] لا يجري أي فحص للحدود - فالفهرس خارج النطاق سلوك غير معرَّف (انهيار أو فساد صامت للبيانات). أما v.at(i) فيفحص الفهرس ويُطلق std::out_of_range إذا كان غير صالح. استخدم [] في الحلقات الساخنة حيث تكون قد تحققت من الفهرس مسبقًا، واستخدم at() عندما تريد فشلًا آمنًا وسهل التتبّع.

هل يُبطل push_back المؤشرات والمراجع إلى داخل vector في ++C؟

نعم، من المحتمل ذلك. عندما تنفد سعة vector، يعيد push_back تخصيص مخزنه إلى كتلة جديدة، ما يُبطل كل مؤشر ومرجع ومُكرِّر يشير إلى العناصر القديمة. لا تحتفظ بمرجع لعنصر عبر استدعاء push_back، واستدعِ reserve() مسبقًا إن أمكن لتجنّب عمليات إعادة التخصيص المفاجئة.

Coddy programming languages illustration

تعلّم البرمجة مع Coddy

ابدأ الآن