Menu

vector ב-C++: מערכים דינמיים עם std::vector

std::vector הוא המערך בגודל משתנה של C++, הקונטיינר שכדאי לבחור בו כברירת מחדל. למדו ליצור vector, לגשת לאיברים, להגדיל אותו ולעבור עליו בלולאה, וגם את המלכודות של ביטול iterators וגישה מחוץ לגבולות.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

למה vector ולא מערך גולמי

למערך גולמי יש גודל קבוע שנקבע בזמן קומפילציה, והוא שוכח מה האורך שלו ברגע שמעבירים אותו לפונקציה. std::vector פותר את שתי הבעיות: זה מערך בגודל משתנה שעוקב אחרי האורך שלו, גדל לפי הצורך ומנקה את הזיכרון של עצמו. ב-C++ מודרנית, vector הוא קונטיינר ברירת המחדל: פנו למערך גולמי רק כשיש לכם סיבה ספציפית.

כללו את <vector>, ואז הצהירו על אחד עם טיפוס האיברים בסוגריים משולשים:

scores.size() תמיד מחזיר את האורך הנוכחי: אין int n נפרד שצריך לשמור מסונכרן, ואין טריקים עם sizeof. ה-{90, 75, 100, 60} הוא brace initializer (רשימת אתחול בסוגריים מסולסלים); ה-vector מבין לבד שהוא צריך ארבעה מקומות.

יצירה ואתחול של 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]. מאז C++11 אפשר גם להשתמש ב-emplace_back(args...) כדי לבנות איבר במקום, מה שחוסך עותק זמני בטיפוסים כבדים יותר.

טעות נפוצה של מתחילים היא לקרוא ל-front() או ל-back() על vector ריק. זו התנהגות לא מוגדרת, לא שגיאה: תמיד הגנו קודם עם if (!v.empty()).

קריאת איברים: [] מול at()

ניגשים לאינדקס ב-vector בדיוק כמו במערך, עם []. אבל [] לא בודק גבולות: אינדקס מחוץ לטווח הוא undefined behavior, שיכול לקרוא זבל בשקט או לקרוס אחר כך במקום מבלבל:

vector<int> v = {1, 2, 3};
cout << v[10];   // התנהגות לא מוגדרת: בלי בדיקה, בלי שגיאה

כשרוצים בטיחות, משתמשים ב-at(). הוא בודק את האינדקס וזורק std::out_of_range בגישה לא תקינה, אחת מהחריגות של הספרייה הסטנדרטית שאפשר לתפוס, כך שמקבלים כישלון ברור במקום השחתה:

כלל אצבע: השתמשו ב-[] בלולאות צפופות שבהן כבר הוכחתם שהאינדקס תקין, וב-at() בגבולות שבהם קלט גרוע יכול לחדור.

לולאה על vector

הדרך הנקייה ביותר לעבור על vector היא לולאת for מבוססת טווח. קחו איברים כ-const auto& כדי לקרוא בלי להעתיק, או כ-auto& כדי לערוך אותם במקום:

אם באמת צריך את האינדקס (למשל כדי להשוות שכנים), השתמשו בלולאת ספירה קלאסית, אבל שימו לב ש-size() מחזיר טיפוס unsigned (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 חוזר הוא זול בממוצע (amortized), אבל כל הקצאה מחדש בפני עצמה לא בחינם:

אם אתם יודעים בערך כמה איברים תוסיפו, קראו קודם ל-reserve() כדי לדלג על ההקצאות החוזרות. שימו לב ש-reserve() משנה את הקיבולת, לא את הגודל: ל-vector עדיין יש אפס איברים עד שתוסיפו אותם.

ההקצאה מחדש הזו היא גם המקור לבאג הכי מרושע של vector. מכיוון שהגדילה מזיזה את האחסון, כל מצביע, הפניה או iterator ששמרתם לתוך ה-vector הופכים לתלויים (dangling) אחרי push_back שמקצה מחדש:

vector<int> v = {1, 2, 3};
int& first = v[0];     // הפניה לתוך ה-vector
v.push_back(4);        // עלול להקצות מחדש...
cout << first;         // תלויה: עלולה להצביע על זיכרון משוחרר

אותו דבר נכון ל-iterators: אל תעשו push_back או erase בזמן מעבר עם iterator שמור. אם חייבים להסיר פריטים תוך כדי לולאה, השתמשו בערך ההחזרה של erase, או ב-erase-remove idiom עם 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 מעביר את האחסון שלו לבלוק חדש, וזה מבטל כל מצביע, הפניה ו-iterator לאיברים הישנים. אל תחזיקו הפניה לאיבר לאורך push_back, וקראו ל-reserve() מראש אם אפשר כדי להימנע מהקצאות מחדש מפתיעות.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל