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

نماذج الدوال في لغة C: التصريح مقابل التعريف

لماذا تحتاج C إلى رؤية شكل الدالة قبل استدعائها - كتابة النماذج، وإصلاح أخطاء التصريح الضمني، ووضع النماذج في ملفات الترويسة، والفرق الحقيقي بين f(void) وf().

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

المترجم يقرأ من أعلى إلى أسفل

يعالج مترجم C ملفًا مصدريًا في مرور واحد، من السطر الأول إلى الأخير. وحين يبلغ استدعاء دالة، يحتاج إلى أن يعرف سلفًا ثلاثة أشياء: ما الذي تُعيده الدالة، وكم وسيطًا تأخذ، وما أنواعها. وبدون ذلك لا يستطيع توليد شيفرة صحيحة ولا فحص الاستدعاء.

فهذا إذن مشكلة:

#include <stdio.h>

int main(void) {
    printf("%d\n", add(2, 3));   /* add has not been seen yet */
    return 0;
}

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

والحلّ هو نموذج: بصمة الدالة، مكتوبة قبل الاستدعاء، مع فاصلة منقوطة مكان الجسم.

الآن تستطيع main أن تأتي أولًا، وهكذا تُنظَّم معظم ملفات C: النماذج في الأعلى تقول ما يقدّمه الملف، وتُقرأ main كمخطّط عام، وتأتي التفاصيل أدناه.

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

كلمتان يسهل خلطهما ويستحقّ الفصل بينهما:

  • التصريح يقول إن الدالة موجودة ويعطي نوعها. وينتهي بفاصلة منقوطة وليس له جسم. ويمكنك كتابته ما شئت من المرات.
  • التعريف يعطي الجسم. ويجب أن يظهر مرة واحدة بالضبط في البرنامج كله - فتعريفان للدالة نفسها خطأ رابط ("multiple definition of").
int add(int a, int b);                        /* تصريح (نموذج) */
int add(int a, int b) { return a + b; }       /* تعريف - وهو تصريح أيضًا */

وأسماء الوسائط اختيارية في النموذج؛ فالأنواع وحدها تهمّ المترجم:

int add(int, int);              /* مسموح، ومكافئ */
int add(int a, int b);          /* أفضل: الأسماء توثّق الترتيب */

استخدم الأسماء. فـ void drawRect(int, int, int, int); لا تخبر القارئ شيئًا، بينما void drawRect(int x, int y, int width, int height); تخبره كل شيء.

ما الذي يشتريه لك النموذج

ليس القدرة على إعادة ترتيب التعريفات فحسب - بل هو ما يجعل المترجم يفحص استدعاءاتك.

ومع وجود النموذج في النطاق، يُحوَّل 3 و2 إلى double قبل الاستدعاء. وبدونه كانا سيُدفَعان بوصفهما int وكانت scale ستقرؤهما بوصفهما double - قيم مهملة، وبلا أي تشخيص في لهجات C القديمة.

كما يلتقط النموذج أعداد الوسائط الخاطئة والأنواع غير المتوافقة وقت الترجمة:

scale(3.0);              /* خطأ: وسائط أقلّ من اللازم */
scale(3.0, "two");       /* خطأ: تمرير char * حيث يُتوقَّع double */

وتلك هي الأخطاء التي تريدها - صاخبة، وقت الترجمة، وتشير إلى السطر.

أخطاء التصريح الضمني

استدعِ دالة لم يسمع بها المترجم قطّ فتحصل على واحدة من أشيع الرسائل في C:

warning: implicit declaration of function 'add' [-Wimplicit-function-declaration]

في C89 كان المترجم يخمّن: يفترض أن الدالة تُعيد int ويقبل أي وسائط مرّرتها. وذلك التخمين خاطئ في الأغلب، وحين تُعيد الدالة الحقيقية double أو مؤشّرًا تكون النتيجة هراءً. وقد أزال C99 التصاريح الضمنية من اللغة، فهذا خطأ في C99 وما بعده - وإصدارات GCC وclang الحديثة ترفضه افتراضيًا.

