Menu

קובצי header ב-C: .h מול .c, include guards ותוכניות מרובות קבצים

איך מפצלים תוכנית C לכמה קבצים: מה שייך ל-.h, מה שייך ל-.c, include guards שמונעים הכללה כפולה, קומפילציה של כמה קבצים יחד ושיתוף משתנים גלובליים עם extern.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

קובץ .c יחיד הוא בסדר עד שהוא מגיע ל-2,000 שורות. פיצול תוכנית לכמה קבצים מאפשר לקמפל כל חלק בנפרד, להשתמש בו שוב בתוכניות אחרות ולקרוא אותו בפני עצמו, אבל ל-C אין מערכת import. מה שיש לה זה preprocessor שמדביק טקסט, ועוד linker שמחבר את החלקים בסוף.

קובץ header (.h) הוא החוזה המשותף בין החלקים האלה: הוא אומר לכל קובץ מקור מה קיים במקום אחר, בלי להכיל את המימוש.

הצהרות מול הגדרות

כל העיצוב נשען על הבחנה אחת.

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

int add(int a, int b);        /* הצהרה על פונקציה (אב טיפוס) */
extern int error_count;       /* הצהרה על משתנה */
struct Point { int x, y; };   /* הגדרת טיפוס: בטוח לחזור עליה בכל קובץ */

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

int add(int a, int b) { return a + b; }   /* הגדרת פונקציה */
int error_count = 0;                      /* הגדרת משתנה */

קובצי header מחזיקים הצהרות. קובצי מקור מחזיקים הגדרות. הפכו את זה וה-linker יתלונן על "multiple definition of ...", הודעת השגיאה האחת שכמעט תמיד אומרת שהגדרה נדדה לתוך header.

תוכנית בשני קבצים

הנה הפיצול השימושי הקטן ביותר. header שמצהיר על שתי פונקציות:

/* math_utils.h */
#ifndef MATH_UTILS_H
#define MATH_UTILS_H

int add(int a, int b);
int max_of(int a, int b);

#endif

קובץ המקור שמממש אותן, ושימו לב שהוא כולל את ה-header של עצמו:

/* math_utils.c */
#include "math_utils.h"

int add(int a, int b) {
    return a + b;
}

int max_of(int a, int b) {
    return (a > b) ? a : b;
}

והתוכנית שמשתמשת בהן:

/* main.c */
#include <stdio.h>
#include "math_utils.h"

int main(void) {
    printf("add(3, 4)    = %d\n", add(3, 4));
    printf("max_of(3, 4) = %d\n", max_of(3, 4));
    return 0;
}

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

gcc main.c math_utils.c -o app
./app

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

שנית, math_utils.h לא מופיע בשורת הפקודה של gcc. קובצי header אף פעם לא מקומפלים; הם מודבקים לתוך קובצי .c על ידי #include. העברת .h לקומפיילר מייצרת קובץ precompiled header מיותר ושום קוד שאפשר לקשר.

אותה תוכנית דחוסה לקובץ אחד, כדי שתוכלו להריץ אותה כאן:

ההצהרות לפני main הן מה שה-header מספק בגרסה האמיתית, וזו בדיוק הסיבה שאבות טיפוס של פונקציות וקובצי header הם אותו רעיון בשני קני מידה.

Include guards

#include מדביק טקסט, וטקסט שהודבק פעמיים הוא טקסט כפול. זה לא מזיק עבור אב טיפוס של פונקציה, אבל קטלני עבור struct:

/* shapes.h בלי guard */
struct Point { int x, y; };

אם main.c כולל גם את shapes.h וגם את canvas.h, ו-canvas.h כולל גם הוא את shapes.h, הקומפיילר רואה את struct Point מוגדר פעמיים באותה יחידת תרגום ועוצר עם "redefinition of 'struct Point'". בפרויקט אמיתי השרשראות האלה נעשות עמוקות מספיק כדי שאי אפשר יהיה לעקוב אחריהן ידנית.

הפתרון הוא include guard: מאקרו שרושם "ה-header הזה כבר הודבק".

/* shapes.h */
#ifndef SHAPES_H
#define SHAPES_H

