|
|||||||
קורס:"שפת 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). דוגמאות לסביבות פיתוח:
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_proj.
קוד התכנית מובא בעמ' 277-282. הקדם-מעבד (Pre-Processor)הקדם-מעבד הוא תת-תכנית במהדר העוברת על הקוד שבקובץ ומבצעת את כל ההוראות המתחילות בסימן #. הקדם-מעבד מטפל בהוראות ממספר סוגים:
הכללות קבציםראינו שההוראה #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.
סיכום
תרגילי סיכוםבצע/י את תרגילי הסיכום שבסוף פרק זה.
|