سببان، وحلّان:

دالتك أنت، ولم يُصرَّح عنها بعد. أضف النموذج فوق الاستدعاء، أو انقل التعريف إلى موضع أسبق.

دالة مكتبة نسيت ترويستها. فنماذج المكتبة تعيش في ترويستها، فتحتاج إلى #include:

printf, scanf, fopen        ->  #include <stdio.h>
malloc, free, exit, atoi    ->  #include <stdlib.h>
strlen, strcpy, strcmp      ->  #include <string.h>
sqrt, pow, sin, fabs        ->  #include <math.h>
isdigit, toupper            ->  #include <ctype.h>
bool, true, false           ->  #include <stdbool.h>

وثمّة رسالة ذات صلة، conflicting types for 'add'، تعني أن النموذج والتعريف لا يتّفقان - فنوع وسيط مختلف، أو نوع الإرجاع كذلك. أصلِح أيّهما الخاطئ؛ فعليهما أن يتطابقا تمامًا.

العودية المتبادلة تحتاج نموذجًا

أحيانًا لا تحلّ إعادة الترتيب المشكلة: فدالّتان تستدعي كلٌّ منهما الأخرى لا يمكن أن تأتيا كلتاهما أولًا.

نموذج واحد يكسر الدائرة. وهذه هي الآلية نفسها التي تعتمد عليها دالة عودية عادية - فاسم الدالة نفسه ضمن النطاق داخل جسمها، فالعودية المباشرة لا تحتاج نموذجًا، أما المتبادلة فتحتاجه.

النماذج في ملفات الترويسة

بمجرّد أن يمتدّ برنامج على عدة ملفات .c، تنتقل النماذج إلى ترويسة ليرى كل ملف التصاريح نفسها من مكان واحد.

mathutils.h - الواجهة:

#ifndef MATHUTILS_H
#define MATHUTILS_H

int add(int a, int b);
int multiply(int a, int b);
double average(const int values[], int count);

#endif

mathutils.c - التنفيذ:

#include "mathutils.h"

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

int multiply(int a, int b) {
    return a * b;
}

double average(const int values[], int count) {
    if (count == 0) return 0.0;
    int total = 0;
    for (int i = 0; i < count; i++) total += values[i];
    return (double) total / count;
}

main.c - مستخدِم:

#include <stdio.h>
#include "mathutils.h"

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

ثم:

gcc main.c mathutils.c -o program

وعادتان تستحقّان التبنّي هنا. غلاف #ifndef / #define / #endif هو حارس تضمين: يمنع معالجة التصاريح مرتين إن ضُمّنت الترويسة عبر مسارين. وmathutils.c تضمّن ترويستها هي - وهو ما يبدو زائدًا وليس كذلك، لأنه يجعل المترجم يفحص كل تعريف مقابل التصريح الذي ستستخدمه الملفات الأخرى. فإن انحرفا يومًا، اكتشفت ذلك فورًا بدل أن تكتشفه وقت الربط. وتتناول ملفات الترويسة البقية.

والأقواس الزاوية (<stdio.h>) تبحث في مسارات التضمين النظامية؛ وعلامات الاقتباس ("mathutils.h") تبحث في دليل مشروعك أولًا. استخدم علامات الاقتباس لترويساتك أنت.

f(void) مقابل f()

هذه مفاجئة فعلًا، وهي سبب كتابة كل مثال في هذه المستندات int main(void).

void ping(void);      /* لا تأخذ وسائط - والاستدعاءات بوسائط مرفوضة */
void pong();          /* لا تقول شيئًا عن الوسائط */

void ping(void); نموذج: يصرّح بأن الدالة لا تأخذ وسائط، فـ ping(1, 2, 3) خطأ ترجمة.

