Menu

מערכים ב-C++: הצהרה, אינדקסים ומלכודות נפוצות

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

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

שורה של ערכים בגודל קבוע

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

מצהירים על מערך על ידי ציון טיפוס האיברים, שם וגודל בסוגריים מרובעים. מוסיפים רשימה בסוגריים מסולסלים כדי למלא אותו:

האינדקסים מתחילים מאפס: האיבר הראשון הוא scores[0], ולמערך בגודל 4 יש אינדקסים תקינים מ-0 עד 3. הגודל חייב להיות קבוע שידוע בזמן הידור: אי אפשר לכתוב int n = readInput(); int a[n]; ב-C++ סטנדרטית (זו הרחבה לא ניידת). כשצריך גודל שנקבע בזמן ריצה, השתמשו ב-vector במקום.

אתחול מערכים

יש כמה דרכים למלא מערך, וכמה קיצורי דרך שכדאי להכיר:

הנקודה שכדאי להפנים: כשמספקים פחות ערכי אתחול מהגודל, השאריות עוברות אתחול ערך (אפס לטיפוסים מספריים), ולא נשארות זבל. אבל מערך בלי שום אתחול, int e[4]; שמוצהר כמשתנה מקומי, מכיל ערכים לא מוגדרים, וקריאה שלהם לפני השמה היא undefined behavior.

מעבר על מערך בלולאה

מכיוון שהאיברים מאוחסנים ברצף, עוברים על מערך בלולאה פשוטה עם אינדקס. כדי להישאר בגבולות, הריצו את הלולאה לפי האורך האמיתי של המערך ולא לפי מספר שכתוב קשיח בקוד:

sizeof(scores) הוא מספר הבתים הכולל של המערך כולו; חלוקה ב-sizeof(scores[0]) (הגודל של איבר אחד) נותנת את מספר האיברים. מאז C++17 יש כתיב נקי יותר, std::size(scores), שנקרא טוב יותר ומסרב להתהדר אם בטעות מעבירים לו מצביע. ופשוט עוד יותר כשצריך רק את הערכים: for מבוסס טווח חוסך לגמרי את החישובים עם האינדקס.

המלכודת של חריגה מהגבולות

C++ לא בודקת גבולות ב-arr[i]. גישה לאינדקס שאחרי האיבר האחרון לא זורקת חריגה ולא מזהירה: היא קוראת או כותבת את מה שנמצא במקרה בזיכרון שם. זה הבאג הנפוץ ביותר במערכים, ו-undefined behavior קלאסי:

int a[3] = {1, 2, 3};
a[3] = 99;          // OOPS - valid indices are 0..2, not 3
cout << a[5];       // garbage, crash, or corruption - undefined behavior

טעות ה-off-by-one מסתתרת בדרך כלל בתנאי של הלולאה. כתיבת i <= n במקום i < n מריצה צעד אחד יותר מדי ונוגעת ב-arr[n], שלא קיים:

for (int i = 0; i <= n; i++)   // BUG: when i == n, arr[i] is out of bounds
    cout << arr[i];

התיקון הוא המשמעת מהסעיף הקודם: לולאה עם i < size, אף פעם לא <=, וחישוב הגודל מתוך המערך במקום להקליד שוב מספר שמתיישן כשמוסיפים איבר.

Decay של מערך: המצביע הנסתר

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

שימו לב ש-int arr[] ו-int* arr זהים לגמרי כפרמטרים של פונקציה: הסוגריים הם קוסמטיים בלבד. מכיוון שהספירה אבדה, אתם חייבים להעביר את האורך יחד עם המערך בעצמכם:

int sum(const int* arr, int n) {
    int total = 0;
    for (int i = 0; i < n; i++) total += arr[i];
    return total;
}

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

כמה מילים על מערכים רב-ממדיים

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

ניגשים אליו כ-grid[row][col]. אותן הסתייגויות לגבי גבולות ו-decay תקפות, ואפילו חמורות יותר, כי העברת מערך דו-ממדי לפונקציה דורשת לפרט כל ממד חוץ מהראשון (void f(int g[][3])). לכל דבר מעבר לרשת קטנה וקבועה, vector של vectorים הרבה פחות מועד לטעויות.

הבא בתור: Vector

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

שאלות נפוצות

איך מצהירים על מערך ומאתחלים אותו ב-C++?

כותבים את טיפוס האיברים, שם, ואת הגודל בסוגריים מרובעים, ואז אפשר להוסיף רשימה בסוגריים מסולסלים: int scores[4] = {90, 75, 100, 60};. אפשר להשמיט את הגודל כשמספקים ערכי אתחול: int scores[] = {90, 75, 100, 60}; נותן למהדר לספור אותם בשבילכם.

איך מקבלים את האורך של מערך ב-C++?

למערך אמיתי שעדיין בטווח ההכרזה, השתמשו ב-std::size(arr) (C++17) או ב-sizeof(arr) / sizeof(arr[0]). זה לא עובד אחרי שהמערך עבר decay למצביע (למשל בתוך פונקציה שקיבלה int arr[]), שם sizeof נותן את הגודל של המצביע ולא של המערך.

מה קורה אם ניגשים למערך מחוץ לגבולות ב-C++?

זה undefined behavior. C++ לא בודקת גבולות ב-arr[i], ולכן קריאה או כתיבה מעבר לסוף עלולות לקרוס, להחזיר זבל או להשחית בשקט זיכרון סמוך. תמיד שמרו על האינדקס בטווח 0 עד size - 1.

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

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

להתחיל