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

النطاق في لغة C: نطاق الكتلة والدالة والملف

أين يكون متغيّر C مرئيًا وكم يدوم - نطاق الكتلة، ووسائط الدوال، والمتغيّرات العامة على نطاق الملف، والمتغيّرات المحلّية static التي تبقى بين الاستدعاءات، والدوال static، والحجب، ولماذا تسبّب المتغيّرات العامة المتاعب.

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

سؤالان عن كل متغيّر

لكل متغيّر في C خاصّيتان منفصلتان، والخلط بينهما مصدر معظم المفاجآت في هذه الصفحة:

  • النطاق - أين يمكن استخدام الاسم. سؤال يخصّ وقت الترجمة.
  • العمر - كم يدوم وجود التخزين. سؤال يخصّ وقت التشغيل.

وهما يتحرّكان معًا عادةً (فالمتغيّر المحلّي العادي مرئي في كتلته ويعيش بالقدر نفسه تمامًا)، لكن static تفصل بينهما، ولهذا هي الكلمة المفتاحية الجديرة بالفهم هنا.

نطاق الكتلة

المتغيّر المعرَّف داخل أقواس معقوفة مرئي من تعريفه حتى القوس المغلق، ولا شيء بعد ذلك.

تستطيع الكتلة الداخلية رؤية outer، لأنها متداخلة داخلها. ولا تستطيع الكتلة الخارجية رؤية inner. وينطبق هذا على كل زوج من الأقواس المعقوفة - أجسام الدوال، وأجسام if، وأجسام الحلقات، أو كتلة مجرّدة كالتي أعلاه.

والحلقات تجعل الأمر ملموسًا. فالعدّاد المعرَّف في ترويسة for ينتمي إلى الحلقة:

for (int i = 0; i < 5; i++) {
    /* i lives here */
}
/* i does not exist here */

وهذا بالضبط ما تريده: يمكن لحلقتين في الدالة نفسها أن تستخدم كلٌّ منهما i دون تداخل، ولا يمكن قراءة العدّاد سهوًا بعد الحلقة. وإن احتجت إلى القيمة بعدها - كالفهرس الذي توقّف عنده بحث - فعرّفها قبل الحلقة.

ولوسائط الدوال نطاق جسم الدالة، فتتصرّف كمتغيّرات محلّية معرَّفة في أعلاه.

وتسمح C99 بالتعريف في أي موضع من الكتلة، لا في رأسها فقط. عرّف المتغيّرات حيث تحتاجها أول مرة؛ فالمتغيّر ذو النطاق القصير متغيّر له طرق أقلّ ليكون خاطئًا.

العمر: التخزين التلقائي

للمتغيّر المحلّي العادي تخزين تلقائي: يوجد حين يدخل التحكّم كتلته ويُدمَّر حين يغادر. وذاكرته تعيش على المكدّس.

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

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

/* BUG: buf does not exist after the return */
char *broken(void) {
    char buf[64] = "hello";
    return buf;
}

المتغيّرات المحلّية static: ذاكرة بين الاستدعاءات

ضع static على متغيّر محلّي فيصبح عمره عمر البرنامج كله، بينما يبقى نطاقه كما هو تمامًا.

يطبع العدّاد الآن 1 و2 و3. وتُنفَّذ = 0 مرة واحدة، قبل بدء main - لا في كل استدعاء.

ثلاث قواعد للمتغيّرات المحلّية static:

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

والنقطة الأخيرة هي كل الجاذبية: تستطيع دالة أن تتذكّر شيئًا بين الاستدعاءات دون كشف متغيّر عام يمكن لأي شيء تعديله. استخدمها لعدّادات الاستدعاء، وجداول البحث المبنية عند أول استخدام، ورايات التهيئة لمرة واحدة.

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

نطاق الملف: المتغيّرات العامة

المتغيّر المعرَّف خارج كل الدوال له نطاق ملف. وهو مرئي من تعريفه حتى نهاية الملف، في كل دالة تحته، وعمره هو عمر البرنامج بأكمله.

تُهيَّأ المتغيّرات العامة بصفر افتراضيًا، فتبدأ totalOperations من 0 حتى دون = 0.

وللمتغيّر العام افتراضيًا ربط خارجي أيضًا: فتستطيع ملفات .c أخرى في البرنامج نفسه بلوغه بتعريفه extern:

/* in stats.c */
int totalOperations = 0;          /* the definition - exactly one in the program */

/* in main.c */
extern int totalOperations;       /* a declaration: "it exists somewhere" */

ضع تصريح extern في ملف ترويسة ليرى كل مستخدم التصريح نفسه. ولاحظ عدم التماثل: تصريح extern يقول إن المتغيّر موجود، ويجب أن يعرّفه فعليًا ملف .c واحد بالضبط.

لماذا تعضّ المتغيّرات العامة

المتغيّرات العامة أسهل طريقة لمشاركة البيانات وأسهل طريقة لإنشاء أخطاء لا تستطيع حصرها. أربع مشكلات ملموسة:

