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

الاتّحاد في لغة C: الذاكرة المشتركة والاتّحاد مقابل البنية والاتّحادات الموسومة

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

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

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

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

تعريف اتّحاد واستخدامه

تعكس الصياغة البنية تمامًا؛ ولا تتغيّر إلا الكلمة المفتاحية.

يُوصَل إلى الأعضاء بـ . (أو -> عبر مؤشّر)، تمامًا كما مع بنية. والمختلف أن العضو المكتوب أحدثَ الكتابة وحده يحمل قيمة ذات معنى. فبعد v.f = 3.5f، لا تعطيك قراءة v.i القيمة 3 - بل تعطيك أي عدد صحيح يصادف أن يهجئه نمط بتات 3.5f.

الحجم: أكبر عضو يفوز

قارن توزيع الذاكرة لبنية واتّحاد بالأعضاء نفسها:

على جهاز نموذجي تكون البنية 24 بايتًا (4 للعدد الصحيح، و8 للعدد العشري، و1 للمحرف، زائد الحشو) بينما يكون الاتّحاد 8 - حجم double فيه، مقرَّبًا لأعلى للمحاذاة. والعناوين الثلاثة المطبوعة متطابقة، وهذه هي قصة الاتّحادات كلها في سطر خرج واحد.

ولاحظ تحويلات %p إلى void *. فـ printf تتوقّع ذلك بالضبط لأجل %p؛ وتمرير نوع مؤشّر مختلف سلوك غير معرَّف وإن بدا عاملًا عادةً. انظر محدّدات التنسيق.

تهيئة اتّحاد

مهيّئ بأقواس معقوفة بلا مسمٍّ يهيّئ العضو الأول:

والصيغة المسمّاة هي ما ينبغي استخدامه. فـ {42} تعتمد بصمت على ترتيب الأعضاء، فإعادة ترتيب التعريف لاحقًا تغيّر أي عضو يُهيَّأ - وهو خطأ خبيث فعلًا، لأن لا شيء في الشيفرة يبدو مختلفًا.

المشكلة الحقيقية: أي عضو حيّ؟

لا يسجّل الاتّحاد أي عضو كتبته آخرًا. فهو مجرّد بايتات؛ والمعرفة تعيش في رأسك، وهذا بالضبط حيث تضيع المعرفة.

الرقم المطبوع كبير وغريب - نمط بتات 1.0f مقروءًا كـ int. لم ينهر شيء ولم يحذّر شيء، والبرنامج خاطئ بهدوء. فقد فعل الاتّحاد بالضبط ما وعد به؛ وكان الخطأ خطأنا في نسيان أي عضو كان حيًّا.

الحلّ: اتّحاد موسوم

الحلّ المعياري هو قرن الاتّحاد بـتعداد يسجّل العضو الحيّ، وتغليف الاثنين في بنية. ويُسمّى ذلك المزيج اتّحادًا موسومًا (أو اتّحادًا مميَّزًا)، وهو كيف ينبغي أن تكتب كل اتّحاد في شيفرة التطبيقات عمليًا.

تمرّ كل قراءة الآن عبر switch على kind، فيستحيل قراءة عضو لم يُكتب قط - ما دامت كل كتابة تضبط الوسم أيضًا. وتغليف الكتابات في دوال بانية صغيرة (value_from_int وvalue_from_string) هو الطريقة المعتادة لجعل ذلك مستحيل النسيان.

والتوفير في الذاكرة حقيقي: فكل Value هنا تكلّف 24 بايتًا من الحمولة زائد الوسم، بدلًا من 4 + 4 + 24 لبنية تحمل الثلاثة. ومع مئة ألف منها، يهمّ ذلك.

والترجمة بـ -Wall تضيف شبكة أمان ثانية: فإن أضفت لاحقًا VAL_BOOL إلى التعداد ونسيت case له، حذّرك GCC من قيمة التعداد غير المعالَجة.

الاتّحادات مجهولة الاسم

تسمح C11 بعضو اتّحاد بلا اسم داخل بنية، فترقّي أعضاءه إلى فضاء أسماء البنية الخارجية:

ولأن الاتّحاد نفسه بلا اسم، تكتب s->circle.r بدل s->as.circle.r. أقصر قراءةً، بثمن إخفاء وجود اتّحاد أصلًا - وهو مقبول حين يكون الوسم بجواره مباشرةً.

أين تكسب الاتّحادات قيمتها فعلًا

أربعة استخدامات متكرّرة:

  • القيم المتغايرة. المفسّرات، ومحلّلات JSON والتهيئة، وطوابير الرسائل، كلها تحمل قيمًا يُحدَّد نوعها في وقت التشغيل. والاتّحاد الموسوم هو التمثيل النموذجي.
  • السجلّات الضيّقة الذاكرة. حين تحمل بنية عدة حقول يستبعد بعضها بعضًا وتملك ملايين منها، يكون تراكبها توفيرًا مباشرًا.
  • توزيعات البروتوكولات والعتاد. فحزمة تعتمد حمولتها على بايت ترويسة تنطبق طبيعيًا على اتّحاد موسوم، وكذلك سجلّات الأجهزة.
  • فحص البايتات. فتراكب قيمة مع مصفوفة unsigned char[] يتيح لك النظر في بايتاتها المفردة، مثلًا لتحديد ترتيب البايتات:

وهذا إعادة تفسير للنوع - أي قراءة البايتات عمدًا بنوع مختلف. والقراءة عبر اتّحاد بهذه الطريقة مسموح بها صراحةً كنتيجة يحدّدها التطبيق في C (بخلاف تحويل المؤشّرات بين أنواع غير مترابطة، وهو ما يكسر قواعد التسمية المستعارة)، وفحص البايتات كـ unsigned char آمن دائمًا. أما إعادة تفسير int كـ float فمسألة أخرى: فالنتيجة تعتمد كليًا على تمثيل منصّتك، فأبقِها خارج الشيفرة القابلة للنقل.

أخطاء شائعة

  • قراءة عضو لم تكتبه. الخطر الرئيسي. استخدم وسمًا.
  • افتراض أن الاتّحاد يحوّل. إنه لا يفعل. فـ u.i = 3; float f = u.f; تعيد تفسير البتات؛ وint i = 3; float f = i; تحوّل. انظر تحويل الأنواع.
  • وضع مؤشّر في اتّحاد وفقدان أثره. فإن حمل أحد الفروع مؤشّر char * خصّصته، فإن الكتابة فوق الاتّحاد بعضو مختلف تسرّبه - فلا يبقى شيء يشير إلى المخزن. حرّر قبل تبديل الفروع.
  • توقّع أن يفحص المترجم. لن يفعل. فالاتّحادات من الخصائص القليلة في C التي لا تقدّم فيها اللغة أي مساعدة تتجاوز الحجم والمحاذاة؛ والوسم حاجزك الوحيد.

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

ما الاتّحاد في لغة C؟

الاتّحاد نوع يتشارك كل أعضائه الذاكرة نفسها. فالكتابة في عضو تكتب فوق الأعضاء الأخرى، فيحمل الاتّحاد عضوًا واحدًا من أعضائه في كل لحظة. ويُعرَّف كالبنية لكن بالكلمة المفتاحية union: union Value { int i; float f; };.

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

تمنح البنية كل عضو تخزينه الخاص وتحملهم كلهم في آن، فحجمها لا يقلّ عن مجموع أعضائها. أما الاتّحاد فيضع كل عضو فوق الآخر في العنوان نفسه ويحمل واحدًا في كل مرة، فحجمه حجم أكبر أعضائه.

ما حجم الاتّحاد في لغة C؟

كبير بما يكفي لأكبر أعضائه، مقرَّبًا لأعلى للمحاذاة. فاتّحاد من int (4 بايتات) وdouble (8 بايتات) حجمه 8 بايتات - لا 12. وsizeof هي وسيلة التحقّق على منصّتك.

ماذا يحدث إن قرأت عضو اتّحاد لم تكتبه؟

تعيد تفسير البايتات نفسها كنوع مختلف. فكتابة u.i = 1 ثم قراءة u.f لا تحوّل - بل تقرأ نمط بتات العدد الصحيح كعدد عشري، فتعطي رقمًا بلا معنى. وفي C المعيارية ذلك غير محدّد في أحسن الأحوال، ولهذا وُجد نمط الاتّحاد الموسوم: احتفظ بوسم إلى جانبه يقول أي عضو حيّ.

Coddy programming languages illustration

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

ابدأ الآن