ברוכים הבאים ליחידת הלימוד בנושא מערכים ומחרוזות בקורס "מעבדה בתכנות מערכות" (20465). יחידה זו היא אבן יסוד בתכנות מערכות ב-C, שכן היא עוסקת בטיפול באוספי נתונים ובמחרוזות תווים – מבנים בסיסיים וחיוניים לכל מתכנת. נלמד כיצד להצהיר, לאתחל ולגשת לאלמנטים במערכים, נבין את המעמד המיוחד של מחרוזות כמערכי תווים, ונסקור פונקציות ספריה חשובות לטיפול במחרוזות. שליטה בחומר זה קריטית להבנה מעמיקה של זיכרון, מצביעים, ולכתיבת קוד C יעיל ובטטוח.
מערכים ב-C: יסודות הטיפול באוספי נתונים
מערך (Array) ב-C הוא אוסף של משתנים מאותו טיפוס נתונים, המאוחסנים בזיכרון באופן רציף. גישה לאלמנטים במערך מתבצעת באמצעות אינדקס (מיקום), כאשר האינדקס הראשון הוא 0.
הצהרה ואתחול מערכים
כדי להצהיר על מערך, יש לציין את טיפוס הנתונים של האלמנטים, את שם המערך ואת גודלו בסוגריים מרובעים:
int numbers[10]; // מערך של 10 מספרים שלמים
double grades[5]; // מערך של 5 מספרים עשרוניים
ניתן לאתחל מערך בזמן ההצהרה, או להקצות ערכים לאלמנטים בודדים לאחר מכן.
- אתחול מלא:
int arr1[] = {10, 20, 30, 40, 50}; // גודל המערך נקבע אוטומטית (5) int arr2[3] = {1, 2, 3}; // מערך בגודל 3 עם ערכים 1, 2, 3 - אתחול חלקי:
int arr3[5] = {10, 20}; // האלמנטים הנותרים (arr3[2], arr3[3], arr3[4]) יאותחלו ל-0 - מערכים לא מאותחלים:
מערך מקומי (local array) שלא אותחל יכיל "זבל" (garbage values). מערך גלובלי או סטטי שלא אותחל יאותחל אוטומטית ל-0.
גישה לאלמנטים במערך
גישה לאלמנט ספציפי במערך מתבצעת באמצעות שם המערך והאינדקס שלו (החל מ-0):
int myArr[4] = {100, 200, 300, 400};
int firstElement = myArr[0]; // firstElement יכיל 100
myArr[2] = 350; // האלמנט השלישי במערך ישונה ל-350
myArr[4] במערך בגודל 4) תוביל להתנהגות בלתי מוגדרת (Undefined Behavior), שעלולה לגרום לקריסת התוכנית, שחיתות נתונים, או אף פרצות אבטחה. זוהי אחת הטעויות הנפוצות והמסוכנות ביותר בתכנות C.
מחרוזות ב-C: מערכי תווים עם סיום מיוחד
ב-C, מחרוזת (String) היא למעשה מערך של תווים (char) המסתיימת בתו מיוחד, תו ה-Null (\0). תו זה משמש כסמן לסוף המחרוזת ומאפשר לפונקציות ספריה לדעת היכן המחרוזת נגמרת.
הצהרה ואתחול מחרוזות
ניתן להצהיר ולאתחל מחרוזות במספר דרכים:
- באמצעות ליטרל מחרוזת: הדרך הנפוצה ביותר. הקומפיילר מוסיף אוטומטית את תו ה-Null.
char greeting[] = "Hello"; // המערך יהיה בגודל 6 ('H', 'e', 'l', 'l', 'o', '\0') char name[20] = "Alice"; // המערך בגודל 20, "Alice" מאותחלת, השאר \0 - באמצעות אתחול ידני של תווים:
char city[5] = {'T', 'e', 'l', 'A', '\0'}; // חובה להוסיף את \0 ידנית! - הערה: הצהרה כמו
char *str = "Hello";יוצרת מצביע למחרוזת קבועה בזיכרון (read-only), ולא מערך שניתן לשנות. נדון בכך בהרחבה ביחידת המצביעים.
פונקציות ספריה למחרוזות (string.h)
ספריית <string.h> מספקת מגוון פונקציות שימושיות לטיפול במחרוזות. חשוב להכיר אותן ולהבין את אופן פעולתן, במיוחד בהקשר של ניהול זיכרון ובטיחות.
strlen()
מחזירה את אורך המחרוזת (מספר התווים) ללא ספירת תו ה-Null (\0).
char s[] = "World";
size_t len = strlen(s); // len יהיה 5
strcpy()
מעתיקה מחרוזת מקור למחרוזת יעד. מסוכנת! אינה בודקת את גודל היעד, עלולה לגרום ל-Buffer Overflow.
char dest[10];
char src[] = "Hello";
strcpy(dest, src); // dest יכיל "Hello"
strncpy()
מעתיקה עד n תווים ממחרוזת מקור למחרוזת יעד. בטוחה יותר אך דורשת זהירות: אם המקור ארוך מ-n, היעד לא יסתיים ב-Null. אם המקור קצר מ-n, השאר ימולא ב-Null.
char dest[5];
char src[] = "LongString";
strncpy(dest, src, 4); // dest יכיל "Long" (ללא \0!)
dest[4] = '\0'; // חובה להוסיף ידנית
strcat()
משרשרת (מדביקה) מחרוזת מקור לסוף מחרוזת יעד. מסוכנת! אינה בודקת את גודל היעד, עלולה לגרום ל-Buffer Overflow.
char s1[20] = "Hello ";
char s2[] = "World";
strcat(s1, s2); // s1 יכיל "Hello World"
strcmp()
משווה שתי מחרוזות מבחינה לקסיקוגרפית (לפי סדר אלפביתי). מחזירה 0 אם שוות, ערך שלילי אם הראשונה קטנה, ערך חיובי אם הראשונה גדולה.
strcmp("apple", "banana"); // < 0
strcmp("apple", "apple"); // = 0
strcmp("banana", "apple"); // > 0
strncmp()
משווה עד n תווים משתי מחרוזות. שימושית להשוואה חלקית או בטוחה יותר.
strncmp("testing", "test", 4); // = 0
בעת שימוש בפונקציות אלו, ובמיוחד אלו שמעתיקות או משרשרות, יש לוודא תמיד שלמערך היעד יש מספיק מקום כדי למנוע גלישת חוצץ (Buffer Overflow).
שאלות לדיון
- מדוע C אינה מבצעת בדיקת גבולות למערכים בזמן ריצה, ומהן ההשלכות האפשריות של גישה מחוץ לגבולות המערך?
- הסבירו את ההבדל המהותי בין מערך תווים רגיל (
char arr[SIZE]) לבין מחרוזת C-style (char str[] = "..."). - מתי תעדיפו להשתמש בפונקציה
strncpy()על פניstrcpy(), ואילו אמצעי זהירות עליכם לנקוט בעת השימוש בה? - תארו תרחיש שבו שימוש ב-
scanf("%s", buffer)עלול להוביל לפרצת אבטחה, והציעו דרך בטוחה יותר לקלוט מחרוזות מהמשתמש.
נקודות לתשובת מודל
- C תוכננה לביצועים וגישה נמוכה לחומרה; בדיקות גבולות בזמן ריצה היו פוגעות בביצועים. ההשלכות כוללות קריסות תוכנה, שחיתות נתונים, ופרצות אבטחה (כגון הרצת קוד זדוני).
- מחרוזת C-style היא מערך תווים המסתיים בתו Null (
\0). תו זה הוא קריטי ומסמן את סוף המחרוזת עבור פונקציות ספריה. מערך תווים רגיל אינו מחויב לתו Null ואינו נחשב למחרוזת אלא אם כן המתכנת דואג לכך. strncpy()עדיפה כאשר אורך מחרוזת המקור אינו ידוע או עלול להיות גדול מיכולת הקיבולת של היעד, כדי למנוע גלישת חוצץ. אמצעי זהירות: יש לוודא שהיעד תמיד מסתיים ב-Null באופן ידני, במיוחד אם המקור ארוך מ-n, על ידי הצבת\0ב-dest[n-1]אוdest[sizeof(dest)-1].scanf("%s", buffer)קוראת תווים מהקלט עד לרווח לבן או סוף קלט, ואינה בודקת את גודלbuffer. אם הקלט ארוך מהגודל שהוקצה ל-buffer, תתרחש גלישת חוצץ. דרך בטוחה יותר: להגביל את מספר התווים שנקראים (לדוגמה:scanf("%9s", buffer)עבורbufferבגודל 10), או להשתמש ב-fgets(buffer, sizeof(buffer), stdin).