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

وسائط الدوال في لغة C: التمرير بالقيمة والمؤشّرات والمصفوفات

تمرّر C كل شيء بالقيمة - فالدوال تحصل على نسخ لا على الأصول. ماذا يعني ذلك عمليًا، وكيف تحاكي المؤشّرات التمرير بالمرجع، ولماذا تتصرّف المصفوفات على نحو مختلف، وكيف تُعيد عدة نتائج.

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

الوسائط الشكلية والفعلية

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

  • الوسيط الشكلي هو المتغيّر في تعريف الدالة - int n في int square(int n).
  • الوسيط الفعلي هو القيمة التي تزوّدها عند موضع الاستدعاء - 4 في square(4).

واستدعاء دالة يُنشئ وسائطها الشكلية كمتغيّرات محلية جديدة وينسخ الوسائط الفعلية إليها. وتلك النسخة هي موضوع هذه الصفحة بأكمله.

C تمرّر بالقيمة - دائمًا

هذه هي القاعدة، ولا استثناء لها في C: الدالة تتلقّى نسخًا من وسائطها. والإسناد إلى وسيط يغيّر النسخة فقط.

ما زالت value تساوي 42. فقد حصلت الدالة على نسخة اسمها x، وضبطت النسخة على صفر، واختفت النسخة حين عادت الدالة. وإعادة تسمية الوسيط إلى value لا تغيّر شيئًا - فالاسم لا يربط بينهما؛ إنهما متغيّران مختلفان.

والأمر نفسه صحيح لكل نوع: int وdouble وchar ومؤشّر، بل وبنية كاملة (تُنسَخ حقلًا حقلًا). وإن كنت قادمًا من بايثون أو جافا أو جافاسكريبت، فلاحظ أن تلك اللغات تمرّر مراجع الكائنات بالقيمة، فيكون تحوير كائن داخل دالة مرئيًا في الخارج. أما C فلا تملك ذلك - إنها تنسخ الكائن نفسه.

والتمرير بالقيمة ميزة لا قيد. فلا تستطيع الدالة إفساد بيانات المستدعي عرضًا، وهو ما يجعل التفكير فيها أسهل بكثير.

الإخفاق الكلاسيكي: swap

العرض المدرسي هو دالة تحاول تبادل قيمتين:

المنطق صحيح - فهي تبادل a وb فعلًا. لكن a وb نسختان، فيُرمى العمل لحظة عودة الدالة.

محاكاة التمرير بالمرجع بالمؤشّرات

لتسمح لدالة بتغيير متغيّر المستدعي، أعطها عنوان المتغيّر بدل قيمته. وذلك العنوان نفسه يُمرَّر بالقيمة - فالمؤشّر يُنسَخ - لكن النسخة ما زالت تشير إلى الكائن الأصلي، فالكتابة عبرها تصل إلى ذاكرة المستدعي.

ثلاث قطع صياغية تحمل الفكرة كلها:

  • int *a في قائمة الوسائط تعرّف a بأنها "مؤشّر إلى int".
  • &x عند موضع الاستدعاء تُنتج عنوان x.
  • *a داخل الدالة تعني "العدد int الذي تشير إليه a" - قابل للقراءة والإسناد.

وهذا ما يعنيه مبرمجو C بـ "التمرير بالمرجع"، ويستحقّ الدقّة: فهو ما زال تمريرًا بالقيمة. فـ المؤشّر يُنسَخ. والإسناد إلى a نفسها (a = NULL;) كان سيغيّر النسخة فقط؛ أما الإسناد إلى *a فيغيّر متغيّر المستدعي. وتغطّي المؤشّرات النموذج كاملًا.

والثمن أن موضع الاستدعاء لم يعد يخبرك هل يمكن للمتغيّر أن يتغيّر - وهذا بالضبط سبب اشتراط &. فـ swap(x, y) لن تُترجَم؛ عليك كتابة swap(&x, &y)، وتلك & هي العلامة المرئية على أن هذا الاستدعاء قد يعدّل x.

إعادة عدة نتائج

تُنتج return قيمة واحدة، فتسافر المخرجات الإضافية عبر وسائط مؤشّرات. والشكل الشائع هو رمز حالة كقيمة إرجاع والنتائج الحقيقية مكتوبة عبر مؤشّرات:

لاحظ أن q وr تحتفظان بقيمتيهما السابقتين حين يفشل الاستدعاء - فقد عادت الدالة قبل كتابة أي شيء. وذلك هو العقد الذي على المستدعي احترامه، وهو سبب مجيء فحص الحالة قبل استخدام المخرجات.

والبديل هو إعادة بنية تحمل الحقلين، وهو ما يتجنّب المؤشّرات كليًا ويكون التصميم الأجمل غالبًا حين تنتمي القيمتان معًا فعلًا.

المصفوفات مختلفة

لا تتبع المصفوفات قاعدة النسخ، وهذا يعثّر الجميع تقريبًا. فحين تمرّر مصفوفة، فإنها تتحوّل إلى مؤشّر إلى عنصرها الأول - فتحصل الدالة على عنوان، لا على نسخة من البيانات.