وvoid pong(); تصريح بالأسلوب القديم موروث من C ما قبل المعيار. فهو يصرّح بنوع الإرجاع ولا شيء إطلاقًا عن الوسائط، فلا يستطيع المترجم فحص الاستدعاءات - وpong(1, 2, 3) تُترجَم بهدوء وتفعل شيئًا غير معرَّف.

int main(void) {      /* صحيح: main لا تأخذ وسائط */
int main() {          /* مسموح، لكن فحص الوسائط معطّل */

اكتب (void) دائمًا لدالة بلا وسائط. وتغيّر C23 معنى () ليطابق (void)، وهو ما يزيل الفخّ أخيرًا - لكن كثيرًا من الشيفرات والمترجمات لم تصل بعد، و(void) صحيحة في كل معيار من معايير C.

والتمييز نفسه ينطبق على التعريف. فـ void ping(void) { } تعريف بأسلوب النموذج؛ وvoid ping() { } ليست كذلك، ولا تفعّل فحص الاستدعاءات التي تسبقها.

أخطاء شائعة

  • فاصلة منقوطة على التعريف. int add(int a, int b); { return a + b; } تصرّح عن add ثم تترك كتلة شاردة. ورسالة الخطأ محيّرة؛ والسبب محرف واحد.
  • فاصلة منقوطة ناقصة على النموذج. يقرأ المترجم إلى داخل ما يليه ويبلّغ عن شيء محيّر بعد أسطر عدّة.
  • اختلاف النموذج عن التعريف. conflicting types for .... اجعلهما متطابقين - أو الأفضل، ضمّن الترويسة في ملف التنفيذ ليكون الفحص تلقائيًا.
  • التصريح عن دالة داخل دالة أخرى. مسموح (int add(int, int); داخل main)، لكن نطاق التصريح يصير محصورًا في تلك الدالة. ضع النماذج على نطاق الملف.
  • تعريف دالة في ترويسة. ضمّنها من ملفَّي .c فيبلّغ الرابط عن تعريف مكرّر. فالترويسات تحمل التصاريح؛ والتعريفات تذهب في ملف .c.
  • الاعتماد على () للفحص. فهي لا تفحص. اكتب (void).

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

ما نموذج الدالة في لغة C؟

تصريح عن بصمة الدالة - نوع الإرجاع والاسم وأنواع الوسائط - ينتهي بفاصلة منقوطة بدل جسم: int add(int a, int b);. وهو يخبر المترجم كيف تُستدعى الدالة ليتمكّن من فحص الاستدعاءات، دون أن يقول ما الذي تفعله.

ما الفرق بين التصريح والتعريف في لغة C؟

التصريح يقدّم الاسم والنوع (int add(int, int);) ويمكن أن يظهر مرات كثيرة. أما التعريف فيوفّر الجسم (int add(int a, int b) { return a + b; }) ويجب أن يظهر مرة واحدة بالضبط في البرنامج كله. وكل تعريف هو تصريح أيضًا.

كيف أُصلِح "implicit declaration of function" في لغة C؟

أضف نموذجًا قبل الاستدعاء. فلدوالك أنت، ضع returnType name(paramTypes); قرب أعلى الملف أو في ترويسة؛ ولدوال المكتبات، ضمّن الترويسة الصحيحة - #include <stdio.h> لـ printf، و<stdlib.h> لـ malloc، و<string.h> لـ strlen، و<math.h> لـ sqrt.

ما الفرق بين f(void) وf() في لغة C؟

void f(void); يصرّح عن دالة لا تأخذ وسائط، ويرفض المترجم أي استدعاء يمرّر بعضها. أما void f(); فتصريح بالأسلوب القديم لا يقول شيئًا عن الوسائط، فيتعطّل فحص الوسائط. اكتب (void) دائمًا؛ وC23 تجعل الاثنين يعنيان الشيء نفسه أخيرًا، لكن كثيرًا من الشيفرات والمترجمات لم تصل بعد.

Coddy programming languages illustration

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

ابدأ الآن