struct Point { int x, y; };
struct Point origin_point(void);

#endif /* SHAPES_H */

ההכללה הראשונה מוצאת ש-SHAPES_H לא מוגדר, ולכן התוכן נשמר, ובדרך הוא מגדיר את SHAPES_H. כל הכללה מאוחרת יותר באותו קובץ מוצאת אותו מוגדר ומדלגת ישר ל-#endif. שם המאקרו חייב להיות ייחודי בכל הפרויקט; FILENAME_H שנגזר מהנתיב הוא המוסכמה הרגילה.

החלופה בשורה אחת נתמכת על ידי כל קומפיילר מרכזי:

/* shapes.h */
#pragma once

struct Point { int x, y; };

ל-#pragma once לא יכולה להיות התנגשות שמות, ושגיאת הקלדה ב-#endif לא יכולה לשבור אותו. החיסרון היחיד שלו הוא שהוא לא בתקן C, כך שפרויקט שחייב להיבנות על קומפיילרים לא שגרתיים צריך להעדיף את הצורה עם #ifndef. כך או כך, כל header מקבל guard, בלי יוצאים מן הכלל, כולל headers שאתם חושבים שאף אחד אחר לא יכלול.

מה שייך ל-header

שימו בקובץ .h:

  • אבות טיפוס של פונקציות
  • הגדרות של struct, union ו-enum
  • הצהרות typedef
  • מאקרו שנועדו לשיתוף
  • הצהרות extern על משתנים גלובליים משותפים
  • ה-#include שה-header עצמו צריך כדי לעמוד בפני עצמו

השאירו מחוץ ל-.h:

  • גופי פונקציות (אלא אם הם static inline בכוונה)
  • הגדרות של משתנים: int counter; ב-header מגדיר משתנה נפרד בכל קובץ שכולל אותו, או גורם לשגיאת קישור, תלוי בקומפיילר
  • #include של headers שה-header עצמו לא צריך: זה מעמיס את התלות הזו על כל מי שבהמשך השרשרת

כדאי להפוך את "עומד בפני עצמו" לכלל: header צריך להתקמפל כשהוא נכלל ראשון, לפני כל דבר אחר. אם shapes.h משתמש ב-size_t, הוא כולל את <stddef.h> בעצמו במקום לקוות שהקובץ שכולל אותו עשה את זה.

header שלם ובנוי היטב:

/* inventory.h */
#ifndef INVENTORY_H
#define INVENTORY_H

#include <stddef.h>   /* עבור size_t, שבשימוש בהמשך */

#define MAX_NAME 64

typedef struct {
    char   name[MAX_NAME];
    int    quantity;
    double price;
} Item;

/* משותף לכל התוכנית, מוגדר פעם אחת ב-inventory.c */
extern int item_count;

void   inventory_add(const Item *item);
double inventory_total(void);
size_t inventory_size(void);

#endif /* INVENTORY_H */

שיתוף משתנה גלובלי עם extern

משתנה גלובלי חייב להיות מוגדר בקובץ .c אחד בדיוק ומוצהר בכל מקום אחר. extern הוא מה שיוצר את ההצהרה:

/* inventory.h: הצהרה, בלי אחסון */
extern int item_count;
/* inventory.c: ההגדרה היחידה */
#include "inventory.h"
int item_count = 0;
/* main.c: משתמש בו, דרך ה-header */
#include <stdio.h>
#include "inventory.h"

int main(void) {
    printf("%d items\n", item_count);
    return 0;
}

הסירו את ה-extern מה-header וכל קובץ שכולל אותו יגדיר item_count משלו, וזו במקרה הטוב שגיאת קישור "multiple definition" ובמקרה הרע שני מונים עצמאיים.

הצורך ההפוך נפוץ באותה מידה: משתנה או פונקציית עזר שצריכים להישאר פרטיים לקובץ .c אחד. static בטווח הקובץ עושה את זה: הוא נותן לשם internal linkage, כך שהוא בלתי נראה ל-linker ולכן לכל קובץ אחר:

/* inventory.c */
static Item storage[256];          /* פרטי לקובץ הזה */
static int  find_slot(const char *name);   /* פונקציית עזר פרטית */

