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

المؤشّر إلى بنية في لغة C: عامل السهم والبنى على الكومة

كيف تشير إلى بنية في لغة C - عامل السهم، ولماذا تحتاج (*p).x إلى أقواسها، وتمرير البنى بالمؤشّر لتعديلها أو لتجنّب النسخ، وتخصيص بنية بـ malloc، وبناء عقدة قائمة مترابطة.

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

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

أخذ مؤشّر إلى بنية

لا يلزم شيء خاص. فـ & تعطيك العنوان، والنوع "مؤشّر إلى بنية":

تطبع الثلاثة كلها 3. والأخيرتان هما العملية نفسها مكتوبة بطريقتين.

لماذا تحتاج (*ptr).x إلى أقواسها

قد تحاول إسقاطها وتكتب *ptr.x. فتُترجَم إلى شيء مختلف تمامًا، أو ترفض الترجمة أصلًا في الأغلب - لأن عامل العضو . له أولوية أعلى من عامل المراجعة *:

*ptr.x        // parses as *(ptr.x)  -- wrong
(*ptr).x      // dereference first, then take the member -- correct
ptr->x        // the same thing, said shortly

تطلب ptr.x عضوًا من المؤشّر، والمؤشّر لا أعضاء له. وشكوى المترجم ("request for member 'x' in something not a structure or union") خطأ أولوية متنكّر.

ولأن تلك الصيغة المقوّسة قبيحة ويسهل الخطأ فيها، توفّر C العامل ->. وعمليًا لن ترى (*p).x في شيفرة حقيقية إلا نادرًا جدًا؛ فالجأ إلى p->x وانسَ أن الصيغة الطويلة موجودة، إلا بوصفها شرحًا لما يعنيه السهم.

تمرير بنية بالمؤشّر

يمنح وسيط المؤشّر الدالةَ بنيةَ المستدعي الفعلية، فتثبت التغييرات:

عادتان تستحقّان التكوين هنا:

  • ضع const على مؤشّرات القراءة فقط. فـ const struct Point *p تعد بأن الدالة لن تغيّر ما تشير إليه p. فإن أضاف أحدهم لاحقًا p->x = 0 بالداخل، فشلت الترجمة بدلًا من شحن الخطأ. وهي توثّق القصد عند موضع الاستدعاء أيضًا.
  • مرّر مؤشّرًا للبنى الكبيرة حتى عند القراءة. فبنية بمخزن اسم من 64 بايتًا تكلّف 64 بايتًا من النسخ لكل استدعاء بالقيمة؛ والمؤشّر يكلّف 8.

وتبقى البنى الصغيرة مقبولة للتمرير بالقيمة - فـ struct Point بثمانية بايتات تُنسَخ بالرخص نفسه الذي يُنسَخ به مؤشّر، والنسخة بالقيمة لا يمكن أن تكون NULL، ما يزيل نمط فشل بأكمله.

المؤشّرات داخل مصفوفات البنى

يعمل حساب المؤشّرات على مصفوفات البنى تمامًا كما يعمل على مصفوفات int: فـ p + 1 تتقدّم ببنية كاملة، بحشوها متضمّنًا.

يتحوّل اسم المصفوفة إلى مؤشّر إلى عنصرها الأول، فـ staff هي سلفًا struct Employee * عند الاستدعاء. ولهذا أيضًا تحتاج الدالة إلى تمرير n منفصلًا - إذ ستقيس sizeof داخل الدالة المؤشّرَ لا المصفوفة.

تخصيص بنية على الكومة

تموت بنى المكدّس في نهاية نطاقها. ولصنع واحدة تعمّر أطول من الدالة التي أنشأتها - أو لصنع ما يطلبه الإدخال منها - خصّص بـ malloc:

أربعة تفاصيل في تلك الدالة الصغيرة كلها حاملة للحمل:

  • sizeof *e لا sizeof(struct Employee). تُقرأ "حجم ما تشير إليه e"، فإن تغيّر النوع يومًا تبعه التخصيص تلقائيًا. ولا سبيل لأن ينحرف الاثنان عن بعضهما.
  • افحص NULL. تُرجِع malloc القيمة NULL حين تعجز عن تلبية الطلب. وكتابة e->id عبر مؤشّر فارغ خطأ تجزئة.
  • هيّئ كل عضو. لا تصفّر malloc الذاكرة؛ فتصل البنية مليئة بالقمامة. (وcalloc(1, sizeof *e) تصفّرها نيابةً عنك.)
  • على أحدهم تحريرها بـ free. فإرجاع مؤشّر مخصَّص ينقل ذلك الواجب إلى المستدعي. قُل ذلك في تعليق - فقاعدة الملكية غير الواضحة هي كيف تبدأ تسريبات الذاكرة.

