שפת C/מודולים

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


פרק זה לוקה בחסר. אתם מוזמנים לתרום לוויקיספר ולהשלים אותו. ראו פירוט בדף השיחה.



חלוקת הקוד למודולים (קבצי קוד) מסייעת לתחזוקתו ומייעלת את בנייתו לתוכנית.



Edit-undo.svg

שקול לדלג על נושא זה

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



הצורך במודולים[עריכה]

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

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

בפרק זה נלמד כיצד לחלק את הקוד למודולים, או קבצי קוד שכל אחד מהם מכיל את חלקי הקוד המתאימים לנושא אחד בלבד.

דוגמה לחלוקה למודולים[עריכה]

להלן דוגמה רצה, בה נשתמש בפרק זה. הקובץ הבא מראה ארבע פונקציות:

void g1();
 
void f1()
{
  g1()
}
 
void g1()
{
  ...
}
 
void f2()
{
  ...
}
 
void g2()
{
  ...
  g1();
  ...
  f1();
  ...
}

טכנית, הפונקציות קשורות זו בזו כך:

  • f1 משתמשת בg1
  • g1 אינה משתמשת באף פונקציה אחרת
  • f2 אינה משתמשת באף פונקציה אחרת
  • g2 משתמשת בg1 ובf1

בלי קשר, הוחלט שהקובץ מכיל פונקציות משני נושאים שונים, ואפשר (וכדאי) לחלקו לשני קבצים. (כמובן שבמציאות, דוגמה טובה יותר היתה ארוכה פי כמה.) המתכנתים שמו לב שf1 וg1 מטפלות בנושא אחד, וf2 וg2 מטפלות בנושא אחר. הוחלט, לכן, לכתוב שני קבצים, file_1.c וfile_2.c, ולהעביר אליהם את תוכן הקובץ הקודם. כך נראה file_1.c:

void g1();
 
void f1()
{
  g1()
}
 
void g1()
{
  ...
}

וכך נראה file_2.c:

void f2()
{
  ...
}
 
void g2()
{
  ...
  g1();
  ...
  f1();
  ...
}

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

שלבי בניית התוכנית[עריכה]

כדי להבין פרק זה, יש להבין כיצד בונים תוכנית מקוד C. נשתמש בדוגמה לחלוקה למודולים.

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

שלבי בניית התוכנית.
  1. ראשית מהדרים את קבצי המקור לקבצי ביניים:
    • המהדר מהדר את file_1.c לקובץ ביניים file_1.obj.
    • המהדר מהדר את file_2.c לקובץ ביניים file_2.obj.
  2. לאחר מכן מקשרים: המקשר מקשר את שני קבצי הביניים לprogram.out שהוא קובץ תוכנית הניתן להרצה (executable בלעז).


נשים לב למספר נקודות:

  • אם משנים את אחד הקבצים, נניח file_2.c, אז אין צורך להדר מחדש את file_1.c. מספיק להדר מחדש את הקובץ שהשתנה, ולקשר את קבצי הביניים.
  • פעולות ההידור תמיד נעשית לפני פעולת הקישור.

קבצי כותרת[עריכה]

הצורך בקבצי כותרת[עריכה]

נחזור לדוגמה לחלוקה למודולים. לאחר החלוקה לשני קבצים, כך נראה file_2.c:

void f2()
{
  ...
}

void g2()
{
  ...
  g1();
  ...
  f1();
  ...
}

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

g1();
...
f1();

הוא אינו יודע מהן הפונקציות האלה; הן הוגדרו בכלל ב-file_1.c. אמנם נכון שאנו מתכוונים לקשר את קבצי הביניים של שני file_1.c ו-file_2.c, אך כפי שמראה התרשים בשלבי בניית התוכנית, המהדר פועל לפני המקשר, והוא אינו יודע זאת.

מהו קובץ כותרת?[עריכה]

קובץ כותרת בנוי בדרך כלל בצורה הבאה:

#ifndef <preproc_def>
#define <preproc_def>

<declarations>

#endif /* #ifndef <preproc_def> */

כאשר preproc_def הוא קבוע הידור, וdeclarations הם הצהרות.

לדוגמה, file_1.h יכול להיראות כך:

#ifndef FILE_1_H
#define FILE_1_H

void f1();
void g1();

#endif /* #ifndef FILE_1_H */

כאן FILE_1_H הוא קבוע ההידור, וההצהרות הן:

void f1();
void g1();


סימון קטע מותנה לקדם מעבד.

