على الرغم من تفادينا لموضوعَي النطاق Scope والربط Linkage في أمثلتنا البسيطة سابقًا، إلا أن الوقت قد حان لشرح هذين المفهومين وأثرهما على قابلية الوصول للكائنات المختلفة في برنامج سي C، ولكن لمَ علينا الاكتراث بذلك على أي حال؟ لأن البرامج العملية التي نستخدمها تُبنى من عدّة ملفات ومكتبات، وبالتالي من المهم للدوال في ملف ما أن تكون قادرةً على الإشارة إلى دوال، أو كائنات في ملفات أو مكتبات أخرى، وهناك عدّة قوانين ومفاهيم تجعل من ذلك ممكنًا.
عُد لاحقًا إلى هذه الجزئية إن كنت جديدًا على لغة سي، لأن هناك بعض المفاهيم الأهم التي يجب عليك معرفتها أوّلًا.
الربط Linkage
هناك نوعان أساسيان من الكائنات في لغة سي C، هما: الكائنات الخارجية والكائنات الداخلية، والفرق بين الاثنين مُعتمدٌ على الدوال؛ إذ أن أيّ شيء يُصرّح عنه خارج الدالة فهو خارجي؛ وأي شيء يُصرّح عنه داخل الدالة بما فيه مُعاملات الدالة فهو داخلي. بما أننا لا نستطيع تعريف دالة ما داخل دالة أخرى، فهذا يعني أن الدوال هي كائنات خارجية دائمًا، وإذا نظرنا إلى بنية برنامج سي على المستوى الخارجي، سنلاحظ أنه يمثّل مجموعةً من الكائنات الخارجية External objects.
يمكن للكائنات الخارجية فقط أن تُشارك في هذا الاتصال عبر الملفات والمكتبات، وتُعرف قابلية الوصول للكائنات هذه من ملف إلى آخر أو ضمن الملف نفسه وفقًا للمعيار باسم الربط Linkage، وهناك ثلاثة أنواع للربط، هي: الربط الخارجي External linkage والربط الداخلي Internal Linkage وعديم الربط No linkage. يكون أي شيء داخلي في الدالة سواءٌ كان وسطاء الدالة أو متغيراتها عديم الربط دائمًا ويمكن الوصول إليه من داخل الدالة فقط، ويمكنك التصريح عن الشيء الذي تريده داخل الدالة مسبوقًا بالكلمة المفتاحية extern
لتجاوز هذا القيد، وهذا سيدل على أن الكائن ليس داخليًّا، وليس عليك القلق بهذا الشأن في الوقت الحالي.
تكون الكائنات ذات الربط الخارجي موجودةً على المستوى الخارجي لبنية البرنامج، وهذا هو نوع الربط الافتراضي للدوال ولأي شيء آخر يُصرَّح عنه خارج الدوال، وتُشير جميع الأسماء المماثلة لاسم الكائن ذو الربط الخارجي إلى الكائن نفسه. ستحصل على سلوك غير محدد من البرنامج، إذا صرحت عن كائن بنفس الاسم مرتين أو أكثر بربط خارجي وبأنواع غير متوافقة. المثال الذي يأتي إلى بالنا مباشرةً بخصوص الربط الخارجي هو الدالة printf
والمُصرّح عنها في ملف الترويسة <stdio.h>
على النحو التالي:
int printf(const char *, ...);
يمكننا فورًا بالنظر إلى التصريح السابق معرفة أن الدالة printf
تُعيد قيمةً من نوع int
باستخدام النموذج الأولي الموضّح، كما نعرف أن للدالة ربطًا خارجيًّا لأنها كائن خارجي (تذكّر، كل دالة هي كائن خارجي افتراضيًا)، وبالتالي فنحن نقصد هذه الدالة تحديدًا عندما نكتب الاسم printf
في أي مكان ضمن البرنامج، إذا استخدمنا الربط الخارجي.
ستحتاج في بعض الأحيان لطريقة تمكّنك من التصريح عن الدوال والكائنات الأخرى في ملفٍ واحد بحيث تسمح لهم بالإشارة إلى بعضهم البعض دون القدرة على الوصول إلى تلك الكائنات والدوال من خارج الملف. نحتاج هذه الطريقة غالبًا في الوحدات modules التي تدعم دوال المكتبات، إذ من الأفضل في هذه الحالة إخفاء بعض الكائنات التي تجعل استخدام هذه الدالة ضمن المكتبة ممكنًا، فهي ليست ضرورية المعرفة لمستخدم المكتبة وستكون سببًا للإزعاج لا غير، ونستطيع تحقيق ذلك الأمر عن طريق استخدام الربط الداخلي.
تُشير الأسماء ذات الربط الداخلي للكائن ذاته ضمن ملف الشيفرة المصدرية الواحد، ويمكنك التصريح عن كائن ذي ربط داخلي عن طريق بدء التصريح بالكلمة المفتاحية "static" التي ستغيّر ربط الكائن من ربط خارجي (افتراضي) إلى ربط داخلي، كما يمكنك التصريح عن كائنات داخلية باستخدام "static" بهدف استخدامٍ آخر ولكننا لن نتطرق لهذا الاستخدام حاليًا.
من المُربك استخدام المصطلحين "داخلي" و"خارجي" لوصف نوع الربط ونوع الكائن، ولكن يعود ذلك لأسباب تاريخية، إذ سيتذكر مبرمجو لغة سي القدماء كون الاستخدامين متساويين، وأن استخدام أحدهما يتضمن تحقّق الآخر، ولكن تغيّر ذلك الأمر للأسف حديثًا وأصبح معنى الاستخدامين مختلف، ولنلخّص الفرق فيما يلي:
نوع الربط | نوع الكائن | قابلية الوصول |
---|---|---|
خارجي | خارجي | يمكن الوصول إليه من أي مكان ضمن البرنامج |
داخلي | خارجي | يمكن الوصول إليه عبر ملف واحد فقط |
لا يوجد ربط | داخلي | محلّي لدالة واحدة |
[جدول 1 الربط وقابلية الوصول]
أخيرًا وقبل أن ننتقل إلى مثالٍ آخر، علينا أن نعرف أن لجميع الكائنات ذات الربط الخارجي تعريفٌ واحدٌ فقط، مع أنه بالإمكان أن يوجد عدة تصريحات متوافقة حسب حاجتك. إليك المثال:
/* الملف الأول */ int i; /* تعريف*/ main () { void f_in_other_place (void); /* تصريح*/ i = 0 } /* نهاية الملف الأول */ /* بداية الملف الثاني */ extern int i; /* تصريح*/ void f_in_other_place (void){ /* تعريف*/ i++; } /* نهاية الملف الثاني */
[مثال 1]
على الرغم من تعقيد القوانين الكاملة التي تحدد الفرق بين التعريف والتصريح إلا أن هناك طريقةً بسيطةً وسهلة، هي:
- التصريح عن الدالة دون تضمين متن الدالة هو تصريحٌ لا غير.
- التصريح عن الدالة مرفقًا بمتن الدالة هو تعريف.
-
عمومًا، التصريح عن كائن على المستوى الخارجي للبرنامج (مثل المتغير
i
في المثال السابق) هو تعريف، إلا إذا سُبق بالكلمة المفتاحيةextern
وعندها يصبح تصريحًا فقط.
وسنتكلم لاحقًا عن التعريف والتصريح بصورةٍ أعمق لا تبقي مجالًا للشك.
من الواضح في مثالنا السابق أنه من السهل الوصول من أي ملف للكائنات المعرفة في ملفات أخرى باستخدام أسمائها فقط، وسيدلك المثال على كيفية بناء برامج ذات ملفات ودوال ومتغيرات متعددة سواءٌ كانت مُعرّفةً أو مصرّحٌ عنها وفق ما يناسب كل حالة.
إليك مثالًا آخر يوضح استخدام "static" لتقييد قابلية الوصول إلى الدوال والأشياء الأخرى.
/* مثال عن وحدة في مكتبة */ /* الدالة callable هي الدالة الوحيدة المرئية على المستوى الخارجي */ static buf [100]; static length; static void fillup(void); int callable (){ if (length ==0){ fillup (); } return (buf [length--]); } static void fillup (void){ while (length <100){ buf [length++] = 0; } }
[مثال 2]
يمكن للمستخدم -بفرض استخدام المثال السابق وحدةً مستقلّة- إعادة استخدام الأسماء "length" و "buf" و "fillup" بأمان دون أي تأثيرات جانبية أو أخطاء غير متوقعة، ونستثني من ذلك الاسم "callable"، إذ إنه قابل الوصول خارج الملف (الوحدة المستقلة).
تكون قيمة الكائن الخارجي الذي يمتلك مُهيّئًا initializer واحدًا مساوية للصفر قبل بدء البرنامج (لم نتكلم عن أي مُهيّئات عدا الدوال حتى الآن)، وتعتمد الكثير من البرامج على ذلك، بما فيها المثال السابق لقيمة length
الابتدائية.
تأثير النطاق
لا تقتصر عملية مشاركة الأسماء وقابلية الوصول إليها على الربط ببساطة، فالربط يسمح لك باستخدام عدة أسماء والوصول إليها سويًّا ضمن البرنامج أو ضمن الملف، ولكن النطاق Scope يحدد رؤية الأسماء، وقواعد النطاق لحسن الحظ مستقلةٌ عن مبدأ الربط، لذا ليس عليك حفظ أي قواعد مركبة بين المفهومين.
تزيد الكلمة المفتاحية "extern" من تعقيد البرنامج، فعلى الرغم من وضوح استخدامها وتأثيرها إلا أنها تغيّر من بنية برنامج لغة سي الكُتلية التي اعتدنا عليها، وسنناقش المشاكل الناتجة عن استخدامها الخاطئ وغير المسؤول لاحقًا، وقد نظرنا إلى استخدامها مُسبقًا للتأكد من أن التصريح لشيء ما ضمن المستوى الخارجي للبرنامج هو تصريحٌ وليس تعريف.
ملاحظة: يمكنك تجاوز الكلمة المفتاحية "extern" عن طريق مُهيّئ للكائن.
التصريح عن أي كائن بيانات (ليس بدالة) ضمن المستوى الخارجي للبرنامج هو تعريفٌ، إلا إذا سبق التصريح الكلمة المفتاحية "extern"، راجع المثال 9.4 من أجل ملاحظة هذه النقطة عمليًّا.
تحتوي تصريحات الدوال الكلمة المفتاحية "extern" ضمنيًّا سواءٌ كانت مكتوبةً أم لا، والطريقتان التاليتان للتصريح عن الدالة some_function
متكافئتان، وتُعدان تصريحًا وليس تعريفًا:
void some_function(void); extern void some_function(void);
الشيء الوحيد الذي يفصل التصريحات السابقة عن كونها تعريفات هو متن الدالة، الذي يُعد مُهيِّئًا للدالة، لذلك عند إضافة المُهيّئ يتحول التصريح إلى تعريف، لا توجد أي مشكلة بخصوص ذلك.
لكن ما الذي يحدث في المثال التالي؟
void some_function(void){ int i_var; extern float e_f_var; } void another_func(void){ int i; i = e_f_var; /* مشكلة تتعلق بالنطاق */ }
ما الهدف من المثال السابق؟ من المفيد في بعض الأحيان أن تستخدم كائنًا خارجيًّا ضمن دالة ما، وإن اتبعت الطريقة الاعتيادية بالتصريح عن الكائن في بداية ملف الشيفرة المصدرية، فسيكون صعبًا على القارئ معرفة أي من الدوال تستخدم ذلك الكائن؛ بدلًا من ذلك، يمكنك تقييد نطاق الكائن وقابلية الوصول إليه في المكان الذي تريد الوصول إليه، ممّا سيسهّل على القارئ معرفة أن الاسم سيُستخدم فقط في هذا المكان المحدود وليس على كامل نطاق ملف الشيفرة المصدرية. يجدر الانتباه إلى أن معظم طرق إدارة الوسطاء في هذه الحالة عمليةٌ صعبةٌ بعض الشيء.
سنناقش المزيد من القواعد عن الطريقة الأمثل لإنشاء برنامج ذو ملفات متعددة، وما الممكن حدوثه عند المزج بين التصريحات الداخلية والخارجية والكلمات المفتاحية "extern" و "static". لن تكون عملية قراءة هذه القواعد ممتعةً، لكنها ستكون إجابةً لأسئلة من نوع "ماذا لو؟".
الكائنات الداخلية الساكنة
يمكنك التصريح عن كائنات داخلية على أنها كائنات داخلية ساكنة باستخدام الكلمة المفتاحية "static"، وتكتسب المتغيرات الداخلية بعضًا من الخصائص باستخدامها هذه الكلمة المفتاحية ألا وهي:
- تُهيّأ قيمتها إلى الصفر عند بداية البرنامج.
- تحافظ على قيمتها من بداية التعليمة التي تضم تصريحها إلى نهايتها.
- يوجد نسخةٌ واحدةٌ من كل متغيّر داخلي ساكن تتشاركه الاستدعاءات التعاودية للدوال التي تحوي هذه المتغيرات.
يمكن أن تُستخدم المتغيرات الداخلية الساكنة لعدة أمور، أحدها هو عدّ مرات استدعاء دالةٍ ما، إذ تحافظ المتغيرات الداخلية الساكنة على قيمتها بعد الخروج من الدالة بعكس المتغيرات الداخلية الاعتيادية. إليك دالةً تُعيد عددًا بين 0 و15، ولكنها تُبقي عدد المرات التي استُدعيت بها:
int small_val (void) { static unsigned count; count ++; return (count % 16); }
[مثال 3]
يمكن أن نستخدم هذه الطريقة للكشف عن الاستدعاءات التعاودية مفرطة الحدوث:
void r_func (void){ static int depth; depth++; if (depth > 200) { printf ("excessive recursion\n"); exit (1); } else { /* .r_func() نفّذ بعض الأوامر الاعتيادية. قد تتسبب النتائج باستدعاء آخر لدالة التعاود */ x_func(); } depth--; }
[مثال 4]
ترجمة -وبتصرف- لقسم من الفصل Functions من كتاب The C Book
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.