דוא"ל:
תפריט משתמש




שתף |

קורס:"שפת C"

שיעור 11: ניהול קבצי התוכנה בפרוייקט

[ <<< הקודם ] [ תוכן עניינים ] [ הבא >>> ]

מתוך הספר: C - מדריך מקצועי       C - מדריך מקצועי
תוכן עניינים



חלוקת התוכנה למספר קבצים

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

מודול היא יחידת תוכנה העוברת הידור נפרד - בשפת C זהו בדרך כלל בקובץ עם סיומת ".c" . בשלב ההידור כל מודול מהודר בנפרד, ללא תלות בהידור מודולים אחרים.

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

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

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

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

לדוגמא: תוכנה מכילה שלושה מודולים a, b, ו- prog

קבצי ה- .c מבצעים הכללה (#include) לקבצי ה- .h, וכל אחד מהם עובר הידור נפרד לקובץ אובייקט (.obj). בשלב הסופי מאוגדים קבצי האובייקט לקובץ ביצוע יחיד ע"י קישור (link).

קבצי ממשק וקבצי מימוש

מכיוון שקבצי ה- .h מכילים הכרזות של טיפוסים ופונקציות – ללא הגדרת גוף פונקציה או הגדרת משתנים  - הם נקראים קבצי ממשק.

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

קבצי המקור נקראים גם קבצי המימוש מכיוון שהם מכילים את המימוש של הפונקציות והמשתנים שהוכרזו בקבצי הממשק.

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

שיטה זו מאפשרת לנפות את שגיאות הריצה ביתר קלות הואיל והיא מקטינה את הקשרים בין המודולים למינימום.

חלוקה זו של התוכנה לממשק (Interface) ולמימוש (Implementation) היא חשובה מאוד ובאה לידי ביטוי במרבית שפות וגישות התכנות הקיימות.

בתכנות מונחה עצמים ההבחנה בין ממשק ומימוש היא אף חדה יותר: תכונות המודולריות, הסתרת מידע, טיפוסיות חזקה ופולימורפיזם מחייבות הפרדה בין ממשק המחלקות לבין מימושן. להרחבה, עיין/י בספרים "Java על כוס קפה" ו-"תכנות מונחה עצמים ב- C++" בהוצאה זו.

הכללת קבצי ממשק

הכללת קבצי הממשק(סיומת .h) מבוצעת בחלקו הראשון של שלב ההידור ע"י הקדם-מעבד (pre-processor). הוראות אלו מתחילות כולן בפקודה #include.

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

כאשר רוצים לשתף טיפוס חדש, פונקציה או משתנה בין מספר מודולים, מצהירים עליהם בקובץ ממשק (.h) ומבצעים הכללה (#include) בכל אחד מהמודולים.

הערה : הגדרת הפונקציות או המשתנים חייבת להופיע במודול יחיד!!

דוגמא:

פרישת הקובץ המוכלל בקובץ המקור

מה מבצע המהדר (ליתר דיוק הקדם-מעבד שבמהדר) בהכללת קובץ? הפעולה היא פשוטה מאוד: המהדר פורש את קובץ ה- .h בקובץ שבו בוצע לו #include ובכך למעשה הופך אותם לקובץ יחיד המוכן לשלב ההידור.

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

הערות

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

למעשה הגדרות כפולות (וזהות) אלו חוקיות אפילו אם הן מבוצעות באותו קובץ!

2. תיאור פרישת הקובץ הנ"ל אינו מדויק - הקדם-מעבד עובר על כל ההוראות המתחילות ב- # ומבצע אותן לפני שלב ההידור. לכן, גם הגדרת הקבוע

 
#define NOT_FOUND  -1
 

לא תופיע  -  הקדם-מעבד יחליף כל מופע של הקבוע NOT_FOUND בערך -1.

הכרזה על משתנה גלובלי

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

לדוגמא, נניח שמתוך התכנית הראשית בקובץ prog.c אנו מעוניינים לגשת למשתנה הגלובלי file_name המוגדר ב- b.c:

 
String    file_name;
 

לשם כך, יש להכריז על המשתנה הגלובלי בקובץ b.h כך:

 
extern String  file_name;
 

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

הכללת קבצי .h בקבצי .h

לפעמים קבצי .h מכילים בעצמם הוראות הכללה של קבצי .h אחרים. לדוגמא, אם בקובץ b.h הנ"ל נעשה שימוש בהגדרות הקיימות בקובץ stdio.h, יש לבצע הכללה של הקובץ:

 
b.h
#include <stdio.h>
#define NOT_FOUND  -1
typedef char String[256];
 
typedef struct element
{
          int              key;
          String       str;
} Element;
 
void  insert_element(Element *);
void  del_element(int);
 

נניח שגם הקובץ prog.c משתמש בספריית הקלט / פלט stdio.h. תרשים המודולים ייראה כעת כך:



כלומר, כעת הקובץ stdio.h מוכלל פעמיים במודול prog: פעם אחת כתוצאה מהכללה ישירה שלו ופעם שנייה כתוצאה מהכללת b.h המכליל בעצמו את stdio.h.

מניעת הכללה מרובה

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

 
#ifndef STDIO_H
#define STDIO_H
int printf(...);
int scanf(...);
int getchar();
int putchar(int ch);
...
#endif
 

כדי למנוע הכללה כפולה של stdio.h בודקים בתחילתו אם המאקרו STDIO_H (השרירותי) אינו מוגדר.

בדיקה זו מבוצעת ע"י ההוראה #ifndef: תשובה חיובית (כלומר המאקרו לא מוגדר) גורמת לביצוע כל קטע הקוד שעד ההוראה המסיימת #endif.

לעומת זאת תשובה שלילית (המאקרו מוגדר) גורמת לדילוג על כל קטע הקוד שעד ההוראה המסיימת #endif. במילים אחרות, קוד זה לא ייפרש בקובץ המכליל.

בפעם הראשונה שהקדם-מעבד קורא את הקובץ stdio.h המאקרו STDIO_H עדיין לא מוגדר, ולכן התשובה להוראה

 
#ifndef STDIO_H
 

היא חיובית וגוף הקובץ מבוצע,  כלומר מגדירים את המאקרו

 
#define STDIO_H
 

וגוף הקובץ נפרש בקובץ המכליל:

 
int printf(...);
int scanf(...);
int getchar();
int putchar(int ch);
...
 

בפעמים הבאות שהקדם-מעבד קורא את הקובץ stdio.h המאקרו STDIO_H מוגדר ולכן התשובה להוראה

 
#ifndef STDIO_H
 

היא שלילית - והקובץ לא נפרש שוב.

שלב הקישור

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

בביצוע הרצה סביבת הפיתוח מבצעת את הקישור באופן אוטומטי ע"י מרכיב במהדר הנקרא מקשר (Linker). דוגמאות לסביבות פיתוח:

  • בסביבת הפיתוח  MS-DevStudio, יוצרים פרוייקט ע"י בחירה בתפריט File/New/Projects, ובוחרים בסוג הפרוייקט (בחירה ב- "Win32 Console Application" עבור יישומי DOS).
לאחר מכן יוצרים קבצי .h ו- .c ע"י File/New/files ובוחרים בקובץ מקור .c או בקובץ ממשק .h תוך ציון הוספתו לפרוייקט.
  • בסביבת הפיתוח של Borland, Turbo C++, קיים תפריט Project ליצירת פרוייקט ולהוספת קבצים לתוכו.
את הקבצים יוצרים בנפרד ע"י File/new, עורכים אותם ומוסיפים אותם (רק קבצי מימוש!) לפרוייקט ע"י  Project/Add item.
בדרך כלל, שם קובץ ה- .exe הנוצר הוא כשם קובץ הפרוייקט.
  • בסביבת פיתוח טקסטואליות הוראות ההידור והקישור נכתבות באופן מפורש בשורת הפקודה ע"י המתכנת, או מתוך קובץ script.
לדוגמא, במערכת Unix המהדר מופעל ע"י הפקודה cc :

cc  -c   a.c  b.c  prog.c

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

 
cc  -o   prog   a.o    b.o   prog.o
 

פקודה זו תגרום לקישור המודולים a.o, b.o ו- prog.o לקובץ ביצוע בשם prog.

מה מתבצע בקישור?

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

כמו כן בשלב הקישור מצרף המקשר לתכנית את גוף פונקציות הספרייה שקראנו להן. 

לדוגמא, אם בתכנית קיימות קריאות לפונקציות printf ו- scanf שהממשק שלהן מוגדר ב- stdio.h , המקשר דואג לצרף את גוף הפונקציות בזמן קישור מתוך הספרייה התקנית של שפת C (הנקראת גם ספריית זמן ריצה,  Standard C runtime library).

דוגמא: חלוקת תוכנית לקבצים

נבצע חלוקה של תוכנית תקליטורי המוסיקה למספר קבצים:

    cd_arr.h       הכרזות הקבועים, הטיפוסים והפונקציות של מערך התקליטורים
    cd_arr.c        הגדרות המשתנים והגדרות (גוף) הפונקציות של מערך התקליטורים
    cd_main.c    התכנית המשתמשת: קוראת לפונקציות במערך התקליטורים
    
    

ספריית הפרוייקט וקובץ הפרוייקט ייקראו cd_proj.

קוד התכנית מובא בעמ' 277-282.

הקדם-מעבד (Pre-Processor)

הקדם-מעבד הוא תת-תכנית במהדר העוברת על הקוד שבקובץ ומבצעת את כל ההוראות המתחילות בסימן #.

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

  • הכללות קבצים - הוראת #include
  • הגדרות קבועים - הוראות #define, #undef
  • הגדרות פונקציות מאקרו - הוראת #define עם פרמטרים
  • הידור מותנה - הוראות #if, #ifdef, #ifndef, #elif, #else
  • הוראות נוספות - הוראות #error, #line, #pragma

הכללות קבצים

ראינו שההוראה #include מכלילה את הקובץ ששמו נתון לה כפרמטר בקובץ המהודר.

לדוגמא ההוראה

 
#include <stdio.h>
 

בקובץ prog.c תפרוש בקובץ prog.c את תכולת הקובץ stdio.h בשורת ההוראה. אם stdio.h כולל הוראות #include בעצמו, הן יבוצעו באופן רקורסיבי.

כאשר מבצעים הכללה תוך ציון שם הקובץ בין גרשיים

 
#include "myfile.h"
 

המהדר יחפש את הקובץ בספריית המחדל שלו - כלומר הספרייה הנוכחית שבה נמצא הפרוייקט או הקובץ המהודר.

הגדרות קבועים

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

 
#define NOT_FOUND  -1
 

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

 
#define INUM  454
#define FNUM 66.78f
#define FEXP   2.55E5
#define CH                 'A'
#define HELLO         "hello"
 

כמו כן אפשר להגדיר הוראות שלמות לביצוע ע"י קבוע, לדוגמא:

 
#define CHECK_POINT  printf("CP: line %d, file %s", __LINE__, __FILE__);
 

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

 
          int x, ret;
          printf("Enter an integer:");
          ret = scanf("%d", &x);
          CHECK_POINT
 

ניתן גם להגדיר קבועים ללא ערך, כך שישמשו כדגלים בהידור מותנה (ראה/י להלן):

 
#define FLAG
 

ניתן לבטל קבוע לאחר שהוגדר ע"י ההוראה #undef. לדוגמא, ניתן לבטל את הדגל הקודם ע"י

 
#undef FLAG
 

הגדרת פונקציות מאקרו

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

 
          #define     MAX(a,b)                                    a > b? a : b
          #define     POWER2(a)       a*a
 

וכעת ניתן לקרוא לפונקציות המאקרו כאילו היו פונקציות רגילות:

 
          int x,y, ret;
          printf("\nEnter 2 integers:");
          scanf("%d %d", &x, &y);
          printf("\nThe maximum is %d", MAX(x,y));
          printf("\nx^2 = %d", POWER2(x));
 

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

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

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



הידור מותנה

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

לדוגמא, קובץ ממשק myfile.h יוקף ע"י ההוראות הבאות

 
#ifndef MYFILE_H
#define MYFILE_H
...
#endif
 

בכדי למנוע הכללה מרובה של הקובץ באותה יחידת הידור.

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

לדוגמא:

 
#ifdef MY_DEBUG
void my_debug(char *buff, int size)
{
          ...
}
#endif
 

כעת הידורה של הפונקציה ביחידת ההידור בה היא נמצאת תלויה בהגדרת המאקרו MY_DEBUG: אם הוא הוגדר לפני כן ע"י

 
#define MY_DEBUG
 

אזי קטע הקוד ייכלל ביחידת ההידור. אחרת, הפונקציה my_debug  כלל לא תיפרש בהידור.

מקרואים מוגדרים מראש

עיין/י ברשימת המקרואים המוגדרים מראש שבעמ' 287.



הוראות נוספות

הקדם-מעבד כולל את הוראות נוספות המפורטות בעמ' 288.

סיכום

  • נהוג לחלק תוכניות גדולות למספר קבצים, כאשר כל קובץ מטפל בנושא מסוים בתוכנית. מודולריות היא חלוקה של התוכנה למספר מודולים עפ"י נושאים.
כל מודול כולל קובץ מימוש (.c) וקובץ ממשק (.h).
  • קובץ הממשק כולל הכרזות על קבועים, טיפוסים ואבטיפוסים של פונקציות. קובץ המימוש מכיל הגדרות של משתנים גלובליים ואת גוף הפונקציות.
  • קריאה לפונקציה ממודול אחד לשני מבוצעת ע"י הכללת (#include) קובץ הממשק שלו בשלב ההידור.
בזמן הקישור המקשר (linker) יוצר קובץ ביצוע ( .exe ) יחיד וכל קוד הקבצים משולב לתוכו, תוך פתירת הקריאות בין המודולים.
  • כדי למנוע הכללה מרובה של קבצי הממשק, משתמשים בטכניקה המשלבת הוראות תנאי של הקדם-מעבד : #ifndef, #define, #endif.
  • הקדם-מעבד כולל הוראות מסוגים שונים:
  - הכללת קבצי ממשק
  - הגדרות קבועים
  - הגדרות פונקציות מאקרו
  - הידור מותנה
  - הגדרות על הקובץ המהודר
  - הגדרות תלויות סביבה למהדר

תרגילי סיכום

בצע/י את תרגילי הסיכום שבסוף פרק זה.



[ <<< הקודם ] [ תוכן עניינים ] [ הבא >>> ]