ملخص
يشترط std::array أن يُعرف عند الترجمة عدد العناصر التي سيضمّها؛ أما أغلب المسائل فلا تقول ذلك حتى تصل البيانات. ويقدّم هذا الدرس std::vector، التسلسل الذي ينمو بحسب البيانات: كيف يُنشأ، وكيف تُضاف إليه العناصر بـ push_back، وكيف تُقرأ به سلاسل مجهولة الطول. وتليها أنماط المرور القياسية الثلاثة في كل برمجة تتعامل مع المجموعات (التجميع والترشيح والتحويل)، بحلقة وبخوارزميات المكتبة؛ ثم تمرير المتجه إلى دالة، ولماذا كانت إعادة متجه جديد رخيصة. ويُختم بباطن المتجه: حجمه وسعته، وكتلة الذاكرة التي تُستبدل بأخرى ضعفها حين تمتلئ، والمرجع إلى عنصر الذي قد يبقى لذلك مشيرًا إلى ذاكرة محرَّرة.
أهداف التعلم
- إنشاء
std::vectorوإضافة العناصر إليه بـpush_backوالمرور عليه، وقراءة سلسلة من البيانات مجهولة الطول به. - كتابة دوال تستقبل المتجه بالمرجع الثابت لقراءته وبالمرجع لتعديله، وتُعيد متجهًا جديدًا حين تكون النتيجة تسلسلًا.
- تنفيذ أنماط المرور بالتجميع والترشيح والتحويل، والتعرّف في المسألة على النمط الذي يناسبها، واختصاره، حين تسمح المكتبة بذلك، بـ
std::ranges::countأوstd::ranges::maxأوstd::ranges::fold_left. - الشرح بمثال للفرق بين حجم المتجه وسعته، ولماذا قد يُبطَل المرجع إلى عنصر عند إضافة عنصر آخر.
التسلسل الذي ينمو
إنشاء المتجه وملؤه
قدّم الدرس المصفوفات التسلسل المفهرس: عناصر من النوع نفسه، واحدًا بعد آخر، مرقّمة ابتداءً من الصفر. وصورته الحديثة ذات الحجم الثابت، std::array<int, 7>، تحمل الحجم في النوع، ويجب أن يعرفه المترجم. غير أن من يكتب برنامجًا يحسب متوسط درجات صفّ لا يعرف كم طالبًا سيكون فيه. ومن ذلك تلزم الحاجة إلى تسلسل يكون حجمه معطًى آخر يتغيّر أثناء التنفيذ: إنه المتجه، std::vector<T>، من ملف الترويسة <vector>، حيث T نوع العناصر.
#include <print>#include <vector>int main(){ std::vector<int> squares{}; std::println("في البداية: الحجم {}، فارغ: {}", squares.size(), squares.empty()); for (int k{1}; k <= 5; ++k) { squares.push_back(k * k); } std::println("{}", squares); std::println("الحجم {}، الأول {}، الأخير {}", squares.size(), squares[0], squares.back()); squares[1] = 0; squares.pop_back(); std::println("{}", squares);}في البداية: الحجم 0، فارغ: true[1, 4, 9, 16, 25]الحجم 5، الأول 1، الأخير 25[1, 0, 9, 16]ينشئ السطر 6 متجهًا فارغًا من الأعداد الصحيحة: وخلافًا للمصفوفة، لا حاجة إلى الإعلان عن عدد العناصر التي سيضمّها. وتُعيد size() عدد ما فيه، وتخبر empty() هل هو خالٍ من العناصر. وتضيف الحلقة في الأسطر من 8 إلى 10 خمسة مربعات بـ push_back، التي تضع قيمتها الممرَّرة في النهاية وتزيد الحجم واحدًا. ويكتب السطر 11 المتجه كله بصيغة النطاقات التي استعملها الدرس الماضي مع المصفوفات. والوصول بالفهرس هو المعتاد، وsquares.back() اختصار لـ squares[squares.size() - 1]. ويُسند السطر 13 قيمة إلى عنصر موجود، ويحذف السطر 14 العنصر الأخير بـ pop_back. وعمليات هذا الدرس، بإيجاز، هي الآتية.
v.push_back(x)يضيفxفي النهاية؛ وv.pop_back()يحذف الأخير (ومع المتجه الفارغ، هو سلوك غير معرَّف).v.size()هو الحجم، من النوعstd::size_t، العدد الصحيح عديم الإشارة الخاص بالأحجام الذي قدّمه الدرس المصفوفات؛ وv.empty()يكافئv.size() == 0.v[i]وv.at(i)يعطيان العنصر ذا الفهرسi، الذي يجب أن يحققi < v.size()؛ وv.front()وv.back()يعطيان الأول والأخير، ويشترطان ألا يكون المتجه فارغًا.v.resize(n)يجعل في المتجهnمن العناصر: يحذف ما زاد في النهاية أو يضيف في النهاية عناصر مهيّأة كما بالأقواس المعقوصة الفارغة (أصفارًا، للأعداد).
وكما في المصفوفات، لا يضمن v[i] أي تحقق، ويضمنه v.at(i): مع الفهرس 7 وخمسة عناصر، يوقف البرنامج بالرسالة vector::_M_range_check: __n (which is 7) >= this->size() (which is 5)، التي تذكر الفهرس والحجم. والآلية التي تُحدث هذا التوقف، أي الاستثناء، تُدرس في دورة البرمجة بلغة C++ II.
الأقواس المعقوصة والأقواس الهلالية
تهيّئ الدورة كل شيء بالأقواس المعقوصة، والمتجه أول نوع في الدورة يغيّر فيه إبدال الأقواس المعقوصة بالأقواس الهلالية النتيجةَ. فبالأقواس المعقوصة، القيم المكتوبة هي العناصر؛ وبالأقواس الهلالية، العدد الأول هو عدد العناصر والثاني، إن وُجد، قيمة كل منها.
#include <print>#include <vector>int main(){ const std::vector<int> a{5}; const std::vector<int> b(5); const std::vector<int> c{3, 7}; const std::vector<int> d(3, 7); std::println("{} {} {} {}", a, b, c, d);}[5] [0, 0, 0, 0, 0] [3, 7] [7, 7, 7]a فيه عنصر واحد، هو 5؛ وb، خمسة أصفار؛ وc، العددان 3 و7؛ وd، ثلاث سبعات. فالقائمة بين الأقواس المعقوصة تقول إذن «هذه هي العناصر»، والأقواس الهلالية تُخصَّص لطلب n من النسخ لقيمة واحدة، كمتجه عدّادات مبدوءة بالصفر.
العنصر الأخير في متجه فارغ
المتجه الفارغ حالة مشروعة (المدخلات الخالية من البيانات، والمرشِّح الذي لا يجتازه أحد)، وكل صيغة فيها حجمه يجب أن تُراجَع في هذه الحالة؛ وأخطرها خداعًا v.size() - 1. ولأن v.size() عدد صحيح عديم الإشارة، فإن طرح 1 منه حين يساوي 0 لا يعطي ، بل يلتفّ، كما رأينا في الدرس الأعداد الصحيحة وتمثيلها، إلى أكبر قيمة لـ std::size_t.
#include <cstddef>#include <print>#include <vector>int main(){ const std::vector<int> values{}; std::println("values.size() - 1 = {}", values.size() - 1); // حدّ خاطئ: مع المتجه الفارغ يكون الشرط صحيحًا. for (std::size_t i{0}; i <= values.size() - 1; ++i) { std::println("{}", values[i]); }}values.size() - 1 = 18446744073709551615/usr/include/c++/15/bits/stl_vector.h:1282: constexpr std::vector< <template-parameter-1-1>, <template-parameter-1-2> >::const_reference std::vector< <template-parameter-1-1>, <template-parameter-1-2> >::operator[](size_type) const [with _Tp = int; _Alloc = std::allocator<int>; const_reference = const int&; size_type = long unsigned int]: Assertion '__n < this->size()' failed.أوقف النظام البرنامج بالإشارة 6.
السطر الأول من المخرجات هو ، أي ، أكبر قيمة لـ std::size_t في هذه المنصة. ومن ذلك يلزم أن شرط الحلقة، 0 <= 18446744073709551615، صحيح، وأن الجولة الأولى تقرأ values[0] من متجه ليس فيه عنصر 0: سلوك غير معرَّف. وما يُلاحَظ يتوقف على الخيارات، كما في المصفوفات: فعند الترجمة دون تحسين، تتحقق مكتبة GCC 15 من فهرس operator[] وتوقف البرنامج بتوكيد يقول __n < this->size()؛ ومع التحسين يختفي هذا التحقق، والحلقة، التي لا تتوقف في الوقت المناسب، تحاول القراءة حيث لا يوجد أي عنصر (ثبت ذلك على Linux بـ -O2: لأن المتجه الفارغ لا كتلة ذاكرة له، ينتهي البرنامج بخطأ التجزئة، دون رسالة تشرح السبب). والتصحيح أن يُكتب الحدّ بلا طرح، i < values.size()، وهو مع المتجه الفارغ خاطئ منذ البداية؛ وحيث يبدو الطرح ضروريًا، يُسأل أولًا عن values.empty().
جارٍ تحميل المحتوى…