מבוא לתכנות ולמדעי המחשב בשפת C/ניהול זיכרון, מצביעים ומבנים

תבנית:ניווט מבוא

מצביעים וניהול זיכרון

עריכה

השיעור נמצא במסמך pdf זה

דוגמה: מערך של מערכים

עריכה

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

#include <stdio.h> 
#include <stdlib.h> 

const int LENGTH = 5; 

int** makeArray() {
    int **arr2d = (int**)malloc(sizeof(int*)*LENGTH),i,j;
    for(i=0; i<LENGTH; i++) {
	arr2d[i] = (int*)malloc(sizeof(int)*(i+1));
	for(j=0; j<=i; j++)
	    arr2d[i][j] = i*j; /* same as: *(*(arr2d+i)+j) = i*j */
    }
    return arr2d;
}

int main() {
    int **ar = makeArray(),i,j;
    for(i=0; i<LENGTH; i++) {
	for(j=0; j<=i; j++)
	    printf("%d ",*(*(ar+i)+j)); /* same as: printf("%d ",ar[i][j]); */
	printf("\n");
    }

    for(i=0; i<LENGTH; ++i)
	free(ar[i]); 

    free(ar);
    return 0;
}

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

struct - מבנה

עריכה

השיעור שבמסמך מכיל תוכן הקשור למבנים, struct-ים ב c.

הנה הסבר מפורט יותר על מבנים ואיך משתמשים בהם.

ב - C ניתן להגדיר טיפוס המורכב ממספר טיפוסים אחרים. טיפוס כזה נקרא struct או מבנה. לדוגמה:

#include <stdio.h>

struct AA {
  int a; 
  char c; 
};

int main() {
  struct AA b; 
  b.a = 14; 
  b.c = 'F'; 

  printf("%d \n",b.a);
  printf("%c \n",b.c); 

  return 0;
}

פלט:

14
F

הטיפוס struct AA מורכב מ int ו char. כל משתנה מסוג struct AA, מכיל גם מספר וגם תו. בדוגמה זו, הגדרנו משתנה מטיפוס זה בשם b ולכן b מכיל מספר ותו. בכדי לפנות לחלק של המספר, כתבנו b.a ובכדי לפנות לחלק של התו, כתבנו b.c. מכנים את a ו c השדות של ה struct ולעיתים גם כ members שלו.

שימו לב שהטיפוס הוא struct AA ולא רק AA.

האיור הבא מראה איך נראה המשתנה b בזיכרון, כאשר הוא מוקצה על המחסנית:

 

מערך של מבנים

עריכה

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

#include <stdio.h>

struct AA {
  int number; 
  char character; 
};

int main() {
  struct AA arr[10]; 
  int i; 
  for(i=0; i<10; ++i) {
    arr[i].number = i;
    arr[i].character = (char) ( (int)'A'+ i);  
  }
  
  for(i=0; i<10; ++i) 
    printf("arr[%d] = <%d,%c> \n", i, arr[i].number, arr[i].character);

  return 0;
}

פלט:

arr[0] = <0,A> 
arr[1] = <1,B> 
arr[2] = <2,C> 
arr[3] = <3,D> 
arr[4] = <4,E> 
arr[5] = <5,F> 
arr[6] = <6,G> 
arr[7] = <7,H> 
arr[8] = <8,I> 
arr[9] = <9,J> 

בדוגמה זו, שינינו את שמות השדות ל number ו character, הגדרנו מערך של struct AA, מילאנו את כל ערכיו ואז הדפסנו את תכולתו.

הנה דוגמה נוספת לשימוש במערך של מבנים. בדוגמה זו אנו שומרים נתונים של אנשים שונים:

#include <stdio.h>
#include <stdlib.h> 

struct Person {
  int id; 
  char *name; 
};

int main() {
  char* names[] = {"Asif","Agam","Tzion","Yasmin","Ela","Maya","Yael"}; 

  int N = sizeof(names)/sizeof(char*); 

  struct Person* persons = (struct Person*) malloc (N*sizeof(struct Person)); 

  int i; 
  for(i=0; i<N; ++i) {
    persons[i].id = 1000+i; 
    persons[i].name = names[i]; 
  }

  for(i=0; i<N; ++i) 
    printf("%s %d\n",persons[i].name, persons[i].id); 

  free(persons); 
    
  return 0;
}

פלט:

Asif 1000
Agam 1001
Tzion 1002
Yasmin 1003
Ela 1004
Maya 1005
Yael 1006

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

העברה של מבנה כפרמטר לפונקציה

עריכה

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

#include <stdio.h>

struct AA {
  int number; 
  char character; 
};

void foo(struct AA param) {
  printf("in foo: %d %c \n",param.number, param.character);
  param.number = 17; 
  param.character = 'B'; 
  printf("in foo: %d %c \n",param.number, param.character);
}

