ברוכים הבאים ליחידה "מצביעים וניהול זיכרון" בקורס "מעבדה בתכנות מערכות" (20465). יחידה זו היא אבן יסוד בתכנות בשפת C, ובמיוחד בתכנות מערכות. הבנה מעמיקה של מצביעים וניהול זיכרון דינמי חיונית לכתיבת קוד יעיל, גמיש ובטוח, ומאפשרת לנו לשלוט ישירות באופן שבו התוכנה שלנו מתקשרת עם חומרת המחשב. שימוש נכון במצביעים פותח עולם שלם של מבני נתונים מורכבים ואלגוריתמים מתקדמים, אך שימוש שגוי עלול להוביל לבאגים קשים לאיתור, קריסות מערכת ופרצות אבטחה. שיעור זה יסקור את העקרונות המרכזיים, יציג דוגמאות וידגיש נקודות קריטיות לקראת המבחן.
יסודות המצביעים: הצהרה, אתחול וגישה
מצביע הוא משתנה מיוחד המכיל כתובת זיכרון, במקום ערך ישיר. הוא מאפשר לנו לגשת לנתונים במיקום ספציפי בזיכרון ולבצע עליהם פעולות.
&x יחזיר את הכתובת של המשתנה x.*ptr יחזיר את הערך בכתובת ש-ptr מצביע אליה.הצהרה ואתחול מצביעים
- הצהרה:
type *pointer_name;. הכוכבית מציינת שמדובר במצביע. לדוגמה:int *p;מצהיר על מצביע ל-int. - אתחול: יש לאתחל מצביע לכתובת חוקית לפני השימוש בו.
int x = 10;int *p = &x;// p מצביע כעת ל-x*p = 20;// משנה את הערך של x ל-20
- מצביע NULL: מצביע שאינו מצביע לשום מקום חוקי. חשוב לאתחל מצביעים ל-
NULLאם אין להם כתובת מיידית, ולבדוק אותם לפני dereference.int *p = NULL;if (p != NULL) { *p = 5; }
אריתמטיקת מצביעים ומצביעים מורכבים
אריתמטיקת מצביעים מאפשרת לנו לנוע בין כתובות זיכרון על בסיס גודל הטיפוס אליו המצביע מצביע. זהו כלי חזק לטיפול במערכים ומבני נתונים רציפים.
אריתמטיקת מצביעים
- הוספה/החסרה של מספר שלם:
ptr + nמזיז את המצביעnיחידות בגודל הטיפוס. אםptrמצביע ל-int, אזptr + 1יצביע לכתובת הבאה בזיכרון במרחק שלsizeof(int)בתים.int arr[] = {10, 20, 30};int *p = arr;// p מצביע ל-10p++;// p מצביע כעת ל-20 (התקדם ב-sizeof(int) בתים)printf("%d\n", *p);// ידפיס 20
- החסרת מצביעים:
ptr2 - ptr1מחזיר את מספר האלמנטים בין שתי הכתובות (בתנאי שהם מצביעים לאותו מערך).
מצביעים למערכים ולפונקציות
- מצביעים למערכים: שם של מערך הוא למעשה מצביע קבוע (
const pointer) לאלמנט הראשון שלו.int arr[5];int *p = arr;// p מצביע ל-arr[0]arr[i]שקול ל-*(arr + i)
- מצביעים לפונקציות: מאפשרים להעביר פונקציות כארגומנטים, לאחסן אותן במבני נתונים, ולבצע קריאות חוזרות (callbacks).
- הצהרה:
return_type (*pointer_name)(parameter_list); - דוגמה:
int (*add_func)(int, int); int sum(int a, int b) { return a + b; }add_func = ∑// או פשוטadd_func = sum;int result = add_func(5, 3);// קריאה לפונקציה דרך המצביע
- הצהרה:
arr++ אינו חוקי) או בלבול בין גודל המערך (sizeof(arr)) לגודל המצביע (sizeof(int*)). זכרו, כאשר מערך מועבר לפונקציה, הוא "מתנוון" למצביע לאלמנט הראשון שלו, וגודלו המקורי אובד.הקצאת זיכרון דינמית
הקצאת זיכרון דינמית מאפשרת לתוכנה לבקש זיכרון מהמערכת בזמן ריצה, ולא רק בזמן קומפילציה. זה חיוני כאשר גודל הנתונים אינו ידוע מראש.
malloc (Memory Allocation)
void* malloc(size_t size);
מקצה בלוק זיכרון בגודל size בתים. מחזיר מצביע ל-void (יש לבצע cast לטיפוס הרצוי) או NULL במקרה של כשל בהקצאה. התוכן של הזיכרון המוקצה אינו מאותחל.
דוגמה: int *arr = (int*)malloc(5 * sizeof(int));
calloc (Contiguous Allocation)
void* calloc(size_t num, size_t size);
מקצה בלוק זיכרון עבור num אלמנטים, כל אחד בגודל size בתים, ומאתחל את כל הביטים ל-0. מחזיר מצביע ל-void או NULL. שימושי למערכים שצריכים להיות מאופסים.
דוגמה: int *arr = (int*)calloc(5, sizeof(int));
realloc (Re-allocation)
void* realloc(void* ptr, size_t new_size);
משנה את הגודל של בלוק זיכרון שהוקצה בעבר (באמצעות malloc או calloc) ל-new_size בתים. אם אין מספיק מקום במיקום הנוכחי, הוא יכול להעתיק את התוכן למיקום חדש. מחזיר מצביע לבלוק החדש או NULL במקרה של כשל (הבלוק המקורי נשאר ללא שינוי). אם ptr הוא NULL, מתנהג כמו malloc. אם new_size הוא 0, מתנהג כמו free.
דוגמה: arr = (int*)realloc(arr, 10 * sizeof(int));
free (Free Memory)
void free(void* ptr);
משחרר בלוק זיכרון שהוקצה דינמית בחזרה למערכת ההפעלה. חיוני למניעת דליפות זיכרון. לאחר קריאה ל-free(ptr), המצביע ptr הופך למצביע תלוי (dangling pointer) ויש לאפס אותו ל-NULL כדי למנוע גישה לא חוקית.
דוגמה: free(arr); arr = NULL;
שאלות לדיון
- הסבר מדוע הקצאת זיכרון דינמית חיונית בתכנות מערכות, ותאר מקרה שימוש קונקרטי שבו לא ניתן להסתפק בהקצאה סטטית.
- מה ההבדל המהותי בין
mallocל-calloc? מתי תעדיף להשתמש באחד על פני השני? - תאר את הסכנות הכרוכות בשימוש במצביעים, כגון "דליפת זיכרון" ו"מצביעים תלויים" (Dangling Pointers). כיצד ניתן למנוע אותן?
- הסבר את הקשר בין שם של מערך (לדוגמה
int arr[10];) לבין מצביע. אילו פעולות מותרות ואסורות על שם המערך בהקשר זה?
נקודות לתשובת מודל
- חשיבות הקצאה דינמית: מאפשרת לנהל זיכרון בגודל משתנה בזמן ריצה, כאשר גודל הנתונים אינו ידוע מראש (למשל, קריאת קלט משתמש באורך לא ידוע, בניית רשימה מקושרת). מקרה שימוש: בניית מערך בגודל שנקבע על ידי המשתמש בזמן ריצה.
- malloc vs. calloc:
malloc(size): מקצהsizeבתים, התוכן אינו מאותחל (מכיל "זבל").calloc(num_elements, element_size): מקצהnum_elements * element_sizeבתים ומאתחל את כולם ל-0.- העדפה:
callocעדיף כאשר נדרש איפוס (למשל, מוני אובייקטים, דגלים),mallocעדיף כאשר יעילות קריטית ואין צורך באיפוס מיידי.
- סכנות ומניעה:
- דליפת זיכרון: אי-שחרור זיכרון שהוקצה דינמית באמצעות
free. מניעה: הקפדה על קריאה ל-freeעבור כל הקצאה מוצלחת, במיוחד בטיפול בשגיאות או יציאה מפונקציות. - מצביע תלוי: מצביע המצביע לזיכרון שכבר שוחרר (
free). גישה לזיכרון זה היא התנהגות בלתי מוגדרת. מניעה: איפוס מצביעים ל-NULLמיד לאחר קריאה ל-free. - גישה ל-NULL: ניסיון לבצע dereference למצביע
NULL. מניעה: בדיקתNULLלפני כל פעולת dereference.
- דליפת זיכרון: אי-שחרור זיכרון שהוקצה דינמית באמצעות
- מערכים ומצביעים: שם המערך (לדוגמה
arr) הוא מצביע קבוע (const pointer) לכתובת הזיכרון של האלמנט הראשון שלו (&arr[0]).- מותר: גישה לאלמנטים באמצעות אינדקס (
arr[i]) או אריתמטיקת מצביעים (*(arr + i)), העברת המערך לפונקציה (שם המערך "מתנוון" למצביע). - אסור: שינוי כתובת המערך עצמו (לדוגמה,
arr++אוarr = some_other_address;), מכיוון שהוא קבוע.
- מותר: גישה לאלמנטים באמצעות אינדקס (