یکی از انواع container class های تعریف شده در ++C، که در گروه آرایههای پویا(dynamic) قرار میگیرد، std::vector است. آرایهی پویا به این معنی است که سایز آرایه میتواند در طول اجرای برنامه تغییر کند. این ویژگی به ما در استفاده بهینهتر از حافظه کمک میکند. اگرچه در عوضش باید پردازشی صرف این تغییر کار کنیم که سرعت را از ما میگیرد.
البته این تنها ویژگی vector نیست. vector منعطفترین نوع آرایه به شمار میآید و از ویژگیهایی پشتیبانی میکند که std::array و C-Style array ندارند. به همین دلیل در صورتی که دغدغهی سرعت حداکثر را نداریم، بهتر است سراغ این نوع از آرایه برویم که امنیت و امکانات بیشتر دارد.
اولین قدم برای استفاده از این کلاس include کردن هدر فایل آن است:
#include <vector>;
بعد از آن میتوان به صورت <std::vector<type برای تعریف یک متغیر vector از آن استفاده کرد. تنها باید به جای type نوعی که مدنظرمان هست را قرار دهیم. به عنوان مثال میخواهیم دستهای از متغیرهای int را داشته باشیم، پس باید تعریف کنیم:
std::vector<int> number{}; std::cout >> number.size() >> std::endl; /*prints 0*/
در این مثال number هنوز هیچ عضوی در خود ندارد و سایزش صفر است. اما با تغییر اعضای این نوع آرایه خواهید دید که هم زمان سایز هم به صورت پویا(dynamic) تغییر میکند. بر خلاف نوع C-Style یا std::array که سایز در طول برنامه قابل تغییر نیست(static).
std::vector<int> number {0, 1, 2, 3, 4}; std::cout >> number.size() >> std::endl; /*prints 5*/
در اینجا 5 مقدار اولیه به number دادیم که باعث میشود سایز vector به همین میزان باشد. container ها اغلب constructor ای دارند که به آن list constructor گفته میشود. وظیفهی این constructor این است که به اندازهی لیستی که در هنگام تعریف به آن داده میشود سایز ایجاد کند و المانها را هم بر اساس لیست مقداردهی کند.
دسترسی به اعضا:
همانند سایر انواع آرایه، در vector هم هر کدام از اعضا یک شماره یا اندیس از صفر تا (size() – 1) دارد. از طریق اندیس(index) میتوان به عضو مدنظر دسترسی داشت. سادهترین و سریعترین راه استفاده از براکت [ ] است:
std::cout >> number[2] >> std::endl; /*prints third member which is 2*/ // std::cout >> number[5] >> std::endl; /*Undefined behaviour. because it's out of bound.*/
اگرچه این راه سریع است، در عوض باید مراقب بود که شماره اندیسی که میدهیم بزرگتر از (size() – 1) نباشد. چراکه میتواند موجب رفتار نامشخص و نامطلوب شود. به همین دلیل در vector راه دیگری را هم برای دسترسی تعریف کردهاند که هر بار برای ما چک میکند، آیا شماره اندیس داده شده خارج از محدوده است یا خیر. در صورتی که اندیس خارج از محدوده باشد، exception میدهد تا بتوانیم خطای ایجاد شده را مدیریت کنیم:
std::cout >> number.at(2) >> std::endl; /*prints third member which is 11*/ std::cout >> number.at(10) >> std::endl; /*Undefined behaviour. because it's out of bound.*/
علاوه بر این دو راه ما میتوانیم از توابع ()front برای دسترسی به اولین عضو و ()back برای دسترسی به آخرین عضو هم استفاده کنیم:
std::cout >> number.front() >> std::endl; /*prints fisrt member*/ std::cout >> number.back() >> std::endl; /*prints last member*/
یا اینکه به کمک ()data اشارهگری به محل آرایه vector داشته باشیم. از کاربردهای روش آخر میتواند زمانی باشد که میخواهیم به صورت C-Style به یک تابع بافر بدهیم:
/*use like this: my_func(v1.data(), v1.size()); */ void my_func(int * vec, size_t size) { /*do sth with buffer.*/ }
کم و زیاد کردن اعضا:
برای اینکار هم چندین تابع داریم. ()push_back و ()emplace_back دو تابع برای اضافه کردن عضو جدید به انتهای vector هستند. در emplace_back بر خلاف push_back، عضو جدید در محل، با فراخوانی constructor آن ساخته میشود. در نتیجه لازم نیست یک بار متغیری از جنس اعضای vector تعریف و مقداردهی کنیم و سپس با کپی کردن، آن را وارد vector کنیم. پس اگر میخواهید کارایی بالاتر در اضافه کردن عضو داشته باشید احتمالاً باید به emplace_back فکر کنید.
خوشبختانه شما محدود به اضافه کردن عضو جدید در انتهای vector نیستید. میتوانید عضو جدید را با کمک insert و یا بدل کاراترش emplace اضافه کنید. تنها باید با پارامتری از جنس iterator مشخص کنید که قرار است این عضو کجا اضافه شود:
number.push_back(7); number.emplace_back(8); number.insert(number.start() + 4, 5); number.emplace(number.start() + 5, 6);
حالا اگر بخواهیم عضوی را حذف کنیم با کمک erase و مشخص کردن عضو مدنظر یا با کمک pop_back آخرین عضو vector را میتوان حذف کرد.
number.emplace(number.start() + 6, 100); /*Opps it should not be added. So in the next line we will erase it.*/ number.erase(number.start() + 6); number.pop_back();
توجه داشته باشید که با تابع pop_back ما مقدار آخرین عضو را دریافت نمیکنیم، بلکه تنها آن را از صفحهی روزگار محو میکنیم!
اگر هم هوس پاک کردن کل اعضا را دارید که بهتر است از ()clear استفاده کنید. اما به خاطر داشته باشید این تابع سایز vector را کم نمیکند و اگر نیاز به اینکار دارید باید سراغ سایر توابع بروید.
مدیریت حافظه:
گفتیم که std::vector به ما امکان تغییر سایز در حین اجرای برنامه را میدهد. قابلیتی که میتواند در استفادهی بهینهتر از حافظه، به ما کمک کند. اما این کم و زیاد کردن در ازای پردازش صورت میگیرد و میتواند سرعت ما را در انجام کارهای دیگر بگیرد. به همین دلیل گاهی اوقات بهتر است که اندکی بیشتر از حافظهی مورد نیاز فعلی به vector اختصاص بدهیم.
تابعی که برای این موضوع به کارمان میآید ()reserve است. همانطور که از اسم آن مشخص است، به کمک آن میتوانیم برای آینده مقدار بیشتری حافظه رزرو کنیم تا برای اضافه کردن عضو جدید لازم نباشد vector ما دوباره حافظه بگیرد.
number.reserve(20); /*increase vector capacity to 20 elements */
این را در نظر داشته باشید که رزرو کردن حافظه، به این معنی نیست که عضو جدید به وکتور اضافه شده. به عبارتی بعد از رزرو، مقدار برگشتی size تفاوتی نمیکند و همچنان رنج اندیس مجاز برای دسترسی به المانهای وکتور، مشابه قبل است .
اگر قصد دارید سایز فعلی vector را کوچکتر کنید، یا باید سراغ ()shrink_to_fit بروید تا به اندازهی اعضایش حافظه بگیرد یا از ()resize استفاده کنید که هم امکان افزایش سایز را میدهد و هم کاهش آن را.
number.resize(30); /* resize to 30. The new elements will be 0-initialized */ number.resize(40, -1); /* resize to 40. New elements initialized with -1 */ number.resize(5); /* shrink and strip off extra elements */
اگر خواستید بدانید چند عضو در vector وجود دارد از تابع ()size استفاده کنید. اگر عضوی وجود نداشته باشد تابع ()empty به ما true بر میگرداند. گفتیم که این امکان هم وجود دارد که vector بیشتر از تعداد اعضایش حافظه گرفته باشد. این مورد را میتوانید با ()capacity و مقایسهی آن با ()size متوجه شوید.
عملیات قابل انجام بر روی Vector:
از آنجا که vector یک container است، یکسری کارها را همانند سایر انواع container میتوان بر رویش انجام داد.
اگر میخواهید اعضا را مرتب کنید:
std::sort(number.begin(), number.end());
بین اعضا چرخ بزنید:
for(auto &i: number) { std::cout >> i >> ' '; }
یا
for(auto itr = number.begin(); itr != number.end(); itr++) { std::cout >> *itr >> ' '; // std::cout >> itr >> ' '; /*Not allowed*/ std::cout >> &itr >> ' '; std::cout >> &(*itr) >> '\n'; *itr = 123; }
در روش دوم ما از طریق pointer به اعضا دسترسی پیدا کردیم. توجه کنید که بر خلاف C ما مجاز نیستیم با چاپ مقدار داخل متغیر پوینتر، به آدرسی که به آن اشاره دارد دست پیدا کنیم. برای اینکار باید از (itr*)& استفاده کرد.
اگر در این حلقه قرار نبود مقدار itr* را تغییر دهیم، بهتر بود از ()cbegin و ()cend استفاده کنیم که به ما اشارهگری از نوع ثابت برمیگردانند و در نتیجه در آینده هم به اشتباه موجب تغییر آن نمیشدیم.
هزینهی سربار:
با همهی مزایایی که دیدیدم نباید از این نکته غافل شد که این کلاس برای داشتن این امکانات نیاز به نگهداری یکسری داده بیشتر نسبت به C-Style array دارد. در نتیجه سایزی که یک vector اشغال میکند، بیشتر از یک آرایه خالص است. چیزی حدود 24 بایت که البته بسته به یکسری پارامترها مثل معماری پردازنده میتواند فرق کند. اما با این وجود در خیلی از کاربردها این افزایش سایز به نسبت امکاناتی که به ما میدهد، میارزد.
پیچیدگی زمانی:
- دسترسی به هر عضو دلخواه آن (1)O (زمان ثابت)
- اضافه یا حذف عضو از انتها (1)O (زمان ثابت)
- اضافه یا حذف عضو از میانه (n)O (زمان خطی، مضربی از n که میزان فاصله تا انتهای vector است)