int main() {
  struct AA myStruct; 
  myStruct.number = 1; 
  myStruct.character = 'A'; 

  printf("in main: %d %c \n",myStruct.number, myStruct.character);

  foo(myStruct);

  printf("in main: %d %c \n",myStruct.number, myStruct.character);

  return 0;
}

פלט:

in main: 1 A 
in foo: 1 A 
in foo: 17 B 
in main: 1 A 

כפי שאפשר לראות מהדוגמה, הפרמטר הוא העתק בלתי תלוי במבנה המקורי. שינוי של ההעתק לא גורם לשינוי של המבנה המקורי.

מצביע למבנה

עריכה

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

#include <stdio.h> 
struct AA {
  int number; 
  char character; 
};

int main() {
  struct AA s; 
  
  struct AA *p; 
  
  p = &s; 

  (*p).number = 17; 
  printf("s.number: %d \n",s.number);

  // *p.number = 12; // will not compile because interpreted as:
                     // *(p.number) = 12  

  p->number = 78;   // same as: (*p).number = 78 
  printf("s.number: %d \n",s.number); 
  
  return 0; 
}

פלט:

s.number: 17 
s.number: 78 

כפי שניתן לראות, אפשר לגשת אל שדות המבנה בכתיב מפורש, באופן הבא:

(*p).number

המשתנה המוצבע ע"י p הוא מבנה ו number הוא אחד השדות של אותו מבנה. הסוגריים בכתיב זה הם הכרחיים מכיוון שאחרת הקומפיילר יבין את הביטוי באופן הבא:

*(p.number) = 17

כלומר, כאילו p הוא מבנה בעל שדה בשם number, השדה number הוא מצביע ואנו מעוניינים במה ש number מצביע עליו. הקומפיילר ידווח על שגיאה כי p איננו מבנה.

מכיוון שהכתיב המופרש, עם הסוגריים, קצת מסורבל, C מציעה כתיב מקוצר, שקול:

p->number

זאת הדרך המקובלת לפנות לשדה של מבנה מתוך מצביע למבנה.

מבנים בתוך מבנים

עריכה

השדות של מבנה יכולים להיות בעלי כל טיפוס שהמערכת מכירה, בפרט טיפוס של מבנה שכבר הוגדר קודם. לדוגמה:

#include <stdio.h> 
#include <stdlib.h> 

struct Address {
    char *street; 
    int number; 
    char *town; 
    int zipCode; 
    char *state; 
}; 

void printAddress(struct Address ad) {
    printf("%s %d, %s. Zip: %d, %s\n",ad.street,
	   ad.number,ad.town,ad.zipCode, ad.state);
}

struct Person {
    int id; 
    char *name; 
    struct Address address; 
}; 

void printPerson(struct Person p) {
    printf("%s, id:%d\n",p.name, p.id); 
    printAddress(p.address); 
}


int main() {
    struct Address ad; 
    ad.street = "Hasivim"; 
    ad.number = 8; 
    ad.town = "Petah Tikva"; 
    ad.zipCode = 3156; 
    ad.state = "Israel"; 

    struct Person per; 
    per.name = "Yakir Yakirov"; 
    per.id = 1234567; 
    per.address = ad; 

    printPerson(per); 

    return 0; 
}

פלט:

 
Yakir Yakirov, id:1234567
Hasivim 8, Petah Tikva. Zip: 3156, Israel

typedef היא פקודה ב C המאפשרת לתת שם נרדף לטיפוס קיים. לדוגמה:

#include <stdio.h> 

typedef int MyInt; 

int main() {
  MyInt a; 
  a = 5; 
  printf("%d\n",a); 
  return 0; 
}

בדוגמה זו, MyInt הוא שם נוסף לטיפוס int. המשתנה a בתוכנית הוא למעשה משתנה מסוג int.

התחביר של הפקודה הוא:

typedef <שם חדש לאותו הטיפוס> <טיפוס קיים>

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

typedef int** AOP;  // Array of Pointers  

int main() {
  AOP arr = (AOP) malloc (sizeof(int*) *100); 
  // ..

אנו נשתמש ב typdef על מנת לתת שם מקוצר לטיפוסים של מבנים. לדוגמה:

struct Person {
  int id; 
  char *name; 
}; 

typedef struct Person Person; 

int main() {
  Person per; 
  per.name = "Avi"; 
  
  Person* p = &per; 
  //...

המטרה פשוט להשתמש בשם Person עבור הטיפוס struct Person.

קיימת גם כתיבה מקוצרת לאותה מטרה:

typedef struct Person {
  int id; 
  char *name; 
} Person; 

int main() {
  Person per; 
  per.name = "Avi"; 
  
  Person* p = &per; 
  //...

אנו נשתמש בכתיב זה על מנת להמנע מכתיבת המילה struct שוב ושוב.

תבנית:ניווט מבוא