تغيّرت مصفوفة المستدعي فعلًا، بلا & في أي مكان - لأن data هي أصلًا عنوان في هذا السياق. وثلاث نتائج:

int a[] وint *a تعنيان الشيء نفسه بالضبط في قائمة وسائط. بل حتى int a[100] كذلك؛ فالحجم يُتجاهَل. وكتابة int a[] توثّق القصد، لكن لا تقرأها على أنها "تُنسَخ مصفوفة".

وsizeof داخل الدالة خاطئة. وهذه هي العلّة:

في main تبلغ sizeof(data) عشرين بايتًا ويخرج الطول 5. وداخل الدالة تكون sizeof(a) حجم مؤشّر - 8 على نظام 64 بتًا - فيكون "الطول" 2. فحجم المصفوفة ببساطة غير متاح هناك. مرّر الطول كوسيط منفصل دائمًا. والمترجمات الحديثة تحذّر من هذه مع -Wall.

استخدم const حين لن تكتب. فـ const int a[] تقول إن الدالة تقرأ فقط، وهو ما يفرضه المترجم ويستطيع القارئ الاعتماد عليه:

int sum(const int a[], int n);        /* يَعِد بعدم تعديل المصفوفة */

والسلاسل النصية مصفوفات أيضًا، فينطبق الأمر نفسه: فـ void greet(const char *name) هي البصمة القياسية لدالة تقرأ سلسلة نصية دون تغييرها.

وماذا عن التكلفة؟

تمرير بنية كبيرة بالقيمة ينسخ كل بايت فيها في كل استدعاء. وبالنسبة لبنية بحفنة حقول يكون ذلك مقبولًا وأسرع غالبًا من الالتفاف الذي يضيفه المؤشّر. أما للكبيرة فمرّر مؤشّرًا - وسِمْه const إن كانت الدالة تقرأ فقط:

struct Config { char name[64]; int flags[32]; double weights[128]; };

void applyConfig(const struct Config *cfg);   /* no 512-byte copy per call */

const struct Config *cfg تعني "مؤشّر إلى Config لن أعدّله"، وهو ما يمنحك كفاءة المؤشّر مع أمان النسخة.

أخطاء شائعة

  • توقّع أن يغيّر وسيط عادي متغيّر المستدعي. لا يستطيع. مرّر مؤشّرًا.
  • نسيان & عند موضع الاستدعاء. swap(x, y) حيث تريد swap مؤشّرات خطأ نوع - يلتقطه المترجم، لكن فقط إن كان النموذج ضمن النطاق.
  • نسيان * داخل الدالة. a = b; على وسيطي مؤشّرين تبادل النسختين المحليتين، لا القيمتين. أنت تريد *a = *b;.
  • استخدام sizeof على وسيط مصفوفة. خاطئ دائمًا. مرّر الطول.
  • إعادة مؤشّر إلى متغيّر محلي. فالمحلي يموت حين تعود الدالة، فيحمل المستدعي مؤشّرًا معلّقًا، وهو سلوك غير معرَّف. أعِد بالقيمة، أو اكتب في مخزن يوفّره المستدعي، أو خصّص بـ malloc.
  • عدم فحص وسيط مؤشّر بحثًا عن NULL. فدالة تفكّ إشارة أيًّا ما سُلّم لها ستنهار على وسيط فارغ.

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

هل تملك C تمريرًا بالمرجع؟

لا. للغة C نمط واحد بالضبط: التمرير بالقيمة. فالدالة تتلقّى دائمًا نسخة من كل وسيط، لذا فإن الإسناد إلى وسيط لا يمسّ متغيّر المستدعي أبدًا. وما يبدو تمريرًا بالمرجع هو تمرير مؤشّر بالقيمة - فالمؤشّر يُنسَخ، لكنه ما زال يشير إلى كائن المستدعي، فتستطيع الدالة تعديله عبر المؤشّر.

كيف تمرّر مصفوفة إلى دالة في لغة C؟

اكتب الوسيط بصيغة int a[] أو int *a - وهما يعنيان الشيء نفسه - ومرّر الطول كوسيط منفصل: int sum(const int a[], int n). ولا تُنسَخ المصفوفة؛ بل تتلقّى الدالة مؤشّرًا إلى عنصرها الأول، فتكون التغييرات على a[i] مرئية للمستدعي.

لماذا لا تعمل sizeof على وسيط مصفوفة في لغة C؟

لأن الوسيط مؤشّر في الحقيقة. فـ sizeof(a) داخل الدالة يعطي حجم مؤشّر (8 بايتات عادةً)، لا حجم المصفوفة - فتُنتج sizeof(a)/sizeof(a[0]) شيئًا مثل 2 بدل عدد العناصر. مرّر الطول صراحةً دائمًا.

كيف يمكن لدالة C أن تُعيد أكثر من قيمة؟

مرّر مؤشّرات للنتائج الإضافية واكتب عبرها: int divide(int a, int b, int *quotient, int *remainder) تُعيد راية نجاح وتملأ كلا الخرجين. والبديل هو إعادة بنية تحزم القيم معًا.

Coddy programming languages illustration

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

ابدأ الآن