לשני קבצים יכול להיות לכל אחד static int counter; בלי התנגשות. זו הגרסה של C לחבר פרטי (private member), וזו ברירת המחדל שכדאי לפנות אליה: רק מה שקבצים אחרים באמת צריכים נכנס ל-header.

קומפילציה של תוכניות גדולות יותר

רישום של כל הקבצים עובד, אבל איטי, כי כל קובץ מקומפל מחדש בכל פעם:

gcc main.c inventory.c report.c -o app

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

gcc -c main.c        # produces main.o
gcc -c inventory.c   # produces inventory.o
gcc -c report.c      # produces report.o
gcc main.o inventory.o report.o -o app

עכשיו שינוי ב-report.c דורש רק gcc -c report.c וקישור מחדש. זו בדיוק הנהלת החשבונות ש-make עושה אוטומטית:

app: main.o inventory.o report.o
	gcc main.o inventory.o report.o -o app

%.o: %.c
	gcc -Wall -Wextra -c $< -o $@

אם ה-headers שלכם נמצאים בתת-תיקייה, -Iinclude מוסיף אותה לנתיב החיפוש של הסוגריים המשולשים.

שגיאות ומה הן אומרות

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

"undefined reference to 'add'": שגיאת linker. ההצהרה נמצאה, ההגדרה לא. או ששכחתם לרשום את קובץ ה-.c בשורת הפקודה, או שהפונקציה היא static, או שהשם מאוית לא נכון באחד משני המקומות.

"multiple definition of 'item_count'": גם שגיאת linker, תמונת הראי: הגדרה הגיעה ל-header, או לשני קובצי מקור. העבירו אותה לקובץ .c אחד והשאירו הצהרת extern ב-header.

"redefinition of 'struct Item'": שגיאת קומפיילר, שאומרת ש-header הודבק פעמיים לתוך קובץ אחד. הוסיפו include guard.

"implicit declaration of function 'add'": אזהרת קומפיילר (שגיאה במצבי C99 ואילך) שאומרת שאב הטיפוס לא נראה אף פעם. שכחתם את ה-#include, או שה-header לא מצהיר עליה.

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

שאלות נפוצות

מה נכנס לקובץ .h ומה נכנס לקובץ .c?

ה-header מחזיק הצהרות: אבות טיפוס של פונקציות, הגדרות של struct ו-typedef, enum, מאקרו והצהרות extern על משתנים גלובליים משותפים. קובץ ה-.c מחזיק הגדרות: גופי הפונקציות והמשתנים עצמם. כלל האצבע: header אומר מה קיים, קובץ מקור אומר מה הוא עושה.

מה זה include guard ולמה צריך אותו?

#include מדביק טקסט, כך שהכללה של header פעמיים מדביקה את התוכן שלו פעמיים, מה שמגדיר מחדש כל struct ו-typedef שבו ונכשל בקומפילציה. include guard עוטף את ה-header ב-#ifndef MYHEADER_H / #define MYHEADER_H / #endif, כך שההכללה השנייה רואה שהמאקרו כבר מוגדר ומדלגת על התוכן.

איך מקמפלים תוכנית C עם כמה קבצים?

רשמו כל קובץ .c בשורת הפקודה: gcc main.c math_utils.c -o app. לעולם אל תשימו שם קובץ .h: קובצי header מודבקים על ידי #include, לא מקומפלים בפני עצמם. בפרויקטים גדולים יותר, קמפלו לקובצי אובייקט (gcc -c main.c) וקשרו אותם, וזה מה ש-Makefile עושה אוטומטית.

כדאי להשתמש ב-#pragma once או ב-include guards עם #ifndef?

שניהם עובדים. #pragma once הוא שורה אחת ולא יכולה להיות לו התנגשות שמות, וכל קומפיילר מרכזי תומך בו, אבל הוא לא חלק מתקן C. הצורה #ifndef/#define/#endif תקנית ועובדת בכל מקום. בחרו אחת והשתמשו בה בעקביות בפרויקט; לניידות מרבית בחרו בצורה עם #ifndef.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל