Menu
flag Ar iconالعربيةdown icon

ملفات الترويسة في لغة C: ‏.h مقابل .c وحراس التضمين والبرامج متعدّدة الملفات

كيف تقسّم برنامج C على عدة ملفات: ما ينتمي إلى ملف .h، وما ينتمي إلى ملف .c، وحراس التضمين التي تمنع التضمين المزدوج، وترجمة عدة ملفات معًا، ومشاركة المتغيّرات العامة بـ extern.

تحتوي هذه الصفحة على محررات قابلة للتشغيل - حرّر، شغّل، وشاهد النتيجة فوراً.

ملف .c واحد مقبول حتى يبلغ ألفي سطر. وتقسيم البرنامج على عدة ملفات يتيح ترجمة كل جزء منفصلًا، وإعادة استخدامه في برامج أخرى، وقراءته وحده - لكن C لا تملك نظام استيراد. وما تملكه هو المعالج المسبق يلصق نصًا، إضافةً إلى رابط يضمّ القطع في النهاية.

وملف الترويسة (.h) هو العقد المشترك بين تلك القطع: يخبر كل ملف مصدر بما هو موجود في موضع آخر، دون أن يحتوي على التنفيذ.

التصريحات مقابل التعريفات

يقوم التصميم كله على تمييز واحد.

التصريح يقول هذا موجود في مكان ما وهذا شكله. ولا يولّد شيفرة ويمكن أن يظهر أي عدد من المرات:

int add(int a, int b);        /* function declaration (prototype) */
extern int error_count;       /* variable declaration */
struct Point { int x, y; };   /* type definition - safe to repeat per file */

أما التعريف فينشئ الشيء. ويجب أن يظهر مرة واحدة بالضبط في البرنامج كله:

int add(int a, int b) { return a + b; }   /* function definition */
int error_count = 0;                      /* variable definition */

تحمل الترويسات التصريحات. وتحمل ملفات المصدر التعريفات. اعكس ذلك فيشتكي الرابط من "multiple definition of ..." - وهي رسالة الخطأ الوحيدة التي تعني بشكل موثوق أن تعريفًا تسلّل إلى ترويسة.

برنامج من ملفين

إليك أصغر تقسيم مفيد. ترويسة تصرّح بدالّتين:

/* 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

وملف المصدر الذي ينفّذهما - ولاحظ أنه يضمّن ترويسته الخاصة:

/* 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 ترويسته الخاصة - وهذا ليس تكرارًا. فهو يجعل المترجم يتحقّق من أن كل تعريف يطابق تصريحه، فإن غيّرت النموذج الأوّلي في الترويسة ونسيت ملف .c حصلت على خطأ فورًا بدل عدم تطابق عند الربط.

وثانيًا، math_utils.h ليست في سطر أوامر gcc. فالترويسات لا تُترجَم أبدًا؛ بل تُلصَق في ملفات .c بـ #include. وتمرير .h إلى المترجم ينتج ملف ترويسة مترجَمة مسبقًا شاردًا ولا شيفرة قابلة للربط.

والبرنامج نفسه مضغوطًا في ملف واحد، لتستطيع تشغيله هنا:

والتصريحات قبل main هي ما توفّره الترويسة في النسخة الحقيقية - وهذا بالضبط سبب كون النماذج الأوّلية للدوال والترويسات الفكرةَ نفسها على مقياسين.

حراس التضمين

#include يلصق نصًا، والنص الملصوق مرتين نصّ مكرّر. وهذا غير ضارّ لنموذج أوّلي لدالة وقاتل لبنية:

/* shapes.h WITHOUT a guard */
struct Point { int x, y; };

فإن ضمّن main.c كلًّا من shapes.h وcanvas.h، وضمّن canvas.h أيضًا shapes.h، رأى المترجمُ struct Point معرَّفة مرتين في وحدة ترجمة واحدة وتوقّف بـ "redefinition of 'struct Point'". وفي مشروع حقيقي تصبح هذه السلاسل عميقة بما لا يمكنك تتبّعه يدويًا.

والإصلاح حارس تضمين: ماكرو يسجّل "هذه الترويسة لُصقت سلفًا".

/* 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. وفي الحالتين، كل ترويسة تأخذ واحدًا - بلا استثناءات، بما في ذلك الترويسات التي تظنّ ألّا شيء آخر سيضمّنها.

ما ينتمي إلى ترويسة

ضع في ملف .h:

  • النماذج الأوّلية للدوال
  • تعريفات struct وunion وenum
  • تصريحات typedef
  • الماكرو المقصود مشاركته
  • تصريحات extern للمتغيّرات العامة المشتركة
  • أوامر #include التي تحتاجها تلك الترويسة لتكون مكتفية بذاتها

وأبقِ خارج ملف .h:

  • أجسام الدوال (إلا إن كانت static inline عن قصد)
  • تعريفات المتغيّرات - فـ int counter; في ترويسة تعرّف متغيّرًا منفصلًا في كل ملف يضمّنها، أو تنتج خطأ ربط، بحسب المترجم
  • تضمين ترويسات لا تحتاجها الترويسة نفسها - فذلك يدفع تلك التبعية إلى كل من يليها

و"مكتفية بذاتها" تستحق أن تكون قاعدة: ينبغي أن تُترجَم الترويسة حين تُضمَّن أولًا قبل أي شيء آخر. فإن استخدمت shapes.h النوعَ size_t، فإنها تضمّن <stddef.h> بنفسها بدل أن ترجو أن يكون الملف المضمِّن قد فعل.

وترويسة كاملة حسنة التكوين:

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

#include <stddef.h>   /* for size_t, used below */

#define MAX_NAME 64

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

/* shared across the program, defined once in 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  - declaration, no storage */
extern int item_count;
/* inventory.c  - the one definition */
#include "inventory.h"
int item_count = 0;
/* main.c - uses it, via the header */
#include <stdio.h>
#include "inventory.h"

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

أسقِط extern من الترويسة فيعرّف كل ملف مضمِّن item_count الخاص به، وهذا خطأ ربط "multiple definition" في أحسن الأحوال وعدّادان مستقلّان في أسوئها.

والحاجة المعاكسة شائعة بالقدر نفسه: متغيّر أو دالة مساعِدة ينبغي أن تبقى خاصة بملف .c واحد. وstatic على نطاق الملف تفعل ذلك - فهي تمنح الاسم ربطًا داخليًا، غير مرئي للرابط وبالتالي لكل ملف آخر:

/* inventory.c */
static Item storage[256];          /* private to this file */
static int  find_slot(const char *name);   /* private helper */

يمكن لملفين أن يحمل كلٌّ منهما static int counter; بلا تصادم. وهذه نسخة C من العضو الخاص، وهي الافتراضي الذي ينبغي أن تلجأ إليه - فلا يذهب إلى الترويسة إلا ما تحتاجه الملفات الأخرى فعلًا.

ترجمة برامج أكبر

إدراج كل ملف ينجح وهو بطيء، لأن كل ملف يُعاد ترجمته في كل مرة:

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 $@

وإن كانت ترويساتك في مجلّد فرعي، فإن -Iinclude تضيفه إلى مسار البحث بالأقواس الزاوية.

الأخطاء ومعانيها

نمطا الفشل يسهل التمييز بينهما بمجرّد معرفة أي مرحلة أنتجتهما.

"undefined reference to 'add'" - خطأ رابط. وُجد التصريح ولم يوجد التعريف. فإما أنك نسيت إدراج ملف .c في سطر الأوامر، أو أن الدالة static، أو أن الاسم مكتوب خطأ في أحد الموضعين.

"multiple definition of 'item_count'" - خطأ رابط أيضًا، وهو الصورة المعاكسة: فقد انتهى تعريف في ترويسة، أو في ملفَي مصدر. انقله إلى ملف .c واحد واترك تصريح extern في الترويسة.

"redefinition of 'struct Item'" - خطأ مترجم، ويعني أن ترويسة لُصقت مرتين في ملف واحد. أضف حارس التضمين.

"implicit declaration of function 'add'" - تحذير مترجم (وخطأ في أوضاع C99 فما بعد) يعني أن النموذج الأوّلي لم يُرَ قط. فقد نسيت #include، أو أن الترويسة لا تصرّح به.

وبمجرّد امتداد برنامج على عدة ملفات، يكون ما تريده تاليًا عادةً تنويع ما يُترجَم بحسب المنصّة أو نوع البناء - وذلك هو الترجمة الشرطية.

الأسئلة الشائعة

ماذا يوضع في ملف .h وماذا يوضع في ملف .c؟

تحمل الترويسة التصريحات - النماذج الأوّلية للدوال، وتعريفات struct وtypedef، والتعدادات، والماكرو، وتصريحات extern للمتغيّرات العامة المشتركة. ويحمل ملف .c التعريفات - أجسام الدوال والمتغيّرات الفعلية. والقاعدة الإرشادية: الترويسة تقول ما هو موجود، وملف المصدر يقول ماذا يفعل.

ما حارس التضمين ولماذا أحتاجه؟

#include يلصق نصًا، فتضمين ترويسة مرتين يلصق محتواها مرتين - وهذا يعيد تعريف كل بنية وكل typedef فيها ويفشل في الترجمة. ويغلّف حارس التضمين الترويسةَ بـ #ifndef MYHEADER_H و#define MYHEADER_H و#endif، فيجد التضمين الثاني الماكرو معرَّفًا سلفًا ويتخطّى الجسم.

كيف أترجم برنامج C بعدة ملفات؟

أدرِج كل ملف .c في سطر الأوامر: gcc main.c math_utils.c -o app. ولا تضع ملف .h هناك أبدًا - فالترويسات يلصقها #include ولا تُترجَم وحدها. وللمشاريع الأكبر، ترجم إلى ملفات كائنية (gcc -c main.c) واربطها، وهو ما يؤتمته Makefile.

هل أستخدم ‎#pragma once أم حراس ‎#ifndef؟

كلاهما يعمل. فـ #pragma once سطر واحد ولا يمكن أن يتصادم اسمه، وكل مترجم رئيسي يدعمه - لكنه ليس في معيار C. وصيغة #ifndef/#define/#endif معيارية وتعمل في كل مكان. اختر واحدة واستخدمها باتّساق في مشروع؛ ولأقصى قابلية نقل اختر صيغة #ifndef.

Coddy programming languages illustration

تعلّم البرمجة مع Coddy

ابدأ الآن