:: القيمة العائدة ( Return Value ) ::
تعلمنا حتى الأن كيف نقوم بكتابة التوابع و كيف نقوم بإدخال البيانات اللازمة للتابع لكي يجري عليها العمليات اللازمة .. و لكن افرض أننا نريد أن نرجع ناتج العمليات التي قام بها التابع إلى التابع الأخر الذي قام باستدعائه ؟ نستطيع ذلك عن طريق القيمة العائدة .. و على سبيل المثال :
في التابع السابق الذي يحسب متوسط عددين .. افرض أننا نريد من نجعل أي تابع آخر ( main مثلاً ) أن يستفيد من ذلك الناتج و يجري عليه عمليات أخرى .. لذلك لا يكفي في التابع avr أن ندخل عددين فقط بل يجب أن نعطي التوابع الأخرى ناتج عملية إيجاد المتوسط .
قاعدة 1 / هل تتذكر كيف شرحنا طريقة تعريف تابع ( الإعلان عن التوابع ) ؟ قلنا أنه يجب ذكر نوع التابع ثم اسمه ثم المتحولات الوسيطة ضمن قوسين ثم يأتي جسم ذلك التابع .. الأن حان الوقت لتغيير النوع void .. و لكن يجب أولاً توضيح بعض النقاط :
1 – التوابع التي لا تعيد قيمة : يكون نوعها void .
2 – التوابع التي تعيد قيمة : يمكن أن يكون نوعها بأي الأنواع المعرفة من قبل اللغة .. مثل : int و float و bool .. أو بأي من نوع من أنوع الكائنات أو السجلات ( سنأخذ ذلك لاحقاً ) .. و المهم في هذا الدرس الأنواع المعرفة مسبقاً .
قاعدة 2 / يجب استخدام التعليمة return مع التوابع التي تعيد قيمة .. و طريقة استخدامها هي كالتالي :
حيث أن Value ممكن أن تكون أي قيمة أو ممكن أن تكون متحول ما .. و يشترط أن تلك القيمة مناسبة لنوع التابع .
قاعدة 3 / لا يمكن استدعاء التوابع التي تعيد قيمة بذكر اسمها فقط !!! بل يجب أن توضع إما كإسناد قيمة أو في تعليمات cout أو ... إلخ .. أمثلة :
بفرض أن لدينا التابع avr الذي يحسب المتوسط و يعيد الناتج .. و بفرض أن c هو متحول ما من النوع float :
كود:
c = avr( 2, 3 );
cout << avr( x, y ) << endl;
و بهذا تختلف عن التوابع التي من النوع void التي يمكن استدعاؤها يذكر اسمها فقط :
بفرض أن read تابع من النوع void .. و بالتالي يمكن استدعاؤه كالتالي:
مثال / إيجاد متوسط عددين :
كود:
float avr( float a, float b )
{
float c;
c = ( a+b ) / 2;
return c;
}
void main()
{
float x, y;
cin >> x >> y;
cout << avr( x, y ) << endl;
}
و من الممكن أيضاً كتابة التابع avr بشكل آخر .. بحيث لا نستخدم متحول جديد لإرجاع القيمة :
كود:
float avr( float a, float b )
{
return ( a+b ) / 2;
}
ملاحظة / هل عرفت الأن أن التابع الذي كتبناه ( avr ) يشابه تماماً توابع أخرى موجودة في المكتبات القياسية للغة ؟ انظر كيف استخدمنا التابع avr .. و كيف استخدمنا التابع sqrt الموجود في المكتبة math.h :
كود:
cout << sqrt(9) << endl;
وبذلك تستطيع وضع مكتباتك الخاصة و لكن سنتطرق إلى الطريقة فيما بعد .
:: أماكن كتابة التوابع ::
في الأمثلة السابقة قمنا بكتابة التوابع فوق التابع الأساسي main .. و لذلك لكي يتعرف عليها برنامج حين استدعائها .. بينما إذا وضعناها أسفل التابع main فسيحدث خطأ .. لأنك قمت باستدعاء تابع لم يتم التعرف عليه بعد .. و كما قلنا فإن نمط البرمجة يتم سطراً سطراً من أول سطر في برنامجك إلى آخر سطر و لكن التنفيذ يتم من التابع main .. لذلك إذا استخدمت تابع و لم تقم قبل ذلك بتعريفه فسيحدث خطأ .
و لحل هذه المشكلة ( بأننا نريد وضع التوابع أسفل التابع main ) .. نضع فقط في أول البرنامج رؤوس تعريفات تلك التوابع .. بحيث تضع نوع التابع ثم اسمه ثم أنواع متحولاته الوسيطة فقط ( دون ذكر أسمائها ) مع كتابة الفاصلة المنقوطة في النهاية .. و دون كتابة جسم ذلك التابع .
ملاحظة / إذا وضعت أسماء المتحولات الوسيطة فلا بأس و لكن المترجم سيتجاهلها .
مثال /
كود:
int f1();
bool f2( int, int);
void f3(float);
void main()
{
}
int f1()
{
return 0;
}
bool f2(int a, int b)
{
return true;
}
void f3(float a)
{
}
ملاحظة 1 / لا يشترط التوافق في ترتيب ذكر رؤوس التوابع مع أجسامها في الأسفل .
ملاحظة 2 / قلنا أنه لا يشترط وضع أسماء المتحولات الوسيطة .. و لكن يجب ذكرها في أجسام التوابع .
ملاحظة 3 / بهذه الطريقة إذا كنت قد كتبت عدد كبير من التوابع .. فستجد أن التابع main موجود في الأعلى دائماً و بالتالي ستسهل على نفسك عناء البحث عنه .
:: الاستدعاء بالقيمة ( by Value ) و الاستدعاء بالمرجع ( by Reference ) ::
إذا أردنا أن ندخل مجموعة من القيم إلى داخل تابع ما .. كنا نستخدم لذلك المتحولات الوسيطة .. و عند استدعاء التابع كنا نرسل القيم التي نريدها عن طريق المتحولات الوسيطة الفعلية ( راجع فقرة المتحولات الوسيطة ) .. و لكن ما معنى الاستدعاء بالقيمة و الاستدعاء بالمرجع ؟
الاستدعاء بالقيمة ( by Value ) :
و هو الوضع الافتراضي لإرسال القيم للتوابع عن طريق المتحولات الوسيطة .. و لكن ما معناها ؟ انظر الاستدعاء التالي للتابع avr و الذي يحسب متوسط عددين ( لا يعيد قيمة ) و بفرض أن x و y هما العددين الذين سنحسب متوسطهما :
كود:
void avr(int a, int b)
{
int c = ( a+b ) / 2;
cout << c << endl;
}
void main()
{
int x, y;
x = 10; y = 20;
avr( x, y );
}
عند الاستدعاء .. سيتم إجراء نسخة عن كل من x و y و إرسالها إلى المتحولات الوسيطة a و b الخاصين بالتابع avr .. و بالتالي لن يتأثر المتغيرين x و y بنتائج عمليات التابع ( أي لن تتغير قيمتهما حتى بعد الانتهاء من التابع ) لأن المترجم قام بنسخ قيم x و y و وضعها في المتحولين الوسيطين a و b .. و بالتالي نستنتج أيضاً أنه سيتم تحميل الذاكرة بقيمتين مشابهتين تماماً لقيم x و y .
و لكن افرض مثلاً أنك تريد إرسال بيانات كبيرة ( كالكائنات أو السجلات مثلاً ) عبر تلك المتحولات الوسيطة .. فسيتم بهذه الطريقة إجراء نسخة أخرى لتلك القيم .. و بهذا سيزداد العبء على الذاكرة و سيزداد حجمها .. و لكن ما الحل لمعالجة هذه المشكلة ؟ الحل هو عن طريق الاستدعاء بالمرجع .. و تفيدنا تلك الطريقة أيضاً في إرجاع أكثر من قيمة بدلاً من قيمة واحدة فقط ( انظر الفقرة التالية ) .
الاستدعاء بالمرجع ( by Reference ) :
يختلف الاستدعاء بالمرجع عن الاستدعاء بالقيمة بأنه هذا الاستدعاء ( بالمرجع ) سيقوم بالتعامل مع عنوان المتحول الوسيط الفعلي في الذاكرة .. و بالتالي إذا حدثت أي تغييرات على المتحول الوسيط الشكلي فإن هذا التغير سيطرأ أيضاً على المتحول الوسيط الفعلي .. أي أن قيمة المتحول الوسيط الشكلي ستعطى للمتحول الوسيط الفعلي . انظروا المثال .
قاعدة / يمكنك استخدام الاستدعاء بالمرجع عن طريق وضع العلامة ( & ) قبل اسم المتحول الوسيط الشكلي الموجود في تعريف التابع .
كود:
void f1( int &a )
{
}
مثال / لتابع ندخل عليه عدد ما فيعطينا العدد الذي يليه :
كود:
void num( int &a )
{
a++;
}
void main()
{
int x;
cin >> x;
num( x );
cout << x << endl;
}
ملاحظة / لاحظ أننا لم نستخدم التوابع التي تعيد قيمة لإخراج الناتج .. بل استخدمنا الاستدعاء بالمرجع .
شرح المثال :
في هذا المثال .. سيدخل المستخدم عدد ما ( x ) ثم سنرسل هذا العدد كمرجع إلى التابع num .. و بهذه الطريقة سيتم إخراج الناتج عن طريق ( x ) أيضاً .. و لكن لن يتم هنا إجراء نسخة مطابقة للمتغير x بل سيتم تعديلها فقط عن طريق المتحول الشكلي a .. فإذا تغيرت a تتغير x و كأن a هي اسم آخر لـ x أي يتم التعامل معه و كأنه المتحول x .
و أريد التأكيد على معنى المرجعية .. قلنا فيما سبق أن المتغيرات تستخدم لتخزين قيم في الذاكرة و بالتالي لكل متغير في الذاكرة معلومتين : الأولى و هي القيمة التي يحملها .. و الثانية هي عنوانه في الذاكرة ( مكان وجوده ) .. و بالتالي عند استخدام الاستدعاء بالمرجع نكون قد وضعنا متغير آخر ( الشكلي ) يشير إلى نفس القيمة المرسلة فإذا تغيرت قيمة أحد المتغيرين ( الفعلي و الشكلي ) يكون قد تغير الأخر و كأننا وضعنا عنوانين في الذاكرة يشيران إلى نفس القيمة .. و هذا يعني أنه لن يتم نسخ قيمة المتحول الفعلي كما هو الحال في الاستدعاء بالقيمة .. حيث يتم في ذلك الاستدعاء إجراء نسخة كاملة عن المتحول الفعلي و وضعها في المتحول الشكلي بحيث يكون لها عنوان مختلف و قيمة مستقلة عن المتحول الفعلي .. و بالتالي إذا تغير أحدهم لن يتغير الأخر .
و في هذا المثال .. إذا فرضنا أن المستخدم أدخل العدد 5 فسيظهر على الشاشة العدد 6 .. و هكذا .
قاعدة 1 / من أجل القيم صغيرة الحجم يمكنك إرسال تلك القيم إلى التوابع باستخدام الاستدعاء بالقيمة .. بينما ينصح باستخدام الاستدعاء بالمرجع بالنسبة للقيمة الكبيرة .. و أيضاً من أجل إرجاع أكثر من قيمة ( أي تكون مخرجات التابع أكثر من قيمة ) .
قاعدة 2 / عليك معرفة متى تستخدم الاستدعاء بالقيمة و الاستدعاء بالمرجع .. حسب وظيفة التابع و على حسب العمليات التي سيقوم .
---------- ---------- ---------- نهاية الدرس الرابع ---------- ---------- ----------
في الدرس القادم إن شاء الله سنأخذ موضوع العودية ( Recursion ) بالإضافة إلى الأمثلة التطبيقية للمعلومات الموجودة في هذا الدرس .. بالتوفيق .