سلسلة ++c للمحترفين الدرس 29: النوع std::vector: المتجهات في Cpp


محمد الميداوي

المتّجه هو مصفوفة ديناميكية تخزّن البيانات تلقائيًّا، ويمكن الوصول إلى العناصر الموجودة في المتّجه بنفس طريقة المصفوفات، مع ميزة أنّ المتّجهات يمكن أن يتغير حجمها ديناميكيًا.

وبشأن التخزين، تُوضع بيانات المتجه عادة في ذاكرة مُخصّصة ديناميكيًا، ومن ثم فإنها تتطلّب بعض الحمل الزائد (overhead)؛ بالمقابل تستخدم المصفوفات من نمط ‎C ومن الصنف ‎std::array‎ وحدةَ تخزين تلقائية مرتبطة بالموضع المُصرَّح عنه، وهكذا لا يكون هناك أيّ حِمل زائد.

الوصول إلى العناصر

هناك طريقتان أساسيتان للوصول إلى عناصر المتّجهات:

الوصول عبر الفهرس

يمكن ذلك إما باستخدام عامل ألفهرسة ‎[]‎ أو الدالة التابعة ‎at()‎، وكلاهما يعيد مرجعًا إلى العنصر الموجود في الموضع المناسب في المتّجه ما لم يكن نوعه ‎vector<bool>‎، وعندها سيكون من الممكن قراءته أو تعديله إذا لم يكن المتّجه ثابت.

يختلف كل من ‎[]‎ و ‎at()‎ في أنّ العامل ‎[]‎ لا يتحقّق من الحدود بالضرورة، وذلك على خلاف ‎at()‎. عند محاولة الوصول إلى العناصر الموجودة في الفهارس الخارجة عن الحدود، أي في حال كان ‎index < ‎ ‎0‎ أو ‎index >= size‎، فسيحدث سلوك غير محدّد بالنسبة لعامل الفهرسة ‎[]‎، في حين أنّ دالة ‎at()‎ سترفع الاعتراض ‎std::out_of_range‎.

ملاحظة: تستخدم الأمثلة أدناه التهيئة بنمَط C++‎ 11 لغرض التبسيط، بيد أنّه يمكن استخدام العوامل مع جميع الإصدارات، إلّا إن ذُكر C++‎ 11 صراحة.

الإصدار ≥ C++‎ 11

std::vector<int> v{ 1, 2, 3 };

// [] استخدام
int a = v[1];    // تساوي 2 a 
v[1] = 4;    // { 1, 4, 3 } تحتوي v 

// at() استخدام
int b = v.at(2);    // تساوي 3 b 
v.at(2) = 5;        //  { 1, 4, 5 } تساوي v 
int c = v.at(3);    // std::out_of_range exception رفع

نظرًا لأنّ التابع ‎at()‎ يتحقّق من الحدود ويرفع اعتراضًا في حال تخطّى الفهرس لحدود المتّجه، فهو أبطأ من العامل ‎[]‎، وهذا يجعل ‎[]‎ أنسب لمن أيقن أنّ الفهرس يقع داخل الحدود. وعمومًا فالوصول إلى عناصر المتّجهات يتم في وقت ثابت، أي أنّ الوصول إلى العنصر الأول من المتجه يستغرق نفس الوقت اللّازم للوصول إلى العنصر الثاني أو العنصر الثالث، … . انظر:

for (std::size_t i = 0; i < v.size(); ++i) {
    v[i] = 1;
}

نعلم في هذا المثال أنّ متغيّرَ الفهرسِ ‎i‎ موجود دائمًا داخل الحدود، لذلك لا داعي للتحقّق ممّا إذا كان ‎i‎ داخل الحدود في كل استدعاء لعامل الفهرسة ‎operator[]‎.

تسمح الدالتان التابعتان ‎front()‎ و ‎back()‎ بالوصول المرجعي (reference access) إلى العنصر الأول والأخير في المتجه على الترتيب، يُستخدَم هذان الموضعان كثيرًا ويمكن أن يكونا أسهل قراءة من العامل ‎[]‎، انظر المثال التالي حيث نشرحه بتفصيل:

std::vector<int> v{ 4, 5, 6 };

تكون الصياغة أكثر إسهابًا في الإصدارات التي قبل C++ 11.

int a = v.front();

a تساوي 4، و v.front تكافئ [v[0.

v.front() = 3;

تحتوي v الآن على {3, 5, 6}

int b = v.back();

b تساوي 6، و v.back تكافئ [v[v.size() - 1

v.back() = 7;    

v تحتوي الآن على {3, 5, 7}.

ملاحظة: استدعاء ‎front()‎ أو ‎back()‎ على متّجه فارغ سيؤدّي إلى سلوك غير محدّد، لذا تحقّق أنّ الحاوية غير فارغة باستخدام الدالة التابعة ‎empty()‎ قبل استدعاء التابعين ‎front()‎ أو ‎back()‎. يوضح المثال التالي استخدام empty()‎ للتحقّق من فراغ المتّجه:

int main() {
    std::vector < int > v;
    int sum(0);

    for (int i = 1; i <= 10; i++) v.push_back(i); // إنشاء وتهيئة المتّجه

    while (!v.empty()) // التكرار على المتّجه إلى أن يصبح فارغًا
    {
        sum += v.back();
        v.pop_back(); // إصدار العنصر مع حذفه من المتّجه  
    }

  std::cout << "total: " << sum << '\n';
    return 0;
}

ينشئ المثال أعلاه متجهًا يحتوي الأعداد من 1 إلى 10. ثم يُخرج عناصر المتّجها إلى أن يصبح فارغًا (باستخدام empty()‎) لمنع حدوث سلوك غير محدّد، ثم يُحسَب مجموع الأعداد في المتّجه ويُعرَض للمستخدم.

الإصدار C++‎ 11

يُعيد التابع ‎data()‎ مؤشّرًا إلى الذاكرة الخام (raw memory) التي يستخدمها المتّجه لتخزين عناصره داخليًا، ويُستخدم هذا غالبًا عند تمرير بيانات المتّجه إلى شيفرة قديمة تتوقّع مصفوفة من نمط C.

std::vector<int> v{ 1, 2, 3, 4 };    // {1, 2, 3, 4} تحتوي على  v 
int * p = v.data();            // يشير إلى 1 p
*p = 4;                    // {4, 2, 3, 4} تحتوي الآن على v
++p;                    // يشير إلى 2 p 
*p = 3;                    //  {4, 3, 3, 4} تحتوي الآن على v
p[1] = 2;                //  {4, 3, 2, 4} تحتوي الآن على v
*(p + 2) = 1;                // {4, 3, 2, 1} تحتوي الآن على  v

الإصدار < C++‎ 11

يمكن محاكاة التابع ‎data()‎ في الإصدارات السابقة لـ C++‎ 11 عبر استدعاء التابع ‎front()‎ وأخذ عنوان القيمة المُعادة:

std::vector<int> v(4);
int* ptr = &(v.front()); // &v[0] أو

وينجح ذلك لأنّ المتّجهات تخزّن عناصرها دائمًا في مواقع متجاورة في الذاكرة على افتراض أنّ محتويات المتجه لا تعيد تعريف (override) المعامل الأحادي ‎operator&‎، وإلّا فسيتعيّن عليك إعادة تقديم std::addressof في الإصدارات السابقة للإصدار C++11. كما يُفترض أيضًا ألا يكون المتّجه فارغًا.

المكررات

تتصرف المُكرّرات (Iterators) بشكل مشابه للمؤشّرات التي تشير إلى عناصر المتّجه:

الإصدار ≥ C++‎ 11

std::vector<int> v{ 4, 5, 6 };

auto it = v.begin();
int i = *it;        // يساوي 4 i 
++it;
i = *it;            // يساوي 5 i 
*it = 6;            //  { 4, 6, 6 } تحتوي  v 
auto e = v.end();    //  v إلى العنصر الموجود بعد e يشير
// يمكن استخدامه للتحقّق مما إذا بلغ المكرر نهاية المتجه

++it;
it == v.end();        // يشير إلى العنصر الموجود في الموضع 2 it : خطأ
++it;
it == v.end();        // true

ينصّ المعيار على أنّ المكرّرات ‎std::vector<T>‎ هي مؤشّرات في الواقع من النوع ‎T*‎، لكنّ معظم المكتبات القياسية لا تطبّق ذلك من أجل تحسين رسائل الخطأ ورصد الشيفرات غير المحمولة، ولتجهيز المُكرّرات بعمليات التحقّق من الأخطاء في عمليات البناء غير المُصدَرة (non-release builds). ثمّ بعد ذلك يمكن حذف الصنف الذي يغلّف المؤشّر الأساسي في عمليات البناء المُصدَرة (release builds) من أجل تحسين الشيفرة.

تستطيع استدامة مرجع أو مؤشّر يشير إلى أحد عناصر المتّجه لاستخدامه للوصول غير المباشر، وتظل هذه المراجع أو المؤشّرات التي تشير إلى عناصر المتجه مستقرة كما يظلّ الوصول ممكنًا إلّا في حال أضفت أو أزلت عناصر قبل موضع ذلك العنصر من المتّجه أو فيه، أو في حال تغيير سعة المتّجه. يكافئ هذا قواعد إبطال [المُكرّرات](رابط الفصل 9).

الإصدار ≥ C++‎ 11

std::vector<int> v{ 1, 2, 3 };
// يشير إلى 2 p
int* p = v.data() + 1;        

// ستؤدّي إلى سلوك غير محدّد *p أصبح غير صالح، محاولة الوصول إلى p
v.insert(v.begin(), 0);    

// يشير إلى 1 p 
p = v.data() + 1;    

// ستؤدّي إلى سلوك غير محدّد *p أصبح غير صالح، محاولة الوصول إلى p    
v.reserve(10);        

// يشير إلى 1 p
p = v.data() + 1;    

// ستؤدي إلى سلوك غير محدّد *p أصبح غير صالح، محاولة الوصول إلى p    
v.erase(v.begin());        

تهيئة متجه

يمكن تهيئة المتّجهات بعدة طرق أثناء التصريح عنها:

الإصدار ≥ C++‎ 11

std::vector<int> v{ 1, 2, 3 };        //  {1, 2, 3}

// std::vector<int> v(3, 6) مختلفة عن
std::vector<int> v{ 3, 6 };        // {3, 6}

// std::vector<int> v{3, 6} in C++11 مختلفة عن
std::vector < int > v(3, 6);        // {6, 6, 6}

std::vector < int > v(4);        // {0, 0, 0, 0}

يمكن تهيئة المتجه من حاوية أخرى عبر عدّة طرق:

  • النسخ (من متّجه آخر)، ووهذا ينسخ البيانات من ‎v2‎:
std::vector<int> v(v2);
std::vector<int> v = v2;

الإصدار ≥ C++‎ 11

  • النّقل (من متّجه آخر)، والذي ينقل البيانات من ‎v2‎:
std::vector<int> v(std::move(v2));
std::vector<int> v = std::move(v2);
  • استخدام مُكرّر (نطاقي) لنسخ العناصر إلى ‎v‎:
// من متّجه آخر
std::vector < int > v(v2.begin(), v2.begin() + 3);        // {v2[0], v2[1], v2[2]}

// من مصفوفة
int z[] = { 1, 2, 3, 4 };
std::vector < int > v(z, z + 3);                // {1, 2, 3}

// من قائمة
std::list<int> list1{ 1, 2, 3 };
std::vector < int > v(list1.begin(), list1.end());        // {1, 2, 3}

الإصدار ≥ C++‎ 11

  • النقل عبر مكرّر باستخدام ‎std::make_move_iterator‎، والذي ينقل العناصر إلى ‎v‎:
// من متّجه آخر
std::vector < int > v(std::make_move_iterator(v2.begin()),
std::make_move_iterator(v2.end());
// من قائمة
std::list<int> list1{ 1, 2, 3 };
std::vector < int > v(std::make_move_iterator(list1.begin()),
std::make_move_iterator(list1.end()));
  • يمكن إعادة تهيئة المتجه بعد إنشائه باستخدام التابع ‎assign()‎:
v.assign(4, 100);                // {100, 100, 100, 100}

v.assign(v2.begin(), v2.begin() + 3);    // {v2[0], v2[1], v2[2]}

int z[] = { 1, 2, 3, 4 };
v.assign(z + 1, z + 4);            // {2, 3, 4}

حذف العناصر

  • حذف العنصر الأخير:
std::vector<int> v{ 1, 2, 3 };
v.pop_back(); // {1, 2}
  • حذف جميع العناصر:
std::vector<int> v{ 1, 2, 3 };
v.clear(); // أصبحت فارغة v
  • حذف العنصر الموجود عند فهرس معيّن:
std::vector<int> v{ 1, 2, 3, 4, 5, 6 };
v.erase(v.begin() + 3); // {1, 2, 3, 5, 6}

ملاحظة: عند حذف أي عنصر من المتّجه -باستثناء العنصر الأخير- فيجب نسخ جميع العناصر الموجودة بعد العنصر المحذوف أو نقلها لسدّ الفجوة التي خلّفتها عملية الحذف.

  • حذف جميع العناصر الموجودة في نطاق معيّن:
std::vector<int> v{ 1, 2, 3, 4, 5, 6 };
v.erase(v.begin() + 1, v.begin() + 5); // {1, 6} أصبحت تساوي  v

ملاحظة: التوابع المذكورة أعلاه لا تغيّر سعة المتّجه، وإنّما تغير حجمه وحسب (انظر أسفله في فقرة حجم المتّجهات وسعتها).

يُستخدم تابع erase -الذي يزيل مجموعة من العناصر- كجزء من مقاربة الحذف والنقل، أي أنّه في البداية ينقل التابع ‎std::remove‎ بعض العناصر إلى نهاية المتّجه، ثم يقطع التابع ‎erase‎ تلك العناصر. هذه العملية مكلّفة نسبيًا إلّا في حال الفهرس الأخير، لأنّه يجب نقل جميع العناصر بعد القطعة المحذوفة إلى مواضع جديدة. أما إن كنت تعمل على تطبيقات تتطلب سرعة كبيرة في إزالة العناصر من الحاويات، فالأفضل استخدام القوائم (lists).

  • حذف العناصر بالقيمة:
std::vector<int> v{ 1, 1, 2, 2, 3, 3 };
int value_to_remove = 2;
v.erase(std::remove(v.begin(), v.end(), value_to_remove), v.end());
// {1, 1, 3, 3} أصبحت تساوي  v
  • حذف العناصر التي تحقّق شرطًا معيّنا. في المثال التالي، تحتاج std::remove_if إلى دالة تأخذ متجهًا كوسيط، وتعيد true إن كان يجب حذف العنصر:
bool _predicate(const int& element) {
return (element > 3); // ستُحذف العناصر الأكبر من 3
}
...
std::vector<int> v{ 1, 2, 3, 4, 5, 6 };
v.erase(std::remove_if(v.begin(), v.end(), _predicate), v.end()); // {1, 2, 3} أصبحت  v
  • حذف العناصر عبر تعابير لامدا دون إنشاء دالّة شرطية إضافية:

الإصدار ≥ C++‎ 11

std::vector<int> v{ 1, 2, 3, 4, 5, 6 };
v.erase(std::remove_if(v.begin(), v.end(), [](auto& element){return element > 3;} ), v.end());
  • حذف العناصر التي تحقّق شرطًا معيّنا في حلقة:
std::vector<int> v{ 1, 2, 3, 4, 5, 6 };
std::vector < int > ::iterator it = v.begin();
while (it != v.end()) {
    if (condition)
        it = v.erase(it); // v إلى العنصر الموالي في 'it' بعد الحذف، سيشير
    else
        ++it; // يشير إلى العنصر الموالي يدويا  'it' جعل   
}

لذا يجب عليك التفكير في استخدام طريقة أخرى عند تكرار الحذف في الحلقات، بما أن من الضروري عدم زيادة ‎it‎ في حال حذف عنصر، فالتابع ‎remove_if‎ أكثر كفاءة وفعالية.

  • حذف العناصر التي تحقّق شرطًا معيّنا في حلقة عكسية:
std::vector<int> v{ -1, 0, 1, 2, 3, 4, 5, 6 };
typedef std::vector < int > ::reverse_iterator rev_itr;
rev_itr it = v.rbegin();

while (it != v.rend()) {        // v بعد الحلقة، لن تبقى إلا الأصفار في
    int value = * it;
    if (value) {
        ++it;
        it = rev_itr(v.erase(it.base()));
    } else
        ++it;
}

هذه بعض الملاحظات بخصوص الحلقة السابقة:

  • إن كان المكرّر العكسي ‎it‎ يشير إلى عنصر ما، فإنّ التابع base‎ سيعيد المُكرّر العادي (غير العكسي) الذي يشير إلى نفس العنصر.
  • يمحو التابع vector::erase(iterator)‎ العنصر المشار إليه من قبل المكرّر، ويعيد مكرّرًا إلى العنصر الذي يتبع العنصر المُعطى.
  • ينشئ التابع reverse_iterator::reverse_iterator(iterator)‎ مكرّرًا عكسيًّا من مُكرّر ما.

هذا يعني أن السطر ‎it = rev_itr(v.erase(it.base()))‎ يقول: أخذ المُكرّر العكسي ‎it‎، واجعل ‎v‎ تمحو العنصر الذي يشير إليه مُكرّرها العادي؛ ثم خذ المُكرِّر الناتج، وأنشئ مكرّرًا عكسيًّا منه، ثم عيّنه إلى المكرّر العكسي ‎it‎.

لن يؤدي استخدام ‎v.clear()‎ لحذف جميع عناصر المتّجه إلى تحرير الذاكرة، إذ تظل سعة المتّجه دون تغيير. ولتحرير مساحة الذاكرة، استخدم:

std::vector<int>().swap(v);

الإصدار ≥ C++‎ 11

يحرّر التابع ‎shrink_to_fit‎ سعة المتّجه غير المستخدم، لكنه لا يضمن تحرير المساحدة بالضرورة، رغم أن أغلب التطبيقات (Implementations) الحالية تضمن ذلك.

v.shrink_to_fit();

التكرار على المتجهات

تُعرّف ‎v‎ في الأمثلة التالية على النحو التالي:

std::vector<int> v;

التكرار الأمامي

الإصدار ≥ C++‎ 11 حلقة for نطاقية:

for (const auto& value: v) {
    std::cout << value << "\n";
}

استخدام حلقة for مع مكرِّر:

for (auto it = std::begin(v); it != std::end(v); ++it) {
    std::cout << *it << "\n";
}

استخدام خوارزمية for_each باستخدام دالة أو صنف كائن دالي (functor):

void fun(int
    const& value) {
    std::cout << value << "\n";
}
std::for_each(std::begin(v), std::end(v), fun);

استخدام خوارزمية for_each باستخدام لامدا:

std::for_each(std::begin(v), std::end(v), [](int
    const& value) {
    std::cout << value << "\n";
});

الإصدار استخدام حلقة for مع مكرِّر:++‎>

std::for_each(std::rbegin(v), std::rend(v), [](auto const& value) {
    std::cout << *it << "\n";
}

استخدام حلقة for مع فهرس:

for (std::size_t i = 0; i < v.size(); ++i) {
    std::cout << v[i] << "\n";
}

التكرار في الاتجاه العكسي

الإصدار ≥ C++‎ 14

لا توجد طريقة معيارية لاستخدام حلقة for النطاقية هنا، وإنما لدينا بدائل لها، انظر ما يلي: استخدام خوارزمية for_each، لاحظ استخدام تعبير لامدا للتوضيح، لكن اعلم أنك تستطيع استخدام صنف كائن دالّي (functor):

std::for_each(std::rbegin(v), std::rend(v), [](auto
    const& value) {
    std::cout << value << "\n";
});

استخدام حلقة for مع مكرر:

for (auto rit = std::rbegin(v); rit != std::rend(v); ++rit) {
    std::cout << *rit << "\n";
}

استخدام حلقة for مع فهرس:

for (std::size_t i = 0; i < v.size(); ++i) {
    std::cout << v[v.size() - 1 - i] << "\n";
}

رغم أنّه لا يوجد تابع مُضمّن يستخدم حلقة for النطاقية للتكرار العكسي، إلا أننا نستطيع استخدام التابعين ‎begin()‎ و ‎end()‎ للحصول على المُكرّرات، ومن ثم محاكاة ذلك باستخدام كائن مُغلّف (wrapper) للحصول على النتائج التي نريد.

الإصدار ≥ C++‎ 14

template < class C >
    struct ReverseRange {
        C c; // يمكن أن تكون مرجعا أو نسخة في حال كانت القيمة الأصلية مؤقتة
        ReverseRange(C&& cin): c(std::forward < C > (cin)) {}
        ReverseRange(ReverseRange&& ) = default;
        ReverseRange& operator = (ReverseRange&& ) = delete;
        auto begin() const {
            return std::rbegin(c);
        }
        auto end() const {
            return std::rend(c);
        }
    };

template < class C >
   ReverseRange<C> make_ReverseRange(C&& c) {return {std::forward<C>(c)};}

int main() {
    std::vector<int> v { 1,2,3,4};
    for(auto const& value: make_ReverseRange(v)) {
        std::cout << value << "\n";
    }
}

فرض العناصر الثابتة

منذ الإصدار C++‎ 11، يسمح لك التابعان ‎cbegin()‎ و ‎cend()‎ بالحصول على مُكرّر ثابت (constant iterator) لمُتجه حتى لو كان المتّجه غير ثابت، وتسمح المكرّرات الثابتة بقراءة محتويات المتّجه لكن لا تسمح بتعديلها، وهو أمر مفيد لفرض الثباتية (const correctness).

الإصدار ≥ C++‎ 11

التكرار الأمامي:

for (auto pos = v.cbegin(); pos != v.cend(); ++pos) {
    // type of pos is vector<T>::const_iterator
    // *pos = 5; // Compile error - can't write via const iterator
}

التكرار العكسي:

for (auto pos = v.crbegin(); pos != v.crend(); ++pos) {
    // type of pos is vector<T>::const_iterator
    // *pos = 5; // Compile error - can't write via const iterator
}
// Functor::operand()(T&) تتوقُّع
for_each(v.begin(), v.end(), Functor());

// Functor::operand()(const T&) تتوقُّع
for_each(v.cbegin(), v.cend(), Functor())

الإصدار ≥ C++‎ 17

يوسّع as_const هذا إلى التكرار النطاقي:

for (auto const& e : std::as_const(v)) {
    std::cout << e << '\n';
}

هذا سهل التنفيذ في الإصدارات السابقة لـ C++‎:

الإصدار ≥ C++‎ 14

template < class T >
    constexpr std::add_const_t<T>& as_const(T& t) noexcept {
        return t;
    }

ملاحظات حول الكفاءة

نظرًا لأنّ الصنف ‎std::vector‎ هو في الأساس صنف يدير مصفوفةً ديناميكية ذات ذاكرة متجاورة، فسَينطبق المبدأ نفسه المُوضّح هنا على متّجهات C++‎، وسيكون الوصول إلى محتوى المتّجه عبر الفهرس أسهل عند اتّباع مبدأ ترتيب الصفوف الرئيسية (row-major order principle). لا شك أن كل محاولة وصول إلى المتّجه ستؤدّي إلى وضع محتوى إدارتها في ذاكرة التخزين المؤقت أيضًا، لكن الفرق في الأداء عند التكرار على متجه صغير ومهمل إن قورن بمصفوفة خام، انظر هذه المناقشات -بالإنجليزية- عن الأمر للتوضيح وهذه أيضًا). وعليه ينطبق مبدأ الكفاءة نفسه الخاص بالمصفوفات الخام في C على المتّجهات في C++‎.

المتجه ‎vector‎: الاستثناء الكبير

ينصّ المعيار (قسم 23.3.7) على أن يُوفَّر تخصيص للمتّجه ‎vector<bool>‎، من أجل تحسين إدارة الذاكرة بترشيد تخزين القيم البوليانية ‎bool‎ بحيث يأخذ كل منها بتّة واحدة فقط. ونظرًا لأنّه لا يمكن معالجة البتات في C++‎، فهذا يعني أنّ العديد من مُتطلبات المتّجهات العادية لن تنطبق على ‎vector<bool>‎:

  • لا يلزم أن تكون البيانات المُخزّنة متجاورة، لذا لا يمكن تمرير متجه ‎vector<bool>‎ إلى واجهة برمجية للغة C تتوقّع مصفوفة ذات قيم منطقية.

  • لا يعيد التابع ‎at()‎ ولا المعامل ‎operator[]‎ ولا تحصيل المُكرّرات مرجعًا إلى قيمة منطقية، بل تعيد كائنًا وكيلًا (proxy object) يحاكي بشكل تقريبي مرجعًا إلى ‎bool‎ من خلال زيادة تحميل عامل إسناده، فقد لا تكون الشيفرة التالية صالحة بالنسبة للمتّجهات من النوع ‎std::vector<bool>‎ لأنّ تحصيل المُكرّر لا يُعيد مرجعًا:

الإصدار ≥ C++‎ 11

std::vector<bool> v = {true, false};
for (auto &b: v) { } // خطأ

وبالمثل، لا يمكن استخدام الدوالّ التي تتوقّع وسيطَا من النوع ‎bool&‎ مع النتيجة المُعادة من قبل ‎operator []‎ أو ‎at()‎ عند تطبيقها على متّجه منطقيّ vector<bool>‎، أو مع نتيجة تحصيل المكرّر الخاص بها:

void f(bool& b);
f(v[0]); // خطأ
f(*v.begin()); // خطأ

يتعلّق تطبيق المتجه ‎std::vector<bool>‎ بكل من المُصرّف والمعمارية، ويُطبَّق التخصيص الخاصّ بهذا النوع من المتّجهات عن طريق تعبئة ‎n‎ قيمة منطقية في أقل قسم من الذاكرة، وهنا تمثّل ‎n‎ الحجم لأقل ذاكرة يمكن معالجتها بالبِتّْ، والتي تساوي في معظم الأنظمة الحديثة بايت واحد، أو 8 بتّات. هذا يعني أنّ بايت واحد يمكنه تخزين 8 قيم منطقية، وهذا أفضل من التنفيذ التقليدي حيث تُخزّن كل قيمة منطقية في بايت واحد من الذاكرة.

ملاحظة: يُظهر المثال التالي القيم المحتملة للبايتات في الصيغة التقليدية مقابل الصيغة المُحسّنة، لن يكون هذا صحيحًا في جميع الأنظمة، لكنّه لغرض توضيح الفرق بين الطريقتين. في الأمثلة أدناه، يُمثّل البايت على هيئة [x، x، x، x، x، x، x، x].

الطريقة التقليدية: تخزين 8 قيم بوليانية في المتجه std::vector<char>‎:

الإصدار ≥ C++‎ 11

std::vector<char> trad_vect = {true, false, false, false, true, false, true, true};

التمثيل البتّي (Bitwise representation):

[0,0,0,0,0,0,0,1], [0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,1], [0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,1], [0,0,0,0,0,0,0,1]

المُخصّص: تخزين 8 قيم بوليانية في المتجه std::vector<bool>‎:

الإصدار ≥ C++‎ 11

std::vector<bool> optimized_vect = {true, false, false, false, true, false, true, true};

التمثيل البتّي:

[1,0,0,0,1,0,1,1]

لاحظ في المثال أعلاه، أنّه في النسخة التقليدية للمتجه ‎std::vector<bool>‎، ستحتل 8 قيم بوليانية 8 بايتات من الذاكرة، بينما في النسخة المُحسّنة من ‎std::vector<bool>‎، فإنها تحتل بايت واحد فقط من الذاكرة. وهذا تحسّن كبير في استخدام الذاكرة. إن احتجت إلى تمرير متجه ‎vector<bool>‎ إلى واجهة برمجية من نمَط C فقد تحتاج إلى نسخ القيم إلى مصفوفة، أو البحث عن طريقة أفضل لاستخدام الواجهة البرمجية (API) في حال كان الأداء واستخدام الذاكرة غير فعّالين.

إدراج العناصر

إلحاق عنصر بنهاية المتجه عن طريق النسخ أو النقل:

struct Point {
    double x, y;
    Point(double x, double y): x(x), y(y) {}
};
std::vector < Point > v;
Point p(10.0, 2.0);
v.push_back(p); // في المتجه p نسخ

الإصدار ≥ C++‎ 11

إلحاق عنصر بنهاية المتجه عبر إنشاء العنصر فوريًّا: تُمرر الوسائط إلى المنشئ الخاص بالنوع المعطى، وينشأ الكائن في المتجه لتجنب النسخ.

std::vector<Point> v;
v.emplace_back(10.0, 2.0); 

لاحظ أنّ المتّجهات ليس لها تابع ‎push_front()‎ لأسباب تتعلق بالأداء، وتؤدّي إضافة عنصر إلى بداية المتجه إلى نقل جميع عناصره، وإن أردت إدراج العناصر بشكل متكرر في بداية الحاوية فالأفضل استخدام النوع ‎std::list‎ أو ‎std::deque‎.

إدراج عنصر في أيّ موضع في المتجه:

std::vector<int> v{ 1, 2, 3 };
v.insert(v.begin(), 9); // {9, 1, 2, 3} تحتوي الآن على v 

الإصدار ≥ C++‎ 11

إدراج عنصر في أيّ موضع من المتجه عن طريق إنشاء العنصر فوريًّا:

std::vector<int> v{ 1, 2, 3 };
v.emplace(v.begin()+1, 9); // {1, 9, 2, 3} تحتوي الآن على v

إدراج متجه آخر في أيّ موضع في المتجه:

std::vector<int> v(4); // 0, 0, 0, 0 تحتوي
std::vector<int> v2(2, 10); // 10, 10 تحتوي
v.insert(v.begin()+2, v2.begin(), v2.end()); // 0, 0, 10, 10, 0, 0 تحتوي

إدراج مصفوفة في أيّ موضع من المتجه:

std::vector<int> v(4); // 0, 0, 0, 0 
int a [] = {1, 2, 3}; // 1, 2, 3 
v.insert(v.begin()+1, a, a+sizeof(a)/sizeof(a[0])); // 0, 1, 2, 3, 0, 0, 0 

استخدم التابع ‎reserve()‎ قبل إدراج عدّة عناصر دفعة واحدة إن كان حجم المتّجه الناتج معروفًا مُسبقًا، لتجنّب تكرير عمليّات إعادة تخصيص الذاكرة (انظر فقرة حجم المتّجهات وسعتها أدناه):

std::vector < int > v;
v.reserve(100);
for (int i = 0; i < 100; ++i)
    v.emplace_back(i);

لا تستدع التابع ‎resize()‎ في هذه الحالة، وإلا فستنشئ عن غير قصد متجه يحتوي 200 عنصر، وستوضع القيم التي تريدها في المئة عنصر الأخيرة من المتجه.

استخدام المتجهات كمصفوفات من نمط C

هناك عدّة أسباب لاستخدام المتّجهات كمصفوفات من نمَط C، كالتوافق مع مكتبات C مثلًا، وهذا ممكن لأنّ عناصر المتجه مُخزّنة في مواضع متجاورة.

الإصدار ≥ C++‎ 11

std::vector<int> v{ 1, 2, 3 };
int* p = v.data();

يجوز تطبيق التابع ‎.data()‎ على المتّجهات الفارغة أيضًا على عكس الحلول السابقة القائمة على معايير C++‎ (انظر أدناه)، إذ أنّه لن يتسبّب في سلوك غير محدّد في هذه الحالة.

كان عليك قبل الإصدار C++‎ 11 أن تأخذ عنوان العنصر الأوّل في المتجه للحصول على مؤشّر مكافئ، وإذا لم يكن المتجه فارغًا، فإنّ هذين التابعين سيكونان متكافئين:

int* p = &v[0]; 
int* p = &v.front(); // مؤشّر إلى العنصر الأول

ملاحظة: إذا كانت المتجه فارغًا، فلا يمكن استخدام ‎v[0]‎ و ‎v.front()‎ لأنّ سلوكهما سيكون غير محدّد.

عند تخزين العنوان الأساسي لبيانات المتجه، لاحظ أنّ العديد من العمليات (مثل ‎push_back‎ و ‎resize‎ وغيرها) يمكن أن تغيّر مواضع بيانات المتّجة في الذاكرة، وذلك سيبطل مؤشّرات البيانات السابقة. مثلا:

std::vector<int> v;
int* p = v.data();
v.resize(42); // صالحا p تغيّر مواضع الذاكرة داخليا، لذا لم يعد

إيجاد عنصر في متجه

يمكن استخدام الدالّة ‎std::find‎، المُعرّفة في الترويسة ، لإيجاد عنصر في متجه، ويستخدم التابعُ std::find المعامل‎operator==‎ للتحقّق من تساوي العناصر، ويعيد مكرّرًا إلى أول عنصر في النطاق يساوي القيمة المبحوث عنها.

إذا لم يُعثر على عنصر يحقّق ذلك، فإنّ التابع ‎std::find‎ سيُعيد ‎std::vector::end‎ (أو ‎std::vector::cend‎ إذا كانت المتجه ثابتًا ‎const‎).

الإصدار  < C++11
++‎>

static const int arr[] = {5, 4, 3, 2, 1};
std::vector<int> v (arr, arr + sizeof(arr) / sizeof(arr[0]) );

std::vector<int>::iterator it = std::find(v.begin(), v.end(), 4);
std::vector<int>::difference_type index = std::distance(v.begin(), it);
// إلى العنصر الثاني في المتجه `it` يشير

std::vector < int > ::iterator missing = std::find(v.begin(), v.end(), 10);
std::vector < int > ::difference_type index_missing = std::distance(v.begin(), missing);
// تساوي  5 index_missing و  v.end() تساوي `missing` 

الإصدار ≥ C++‎ 11

std::vector<int> v { 5, 4, 3, 2, 1 };

auto it = std::find(v.begin(), v.end(), 4);
auto index = std::distance(v.begin(), it);
// إلى العنصر الثاني في المتجه `it` يشير

auto missing = std::find(v.begin(), v.end(), 10);
auto index_missing = std::distance(v.begin(), missing);
// تساوي 5 index_missing و  v.end() تساوي `missing` 

إذا كنت بحاجة إلى إجراء العديد من عمليات البحث في متجه كبير فقد يكون الأفضل أن ترتّب المتجه أولاً، ثم تستخدم خوارزمية البحث الثنائي.

لإيجاد أوّل عنصر يحقّق شرطًا ما في متجه، يمكنك استخدام ‎std::find_if‎. بالإضافة إلى المُعاملين المُمرّرين إلى الدالّة ‎std::find‎، فإنّ الدالّة ‎std::find_if‎ تقبل معاملًا ثالثًا، وهو عبارة عن كائن دالّة (function object)، أو مؤشّر دالّة يشير إلى دالّة شرطية (predicate function). يجب أن تقبل الدالّة الشرطية عنصرًا من الحاوية كوسيط ثم تعيد قيمة قابلة للتحويل إلى قيمة بوليانية ‎bool‎ لكن دون تعديل الحاوية:

الإصدار  ≥ C++11
++‎>

bool isEven(int val) {
    return (val % 2 == 0);
}
struct moreThan {
    moreThan(int limit): _limit(limit) {}

    bool operator()(int val) {
        return val > _limit;
    }

    int _limit;
};

static const int arr[] = {1, 3, 7, 8};
std::vector < int > v(arr, arr + sizeof(arr) / sizeof(arr[0]));

std::vector < int > ::iterator it = std::find_if(v.begin(), v.end(), isEven);
// إلى 8، أول عنصر زوجي `it`يشير

std::vector < int > ::iterator missing = std::find_if(v.begin(), v.end(), moreThan(10));
//لأن كل العناصر أصغر من 10 v.end() يساوي `missing` 

الإصدار ≥ C++‎ 11

// إيجاد أول عنصر زوجي
std::vector<int> v = {1, 3, 7, 8};
auto it = std::find_if(v.begin(), v.end(), [](int val) {
    return val % 2 == 0;
});
// إلى 8، أول عنصر زوجي `it`يشير

auto missing = std::find_if(v.begin(), v.end(), [](int val) {
    return val > 10;
});
// لأن كل العناصر أصغر من 10 v.end() يساوي `missing` 

ضم المتجهات (Concatenating Vectors)

يمكن ضم متجه إلى آخر باستخدام التابع ‎insert()‎:

std::vector<int> a = {0, 1, 2, 3, 4};
std::vector<int> b = {5, 6, 7, 8, 9};
a.insert(a.end(), b.begin(), b.end());

سيفشل هذا الحلّ إذا حاولت ضمّ متجه إلى نفسه لأنّ المواصفات القياسية تنصّ على أنّ المكرّرات المُمرّرة إلى ‎insert()‎ يجب ألّا تكون من نفس نطاق عناصر الكائن المستقبِل.

الإصدار < C++‎ 11

يمكن استخدام الدالتين ‎std::begin()‎ و ‎std::end()‎ بدلاً من استخدام توابع المتجه:

a.insert(std::end(a), std::begin(b), std::end(b));

هذا حلّ أكثر شمولية لأنّ ‎b‎ يمكن أن تكون مصفوفة مثلًا، لكن للأسف، فهذا الحل أيضًا لا يسمح لك بضمّ متجه إلى نفسه.

إذا لم يكن ترتيب العناصر في المتجه المستقبِل مهمًا، فإنّ معرفة عدد العناصر في كل متجه قد يجنّبك عمليات النسخ غير الضرورية:

if (b.size() < a.size())
    a.insert(a.end(), b.begin(), b.end());
else
    b.insert(b.end(), a.begin(), a.end());

استخدام المتجهات كمصفوفات متعدّدة الأبعاد

يمكن استخدام المتّجهات كمصفوفات ثنائية الأبعاد (2D matrix) عبر تعريفها كمُتّجهة مؤلّفة من متّجهات. ويمكن تعريف مصفوفة ذات 3 صفوف و 4 أعمدة ذات قيم مساوية للصفر على النحو التالي:

std::vector<std::vector<int> > matrix(3, std::vector<int>(4));

الإصدار ≥ C++‎ 11

صياغة التهيئة باستخدام قوائم التهيئة مشابه للمتّجهات العادية:

std::vector<std::vector<int>> matrix =    {   {0,1,2,3},
{4,5,6,7},
{8,9,10,11}
};

يمكن الوصول إلى قيم هذه المتجه بنفس طريقة الوصول إلى عناصر المصفوفات ثنائية الأبعاد

int var = matrix[0][2];

التكرار على مصفوفة متعددة الأبعاد يشبة التكرارَ على المتّجهات العادية ولكن مع بعد إضافي.

for (int i = 0; i < 3; ++i) {
    for (int j = 0; j < 4; ++j) {
        std::cout << matrix[i][j] << std::endl;
    }
}

الإصدار ≥ C++‎ 11

for (auto & row: matrix) {
    for (auto & col: row) {
        std::cout << col << std::endl;
    }
}

تمثيل مصفوفة بمتّجه مكوّن من متّجهات يُعدُّ طريقة مناسبة لتمثيل المصفوفات متعددة الأبعاد لكنّها ليست الأفضل: إذ أنّها تُشتّت المتّجهات الفردية في الذاكرة كما أنّ هيكلة البيانات لا تساعد على التخزين المؤقت. أيضًا، يجب أن تكون أطوال صفوف المصفوفة المتعدّدة متماثلة، وهذا ليس حال متّجه المتّجهات، كما أنّ المرونة الإضافية قد تسبب الأخطاء.

استخدام المتجهات المرتبة لتسريع عمليات البحث

توفر الترويسة عددًا من الدوالّ المفيدة لاستخدامها على المتّجهات المُرتبة (sorted vectors)، ولا يمكن العمل مع المتّجهات المُرتّبة إلا إن كانت قيمها المرتّبة قابلة للمقارنة عبر المعامل ‎<‎.

يمكن ترتيب متجه غير مرتب باستخدام الدالّة ‎std::sort()‎:

std::vector<int> v;
// بالعناصر v إضافة شيفرة لملء
std::sort(v.begin(), v.end());

تتيح المتّجهات المرتّبة بحثًا سريعًا عن العناصر باستخدام الدالّة ‎std::lower_bound()‎، وعلى عكس ‎std::find()‎ فإن هذا التابع يجري بحثًا ثنائيا (binary search) فعّالًا على المتجه، لكن الجانب السلبي فيه أنه لا يعطي نتائج صحيحة إلّا إن كانت المتّجهات المُدخلة مُرتّبة:

// بحث عن أول عنصر يساوي 42
std::vector<int>::iterator it = std::lower_bound(v.begin(), v.end(), 42);
if (it != v.end() && *it == 42) {
    // عثرنا على العنصر
}

ملاحظة: إذا لم تكن القيمة المطلوبة جزءًا من المتجه، فسيعيد ‎std::lower_bound()‎ مكرّرًا إلى أوّل عنصر يكون أكبر من القيمة المطلوبة، يسمح لنا هذا السلوك بإدراج العنصر الجديد في مكانه الصحيح في المتّجهات المُرتّبة سلفًا:

int const new_element = 33;
v.insert(std::lower_bound(v.begin(), v.end(), new_element), new_element);

إذا أردت إدراج الكثير من العناصر دفعة واحدة، فقد يكون الأفضل استدعاء ‎push_back()‎ عليهم جميعًا، ثم استدعاء ‎std::sort()‎ بعد إدراج جميع العناصر. في هذه الحالة، يمكن أن تعادل التكلفة المتزايدة لترتيب المتجه التكلفةَ المخُفّضة لإدراج عناصر جديدة في نهاية المتجه بدلًا من البحث عن موضعه المناسب.

إذا كان المتجه يحتوي على عدّة عناصر بنفس القيمة، فسيعيد ‎std::lower_bound()‎ مُكرِّرًا إلى العنصر الأول من القيمة التي يُبحث عنها. لكن إن أردت إدراج عنصر جديد بعد العنصر الأخير من القيمة التي تبحث عنها، فيجب استخدام الدالّة ‎std::upper_bound()‎ لأنّها تقلّل من تحويل العناصر:

v.insert(std::upper_bound(v.begin(), v.end(), new_element), new_element);

إذا أردت الحصول على مكرّر الحدّ الأعلى (upper bound iterators) ومكرّر الحدّ الأدنى (lower bound iterators)، فيمكنك استخدام الدالة ‎std::equal_range()‎ للحصول عليهما معًا:

std::pair<std::vector<int>::iterator,
std::vector<int>::iterator> rg = std::equal_range(v.begin(), v.end(), 42);
std::vector<int>::iterator lower_bound = rg.first;
std::vector<int>::iterator upper_bound = rg.second;

للتحقّق من وجود عنصر ما في متّجه مُرتّب، يمكنك استخدام الدالّة ‎std::binary_search()‎ مع أنها غير مقصورة على المتّجهات:

bool exists = std::binary_search(v.begin(), v.end(), value_to_find);

تقليص سعة متجه

تزيد المتّجهات في سعتها تلقائيًا عند إدراج عناصر جديدة إن كانت هناك حاجة لذلك، لكنها لا تقلّص سعتها بعد إزالة عنصر ما.

// تهيئة متّجه مئوي
std::vector < int > v(100);

// سعة المتّجه دائما ما تكون أكبر من حجمه
auto const old_capacity = v.capacity();
// old_capacity >= 100

// إزالة نصف العناصر
v.erase(v.begin() + 50, v.end());    // تخفيض الحجم من 50 إلى 100
// (v.capacity() == old_capacity)

يمكننا نسخ محتويات المتجه إلى متجه مؤقت جديد إن أردنا تقليل سعته، وسيكون للمتجه الجديد الحد الأدنى من السعة اللازمة لتخزين جميع عناصر المتجه الأصلية والتي يمكن أن تكون أقل بكثير من سعة المتجه الأولى في حال تقليص الحجم الأصلي بشكل كبير. يمكننا بعد ذلك تبديل المتجه الأصلي بالمتجه المؤقت المُقلّص:

std::vector<int>(v).swap(v);

الإصدار ≥ C++‎ 11

في C++‎ 11، يمكننا استخدام التابع ‎shrink_to_fit()‎ لتحقيق التأثير نفسه:

v.shrink_to_fit();

ملاحظة: التابع ‎shrink_to_fit()‎ هو مجرد طلب، ولا يضمن تقليص السعة.

حجم المتجهات وسعتها

حجم المتّجه هو عدد عناصره:

  1. يمكنك الحصول على الحجم الحالي للمتّجه بواسطة الدالة التابعة ‎size()‎، وتعيد الدالّة ‎empty()‎ القيمة ‎true‎ إن كان الحجم يساوي 0:
vector<int> v = { 1, 2, 3 }; // 3 الحجم يساوي
const vector<int>::size_type size = v.size();
cout << size << endl; // 3
cout << boolalpha << v.empty() << endl; // false
  1. تبدأ المتّجهات المُنشأة افتراضيا بالحجم 0:
vector<int> v; // 0 الحجم يساوي
cout << v.size() << endl; // 0
  1. تؤدّي إضافة ‎N‎ عنصر إلى المتجه (مثلا، عبر الدوالّ ‎push_back()‎ أو ‎insert()‎ أو ‎resize()‎) إلى زيادة الحجم بالقيمة ‎N‎.
  2. تؤدّي إزالة ‎N‎ عنصر من المتجه (على سبيل المثال عند استخدام الدوالّ ‎pop_back()‎ أو ‎erase()‎ أو ‎clear()‎) إلى تقليص الحجم بالقيمة ‎N‎.
  3. الحد الأقصى لحجم المتجه يتعلق بالتنفيذ (implementation-specific)، ولكن لا تشغل نفسك بهذا، فعلى الأرجح ستُستنزف ذاكرة الوصول العشوائي (RAM) قبل بلوغ الحد الأقصى:
vector < int > v;
const vector < int > ::size_type max_size = v.max_size();
cout << max_size << endl;
v.resize(max_size); // لن تعمل على الأرجح
v.push_back(1); // بالتأكيد لن تعمل

خطأ شائع: حجم المتجه لا يكون بالضرورة (أو حتى عادة) من النوع ‎int‎:

// شيفرة سيئة
vector < int > v_bad(N, 1); // N إنشاء متجه كبير حجمه
for (int i = 0; i < v_bad.size(); ++i) { // int ليس بالضرورة أن يكون الحجم من النوع
    do_something(v_bad[i]);
}

تختلف سعة المتجه عن حجمه، ففي حين أنّ الحجم يمثّل ببساطة عدد العناصر التي يحتويها المتجه حاليًا، إلا أنّ السعة تمثّل عدد العناصر التي حُجِزت ذاكرة لها، وهذا مفيد لأنّ (إعادة) تخصيص أحجام كبيرة في الذاكرة يمكن أن يكون مكلّفا للغاية.

  1. يمكن الحصول على السعة الحالية للمتّجه عبر التابع ‎capacity()‎. وتذكّر أنّ السعة دائمًا أكبر من أو تساوي الحجم:
vector<int> v = { 1, 2, 3 }; // الحجم يساوي 3 لكنّ السعة يمكن أن تكون أكبر
const vector<int>::size_type capacity = v.capacity();
cout << capacity << endl; 
  1. يمكنك حجز السعة يدويًا عبر دالّة ‎reserve( N )‎ (تغيّر سعة المتجه إلى ‎N‎):
// شيفرة سيئة
vector < int > v_bad;
for (int i = 0; i < 10000; ++i) {
    v_bad.push_back(i); // الكثير من تخصيصات الذاكرة 
}
//  شيفرة جيدة
vector < int > v_good;
v_good.reserve(10000); // هذا جيد، لأنه ستكون هناك عملية تخصيص واحدة فقط
for (int i = 0; i < 10000; ++i) {
    v_good.push_back(i); // لا حاجة لتخصيص الذاكرة
}
  1. يمكنك أن تطلب تحرير السعة الزائدة عبر ‎shrink_to_fit()‎ (لكنّ ذلك غير مضمون). هذا مفيد لتوفير الذاكرة المستخدمة:
vector<int> v = { 1, 2, 3, 4, 5 }; // الحجم يساوي 5، ونفترض أنّ السعة تساوي 6
v.shrink_to_fit(); // السعة من الممكن أنها تساوي الآن 5، لكن يمكن أن تساوي 6
cout << boolalpha << v.capacity() == v.size() << endl; 

يدير المتّجهُ السّعةَ جزئيًّا، فقد تقرّر زيادة سعته عند إضافة عناصر إليه. يفضل الكثير من المنفِّذين (Implementers) استخدام 2 أو 1.5 كعامل توسيع. وعلى الناحية الأخرى، لا تتقلّص سعة المتّجهات تلقائيًّا في العادة. مثلا:

// السعة يمكن أن تساوي 0، لكن ليس بالضرورة
vector < int > v; 

// السعة تساوي 1 على الأرجح الآن
v.push_back(1); 

// الحجم صار 0 لكن السعة ما تزال تساوي 1
v.clear(); 

// لنفترض أن الحجم والسعة يساويان 4
v = { 1, 2, 3, 4 };  

// ازدياد السعة، لنفترض أنها أصبحت تساوي 6، أي بمعدل توسع 1.5
v.push_back(5); 

// لا تغيير في السعة
v.push_back(6); 

// ازدياد السعة، لنفترض أنها أصبحت تساوي 9، أي بمعدل توسّع 1.5
v.push_back(7); 

// السّعة لم تتغير
v.pop_back(); v.pop_back(); v.pop_back(); v.pop_back(); 

إبطال المكررات والمؤشرات (Iterator/Pointer Invalidation)

لا يمكن أن تَبطُل المُكرّرات والمؤشّرات التي تشير إلى متجه ما إلا عند إجراء عمليات معينة، إذ سيؤدّي استخدام مكرّرات أو مؤشّرات غير صالحة إلى سلوك غير محدّد. إليك بعض العمليات التي تُبطل المكرّرات والمؤشّرات:

  • أيّ عملية إدخال تغيّر سعة المتجه ستُبطل جميع المُكرّرات والمؤشّرات التي تشير إلى ذلك المتجه:
// حجم المتجه يساوي 5 وسعتها غير معروفة
vector < int > v(5); 
int *p1 = &v[0];

// بما أن السعة مجهولة p1 ربما أُبطِل
v.push_back(2); 

// السعة تساوي 20 على الأقل الآن
v.reserve(20); 
int *p2 = &v[0];

// يساوي 7 الآن v لأن حجم p2 لم يتم إبطال
v.push_back(4); 

// إدارج 30 عنصرا في النهاية، سيتجاوز الحجم السعة السابقة، لذا
v.insert(v.end(), 30, 9);    
// صار باطلا الآن `p2` فعلى الأرجح أن

int *p3 = &v[0];

// باطلا `p3`تجاوز السعة، وسيصبح
v.reserve(v.capacity() + 20); 

الإصدار ≥ C++‎ 11

auto old_cap = v.capacity();
v.shrink_to_fit();
if(old_cap != v.capacity())
// إبطال المكررات
  • أيّ عملية إدراج لا تزيد في السعة، ستُبطل المكررات والمؤشّرات التي تشير إلى العناصر الموجودة في الموضع الذي حدث الإدراج عنده أو بعده. يتضمّن ذلك المُكرر ‎end‎:
vector < int > v(5);
v.reserve(20); // السعة تساوي 20 على الأقل
int *p1 = &v[0];
int *p2 = &v[3];
v.insert(v.begin() + 2, 5, 0); // أصبح باطلا، ولكن بما أنّ السعة لم تتغير  `p2` 
                                           // يبقى صالحا `p1` فإن
int *p3 = &v[v.size() - 1];
v.push_back(10); // ما زالا صالحين `p3` و`p1` السعة لم تتغير لذا فإنّ
  • ستؤدّي أيّ عملية إزالة إلى إبطال المكرّرات أو المؤشّرات التي تشير إلى العناصر التي أزيلت أو التي بعدها. يتضمن ذلك المكرّر ‎end‎:
vector<int> v(10);
int *p1 = &v[0];
int *p2 = &v[5];
v.erase(v.begin() + 3, v.end()); // `p2` ما زال صالحا على عكس `p1`
  • المعامل ‎operator=‎ و التابع ‎clear()‎ سيبطلان كل المكرّرات أو المؤشّرات التي تشير إلى المتجه.

إيجاد العنصر الأكبر/الأصغر مع فهرسه في متجه

لإيجاد أكبر أو أصغر عنصر مخزّن في متجه، يمكنك استخدام التابعين ‎std::max_element‎ و std::max_element على الترتيب، هذان التابعان مُعرّفان في الترويسة . إذا كانت هناك عدّة عناصر تساوي القيمة الأكبر (أو الأصغر) في متجه، فإن التوابع ستعيد المُكرّر الذي يشير إلى أوّل تلك العناصر. بالنسبة للمتّجهات الفارغة، تُعاد v.end()‎.

std::vector<int> v = {5, 2, 8, 10, 9};
int maxElementIndex = std::max_element(v.begin(), v.end()) - v.begin();
int maxElement = * std::max_element(v.begin(), v.end());

int minElementIndex = std::min_element(v.begin(), v.end()) - v.begin();
int minElement = * std::min_element(v.begin(), v.end());

std::cout << "maxElementIndex:" << maxElementIndex << ", maxElement:" << maxElement << '\n';
std::cout << "minElementIndex:" << minElementIndex << ", minElement:" << minElement << '\n';

المخرجات:

maxElementIndex:3, maxElement:10
minElementIndex:1, minElement:2

الإصدار ≥ C++‎ 11

يمكن الحصول على الحدّ الأصغر والحد الأكبر في متجه معًا، باستخدام التابع std::minmax_element، وهو مُعرّف في الترويسة :

std::vector<int> v = {5, 2, 8, 10, 9};
auto minmax = std::minmax_element(v.begin(), v.end());

std::cout << "minimum element: " << *minmax.first << '\n';
std::cout << "maximum element: " << *minmax.second << '\n';

المخرجات:

minimum element: 2
maximum element: 10

تحويل مصفوفة إلى متجه

يمكن تحويل مصفوفة بسهولة إلى متجه باستخدام ‎std::begin‎ و ‎std::end‎:

الإصدار≥ C++‎ 11

int values[5] = { 1, 2, 3, 4, 5 };    // المصفوفة المصدرية

std::vector < int > v(std::begin(values), std::end(values)); // نسخ المصفوفة إلى متجه جديد

for (auto &x: v)
    std::cout << x << " ";
std::cout << std::endl;

المخرجات:

1 2 3 4 5 

تحويل وسائط main إلى متجه من السلاسل النصية:

int main(int argc, char* argv[]) {
std::vector<std::string>  args(argv, argv + argc);
}

يمكن أيضًا استخدام قائمة تهيئة <>initializer_list (الإصدار C++11) لتهيئة المتجه على النحو التالي:

initializer_list<int> arr = { 1,2,3,4,5 };
vector < int > vec1 {
    arr
};

for (auto & i: vec1)
    cout << i << endl;

الدوال التي تعيد متجهات كبيرة

الإصدار ≥ C++‎ 11

في الإصدار C++‎ 11، يُطلب من المُصرّفات النقل ضمنيًّا من المتغير المحلي المٌعاد. وكذلك يمكن لمعظم المُصرّفات إهمال النسخ (copy elision) في كثير من الحالات، وإسقاط النقل تمامًا. ونتيجةً لذلك فإنّ إعادة الكائنات الكبيرة التي يمكن نقلها بدون كلفة كبيرة لا تتطّلب معالجة خاصة:

#include <vector>
#include <iostream>

// (NRVO) “إن عجز المصرف عن إنجاز “ترشيد القيمة المعادة المسماة
// إلى القيمة المعادة v وتجنّب النقل بشكل كامل، فعليه أن ينقل من
std::vector < int > fillVector(int a, int b) {
    std::vector < int > v;
    v.reserve(b - a + 1);
    for (int i = a; i <= b; i++) {
        v.push_back(i);
    }
    return v; // نقل ضمني
}

int main() { // إعلان المتجه وملؤه
    std::vector < int > vec = fillVector(1, 10);

    // طباعة المتجه
    for (auto value: vec)
        std::cout << value << " "; // "1 2 3 4 5 6 7 8 9 10 "
    std::cout << std::endl;
    return 0;
}

الإصدار < C++11
++‎>

قبل الإصدار C++‎ 11، كان يُسمَح بإهمال النسخ (copy elision)، وقد طُبِّق ذلك في معظم المُصرّفات. ومع ذلك، ونظرًا لغياب دلالات النقل (move semantics)، فقد ترى في الشيفرات القديمة أو الشيفرات التي يجب تصريفها بمصرّفات قديمة لا تدعم هذه الميزة أنّ المتّجهات تُمرَّر كوسائط إخراج (output arguments) لمنع النسخ غير الضروري:

#include <vector>
#include <iostream>

// تمرير المتجه بالمرجع
void fillVectorFrom_By_Ref(int a, int b, std::vector < int > & v) {
    assert(v.empty());
    v.reserve(b - a + 1);
    for (int i = a; i <= b; i++) {
        v.push_back(i);
    }
}

int main() { // التصريح عن متجه
    std::vector < int > vec;

    // ملء المتجه
    fillVectorFrom_By_Ref(1, 10, vec);
    // طباعة المتجه
    for (std::vector < int > ::const_iterator it = vec.begin(); it != vec.end(); ++it)
        std::cout << * it << " "; // "1 2 3 4 5 6 7 8 9 10 "
    std::cout << std::endl;
    return 0;
}

هذ الدرس جزء من سلسلة مقالات عن C++‎.

ترجمة -بتصرف- للفصل Chapter 49: std::vector من كتاب C++ Notes for Professionals





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن