Download presentation
Presentation is loading. Please wait.
Published byIda Susman Modified over 6 years ago
1
ADT גנריים הידור של מספר קבצים Makefile שאלה לדוגמה
תרגול מס' 6 ADT גנריים הידור של מספר קבצים Makefile שאלה לדוגמה
2
מבני נתונים גנריים מחסנית גנרית שימוש במחסנית הגנרית
ADT גנריים מבני נתונים גנריים מחסנית גנרית שימוש במחסנית הגנרית מבוא לתכנות מערכות
3
מבני נתונים גנריים המחסנית שלנו מתאימה רק למספרים שלמים
בדרך כלל נשתמש בטיפוסים מורכבים יותר נצטרך לשכפל את המחסנית לכל טיפוס נכתוב את המחסנית מחדש כמבנה נתונים גנרי המסוגל להחזיק עצמים מכל סוג אלו תכונות של העצמים נצטרך כדי לשמור אותם במחסנית? כיצד ניתן לספק תכונות אלו למחסנית מבלי לפגוע בגנריות? במקרה של המחסנית דרושות לה שתי תכונות בלבד על העצמים: היכולת להעתיק אותם והיכולת לשחרר אותם את התכונות האלה נספק בעזרת מצביעים לפונקציות ב-C מבוא לתכנות מערכות
4
מחסנית גנרית - stack.h typedef כדי להקל על המשתמש במבנה
#ifndef _STACK_H #define _STACK_H /** generic ADT of Stack of integers */ typedef struct stack_t* Stack; typedef void* Element; typedef Element (*CopyFunction)(Element); typedef void (*FreeFunction)(Element); /** possible return values */ typedef enum { STACK_SUCCESS, STACK_BAD_ARGUMENT, STACK_OPERATION_FAIL, STACK_EMPTY, STACK_FULL } StackResult; typedef כדי להקל על המשתמש במבנה קוד שגיאה להתמודדות עם שגיאות בפונקציות הנשלחות ע"י המשתמש מבוא לתכנות מערכות
5
מחסנית גנרית - stack.h /** creates a Stack with maximal capacity of 'maxSize'. if fails, returns NULL */ Stack stackCreate(int maxSize, CopyFunction copyFunction, FreeFunction freeFunction); /** releases the memory allocated for the stack */ void stackDestroy(Stack stack); /** inserts an element to the top of the stack. Error Codes: STACK_BAD_ARGUMENT if stack is NULL STACK_FULL if the stack is full and STACK_OPERATION_ FAIL if the supplied copy function fails. */ StackResult stackPush(Stack stack, Element element); מה שהתחדש פה זה שפונקציית הstackCreate מקבלת את המצביעים לפונקציות מבוא לתכנות מערכות
6
מחסנית גנרית - stack.h /** removes the element at the top of the stack. Error codes: STACK_BAD_ARGUMENT if stack is NULL STACK_EMPTY if the stack is empty */ StackResult stackPop(Stack stack); /** writes to 'element' a copy of the last element that was pushed Error codes: STACK_BAD_ARGUMENT if stack or number are NULL STACK_EMPTY if the stack is empty and STACK_FAIL_OPERATION if the supplied copy function fails */ StackResult stackTop(Stack stack, Element* element); מבוא לתכנות מערכות
7
ADT מחסנית - stack.h /** returns a flag indicating whether the stack is full (meaning elements cannot be pushed) stack must not be NULL */ bool stackIsFull(Stack stack); /** returns a flag indicating whether the stack is empty (meaning elements cannot be popped) bool stackIsEmpty(Stack stack); /** returns the number of elements in the stack. int stackSize(Stack stack); #endif מבוא לתכנות מערכות
8
פתרון הבעיה בעזרת מחסנית גנרית
#include <stdio.h> #include <assert.h> #include <stdlib.h> #include "stack.h" #define MAX_INPUT_SIZE 10 #define UNDO_LAST_COMMAND -1 /* functions that will be used by the stack */ Element copyInt(Element element) { if (element == NULL) { return NULL; } int* newInt = malloc(sizeof(int)); if (newInt == NULL) { *newInt = *(int*)element; return newInt; void freeInt(Element element) { free(element); מבוא לתכנות מערכות
9
פתרון הבעיה בעזרת מחסנית גנרית
int main() { Stack stack = stackCreate(MAX_INPUT_SIZE, copyInt, freeInt); if (stack == NULL) { fprintf(stderr, "failed to create stack\n"); return -1; } int input; while (!stackIsFull (stack) && scanf("%d", &input) == 1) { if (input != UNDO_LAST_COMMAND) { StackResult result = stackPush(stack, &input); assert(result == STACK_SUCCESS); continue; StackResult result = stackPop(stack); if (result == STACK_EMPTY) { printf("Cannot undo\n"); } else { printf("undo\n"); מבוא לתכנות מערכות
10
פתרון הבעיה בעזרת מחסנית גנרית
while (!stackEmpty(stack)) { int* number = NULL; StackResult result = stackTop(stack, (Element*)&number); StackResult result2 = stackPop(stack); assert(result == STACK_SUCCESS && result2 == STACK_SUCCESS); printf("%d\n", *number); freeInt(number); } stackDestroy(stack); return 0; חייבים לבצע המרה של המצביע &number שהוא מטיפוס int** לטיפוס void**, מאחר ששפת c תומכת בהמרה אוטומטית של מצביעים רק מvoid* או אל void*. מבוא לתכנות מערכות
11
שימוש בגנריות נניח שהפעם אנחנו רוצים לקלוט מהקלט מחרוזות שמייצגות פקודות גודל מחרוזת מקסימלי הוא 80 בסוף קליטת הפקודות התוכנית תדפיס את הפקודות בסדר הפוך אחת הפקודות יכולה להיות “UNDO” - היא מבטלת קליטת פקודה קודמת פקודת UNDO אינה נקלטת ואינה מודפסת בסוף התוכנית מבוא לתכנות מערכות
12
דוגמת הרצה > MOVE_UP > MOVE_RIGHT > UNDO undo > MOVE_LEFT > MOVE_DOWN MOVE_DOWN MOVE_LEFT MOVE_RIGHT MOVE_UP מבוא לתכנות מערכות
13
שימוש במחסנית גנרית #include <stdio.h> #include <assert.h> #include <stdlib.h> #include <string.h> #include "stack.h" #define MAX_INPUT_SIZE 100 #define UNDO_COMMAND "UNDO" #define MAX_COMMAND_SIZE 80 /* functions that will be used by the stack */ Element copyString(Element element) { if (element == NULL) { return NULL; } char* newString = malloc (strlen(element) + 1); return newString ? strcpy(newString, element) : NULL; } void freeString(Element element) { free (element); } מבוא לתכנות מערכות
14
שימוש במחסנית גנרית int main() { Stack stack = stackCreate(MAX_INPUT_SIZE, copyString, freeString); if (stack == NULL) { fprintf(stderr, "failed to create stack\n"); return -1; } char input[MAX_COMMAND_SIZE] = ""; while (!stackIsFull(stack) && scanf("%s", input) == 1) { if (strcmp(input,UNDO_COMMAND) != 0) { StackResult result = stackPush(stack, input); assert(result == STACK_SUCCESS); continue; } StackResult result = stackPop(stack); if (result == STACK_EMPTY) { printf("Cannot undo\n"); } else { assert(result == STACK_SUCCESS); printf("undo\n"); } } מבוא לתכנות מערכות
15
שימוש במחסנית גנרית while (!stackIsEmpty(stack)) { char* command = NULL; StackResult result = stackTop(stack, (Element*)&command); StackResult result2 = stackPop(stack); assert(result == STACK_SUCCESS && result2 == STACK_SUCCESS); printf("%s\n", command); freeString(command); } stackDestroy(stack); return 0; מבוא לתכנות מערכות
16
מימוש המחסנית גנרית - stack.c
#include <stdlib.h> #include <assert.h> #include "stack.h" /** The Stack is implemented as an array of Elements. * With nextIndex as an index to the next available position and * maximal size stored in maxsize. */ struct stack_t { Element* array; int nextIndex; int maxSize; CopyFunction copyElement; FreeFunction freeElement; }; מבוא לתכנות מערכות
17
מימוש המחסנית גנרית - stack.c
Stack stackCreate(int maxSize, CopyFunction copyFunction, FreeFunction freeFunction) { if (maxSize <= 0 || !copyFunction || !freeFunction) { return NULL; } Stack stack = malloc(sizeof(*stack)); if (stack == NULL) { return NULL; } stack->array = malloc(sizeof(Element) * maxSize); if (stack->array == NULL) { free(stack); return NULL; } stack->nextIndex = 0; stack->maxSize = maxSize; stack->copyElement = copyFunction; stack->freeElement = freeFunction; return stack; } מבוא לתכנות מערכות
18
מימוש המחסנית גנרית - stack.c
StackResult stackPush(Stack stack, Element element) { if (stack == NULL) { return STACK_BAD_ARGUMENT; } if (stack->nextIndex >= stack->maxSize) { return STACK_FULL; Element newElement = stack->copyElement(element); if (newElement == NULL) { return STACK_OPERATION_FAIL; assert(stack->nextIndex >= 0 && stack->nextIndex < stack->maxSize); stack->array[stack->nextIndex++] = newElement; return STACK_SUCCESS; מבוא לתכנות מערכות
19
מימוש המחסנית גנרית - stack.c
StackResult stackPop(Stack stack) { if (stack == NULL) { return STACK_BAD_ARGUMENT; } if (stack->nextIndex < 1) { return STACK_EMPTY; assert(stack->nextIndex > 0 && stack->nextIndex <= stack->maxSize); stack->freeElement(stack->array[stack->nextIndex - 1]); stack->nextIndex--; return STACK_SUCCESS; מבוא לתכנות מערכות
20
מימוש המחסנית גנרית - stack.c
StackResult stackTop(Stack stack, Element* element) { if (stack == NULL || element == NULL) { return STACK_BAD_ARGUMENT; } if (stack->nextIndex < 1) { return STACK_EMPTY; assert(stack->nextIndex > 0 && stack->nextIndex <= stack->maxSize); Element copy = stack->copyElement(stack->array[stack->nextIndex - 1]); if (copy == NULL) { return STACK_OPERATION_FAIL; *element = copy; return STACK_SUCCESS; כדי לשמור על נכונות ה-ADT חשוב לא להחזיר מצביעים לשדות פנימיים. מצביעים אלו יאפשרו למשתמש לשבור את המבנה מבפנים. לכן קיימות מספר אפשרויות: להחזיר מצביע לאיבר פנימי במקרים בהם זה הכרחי (למשל במקרה ורוצים לאפשר למשתמש לשנות איברים שכבר שומרים במבנה הנתונים) ולהקפיד בקוד שהמשתמש לא יוכל "לשבור" את שאר הקוד בטעות. להחזיר עותק להחזיר תוך שימוש במילה const, אך פתרון זה עובד בצורה טובה רק ב-C++, ולכן לא נהוג ב-C. להחזרת העתק יתרון נוסף - ניהול הזיכרון ע"י ה-ADT שלנו שומר על קוד יותר קוהרנטי אם הזיכרון היה מנוהל לא על ידי ה-ADT עצמו אז הקוד המקצה עצם כלשהו הנכנס למבנה הנתונים היה נפרד מהקוד בו משחררים את העצם, ומנוהל למעשה על ידי המשתמש. קוד כזה מקשה על הבנתו - בעיקר מתי יש לשחרר עצם (במיוחד במקרה של שגיאות) וכן עלול לגרום לבאגים ודליפות זיכרון בסיכוי גבוה יותר. למה יוצרים העתק של העצם המוחזר? מבוא לתכנות מערכות
21
מימוש המחסנית גנרית - stack.c
int stackSize(Stack stack) { assert(stack); return stack->nextIndex; } bool stackIsEmpty(Stack stack) { return stackSize(stack) == 0; bool stackIsFull(Stack stack) { return stackSize(stack) == stack->maxSize; מבוא לתכנות מערכות
22
מימוש המחסנית גנרית - stack.c
void stackDestroy(Stack stack) { if (stack == NULL) { return; } while (!stackIsEmpty(stack)) { StackResult result = stackPop(stack); assert(result == STACK_SUCCESS); free(stack->array); free(stack); מבוא לתכנות מערכות
23
ADT גנריים - סיכום ניתן ליצור מבני נתונים גנריים המסוגלים לשמור כל סוג של עצמים אם כי כל העצמים במבנה מסוים חייבים להיות מאותו הסוג כדי לאפשר למבני נתונים גנריים לעבוד עם סוג מסוים של עצמים, יש לספק להם מצביעים לפונקציות לביצוע הפעולות הבסיסיות שימוש במבני נתונים גנריים מאפשר שימוש חוזר במבנה עבור טיפוסים שונים ומונע שכפול קוד מבוא לתכנות מערכות
24
שלבי ההידור עיבוד מקדים הידור קישור פונקציות סטטיות
הידור של מספר קבצים שלבי ההידור עיבוד מקדים הידור קישור פונקציות סטטיות מבוא לתכנות מערכות
25
הידור של מספר קבצים לא נוח לשמור את כל הקוד בקובץ יחיד עבור תוכנה גדולה ניתן לחלק את הקוד למספר קבצים, לקמפל כל קובץ בנפרד, ולקשר את כל הקבצים לקובץ הרצה יחיד בסוף התהליך כדי להדר מספר קבצים ב-gcc ניתן פשוט לרשום את כל הקבצים כפרמטרים לשורת הפקודה של gcc: > gcc a.c b.c mytest*.c עבודה בקובץ אחד מקשה על התמצאות בתוכנית גדולה הכוללת מגוון רב של הגדרות טיפוסים, מבני נתונים, ואלגוריתמים שונים עבודה בקובץ אחד בייחוד לא פרקטית כאשר מספר אנשים עובדים על פרויקט במשותף מודולים שעשויים לשמש תוכניות נוספות בעתיד נרצה שייכתבו בקבצים נפרדים, כך שיהיה קל להעביר אותם לפרויקטים הבאים כמו כן, הפרדה לקבצים מאפשרת קומפילציה סלקטיבית –קומפילציה רק של קבצי הקוד שהשתנו או שאינם מקומפלים, ולא של כל הקבצים בתוכנית. בפרויקט הכולל מאות ואף אלפי קבצים, ופוטנציאלית מיליוני שורות קוד, פונקציונליות זו יכולה לקצר משמעותית את זמן הקומפילציה – מדקות ואף שעות לשניות. מבוא לתכנות מערכות
26
שלבי הקומפילציה את הידור הקוד ניתן לחלק לשלושה שלבים:
עיבוד מקדים - Preprocessing הידור - Compilation קישור - Linking Source Files a.h Object Files כל השלבים מבוצעים ע"י gcc. ונהוג לקרוא לחבילת התוכנה שמבצעת את כל השלבים מהדר ולכל ההליך הידור. זאת למרות שקיים גם חלק ממנו שקרוי הידור. בפועל שלב ההידור מחולק לעוד חלקים: כתיבת קוד אסמבלי, עיבוד מקדים לקוד אסמבלי ובנייה של הקוד הבינארי בקובץ האובייקט. עם זאת, שלבים אלו אינם מעניינים אותנו בקורס והם מכוסים ביתר פירוט באת"מ. גם הביצוע הטכני של שלב הקישור מפורט באריכות בקורס את"מ. ניתן לבקש מ-gcc לעצור אחרי כל בהליך הזה ולקבל את קבצי הביניים. עם זאת, חוץ מעבור המקרה שבו נבקש את קבצי האובייקט אין לכך הרבה שימושים ולכן לא נזכיר זאת. בפרט, לא נזכיר את שמות וטיפוסי הקבצים אשר קיימים בשלבי הביניים של התהליך. מהדרים אחרים עובדים בצורה דומה אך שמות הקבצים עלולים להיות שונים, למשל vc (הקומפיילר של מיקרוסופט) משתמש בסיומת .obj לקבצי אובייקט. Preprocessor Compiler a.c a.o Linker prog b.c b.o c.c c.o מבוא לתכנות מערכות
27
עיבוד מקדים – Preprocessing
שלב העיבוד המקדים עובר על קובץ הקוד ומבצע את ההוראות עבור ה-preprocessor הוראות עבור ה-preprocessor הן כל השורות המתחילות ב-# פעולות העיבוד המקדים הן כולן פעולות גזירה והדבקה פשוטות #define <macro> <value> - הגדרת מאקרו חדש. לאחר ההגדרה בכל מקום שיופיע <macro> בקוד תבוצע החלפה ל-<value> ניתן להגדיר יותר מסתם קבועים בעזרת מאקרו אך חשוב לשים לב להשלכות #include <filename> - הוסף את תוכן הקובץ לתוך הקוד במיקום הנוכחי הקוד שמתווסף עובר גם הוא עיבוד מקדים אם שם הקובץ מופיע בתוך < > ההתייחסות היא לקובץ מן הספריה הסטנדרטית אם שם הקובץ מופיע במרכאות " " ההתייחסות היא לקובץ שנמצא עם שאר הקוד שכתבנו #ifdef <macro> - אם מוגדר מאקרו בשם <macro> המשך כרגיל, אחרת מחק את כל הקוד עד להופעת #endif מאקרו: כמו שנאמר בהרצאה, שימוש במאקרו עלול להיות מסוכן והגדרתו אינה טריוויאלית. כך למשל הגדרה פשוטה כמו: #define SQUARE(x) x*x מלאה בהמון בעיות. גם אם נשכלל את ההגדרה כך שתהיה טובה יותר, למשל ((x)*(x)) עדיין נישאר עם מקרים כגון SQUARE(i++) שיוצרים קוד שאינו מוגדר. מומלץ לכן להימנע מהגדרת מאקרוים לפחות בשלב זה. הם כלי חשוב ומאוד לפיתוח ב-C אך כדאי להתרגל לשפה יותר לפני תחילת השימוש בהם. בכל מקרה, אם ניתן להחליף מאקרו בפונקציה - עשו זאת! Include: קובץ המופיע ב-<> הוא מהספריה הסטנדרטית או מתיקיות אחרות שהוגדר לקומפיילר במיוחד לחפש גם בהן קבצים. עבור קובץ h שנכתוב בעצמנו, נשתמש ב- #include "filename" שם הקובץ יכול להיות שם יחסי או מוחלט במערכת, עם זאת מומלץ לא לרשום שמות ארוכים ב-include, התוצאה של שמות כאלה היא קשיים בקמפול הקוד על מחשבים אחרים. שימו לב ששמות הקבצים הם תלויים במערכת ההפעלה, לכן למשל שורה כמו #include <STDIO.h> תעבור ב-Windows ותיכשל בלינוקס. #ifdef מאפשר לחלק מהקוד להשתנות בהתאם להגדרות מאקרו נוספות. דוגמה מצוינת היא שימוש במאקרו NDEBUG כדי לכבות את assert. המאקרו assert מוגדר בשתי דרכים שונות, אחת כאשר NDEBUG אינו מוגדר וז המאקרו מבצע את הבדיקה כמו שנלמד ואילו בדרך נוספת כאשר המאקרו מוגדר כך ש-assert לא יבצע כלום. הנחיות מועילות נוספות: #else, #ifndef. מבוא לתכנות מערכות
28
הידור - Compilation שלב ההידור מקבל קבצי קוד מקור שעברו את שלב העיבוד המקדים שלב זה אינו מקבל קבצי h, קבצים אלו "הודבקו" לתוך קבצי ה-c הרלוונטיים לכל קובץ קוד נוצר קובץ בינארי אשר מכיל את הקוד המהודר מקובץ הקוד קובץ זה קרוי object file וסיומתו היא .o עבור gcc ייתכן וחלק מהפונקציות שהוכרזו לא מומשו ביחידת הקומפילציה הנוכחית, במקרה זה קובץ ה-object יכיל "חורים" קובץ זה תלוי מהדר ומערכת הפעלה - לכן קובץ אובייקט הנוצר עבור שרת ה-stud שונה מקובץ הנוצר עבור מחשב ביתי עם Windows קובץ האובייקט בהכרח יכיל חורים, מאחר שהפונקציות הוכרזו אך עדיין לא קושרו למימוש שלהן (וייתכן שמימושן נמצא בכלל בקובץ אחר). בשלב הבא, המהדר יהיה אחראי לקשר כל הכרזה למימוש שלה ("למלא את החורים") מבוא לתכנות מערכות
29
קישור - Linking שלב הקישור מקבל כקלט קבצי אובייקט ומקשר את כולם לקובץ הרצה יחיד בשלב זה לכל "חור" שהושאר בקובץ אובייקט מקושרת הפונקציה המתאימה אם אכן קיים לה מימוש באחד מקבצי האובייקט ייתכנו שגיאות בשלב זה הנקראות שגיאות קישור: לא נמצא מימוש עבור פונקציה מסוימת a.c:11: undefined reference to `my_function' נמצא יותר ממימוש אחד עבור פונקציה מסוימת a.c:10: multiple definition of `main' b.c:10: first defined here מבוא לתכנות מערכות
30
פונקציות סטטיות ניתן להכריז על פונקציה כסטטית:
static <function declaration> במקרה זה הפונקציה אינה נראית מחוץ ליחידת הקומפילציה שלה וה-linker לא ינסה לקשר אותה לקריאות לפונקציה מיחידות קומפילציה אחרות דוגמה: static int square(int n) { return n*n; } במקרה זה אם תהיה הכרזה וקריאה ל-square מיחידת קומפילציה אחרת הפונקציה הסטטית לא תקושר לקריאה זו, ולא תהיה התנגשות בשלב הקישור. ההכרזה על פונקציה כסטטית אומרת גם כי בכמה מודולים יכולות להיות פונקציות שונות בעלות אותה החתימה, בתנאי שלכל היותר אחת מהן היא אינה סטטית. אם פונקציה מוגדרת בשני קבצים כאשר באחד מהקבצים היא סטטית, באותו קובץ (החל מהגדרת הפונקציה) השימוש יהיה בפונקציה הסטטית. כמובן, לא ניתן להשתמש בפונקציה לפני ההכרזה שלה. הערה למתקדמים: ניתן להכריז גם על משתנה גלובלי כ-static כדי לא לאפשר גישה אליו מיחידות קומפילציה אחרות. בשביל לגשת למשתנה גלובלי מיחידת קומפילציה אחרת יש להכריז עליו בעזרת המילה השמורה extern דוגמה, נניח שיש לנו את הקובץ הבא main.c: int square(int n); int main() { int n = square(2); return 0; } ואת הקובץ הבא square.c: static int square(int n) { return n*n; נסיון להדר את שני הקבצים יחד ייכשל בשלב הקישור: > gcc main.c square.c השגיאה המתקבלת תהיה undefined reference עבור square. שימו לב שפונקציות נוספות ב-square.c אם יהיו, יוכלו לקרוא ל-square בחופשיות. אם מוגדרת פונקציה סטטית והיא אינה בשימוש הקומפיילר יכול לפלוט אזהרה על כך. מבוא לתכנות מערכות
31
הידור של מספר קבצים - סיכום
ניתן לקמפל מספר קבצים ביחד לקובץ הרצה יחיד ההידור ניתן לחלוקה לשלושה שלבים: עיבוד-מקדים, הידור וקישור בשלב העיבוד המקדים מוחלפים מאקרוים ומבוצעות פעולות include בשלב ההידור נוצר מכל קובץ c קובץ אובייקט בשלב הקישור מחוברים קבצי האובייקט לקובץ הרצה יחיד כדי להסתיר פונקציות מיחידות קומפילציה אחרות ניתן להגדירן כסטטיות מבוא לתכנות מערכות
32
בנייה הדרגתית של תכונה שימוש ב-make לבנייה אוטומטית
Makefile בנייה הדרגתית של תכונה שימוש ב-make לבנייה אוטומטית מבוא לתכנות מערכות
33
בנייה של תוכנה גדולה כאשר התוכנה גדלה זמן הקומפילציה גדל
לפעמים יותר משעה עבור קומפילציה של כל הקוד בבת אחת נרצה להימנע מקומפילציה מחדש של כל הקוד לאחר שינוי קטן נניח שבתוכנה שלנו קיימים הקבצים a.c, b.c ו-c.c כיצד נבנה את התוכנה כך שנוכל לקמפל מחדש רק חלקים שישתנו? כיצד נקמפל את התוכנה מחדש לאחר שינוי ב-a.c? כדי לבנות את התוכנה בפעם הראשונה נבקש מ-gcc ליצור תחילה את קבצי האובייקט בלבד: > gcc -c a.c b.c c.c לאחר שלב זה יהיה לנו את כל קבצי האובייט ונוכל לקשרם לקובץ הרצה: < gcc a.o b.o c.o -o prog כעת, במקרה של שינוי ב-a.c בלבד נוכל להדר רק את a.c לקבלת קובץ אובייקט מעודכן ולקשר את קבצי האובייט מחדש: > gcc -c a.c > gcc a.o b.o c.o -o prog כך נחסך מאיתנו קמפול מחדש של b.c ו-c.c. כאשר מספר הקבצים גדל זמן זה יכול להיות משמעותית מאוד. מבוא לתכנות מערכות
34
בנייה הדרגתית של תוכנה לדוגמה, התכנית שלנו מורכבת מהקבצים הבאים:
calc.c control.c main_prog.c בכל פעם שנשנה קובץ יחיד מספיק להדר רק אותו שוב ולקשר מחדש לדוגמה, אם נשנה את control.c: בעיה: מה נעשה כשמספר הקבצים והתלויות ביניהם גדלים? > gcc -c calc.c > gcc -c control.c > gcc -c main_prog.c > gcc calc.o control.o main_prog.o -o prog > gcc -c control.c > gcc calc.o control.o main_prog.o -o prog מבוא לתכנות מערכות
35
make Make היא כלי פשוט ויעיל המאפשר קומפילציה הדרגתית של תוכנה בלי חזרה על חלקים שלא עודכנו make מאפשרת יצירת קובץ makefile אשר יכיל הוראות לבניית התוכנה בכל פעם שנרצה לבנות מחדש את התוכנה make תשתמש בהוראות כדי לבנות מחדש רק את החלקים שהשתנו prog: calc.o control.o main_prog.o gcc calc.o control.o main_prog.o -o prog calc.o: calc.c calc.h gcc -c calc.c control.o: control.c calc.h control.h gcc -c control.c main_prog.o: calc.h control.h gcc -c main_prog.c > make מבוא לתכנות מערכות
36
makefile ה-makefile מכיל את רשימת הקבצים הדרושים לתוכנה והתלויות ביניהם ה-makefile מורכב מכללים (rules) כל כלל מתחיל בשורה מהצורה: a.o: a.c a.h b.h שם הכלל, הקרוי גם target הוא שם הקובץ שנוצר מכלל זה התלויות הם הקבצים ששינוי בהם משפיע על שינוי ה-target השורות הבאות בכלל מכילות את רשימת הפקודות שיש לבצע עבור הכלל כל שורת פקודה חייבת להתחיל ב-TAB לדוגמה, כלל עבור יצירת קובץ האובייקט a.o: gcc -c a.c התו # מסמן הערה עד סוף השורה ניתן להשתמש בתו \ כדי לשבור שורה ארוכה (בדומה למאקרו ב-C) ה-makefile רגיש מאוד לרווחים: אסור רווחים בתחילת או סוף שורה חובה להתחיל כל שורת פקודה ב-TAB (וצריך TAB נוסף לאחר שבירת שורה) מבחינת ה-makefile שורה מסתיימת בירידת השורה, כדי ליצור שורה ארוכה ניתן להשתמש בתו \ ולאחריו ירידת שורה כדי לגרום להתעלמות ממנה. בדומה לנדרש במאקרו ארוך ב-C (אשר גם הוא מסתיים בירידת שורה) שם הכלל הוא ה-target ויכול לשמש בהרצת make כדי לבנות רק חלק מהתכנית. מבוא לתכנות מערכות
37
שימוש ב-make לאחר כתיבת קובץ ההוראות (ה-makefile), השימוש ב-make מתבצע בעזרת הפקודה הבאה: > make [-f filename] [target] אם לא מוגדר שם קובץ, make תחפש קובץ בשם makefile או Makefile בתיקיה הנוכחית הארגומנט target מאפשר לבצע רק חלק מתהליך הבנייה או לטפל במקרים מיוחדים. אם הוא אינו מופיע יבוצע הכלל הראשון ב-makefile סביבות עבודה בד"כ מסוגלות ליצור קובץ בנייה אוטומטי, אך בפרויקטים גדולים דרושה התאמה אישית של קבצים אלו מבוא לתכנות מערכות
38
דוגמה קובץ ה-make המתאים ייראה כך: נסתכל על התכנית הבאה:
prog: a.o b.o c.o gcc a.o b.o c.o -o prog a.o: a.c a.h b.h gcc -c a.c b.o: b.c b.h gcc -c b.c c.o: c.c c.h b.h gcc -c c.c a.h: #include "b.h" b.h: c.h: a.c: #include "a.h" b.c: #include "b.h" c.c: #include "b.h" #include "c.h" שתי הפקודות להפעלת make שקולות כמובן מאחר ו-prog הוא הכלל הראשון בקובץ. ניתן גם פקודות לבניית חלק מהאובייקטים כמובן, כמו make c.o. a.o b.o c.o > make > make prog prog מבוא לתכנות מערכות
39
שיטת העבודה של make כאשר make מקבלת את הכלל <name>: <dependencies> לביצוע היא משתמשת באלגוריתם הבא: לכל תלות ב-<dependencies> יש לבצע בדיקה: אם קיים כלל מתאים לתלות הזו נבצע אותו תחילה בצורה רקורסיבית אם לא קיים כלל נבדוק אם הקובץ עודכן מאז הפעם האחרונה שביצענו את הכלל (לפי חתימת הזמן על הקובץ) אם נמצאו תלויות שהשתנו יש לבצע את הפקודות עבור הכלל <name> אם לא היה שינוי באף אחד מהתלויות של הכלל אין צורך לבצע כלום מבוא לתכנות מערכות
40
מציאת תלויות ניתן למצוא את התלויות בתוכנה ולקבל שלד עבור ה-makefile בעזרת הפקודה הבאה > gcc -MM *.c לדוגמה, הפלט עבור הדוגמה הקודמת: a.o: a.c a.h b.h b.o: b.c b.h c.o: c.c c.h b.h מבוא לתכנות מערכות
41
אפשרויות נוספות ב-makefile
למקרה של מחרוזות החוזרות מספר פעמים עבור מחרוזות שצפויות להשתנות בעתיד כדי להגדיר מאקרו מוסיפים בקובץ שורה מהצורה הבאה: <MACRO NAME> = <string> כדי להתייחס למאקרו משתמשים ב-$ ושם המאקרו בסוגריים: $(<MACRO NAME>) קיימים מספר מאקרוים מוגדרים מראש: הוא ה-target הנוכחי $* הוא ה-target הנוכחי ללא סיומת מבוא לתכנות מערכות
42
דוגמה ל-makefile מתקדם יותר
CC = gcc OBJS = a.o b.o c.o EXEC = prog DEBUG_FLAG = # now empty, assign -g for debug COMP_FLAG = -std=c99 -Wall -Werror $(EXEC) : $(OBJS) $(CC) $(DEBUG_FLAG) $(OBJS) -o a.o : a.c a.h b.h $(CC) -c $(DEBUG_FLAG) $(COMP_FLAG) $*.c b.o : b.c b.h $(CC) -c $(DEBUG_FLAG) $(COMP_FLAG) $*.c c.o : c.c c.h b.h $(CC) -c $(DEBUG_FLAG) $(COMP_FLAG) $*.c clean: rm -f $(OBJS) $(EXEC) בדוגמה זו נעשה שימוש במאקרו עבור מספר דברים מועילים: ניתן להחליף את הקומפיילר או הדגלים בקלות ניתן להוסיף את דגל ה-debug בקלות יש צורך בפחות שינויים בעת הוספת קבצים או תלויות ניתן גם ליצור כללים אשר עושים דברים מיוחדים או משתמשים בתוכנות אחרות, למשל הכלל clean נועד לנקות את כל התוכנה שנבנתה. אין לו תלויות והפקודה לביצוע בו היא rm המוחקת קבצים > make clean rm -f a.o b.o c.o prog > make gcc -c -Wall a.c gcc -c -Wall b.c gcc -c -Wall c.c gcc a.o b.o c.o -o prog מבוא לתכנות מערכות
43
קובץ ה-make החדש המנצל את החוקים המובנים
כללים מובנים להרבה מהקבצים שיש ליצור בבניית התוכנה יש כללים קבועים בבנייתם למשל כל קבצי ה-o נוצרים על בעזרת הידור של קובץ c בעל אותו שם באותה צורה כדי למנוע שכפולי קוד בקובץ ה-Make קיימים כללי מובנים ומשתנים מוגדרים מראש לסוגי קבצים מסוימים למשל עבור קבצי o, make יודעת להשתמש בפקודה הבאה ליצירתם מקובץ c כברירת מחדל: CC=gcc OBJS=a.o b.o c.o EXEC=prog DEBUG=# now empty, assign -g for debug CFLAGS=-std=c99 -Wall -Werror $(DEBUG) $(EXEC) : $(OBJS) $(CC) $(DEBUG_FLAG) $(OBJS) -o a.o : a.c a.h b.h b.o : b.c b.h c.o : c.c c.h b.h clean: rm -f $(OBJS) $(EXEC) מספר אי-דיוקים המופיעים בשקף: הכלל ליצירת קובץ object הוא $(CC) $(CPPFLAGS) $(CFLAGS) -c, אבל נתעלם מכך. (במשתנה זה מוחזקים דגלים עבור שלב העיבור המקדים) make יכולה להשלים את הכלל בעצמה לגמרי, למשל אם חסר קובץ a.o וקיים קובץ a.c אז make תבנה את a.o אוטומטית בעצמה. אך הכלל הזה לא יתאים אם קיימות תלויות נוספות בקבצי h מסוימים. קובץ ה-make החדש המנצל את החוקים המובנים $(CC) $(CFLAGS) –c $*.c מבוא לתכנות מערכות
44
Makefile - סיכום ניתן להגדיר קבצי makefile כדי להקל ולזרז את בניית התוכנה הפקודה make משתמשת ב-makefile כדי לבנות את התוכנה בצורה אוטומטית ניתן להגדיר מאקרו בתוך makefile כדי להקל על שינויו בעתיד ניתן להשתמש ב-gcc כדי ליצור שלד של makefile מבוא לתכנות מערכות
45
שאלה לדוגמה - ADT מבוא לתכנות מערכות
46
שאלה לדוגמה מבנה הנתונים ערמה (heap) מאפשר הכנסת איברים והוצאה של האיבר "המקסימלי" לפי סדר שהוגדר. כלומר הפעולות הנדרשות מערמה הן: יצירת ערמה חדשה. שחרור ערמה קיימת. הכנסת איבר לערמה, ניתן להכניס מספר עותקים של אותו איבר. הוצאת האיבר המקסימלי מהערמה. במקרה והערמה ריקה תוחזר שגיאה. א. כתבו את קובץ הממשק עבור ADT של ערמה ב. באילו מה-ADT שנלמדו בקורס כדאי להשתמש למימוש בערמה? מדוע? ג. כתבו את הקוד הדרוש למימוש ה-struct עבור הערמה ד. ממשו את הפונקציה עבור יצירת ערמה חדשה שימו לב שהכוונה ב"מקסימאלי" כמובן אינה לאיבר האחרון שהוכנס (כמו במחסנית) אלא לאיבר שערכו גדול ביותר מכל האיברים שהוכנסו לערמה מבוא לתכנות מערכות
47
ניתן להגדיר את המצביעים ישירות או להוסיף typedef מתאימים
סעיף א' #ifndef _HEAP_H #define _HEAP_H #include <stdbool.h> typedef struct heap_t* Heap; typedef void* Element; typedef enum { HEAP_SUCCESS, HEAP_NULL_ARGUMENT, HEAP_OUT_OF_MEMORY, HEAP_EMPTY } HeapResult; Heap heapCreate(Element (*copy)(Element), void (*release)(Element), bool (*greaterThan)(Element, Element)); HeapResult heapPush(Heap heap, Element element); HeapResult heapPop(Heap heap, Element * element); void heapDestroy(Heap heap); #endif ניתן להגדיר את המצביעים ישירות או להוסיף typedef מתאימים מבוא לתכנות מערכות
48
איפה יישמרו המצביעים לשאר לפונקציות?
סעיפים ב' ו-ג' נבחר להשתמש ב-List עבור מימוש הערמה: ייתכנו העתקים של אותו איבר בערמה יהיה לנו נוח יותר להוציא איבד בודד ממקום כלשהו ברשימה (לעומת מערך או מחסנית) מימוש המבנה בקובץ ה-C: struct heap_t { List items; bool (*greaterThan)(Element, Element); }; פתרון אפשרי הוא להחזיק את הרשימה ממוינת בסדר יורד. כמו כן במצב זה הכנסה תהיה פעולה יחסית "כבדה", אם כי פחות מבכל מבנה אחר שלמדנו (למשל מערך). איפה יישמרו המצביעים לשאר לפונקציות? מבוא לתכנות מערכות
49
סעיף ד' Heap heapCreate(Element (*copy)(Element), void (*release)(Element), bool (*greaterThan)(Element, Element)) { if (!copy || !release || !greaterThan) { return NULL; } Heap heap = malloc(sizeof(*heap)); if (!heap) { heap->items = listCreate(copy, release); if (!heap->items) { heapDestroy(heap); } heap->greaterThan = greaterThan; return heap; מבוא לתכנות מערכות
Similar presentations
© 2025 SlidePlayer.com. Inc.
All rights reserved.