اذهب إلى المحتوى

التصاريح declarations والتعاريف definitions وإمكانية الوصول accessibility في لغة سي C


Naser Dakhel

قدمنا سابقًا مفهوم النطاق scope والربط linkage، ووضحنا كيف يمكن استخدامهما سويًّا للتحكم بقابلية الوصول لأجزاء معينة ضمن البرنامج، وعمدنا إلى إعطاء وصف غامض لما يحدّد التعريف Definition لأن شرح ذلك سيتسبب بتشويشك في تلك المرحلة ولن يكون مثمرًا لرحلة تعلمك، إلا أنه علينا تحديد هذا الأمر في مرحلة من المراحل وهذا ما سنفعله في هذ الجزئية من السلسلة، كما سنتكلم عن صنف التخزين Storage class لجعل الأمر أكثر إثارة للاهتمام.

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

حتى تفهم القوانين جيدًا، عليك أن تفهم ثلاثة مفاهيم مختلفة -ولكن مرتبطة- وهي كما يدعوها المعيار:

ويشرح المعيار هذه المصطلحات، كما أننا ناقشنا النطاق والربط في المقالة سابقة الذكر لكننا سنعيد ذكرهما بصورةٍ مقتضبة.

محددات صنف التخزين

تندرج خمس كلمات مفتاحية تحت تصنيف محدّدات صنف التخزين storage class specifiers، أحدها هو typedef، وبالرغم من أنه أشبه باختصار عن شبهه بخاصيّة، إلا أننا سنناقش هذه الكلمة المفتاحية بالتفصيل لاحقًا. يتبقى لدينا الكلمات المفتاحية auto و extern و register و static.

تساعدك محددات صنف التخزين على تحديد نوع التخزين المستخدم لكائنات البيانات، وهناك صنف تخزين واحد مسموح به عند التصريح، وهذا الأمر منطقي لأن هناك طريقةً واحدةً لتخزين الأشياء. يُطبق المحدّد الافتراضي على عملية التصريح إذا أُهمل محدد صف التخزين، ويعتمد اختيار المحدد الافتراضي على نوع التصريح إذا كان خارج دالة (تصريح خارجي) أو داخل الدالة (تصريح داخلي)، إذ أن محدد التخزين الافتراضي للتصريح الخارجي هو extern بينما auto هو محدد التخزين الافتراضي للتصريح الداخلي، والاستثناء الوحيد لهذه القاعدة هو تصريح الدوال، إذ إن قيمة المحدد الافتراضي لها هو extern دائمًا.

يمكن أن يؤثر مكان التصريح ومحددات صنف التخزين المستخدمة (أو الافتراضية في حال عدم وجودها) على ربط الاسم، بالإضافة إلى تصريحات الاسم ذاته التي تلي التصريح الأولي، ولحسن الحظ فإن ذلك لا يؤثر على النطاق أو المدة الزمنية. سنستعرض المفاهيم الأسهل أوّلًا.

المدة الزمنية

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

  • إذا كان التصريح داخل دالة،
  • ولم يكن التصريح يحتوي على أي من الكلمتين المفتاحيتين static أو extern.
  • وليس التصريح تصريحًا لدالة.

ستجد أن معاملات الدالة الصورية تطابق هذه القوانين الثلاث دائمًا، وبذلك فهي ذات مدة تلقائية. مع أن تواجد الكلمة المفتاحية static ضمن التصريح يعني أن الكائن ذو مدة ساكنة دون أي شك، إلا أنها ليست الطريقة الوحيدة لإنجاز ذلك، ويسبب هذا بعض اللبس لدى الكثير، وعلينا تقبُّل هذا الأمر.

تُمنح كائنات البيانات المصرحة داخل الدوال محدد صنف التخزين الافتراضي auto، إلا إذا استُخدم محدد آخر لصنف التخزين. لن تحتاج الوصول إلى هذه الكائنات من خارج الدالة في معظم الحالات، لذا ستريد أن تكون عديمة الربط no linkage، وفي هذه الحالة نستخدم الحالة الافتراضية auto أو محدّد صنف التخزين register storage، إذ سيعطينا هذا كائنًا عديم الربط وذا مدة تلقائية. لا يمكن تطبيق الكلمة المفتاحية auto أو register ضمن تصريح يقع خارج دالةٍ ما.

