كثيراً ما تحصل مشاكل استخدام المتغيرات في لغات البرمجة، و لتجبن ذالك يجب علينا فهم عملية ترجمة البرنامج من قبل المترجم، حيث ان فهمنا لعمية الترجمة سوف ينعكس على القدرة البرمجية. في هذه المقالة سوف تحاول ان نشرح طرق تجنب مشاكل الإستدعاء في لغة C++.
ان المترجم يعامل الاقتران الرئيسي كغيره من الاقترانات الفرعية باستثناء فرق واحد وهو أن بداية تنفيذ البرامج تكون من الاقتران الرئيسي. ولو حاولنا التصريح عنه في بداية البرنامج فلا حرج في ذلك كم ان المترجم لن يبدي أي اعتراض تقني.
#includee<iostream.h>
void main()//الاقتران الرئيسي
int X=10;
void main()
{//جسم الاقتران
++X;
cout<<X<<endl;
}
تسمح لغة C++ للمبرمج كتابة الإجراءات الفرعية دون الحاجة لعملية التصريح المسبق Declaration بشرط أن يكتب الاقتران في بداية البرنامج وقبل الاقتران الرئيسي main تحديدا كما في المثال التالي.
#include<iostream.h>
void X(int a, int b)
{
if(a>b)
cout<<"The larger is:"<<a<<endl;
else
cout<<"The larger is:"<<b<<endl;
}
void main()
{
X(5,;
}
لاحظ إننا كتبنا الاقتران الفرعي X بصورة مباشرة ولم يسبقه أي عملية تصريح, حيث يتم استدعاء الاقتران X من خلال الاقتران الرئيسي بالقيم 5 و8 لينتقل مسار تنفيذ البرنامج إلى الاقتران X وتنفيذ ما بداخله من تعليمات, دون وقوع أي أخطاء برمجية. لكن لو غيرنا مكان الاقتران X وكتبناه في نهاية البرنامج أسفل الاقتران الرئيسي كما في الشكل التالي.
#include<iostream.h>
void main()
{
X(5,;
}
void X(int a, int b)
{
if(a>b)
cout<<"The larger is:"<<a<<endl;
else
cout<<"The larger is:"<<b<<endl;
}
عندما سيعترض المترجم على هذا الخطأ البرمجي وبإشهار رسالته الشهيرة والتي مفادها إن الاقتران X غير معرف undeclares identifier:"X" error C2065: لكن ما بال المترجم يعترض على هذا الإجراء؟! علما بأننا لم نرد ولم ننقص من الاقتران الفرعي X شيئا باستثناء تغير مكانه فقط؟ لمعرفة ما حدث وسبب اعتراض المترجم قم بتعليق عمل جملة الاستدعاء X ( 5,; وتحويلها لملاحظة Remark ثم اعد ترجمة البرنامج لتجد إن المترجم لا يظهر أي خطا.
والحقيقة إن الخطأ لم يكن بجملة الاستدعاء وإنما بتزامن الاستدعاء, ولمعرفة المزيد من تزامن الاستدعاء ارجع إلى المثال السابق- الخالي من الأخطاء البرمجية- ولاحظ إن المترجم يبدأ بترجمة البرنامج من أول سطر وبمجرد إن يصل للاقتران الفرعي X سيعمل بصورة تلقائية على حجز موقع مخصص للاقتران X داخل ذاكرة المعالج يتضمن هذا الموقع الوصف الشامل لأجزاء الاقتران من نوع البيانات العائدة وعدد ونوع وسائط التمرير بالإضافة للتعليمات البرمجية, لينتقل بعدها ويكمل المترجم عملية الترجمة إلى أن يصل لتعليمة الاستدعاء داخل الاقتران الرئيسي.. ويعود مرة أخرى لذاكرة المعالج ويبحث عن الاقتران المستدعى X ومن ثم تنفيذ ما جاء فيه من تعليمات.
أما في المثال الثاني الذي يحتوي على خطا التزامن, فان المترجم على عاداتها سيبدأ العمل من أول سطر حتى يصل لجملة الاستدعاء التي ستعيد توجيه المترجم لذاكرة المعالج ويبحث بداخلها عن الاقترانX ومن المؤكد انه لن يجد أي اثر للاقتران X وهذا يعتبر خطا برمجيا قاتلا, لذلك سيتوقف المترجم عن متابعة ما تبقى من تعليمات داخل البرنامج ويظهر لنا رسالة الخطأ السابقة. وعندما قمنا بتعطيل وتعليق جملة الاستدعاء X (5, ; سيعمل المترجم على تجاهل وجودها وسينتقل ما يليها من تعليمات حتى يصل للاقتران X وعندها سيعمل على تعريفه داخل ذاكرة المعالج.
والآن يمكننا تحديد آلية تزامن الاستدعاء الصحيحة بالقاعدة التالية: بصورة مطلقة يجب أن يسبق أي عملية استدعاء لاقتران ما نص التصريح عن الاقتران أو الاقتران نفسه بأركانه الأساسية
مثال تطبيقي1: قم بتصميم برنامج يضم اقترانيين فرعيين وهما:
1. get_sall ووظيفته ادخل الراتب الشهري للموظف.
2. Tax لحساب وطباعة صافي الراتب بعد احتساب الضريبة المترتبة على الموظف علما أن نسبة الضريبة هي 10%.
الحل:
#include<iostream.h>
double sall=0;//متغير لتخزين الراتب
void get_sall()
{
cin>>sall;//ادخال الراتب
}
void Tax()
{
sall= sall-(sall*0.10);//حساب الضريبة
cout<<sall;//طباعة صافي الراتب
}
void main()
{
get_sall();//استدعاء الاقتران الاول
Tax();//استدعاء الاقتران الثاني
عند ترجمة البرنامج السابق سيعمل المترجم على تعريف الاقتران get_sall داخل ذاكرة المعالج وكذلك الاقتران Tax , وعندما يصل الاقتران الرئيسي main سيجد تعليمة استدعاء الاقتران الأول get_sall التي ستنقل مسار التنفيذ إلى الاقتران get_sall وتنفيذ تعليمة الإدخال وتخزين القيمة المدخلة في المتغير sall ومن ثم العودة للاقتران الرئيسي ليجد تعليمة استدعاء الاقتران الثاني اليت ستنقل المسار لاقتران حساب وطباعة صافي الراتب.
مثال تطبيقي2: قم بإيجاد وتصحيح الخطأ في البرنامج.
#include<iostream.h>
double sall=0;//متغير لتخزين الراتب
void get_sall()
{
cin>>sall;//ادخال الراتب
Tax();//استدعاء الاقتران الثاني
}
void Tax()
{
sall= sall-(sall*0.10);//حساب الضريبة
cout<<sall;//طباعة صافي الراتب
}
void main()
{
get_sall();//استدعاء الاقتران الاول
}
الحل:
البرنامج السابق هو نفس الحل للمثال الأول ولكن تم نقل تعليمة استدعاء الاقتران الثاني Tax من داخل الاقتران الرئيسي ووضعها داخل الاقتران الأول get_sall . والخطأ الذي نتج عن عملية النقل هو أن المترجم عندما سينفذ الاقتران get_sall سيجد تعليمة الاستدعاء Tax(); وعندما يبحث داخل ذاكرة المعالج عن الاقتران Tax لن يجده وسيعتبره غير معرف ويوقف عمل البرنامج. ولتصحيح الخطأ يجب علينا تطبيق قاعدة تزامن الاستدعاء, بحيث يصبح الاقتران Tax معرفا قبل إن يستدعي داخل الاقتران get_sall لذلك يمكننا نقل الاقتران Tax ووضعه قبل الاقتران get_sall أو العمل على إضافة تصريح Declaration عن الاقتران Tax قبل الاقتران get_sall كما في الشكل التالي:
#include<iostream.h>
double sall=0;//متغير لتخزين الراتب
void tax();//تصريح عن الاقتران الثاني
void get_sall()
{
cin>>sall;//ادخال الراتب
tax();//استدعاء الاقتران الثاني
}
void tax()
{
sall= sall-(sall*0.10);//حساب الضريبة
cout<<sall;//طباعة صافي الراتب
}
void main()
{
get_sall();//استدعاء الاقتران الاول
}
ان المترجم يعامل الاقتران الرئيسي كغيره من الاقترانات الفرعية باستثناء فرق واحد وهو أن بداية تنفيذ البرامج تكون من الاقتران الرئيسي. ولو حاولنا التصريح عنه في بداية البرنامج فلا حرج في ذلك كم ان المترجم لن يبدي أي اعتراض تقني.
#includee<iostream.h>
void main()//الاقتران الرئيسي
int X=10;
void main()
{//جسم الاقتران
++X;
cout<<X<<endl;
}
تسمح لغة C++ للمبرمج كتابة الإجراءات الفرعية دون الحاجة لعملية التصريح المسبق Declaration بشرط أن يكتب الاقتران في بداية البرنامج وقبل الاقتران الرئيسي main تحديدا كما في المثال التالي.
#include<iostream.h>
void X(int a, int b)
{
if(a>b)
cout<<"The larger is:"<<a<<endl;
else
cout<<"The larger is:"<<b<<endl;
}
void main()
{
X(5,;
}
لاحظ إننا كتبنا الاقتران الفرعي X بصورة مباشرة ولم يسبقه أي عملية تصريح, حيث يتم استدعاء الاقتران X من خلال الاقتران الرئيسي بالقيم 5 و8 لينتقل مسار تنفيذ البرنامج إلى الاقتران X وتنفيذ ما بداخله من تعليمات, دون وقوع أي أخطاء برمجية. لكن لو غيرنا مكان الاقتران X وكتبناه في نهاية البرنامج أسفل الاقتران الرئيسي كما في الشكل التالي.
#include<iostream.h>
void main()
{
X(5,;
}
void X(int a, int b)
{
if(a>b)
cout<<"The larger is:"<<a<<endl;
else
cout<<"The larger is:"<<b<<endl;
}
عندما سيعترض المترجم على هذا الخطأ البرمجي وبإشهار رسالته الشهيرة والتي مفادها إن الاقتران X غير معرف undeclares identifier:"X" error C2065: لكن ما بال المترجم يعترض على هذا الإجراء؟! علما بأننا لم نرد ولم ننقص من الاقتران الفرعي X شيئا باستثناء تغير مكانه فقط؟ لمعرفة ما حدث وسبب اعتراض المترجم قم بتعليق عمل جملة الاستدعاء X ( 5,; وتحويلها لملاحظة Remark ثم اعد ترجمة البرنامج لتجد إن المترجم لا يظهر أي خطا.
والحقيقة إن الخطأ لم يكن بجملة الاستدعاء وإنما بتزامن الاستدعاء, ولمعرفة المزيد من تزامن الاستدعاء ارجع إلى المثال السابق- الخالي من الأخطاء البرمجية- ولاحظ إن المترجم يبدأ بترجمة البرنامج من أول سطر وبمجرد إن يصل للاقتران الفرعي X سيعمل بصورة تلقائية على حجز موقع مخصص للاقتران X داخل ذاكرة المعالج يتضمن هذا الموقع الوصف الشامل لأجزاء الاقتران من نوع البيانات العائدة وعدد ونوع وسائط التمرير بالإضافة للتعليمات البرمجية, لينتقل بعدها ويكمل المترجم عملية الترجمة إلى أن يصل لتعليمة الاستدعاء داخل الاقتران الرئيسي.. ويعود مرة أخرى لذاكرة المعالج ويبحث عن الاقتران المستدعى X ومن ثم تنفيذ ما جاء فيه من تعليمات.
أما في المثال الثاني الذي يحتوي على خطا التزامن, فان المترجم على عاداتها سيبدأ العمل من أول سطر حتى يصل لجملة الاستدعاء التي ستعيد توجيه المترجم لذاكرة المعالج ويبحث بداخلها عن الاقترانX ومن المؤكد انه لن يجد أي اثر للاقتران X وهذا يعتبر خطا برمجيا قاتلا, لذلك سيتوقف المترجم عن متابعة ما تبقى من تعليمات داخل البرنامج ويظهر لنا رسالة الخطأ السابقة. وعندما قمنا بتعطيل وتعليق جملة الاستدعاء X (5, ; سيعمل المترجم على تجاهل وجودها وسينتقل ما يليها من تعليمات حتى يصل للاقتران X وعندها سيعمل على تعريفه داخل ذاكرة المعالج.
والآن يمكننا تحديد آلية تزامن الاستدعاء الصحيحة بالقاعدة التالية: بصورة مطلقة يجب أن يسبق أي عملية استدعاء لاقتران ما نص التصريح عن الاقتران أو الاقتران نفسه بأركانه الأساسية
مثال تطبيقي1: قم بتصميم برنامج يضم اقترانيين فرعيين وهما:
1. get_sall ووظيفته ادخل الراتب الشهري للموظف.
2. Tax لحساب وطباعة صافي الراتب بعد احتساب الضريبة المترتبة على الموظف علما أن نسبة الضريبة هي 10%.
الحل:
#include<iostream.h>
double sall=0;//متغير لتخزين الراتب
void get_sall()
{
cin>>sall;//ادخال الراتب
}
void Tax()
{
sall= sall-(sall*0.10);//حساب الضريبة
cout<<sall;//طباعة صافي الراتب
}
void main()
{
get_sall();//استدعاء الاقتران الاول
Tax();//استدعاء الاقتران الثاني
عند ترجمة البرنامج السابق سيعمل المترجم على تعريف الاقتران get_sall داخل ذاكرة المعالج وكذلك الاقتران Tax , وعندما يصل الاقتران الرئيسي main سيجد تعليمة استدعاء الاقتران الأول get_sall التي ستنقل مسار التنفيذ إلى الاقتران get_sall وتنفيذ تعليمة الإدخال وتخزين القيمة المدخلة في المتغير sall ومن ثم العودة للاقتران الرئيسي ليجد تعليمة استدعاء الاقتران الثاني اليت ستنقل المسار لاقتران حساب وطباعة صافي الراتب.
مثال تطبيقي2: قم بإيجاد وتصحيح الخطأ في البرنامج.
#include<iostream.h>
double sall=0;//متغير لتخزين الراتب
void get_sall()
{
cin>>sall;//ادخال الراتب
Tax();//استدعاء الاقتران الثاني
}
void Tax()
{
sall= sall-(sall*0.10);//حساب الضريبة
cout<<sall;//طباعة صافي الراتب
}
void main()
{
get_sall();//استدعاء الاقتران الاول
}
الحل:
البرنامج السابق هو نفس الحل للمثال الأول ولكن تم نقل تعليمة استدعاء الاقتران الثاني Tax من داخل الاقتران الرئيسي ووضعها داخل الاقتران الأول get_sall . والخطأ الذي نتج عن عملية النقل هو أن المترجم عندما سينفذ الاقتران get_sall سيجد تعليمة الاستدعاء Tax(); وعندما يبحث داخل ذاكرة المعالج عن الاقتران Tax لن يجده وسيعتبره غير معرف ويوقف عمل البرنامج. ولتصحيح الخطأ يجب علينا تطبيق قاعدة تزامن الاستدعاء, بحيث يصبح الاقتران Tax معرفا قبل إن يستدعي داخل الاقتران get_sall لذلك يمكننا نقل الاقتران Tax ووضعه قبل الاقتران get_sall أو العمل على إضافة تصريح Declaration عن الاقتران Tax قبل الاقتران get_sall كما في الشكل التالي:
#include<iostream.h>
double sall=0;//متغير لتخزين الراتب
void tax();//تصريح عن الاقتران الثاني
void get_sall()
{
cin>>sall;//ادخال الراتب
tax();//استدعاء الاقتران الثاني
}
void tax()
{
sall= sall-(sall*0.10);//حساب الضريبة
cout<<sall;//طباعة صافي الراتب
}
void main()
{
get_sall();//استدعاء الاقتران الاول
}