المكسب: عقدة قائمة مترابطة

إليك ما لا تستطيع البنى فعله بلا مؤشّرات. فالبنية لا تستطيع احتواء نفسها - إذ ستكون لانهائية الحجم - لكنها تستطيع احتواء مؤشّر إلى واحدة من جنسها، وتلك الحيلة الواحدة تبني كل بنية بيانات مترابطة في C.

لاحظ أن struct Node *next; تشير إلى struct Node بينما ما تزال struct Node قيد التعريف. وهذا مشروع تحديدًا لأن للمؤشّر حجمًا معروفًا بصرف النظر عمّا يشير إليه - فالمترجم لا يحتاج إلى التعريف الكامل بعد. وهو أيضًا الموضع الوحيد الذي يجب فيه استخدام الوسم struct Node حتى لو كتبت typedef أيضًا: فاسم typedef لا يوجد بعد داخل تعريفه نفسه.

وتحفظ حلقة free_list القيمةَ head->next قبل استدعاء free(head). فقراءة head->next بعد التحرير استخدام بعد التحرير - إذ لم تعد الذاكرة ملكك، والقيمة التي تقرؤها هي ما وضعه المخصِّص هناك.

أخطاء شائعة

  • p->x على مؤشّر غير مهيّأ أو NULL. أشيع سبب للانهيار في شيفرة كثيفة البنى. هيّئ المؤشّرات بـ NULL وافحص قبل المراجعة.
  • إرجاع مؤشّر إلى بنية محلّية. فـ struct Point *bad(void) { struct Point p = {1,2}; return &p; } تُرجِع عنوان ذاكرة تكفّ عن الوجود لحظة عودة الدالة. أرجِع البنية بالقيمة، أو خصّصها.
  • التحرير مرتين، أو نسيان التحرير. فكل malloc تقترن بـ free واحدة بالضبط. وبعد التحرير، اضبط المؤشّر على NULL ليتعطّل استخدام لاحق عرضي بصخب بدل إفساد الذاكرة بهدوء.
  • نسخ بنية تحتوي على مؤشّر. فإسناد البنى ينسخ قيمة المؤشّر لا ما يشير إليه. وتشير البنيتان الآن إلى المخزن نفسه، وأيّهما حرّرته أولًا يترك الأخرى معلّقة.

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

ماذا يفعل العامل -> في لغة C؟

p->x تصل إلى العضو x عبر المؤشّر p. وهي مكافئة تمامًا لـ (*p).x - راجِع المؤشّر ثم خُذ العضو. ووُجد السهم لأن المؤشّرات إلى البنى شائعة إلى حدّ يجعل الصيغة الأطول تزحم الشيفرة.

لماذا لا تعمل *p.x مع مؤشّر بنية؟

لأن . ترتبط أوثق من *. فـ *p.x تُحلَّل كـ *(p.x) - خُذ العضو x من p (وهي مؤشّر لا بنية) وراجِع ذلك. أنت تحتاج (*p).x، أو اكتب ببساطة p->x.

هل أمرّر بنية بالقيمة أم بالمؤشّر في لغة C؟

مرّر بالمؤشّر حين يجب على الدالة تعديل بنية المستدعي، أو حين تكون البنية كبيرة ويكون نسخها إهدارًا. ومرّر بالقيمة للبنى الصغيرة للقراءة فقط، حيث تكون النسخة رخيصة والشيفرة أبسط. وضع على وسائط المؤشّرات للقراءة فقط const struct T * ليفرض المترجم الوعد.

كيف تخصّص بنية بـ malloc في لغة C؟

struct Node *n = malloc(sizeof *n); - فصيغة sizeof *n تطلب حجم ما تشير إليه n، فتبقى صحيحة إن غيّرت النوع لاحقًا. افحص النتيجة مقابل NULL، وهيّئ كل عضو، ونفّذ free(n) عند الانتهاء.

Coddy programming languages illustration

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

ابدأ الآن