يُعد صنف التخزين register مثيرًا للاهتمام، فعلى الرغم من قلة استخدامه هذه الأيام إلا أنه يقترح على المصرف تخزين الكائن في مسجل register واحد أو أكثر في الذاكرة والذي ينعكس بالإيجاب على سرعة التنفيذ. لا ينفذ المصرف الأوامر بناءً على هذا الأمر إلا أن متغيرات register لا تمتلك أي عنوان ( يُمنع استخدام العامل & معها) لتسهيل الأمر وذلك لأن بعض الحواسيب لا تدعم المسجلات ذات العناوين. قد يتسبب التصريح عن عدة كائنات register بتأثير عكسي فيبطئ البرنامج بدلًا من تسريعه، وذلك لأن المصرف سوف يضطر إلى حجز مزيدٍ من المسجلات عند الدخول إلى (تنفيذ) الدالة وهي عملية بطيئة، أو لن يكون هناك العدد الكافي من المسجلات المتبقية لتُستخدم في العمليات الحسابية الوسيطة.

يعود الاختيار في استخدام المسجلات إلى الآلة المُستخدمة، ويجب استخدام هذه الطريقة فقط عندما توضح الحسابات أن هذا النوع ضروري لتسريع تنفيذ دالة ما بعينها، وعندها يتوجب عليك فحص البرنامج وتجربته. يجب ألّا تصرح عن متغيرات المسجلات أبدًا خلال عملية تطوير البرنامج برأينا، بل تأكد من أن البرنامج يعمل ومن ثم أجرِ بعض القياسات، وعندها قرّر استخدام هذا النوع من المتغيرات حسب النتائج فيما إذا كان يتسبب استخدامها بتحسن ملحوظ في الأداء، ولكن عليك تكرار العملية ذاتها إذا اتبعت هذه الطريقة في كل نوع من المعالجات المسبقة التي تنقل برنامجك إليها، إذ يحتوي كل نوع من المعالجات المسبقة على خصائص مختلفة.

ملاحظة أخيرة بخصوص متغيرات register: يُعد محدد صنف التخزين هذا المحدد الوحيد الممكن استخدامه ضمن نموذج دالة أولي function prototype أو تعريف دالة، ويجري تجاهل محدد صنف التخزين في حالة نموذج الدالة الأولي، بينما يشير تعريف الدالة على أن المعامل الفعلي مخزّن في مسجّل إذا كان الأمر ممكنًا. إليك مثالًا يوضح كيفية يمكن توظيف ذلك:

#include <stdio.h>
#include <stdlib.h>

void func(register int arg1, double arg2);

main(){
      func(5, 2);
      exit(EXIT_SUCCESS);
}

/*
توضح الدالة أنه يمكن التصريح عن المعاملات الصورية باستخدام صنف تخزين مسجّل
*/
void func(register int arg1, double arg2){

      /*
هذا الاستخدام لأهداف توضيحية، فلا أحد يكتب ذلك في هذا السياق
‪لا يمكن أخذ عنوان ‫arg1 حتى لو أردت ذلك
       */
      double *fp = &arg2;

      while(arg1){
              printf("res = %f\n", arg1 * (*fp));
              arg1--;
      }
}

[مثال 1]

تعتمد إذًا المدة الزمنية لكائن على محدد صنف التخزين المستخدَم -بغض النظر عن كون الكائن كائن بيانات أو دالة-، كما تعتمد على مكان التصريح (في كتلة داخلية أو على كامل نطاق الملف؟). يعتمد الربط أيضًا على محدد صنف التخزين، إضافةً إلى نوع الكائن ونطاق التصريح عنه. يوضح الجدول 1 والجدول 2 التاليين مدة التخزين الناتجة والربط لكل من الحالات الممكنة عند استخدام محددات صنف التخزين الممكنة، وموضع التصريح. يُعد ربط الكائنات باستخدام المدة الساكنة أكثر تعقيدًا، لذا ننصحك باستخدام هذه الجداول لتوجيهك في الحالات البسيطة وانتظر قليلًا حتى نصل إلى الجزء الذي نتكلم فيه عن التعريفات.

محدد صنف التخزين دالة أو كائن بيانات الربط المدة الزمنية
static أحدهما داخلي ساكنة
extern أحدهما خارجي غالبًا ساكنة
لا يوجد دالة خارجي غالبًا ساكنة
لا يوجد كائن بيانات خارجي ساكنة

أهملنا في الجدول السابق المحددين register و auto لأنه من غير المسموح استخدامهما على نطاق التصريحات الخارجية (على نطاق الملف).

محدد صنف التخزين دالة أو كائن بيانات الربط المدة الزمنية
register كائن بيانات فقط لا يوجد تلقائية
auto كائن بيانات فقط لا يوجد تلقائية
static كائن بيانات فقط لا يوجد ساكنة
extern كلاهما خارجي غالبًا ساكنة
لا يوجد none كائن بيانات لا يوجد تلقائية
لا يوجد none دالة خارجي غالبًا ساكنة

تحتفظ المتغيرات الساكنة static الداخلية بقيمها بين استدعاءات الدوال التي تحتويها، وهذا شيءٌ مفيدٌ جدًا في بعض الحالات.

النطاق

علينا الآن إلقاء نظرة على نطاق أسماء الكائنات مجددًا، والذي يعرف أين ومتى يكون لاسمٍ ما معنًى محدد. هناك أنواعٌ مختلفة للنطاق هي:

  • نطاق دالة.
  • نطاق ملف.
  • نطاق كتلة.
  • نطاق نموذج دالة أولي.

النطاق الأسهل هو نطاق الدالة، إذ ينطبق ذلك على عناوين labels الأسماء المرئية ضمن دالة ما صُرّح عنها بداخلها، ولا يمكن لعنوانين داخل نفس الدالة أن يمتلكان الاسم ذاته، لكن يمكن للعنوان استخدام الاسم ذاته إذا كان ضمن أي دالةٍ أخرى بحكم أن النطاق هو نطاق دالة. ليست العناوين كائنات فهي لا تمتلك أي مساحة تخزينية ولا يعني مفهوم الربط والمدة الزمنية أي معنًى في عالمها.

يمتلك أي اسم مُصرّح عنه خارج الدالة نطاق ملف، وهذا يعني أن الاسم قابلٌ للاستخدام ضمن أي نقطة من البرنامج من لحظة التصريح عنه إلى نهاية ملف الشيفرة المصدرية الذي يحتوي ذلك التصريح، ومن الممكن طبعًا لهذه الأسماء أن تصبح مخفية مؤقتًا باستخدام تصريحات ضمن تعليمات مركبة، لأنه كما نعلم، ينبغي على تعريفات الدالة أن تكون خارج الدوال الأخرى حتى يكون اسم الدالة في التعريف ذو نطاق ملف.

يمتلك الاسم المُصرّح عنه بداخل تعليمة مركبة أو معامل دالة صوري نطاق كتلة ومن الممكن استخدامه ضمن الكتلة حتى الوصول للقوس { الذي يغلِق التعليمة المركبة، ويُخفي أي تصريح لاسم ضمن تعليمة مركبة أي تصريح خارجي آخر للاسم ذاته حتى نهاية التعليمة المركبة.

يعدّ نطاق النموذج الأولي للدالة مثالًا مميزًا وبسيطًا من النطاقات، إذ يمتد تصريح الاسم حتى نهاية النموذج الأولي للدالة فقط، وهذا يعني أن ما يلي خاطئ (باستخدام نفس الاسم مرتين):

void func(int i, int i);

وهذا هو الاستعمال الصحيح:

void func(int i, int j);

وتختفي الأسماء المُصرّح عنها بداخل الأقواس خارجها. نطاق الاسم مستقل تمامًا عن أي محدد صنف تخزين مُستخدم في التصريح.

الربط

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

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

هناك ثلاثة أنواع مختلفة من الربط:

  • الربط الخارجي.
  • الربط الداخلي.
  • عديم الربط.

إذا كان اسم الكائن ذو ربط خارجي فهذا يعني أن جميع النسخ instances الموجودة في البرنامج -الذي قد يكون مؤلفًا من عدد من ملفات الشيفرة المصدرية والمكتبات- تعود إلى الكائن ذاته، ويعني الربط الداخلي لكائن ما أن نُسخ هذا الكائن الموجودة في ملف الشيفرة المصدرية نفسه فقط تُشير إلى الشيء ذاته، بينما يعني الكائن عديم الربط أن كل كائن ذي اسم مماثل له هو كائنٌ منفصلٌ عنه.

الربط والتعاريف

يجب لكل كائن بيانات أو دالة مُستخدمة في برنامج (عدا معاملات عامل sizeof) أن تمتلك تعريفًا واحدًا فقط، ومع أننا لم نتطرق إلى هذا بعد، إلا أن هذا الأمر مهمٌ جدًا؛ ويعود السبب بعدم تطرقنا لهذا الأمر إلى استخدام جميع أمثلتنا كائنات بيانات ذات مدة تلقائية فقط وكون تصاريحها تعاريفًا، أو دوالًا كنا قد عرّفناها من خلال كتابة متنها.

تعني القاعدة السابقة أنه يجب للكائنات ذات الربط الخارجي أن تحتوي على تعريفٍ واحد فقط ضمن كامل البرنامج، ويجب للكائنات ذات الربط الداخلي (المقيدة داخل ملف شيفرة مصدرية واحد) أن يُعرّف عنها لمرة واحدة فقط ضمن الملف المُصرّح فيه عنها، كما أن للكائنات عديمة الربط تعريفٌ واحد فقط، إذ أن التصريح عنها هو تعريفٌ أيضًا.

لجمع النقاط الآنف ذكرها، نسأل الأسئلة التالية:

  1. كيف أستطيع الحصول على نوع الربط الذي أريده؟
  2. ما الشيء الذي يحدد التعريف؟

علينا أن ننظر إلى الربط أولًا ومن ثم التعريف.

إذّا، كيف نحصل على التعريف المناسب لاسمٍ ما؟ القوانين معقدة بعض الشيء.

  1. ينتج التصريح خارج دالة (نطاق ملف) تحتوي على محدد صنف تخزين ساكن ربطًا داخليًا لهذا الاسم، ويحدد المعيار وجوب وجود تصاريح الدالة التي تحتوي على الكلمة المفتاحية static على مستوى الملف وخارج أي كتلة برمجية.
  2. إذا احتوى التصريح على محدد صنف التخزين extern، أو إذا كان تصريح الدالة لا يحتوي على محدد صنف التخزين، أو كلا الحالتين، فهذا يعني:
    • إذا وجِد تصريح للمعرف ذاته بنطاق ملف، فهذا يعني أن الربط الناتج مماثلٌ لهذا التصريح السابق المرئي.
    • وإلا، فالنتيجة هي ربط خارجي.
  3. إذا لم يكن التصريح ذو نطاق الملف تصريحًا لدالة أو لم يحتوي على محدد صنف تخزين واضح، فالنتيجة هي ربط خارجي.
  4. أي شكل آخر من التصاريح سيكون عديم الربط.
  5. إذا وجِد معرف ذو ربط داخلي وخارجي في ذات الوقت ضمن ملف شيفرة مصدرية ما، فالنتيجة غير محددة.

استُخدمت القوانين السابقة لكتابة جدول الربط السابق (جدول 2) دون تطبيق كامل للقاعدة 2، وهذا السبب في استخدامنا الكلمة "خارجي غالبًا"، وتسمح لك القاعدة 2 بتحديد الربط بدقة في هذه الحالات.

ما الذي يجعل التصريح تعريفًا؟

  • التصاريح التي تعطينا كائنات عديمة الربط هي تعاريف أيضًا.
  • التصاريح التي تتضمن قيمةً أولية هي تعاريف دائمًا، وهذا يتضمن تهيئة دالة ما بكتابة متن الدالة، ويمكن للتصاريح ذات نطاق الكتلة أن تحتوي على قيم أولية فقط في حال كانت عديمة الربط.
  • وإلا، فالتصريح عن الاسم على نطاق ملف بدون محدد صنف التخزين أو مع محدد صنف التخزين static هو تعريف مبدئي tentative definition، وإذا احتوى ملف شيفرة مصدرية على تعريف مبدئي واحد أو أكثر لكائن ما وكان الملف لا يحتوي على أي تعاريف فعلية، يصبح للكائن تعريفٌ افتراضيٌ وهو مشابه لحالة إسناد القيمة الأولية "0" إليه (تُهيّأ عناصر المصفوفات والهياكل جميعها إلى قيمة "0")، ولا يوجد للدوال تعريفٌ مبدئي.

طبقًا لما سبق، لا تتسبب التصريحات التي تحتوي على محدد صنف تخزين خارجي extern بتعريف، إلا إذا ضمّنت قيمةً أوليةً للتصريح.

الاستخدام العملي لكل من الربط والتعاريف

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

أنواع إمكانية أو قابلية الوصول الثلاث التي ستريدها لكائنات البيانات أو الدوال هي:

  • على كامل نطاق البرنامج.
  • مقيّد بنطاق ملف شيفرة مصدرية واحد.
  • مقيّد بنطاق دالة واحدة، أو تعليمة مركبة واحدة.

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

هيكل ملف الشيفرة المصدرية

[شكل 1 هيكل ملف الشيفرة المصدرية]

يمكن أن تُسبق تصاريح الربط الخارجية بالكلمة المفتاحية extern وتصاريح الربط الداخلية بالكلمة المفتاحية static. إليك مثالًا عن ذلك:

/* مثال عن هيكل ملف الشيفرة المصدرية */
#include <stdio.h>

/*
 يمكن الوصول للأشياء ذات الربط الخارجي عبر البرنامج
ما يلي هو تصاريح وليس تعاريف، لذا نفترض أن التعاريف في مكان ما آخر
*/

extern int important_variable;
extern int library_func(double, int);

/*
تعاريف ذات ربط خارجي
*/
extern int ext_int_def = 0;     /* تعريف صريح */
int tent_ext_int_def;           /* تعريف مبدئي */

/*
* يمكن الوصول للأشياء ذات الربط الداخلي فقط من داخل الملف
* يعني استخدام المحدد الساكن أن التعريفات هي تعريفات مبدئية
*/

static int less_important_variable;
static struct{
        int member_1;
        int member_2;
}local_struct;

/*
 ذات ربط داخلي لكنها ليست بتعريف مبدئي لأنها دالة
*/
static void lf(void);

/*
* التعريف مع الربط الداخلي
*/
static float int_link_f_def = 5.3;

/*
وأحيرًا إليك تعاريف الدوال ضمن هذا الملف
*/

/*
للدالة التالية ربط خارجي ويمكن استدعاؤها من أي مكان ضمن البرنامج
*/
void f1(int a){}

/*
يمكن استخدام الدالتين التاليتين باسمهما ضمن هذا الملف
*/
static int local_function(int a1, int a2){
        return(a1 * a2);
}

static void lf(void){
        /*
    متغير ساكن عديم الربط، لذا يمكن استخدامه فقط ضمن هذه الدالة، 
وهو تعريف بحكم أنه عديم الربط
         */
        static int count;
        /*
    متغير تلقائي عديم الربط ولكنه ذو مُهيّأ بقيمة أولية
         */
        int i = 1;

        printf("lf called for time no %d\n", ++count);
}
/*
ضُمّنت التعاريف الفعلية لجميع التعاريف المبدئية المتبقية في نهاية الملف
*/

[مثال 2]

نقترح عليك قراءة الفقرات السابقة مجددًا لملاحظة القوانين التي طُبّقت في المثال 2.

ترجمة -وبتصرف- لقسم من الفصل Specialized Areas of C من كتاب The C Book.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...