הכלת קבצי כותרת[עריכה]

הכלת קבצים בעזרת הקדם מעבד

#include "file_1.h"

void f2()
{
  ...
}

void g2()
{
  ...
  g1();
  ...
  f1();
  ...
}

צמד הקבצים הקלאסי[עריכה]

file_1.h

#ifndef FILE_1_H
#define FILE_1_H

void f1();
void g1();

#endif /* #ifndef FILE_1_H */

file_1.c

#include "file_1.h"

void f1()
{
  g1()
}

void g1()
{
  ...
}

file_2.h

#ifndef FILE_2_H
#define FILE_2_H

void f2();
void g2();

#endif /* #ifndef FILE_1_H */

file_2.c

#include "file_1.h"
#include "file_2.h"

void f2()
{
  ...
}

void g2()
{
  ...
  g1();
  ...
  f1();
  ...
}

בניית הדוגמה[עריכה]

פרק זה לוקה בחסר. אתם מוזמנים לתרום לוויקיספר ולהשלים אותו. ראו פירוט בדף השיחה.



gcc בלינוקס או Cygwin[עריכה]

ראשית, נהדר את הקבצים: gcc -Wall -c file1.c
gcc -Wall -c file2.c

הפעולה תיצור לנו את הקבצים file1.o ו-file2.o, שהם קבצים מהודרים, אך עדיין אינם קבצי הרצה. כעת נבצע את פעולת הקישור (Linkage): gcc -Wall file1.o file2.o -o program

פקודה זו תיצור קובץ הרצה בשם program. לחילופין, ניתן לבצע את כל הפעולות האלה בבת אחת: gcc -Wall file1.c file2.c -o program

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


כדאי לדעת:

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

סביבת פיתוח בחלונות[עריכה]

פרק זה לוקה בחסר. אתם מוזמנים לתרום לוויקיספר ולהשלים אותו. ראו פירוט בדף השיחה.



גישה למשתנים ממודולים אחרים[עריכה]

משתנים סטטיים[עריכה]

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

(מילת המפתח static משמשת גם לדבר שונה לחלוטין. ראה כאן.)

דוגמה: חזרה לרשימות מקושרות[עריכה]

פרק זה לוקה בחסר. אתם מוזמנים לתרום לוויקיספר ולהשלים אותו. ראו פירוט בדף השיחה.



מעט על מבנים והנדסת תוכנה

list.h

#ifndef LIST_H
#define LIST_H


struct link_
{
  struct link_ *next;
  
  int data;
};


typedef struct link_ link;


struct list_
{
	link *head;
	
	unsigned long size;
};


typedef struct list_ list;


void list_ctor(list *list);
void list_dtor(list *list);
unsigned long list_size(const list *list);
int list_push(list *list, int d);
int list_pop(list *list);
int list_head(const list *list);


#endif /* #ifndef LIST_H */

list.c

#include "list.h"
#include <stddef.h>
#include <malloc.h>


void list_ctor(list *list)
{
  list->head = NULL;
  list->size = 0;
}


void list_dtor(list *list)
{
  link *l = list->head;
  
  while(l != NULL)
  {
  	link *const old = l;
  	
  	l = old->next;
  	
  	free(old);
  }
  
  list_ctor(list);
}


unsigned long list_size(const list *list)
{
  return list->size;
}


int list_push(list *list, int d)
{
  link *const l = (link *)malloc(sizeof(link));
  
  if(l == NULL)
    return -1;
    
  l->data = d;
  l->next = list->head;
  
  list->head = l;
  ++list->size;
    
  return 0;
}


int list_pop(list *list)
{
  link *const l = list->head; 
  const int d = l->data;
  
  list->head = l->next;
  --list->size;
  
  free(l);
	
  return d;
}


int list_head(const list *list)
{
  const link *const l = list->head; 
  return l->data;
}

main.c

#include <stdio.h>
#include "list.h"


int main()
{
  int c;
  list lst;

  list_ctor(&lst);

  do
  {
    int d;
  
    printf("Please enter a number: ");
    scanf("%d", &d);
  
    list_push(&lst, d);

    printf("Please enter 0 to quit, or any other number to continue: ");
    scanf("%d", &c);  
  }
  while(c != 0);

  printf("The numbers you entered, in reverse order, are:\n");
    
  while(list_size(&lst) > 0)
    printf("%d\n", list_pop(&lst));

  list_dtor(&lst);

  return 0;
}


הפרק הקודם:
הקדם מעבד
מודולים הפרק הבא:
שימוש בספריות