أي شيء يستطيع تغييرها. فحين تحمل totalOperations قيمة خاطئة، يكون المذنب أي سطر في أي ملف. أما مع وسيط فالمشتبه بهم هم مواضع الاستدعاء التي تراها.

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

الاسم على مستوى البرنامج. فمتغيّر عام اسمه count أو buffer سيتصادم في النهاية مع اسم شخص آخر.

يجب أن تكون مُهيِّئاتها ثوابت زمن الترجمة. في C، لا يمكن تهيئة متغيّر عام إلا بتعبير ثابت - فـ int limit = readConfig(); لا تُترجَم، وint b = a * 2; في نطاق الملف كذلك. وكل ما يُحسَب يجب إسناده في زمن التشغيل بدالة إعداد ما، وكل ملف يمسّ المتغيّر العام قبل ذلك الاستدعاء يرى 0 صامتة.

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

static على نطاق الملف: الخصوصية

على متغيّر عام أو دالة، تعني static شيئًا مختلفًا كليًا عمّا تعنيه على متغيّر محلّي: ربط داخلي. فيصبح الاسم خاصًّا بملف .c الخاص به ولا يمكن بلوغه من أي ملف آخر.

/* counter.c */
#include "counter.h"

static int count = 0;           /* private to this file - no other file can touch it */

static void validate(void) {    /* a private helper, not part of the interface */
    if (count < 0) count = 0;
}

void increment(void) {          /* public: declared in counter.h */
    count++;
    validate();
}

int getCount(void) {            /* public */
    return count;
}

يستطيع ملف آخر استدعاء increment وgetCount، لكنه لا يستطيع رؤية count ولا استدعاء validate - فلن يحلّ الرابط تلك الأسماء. وذلك هو نظام الوحدات في C، على ما هو عليه: ترويسة تصرّح بالدوال العامة، وstatic على كل ما عداها.

وفائدتان عمليّتان أبعد من الترتيب. يمكن لملفين أن يحمل كلٌّ منهما static void validate(void) دون تصادم، وهو ما لم يكن ممكنًا لو كانت الدالتان عامّتين. ويعلم المترجم أن دالة static ليس لها مستدعون خارج الملف، فيستطيع تضمينها بجرأة أكبر أو التحذير من أنها غير مستخدمة.

فللكلمة المفتاحية إذًا معنيان متمايزان، يحدّدهما موضع ظهورها:

static on a LOCAL variable   ->  lifetime becomes permanent (scope unchanged)
static at FILE scope         ->  linkage becomes internal    (lifetime unchanged)

الحجب

تعريف اسم في نطاق داخلي يوجد سلفًا في نطاق خارجي يحجب الاسم الخارجي: فداخل النطاق الداخلي يشير الاسم إلى المتغيّر الجديد ويتعذّر بلوغ الخارجي.

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

وgcc -Wshadow يبلّغ عن كل حالة. وهو ليس ضمن -Wall، ففعّله صراحةً:

gcc -Wall -Wextra -Wshadow program.c -o program

والعادة التي تتفادى المشكلة كليًا هي التسمية بحسب الدور لا بحسب النوع: فـ totalScore وitemScore لا يمكن أن يحجب أحدهما الآخر، بينما متغيّران كلاهما باسم n سيفعلان ذلك في النهاية.

مرجع سريع

declared inside a block        block scope, automatic lifetime, garbage if uninitialised
function parameter             block scope of the function body, a copy of the argument
static inside a function       block scope, PROGRAM lifetime, zero-initialised, kept between calls
declared outside all functions file scope, program lifetime, zero-initialised, visible to other files
static outside all functions   file scope, program lifetime, PRIVATE to this .c file
extern declaration             names a variable defined in another file

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

ما النطاق في لغة C؟

هو منطقة الشيفرة التي يكون فيها الاسم مرئيًا. فالمتغيّر المعرَّف داخل كتلة (زوج من الأقواس المعقوفة) مرئي في تلك الكتلة فقط - وهذا نطاق الكتلة. أما المعرَّف خارج كل الدوال فله نطاق ملف وهو مرئي من تعريفه حتى نهاية الملف.

ماذا تفعل static بمتغيّر محلّي في لغة C؟

تغيّر عمر المتغيّر دون تغيير نطاقه. فالمتغيّر المحلّي static يُنشأ مرة واحدة ويُهيَّأ مرة واحدة ويحتفظ بقيمته بين الاستدعاءات، لكنه يبقى مرئيًا داخل دالته فقط. وهو الطريقة لمنح دالة ذاكرةً دون استخدام متغيّر عام.

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

المتغيّر العام العادي مرئي لكل ملف في البرنامج، وتستطيع الملفات الأخرى بلوغه بـ extern. أما المتغيّر أو الدالة static على نطاق الملف فخاصّ بذلك الملف .c وحده - ولا تستطيع الملفات الأخرى الربط به. فـ static على نطاق الملف عن الإخفاء؛ وstatic على متغيّر محلّي عن العمر.

لماذا تُعدّ المتغيّرات العامة سيئة في لغة C؟

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

Coddy programming languages illustration

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

ابدأ الآن