الدرس الرابع: الدوال في لغة السي بلس بلس ++C (ج١)


#1

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

أساسيات البرمجة بلغة السي بلس بلس ++C

بعنوان

الدوال في لغة السي بلس بلس ++C (ج١)

غالبا ما تكون البرامج بلغة السي++ كبيرة جدا ومعقدة وليست كما رأينا في الامثلة السابقة فقد يحتوي البرنامج على العديد من الوظائف ولتنسيق البرنامج نقوم بعمل بعض الدوال functions لتحتوي وظائف معينة ونقوم باستدعائها من الدالة main.

سيكون شكل البرنامج كالاتي

int main()
{
   callFunction1();
   callFunction2(); 
   return 0;
}

سنتعرف في هذا الدرس على كيفية تعريف الدوال وايضا كيفية استعداء الدوال التي قمنا بعملها او الدوال الجاهزة في لغة السي++ سنتعرف ايضا على كيفية اعطاء الدوال بعض المتغيرات والحصول على النتائج.

تتيح الدوال ان تكون البرامج في لغة السي++ على هيئة نماذج عن طريق فصل الوظائف المعينة في وحدات مخصصة وهي الدوال فمثلا إذا اردنا عمل برنامج الالة الحاسبة سنقوم بعمل دالة للجمع ودالة للطرح وهكذا ونقوم باستدعاء هذه الدوال عندما نحتاجها من البرنامج الرئيسي.

تتيح لنا الدوال ايضا التخطيط والتصميم للبرنامج فمثلا يمكننا عمل برنامج صغير جدا للآلة الحاسبة كما يلي

int main()
{
   funcReadUserInput();
   
   funcProcessMath();
   
   funcWriteUserOutput();

   return 0;
}

والان داخل الدالة funcProcessMath سنقوم بعمل الوظائف الاتية

funcProcessMath()
{
   doSum();
   
   doSubtract();
   
   doMultiply();
   
   doDivision():
}

ونلاحظ هنا اننا نقوم بعمل تصميم للبرنامج ونموذج مصغر وسنقوم ببناء كل ووظيفة في البرنامج على حدة حتى يكتمل.

الدوال ايضا تكون مفيدة إذا أردنا تكرار جزء معين من الكود في اجزاء عديدة من البرنامج فيمكننا وضعه في دالة واستدعاءه عند الحاجة بدلا من كتابة كل مرة ويصبح البرنامج طويل بسبب كثرة التكرار.

والدوال تكون نوعين دوال عامة global functions وهي التي يمكن استعدائها من اي ملف في المشروع بشرط ارفاق الملف التي تم تعريف فيه هذه الدالة وعادة يكون ملف الهيدر التي تعرف فيه الدوال العامة header files وتعريف الدالة يسمي function prototype فثلا عندما اردنا استخدام الدالة cout كان لابد من ارفاق الملف iostream وهو الذى يحتوي على تعريف الدالة cout واستخدمنا العبارة الاتية

#include <iostream>

مكتبة الدوال الحسابية بلغة السي++ Math Library

هناك مكتبة ضخمة جدا في لغة السي++ تحتوي على الكثير من العمليات الحسابية الجاهزة والتي يمكننا استخدامها مباشرة ولكن لابد من ارفاق ملف الهيدر المناسب فمثلا اذا اردنا استخدام الدالة pow وهي التي تقوم برفع متغير لاس معين يجب علينا ارفاق ملف الهيدر سنتعرف في الجزء القادم على ابرز الدوال الموجودة في هذا الملف.

فثلا البرنامج الاتي يقوم بحساب الجزء التربيعي

#include <iostream>
#include <cmath>

using namespace std;

int main()
{
   int inputNumber;
   double outputNumber;

   cout << "Enter Number: ";
   cin >> inputNumber;

   outputNumber = sqrt( inputNumber );

   cout << "The Square root of " << inputNumber << " is "
         << outputNumber << endl;

   return 0;
}

يلاحظ اننا استخدمنا المتغير outputNumber من النوع double ويعني انه سيحتوي على علامة عشرية ومن المعلوم ان الجذر التربيعي من الممكن ان يحتوي على علامة عشرية مثلا إذا أدخلنا الرقم ١٠ ستكون نتيجة تنفيذ البرنامج كما يلي

Enter Number: 10
The Square root of 10 is 3.16228

واذا كنا استخدمنا المتغير outputNumber من النوع int فالنتيجة ستكون 3 حيث يتم تجاهل ما بعد العلامة العشرية ويتم استخدام الجزء الصحيح فقط من الرقم يمكننا ايضا تعديل البرنامج حتي نسمح للمستخدم بإدخال رقم يحتوي على علامة عشرية

مثلا

Enter Number: 12.5
The Square root of 12.5 is 3.53553

وهذه بعض الدوال الهامة في المكتبة

y = ceil( x )

يقوم بتقريب قيمة x الي اكبر رقم صحيح فمثلا اذا كانت x تساوي 9.2 فستكون قيمة y تساوي 10.0 أو اذا كانت x تساوي -9.8 فستكون قيمة y تساوي -9.0

y = cos( x )

دالة حساب جيب التمام

y = exp( x )

الدالة الأسية للأساس e مرفوع للأس x

y = fabs( x )

تقوم باستخراج القيمة المطلقة بدون علامة السالب

فمثلا اذا كانت x تساوي 5.1 فستكون قيمة y تساوي 5.1 أو اذا كانت x تساوي -8.76 فستكون قيمة y تساوي 8.76

y = floor( x )

يقوم بتقريب قيمة x الي اصغر رقم صحيح فمثلا اذا كانت x تساوي 9.2 فستكون قيمة y تساوي 9.0 أو اذا كانت x تساوي -9.8 فستكون قيمة y تساوي -10.0

z = fmod( x, y )

تقوم بحساب باقي القسمة للاعداد الحقيقية قمثلا اذا كانت x تساوي 2.6 و y تساوي 1.2 ستكون قيمة z هي 0.2 حيث ان

2.6 = 1.2 + 1.2 + 0.2

.

y = log( x )

دالة حساب اللوغرييتم الطبيعي للرقم x للاساس e

y = log10( x )

دالة حساب اللوغرييتم الطبيعي للرقم x للاساس 10

z = pow( x, y )

تقوم بحساب قيمة x مرفوعة للاس y

فمثلا
اذا كانت x تساوي 2 و y تساوي 7 ستكون قيمة z هي 128

y = sin( x )

دالة حساب الجيب

y = sqrt( x )

حساب الجذر التربيعي فمثلا اذا كانت x تساوي 9.0 فستكون قيمة y تساوي 3.0 ويلاحظ هنا ان x يجب ان تكون عدد موجب مثلا اذا اردنا تجربة برنامجنا السابق لحساب الجذر التربيعي برقم سالب ستكون النتيجة كما يلي

Enter Number: -10
The Square root of -10 is nan

.

y = tan( x )

دالة حساب الظل

كيفية تعريف الدوال في لغة السي++

سنتعامل الان مع برنامج الالة الحاسبة الذى قمنا بتصميمه من قبل سنقوم بتعريف الدوال في نفس الملف أي ستكون دوال خاصة يمكن استعداءها من داخل الملف وسيكون تعريفها في ملف source نفس الموجود به الدالة main

سنقوم الان فقط بتعريف الدالة funcReadUserInpu وهذه الدالة ستطلب من المستخدم ادخال رقم وستقوم بإرجاعه للدالة main إذا اردنا من المستخدم اكثر من رقم سنقوم بإعطاء الدالة معطيات Parameters وهو رقم الدخل سيكون ١ او ٢ اذا كنا سنطلب الدالة مرتين.

تعريف الدوال الخاصة يمكن ان يكون في أي مكان بملف source ولكن يرجى الانتباه قبل استعداء الدالة ان يكون قد تم تعريفها ولذلك من الافضل وضع تعريف الدوال دائما في الأعلى.

اما محتوى الدالة فيمكن ان يكون في أي مكان طالما كان هناك التعريف في الأعلى. كما يمكننا الاستغناء عن التعريف ان كان محتوى الدالة موجود قبل المكان الذي يتم فيه استدعاء الدالة

تعريف الدوال مهم جدا حيث يخبر المجمع باسم الدالة وبيانات الادخال والاخراج الخاصة بها

مثلا البرنامج الاتي

#include <iostream>
#include <cmath>

using namespace std;

double funcReadUserInput (int);


double funcReadUserInput( int InputNo)
{
   double readDoubleNumber;

   // Check Input Number
   if ( 1 == InputNo )
   {
      cout << "Enter the first number: ";
   }
   else if ( 2 == InputNo )
   {
      cout << "Enter the second number: ";
   }
   else
   {
      // Input number is not valid, return nan
      return nan("NaN");
   }

   // Read from user
   cin >> readDoubleNumber;

   return readDoubleNumber;
}

int main()
{
   double readDoubleNumber1;

   readDoubleNumber1 = funcReadUserInput((int) 0);

   // check return value
   if (isnan(readDoubleNumber1))
   {
      return 1;
   }

   return 0;
}

قمنا بعمل تعريف للدالة

double funcReadUserInput (int);

يحتوي تعريف الدالة على نوع المتغير الذي ستأخذه الدالة وهو من النوع int ونوع المتغير الذي سترجعه لنا، وهنا من النوع double يمكن للدوال في السي++ ان تأخذ أكثر من متغير ولكن يمكنها ارجاع متغير واحد فقط.

جزء من تعريف الدالة يسمى التوقيع function signature وهو الجزء الذي يحتوي على اسم الدالة وانواع متغيرات الادخال ولكن لا يحتوي على نوع المتغير الخارج من الدالة. وكل الدوال من نفس النوع يجب ان يكون لهم توقيع مختلف من دالة لأخرى.

فمثلا إذا أردنا تعريف دالتين لهم نفس الاسم ومتغيرات الادخال يجب علينا تعريفهم في اماكن مختلفة او في ملفات مختلفة.

وبعدها يأتي محتوى الدالة

double funcReadUserInput( int InputNo)
{
	…………

   return readDoubleNumber;
}

وسنجد ايضا اننا اعطينا اسم للمتغير الذي ستأخذه الدالة ويمكن استخدامه داخل الدالة لان قيمته ستكون معرفه من الدالة الذي ستستدعي الدالة funcReadUserInput

وفى النهاية هناك الدالة main وبها استدعاء للدالة funcReadUserInput

يمكننا ازالة التعريف طالما ان الدالة موجودة قبل الاستدعاء اما إذا وضعنا الدالة main قبل الدالة funcReadUserInput فيجب علينا الابقاء على التعريف ودائما من الافضل الابقاء على التعريف حتى يمكننا استخدام الدالة في أي مكان دون القلق هل تم تعريفها من قبل ام لا

نلاحظ في الدالة funcReadUserInput الامر الاتي

  // Input number is not valid, return nan
  return nan("NaN");

الدالة

nan("NaN") 

وهي تقوم بإرجاع القيمة ليست رقم حيث يمكننا اختبار ذلك في الدالة main

   // check return value
   if (isnan(readDoubleNumber1))
   {
      return 1;
   }

والدالة main تقوم بإرجاع القيمة 1 للدلالة على وجود خطأ

وفى اكلبس إذا قمنا بالوقوف على اسم الدالة سيظهر لنا تعريف لها

يمكن ايضا للدوال ان لا ترجع اي شيء وفى هذه الحالة سنقوم باستخدام void وسيكون التعريف كما يلي

void funcReadUserInput (int);

وإذا اردنا ايضا عدم اعطاء أي متغيرات للدالة فيمكننا عمل الاتي

void funcReadUserInput (void);

وهذا ليس معناه ان هذه الدالة لن تقوم باي وظيفية فمثلا يمكن للدالة عمل وظيفة معينة وبدلا من اخراجها ستقوم بكتابة النتيجة في مكان معين معروف في الذاكرة او كتابتها في متغير عام داخل ملف السي++ او عموما على مستوى البرنامج

توجد ايضا خاصية هامة للدوال في السي++ وهي اجبار الدالة على قبول متغيرات ليس من نفس النوع الموجود في التعريف Argument Coercion حيث سيقوم المجمع بتغير نوع متغير الاخال الي نفس النوع الذي تقبله الدالة.

ترقية متغيرات ادخال الدوال Argument Promotion

سيقوم المجموع بتغير نوع متغيرات الادخال حتى تتناسب مع الدالة ولا يحدث اي خطأ، في لغة السي++ هناك قواعد معينة للتحويل من نوع الي اخر بدون فقد للبيانات فمثلا يمكن لأي متغير int ان يتم تحويله بسهولة الي double بدون اي تغير في القيمة فمثلا إذا كانت القيمة 3 ستصبح 3.0

ولكن إذا اردنا تحويل المتغير من النوع double الي int فسنفقد جزء من قيمة المتغير فمثلا القيمة 3.5 ستصبح 3 وايضا سنفقد جزء من البيانات اذا قمنا بتحويل متغير من نوع ذو حجم اكبر فمثلا التحويل من long الي short او من signed الي unsigned او من unsigned الي signed سينتج عنه تغيير في القيمة.

والمجمع يراعي بعض القواعد عند عملية التحويل فمثلا تتم الترقية دائما الي النوع الاعلى

وهذه قائمة بالأنواع الاساسية مرتبة من الأعلى الي الاسفل

long double
double
float
unsigned long int
long int
unsigned int 
int
unsigned short int
short int 
unsigned char
char
bool

وكما ذكرنا سابقا فان عملية التحويل التي يقوم بها المجمع تتم دائما من الاسفل الي الاعلى لأنه في حالة التحويل من الاعلى الي الاسفل يتم فقد جزء من قيمة المتغير او في بعض الاحيان يتم تغيير القيمة كليا ولهذا لا يقوم المجمع بالتحويل في مثل هذه الحالات.

ولكن يمكن للمبرمج فعل هذا الامر وفي بعض الاحيان قد يكون هناك تحذير من المجمع وهناك طريقة للتحويل تسمى casting سنتعرض لها لاحقا

في المثال الاتي

#include <iostream>
using namespace std;

void func(double);

void func(double number)
{
   cout << "number = " << number << endl;
}

int main (void)
{
   double number = 15.1515;

   cout << "number = " << number << endl;

   func(number);

   return 0;
}

قمنا بتعريف الدالة func لتأخذ متغير من النوع double وقمنا بإضافة محتوى لها وقمنا باستدعائها بقيمة double ولذلك ليس هناك أي مشكلة وستكون نتيجة التنفيذ كما يلي

number = 15.1515
number = 15.1515

وحتى ان قمنا بحذف السطر الاتي

 void func(double);

لن يكون هناك أي مشكلة لان التعريف غير ضروري إذا كان المحتوى موجود قبل استدعاء الدالة

والان إذا اضفنا التعريف كما يلي

void func(int);

حتى نرى ان كان المجمع سيقوم بتحويلها ام لا ولكن لم يحدث اي تحويل والنتيجة كما يلي

number = 15.1515
number = 15.1515

وهنا تم تجاهل التعريف لأنه يطلب اجبار دالة تأخذ متغير من نوع مختلف عن تعريف محتوى الدالة وكذلك القيمة التي تم الاستدعاء بها

مكتبات الدوال في السي++

عرفنا كيفية استخدام مكتبات الدوال الموجودة في السي++ عن طريق ادراج ملف التعريف header files ، وعادة تحتوي ملفات الهيدر على تعريف الدالة وليس المحتوى ويكون هناك ملف المصدر source file يوجد به محتوى الدالة.

تستخدم ملفات الهيدر في ربط ملفات السي++ وتكون مفيدة للمجمع لمعرفة كيفية بناء البرنامج

ملفات التعريف يكون لها دائما الامتداد .h ولكن في حالة مكتبات الدوال فلا يستخدم الامتداد

تحتوي لغة السي++ على المكتبات الاتية

<iostream>

توجد بها تعريف دوال الادخال والاخراج

<iomanip>

توجد بها تعريف دوال الادخال والاخراج الخاصة بالبيانات المتسلسلة

<cmath>

توجد بها تعريف الدوال الحسابية

 <cstdlib>

يوجد بها الدوال الخاصة بتحويل النصوص الي ارقام والعكس كذلك تخصيص اماكن في الذاكرة والعديد من الوظائف الاخرى سوف نتعرف عليها لاحقا

 <ctime>

يوجد بها تعريف دوال الوقت والتاريخ

<vector>, 
<list>, 
<deque>, 
<queue>, 
<stack>, 
<map>, 
<set>, 
<bitset>

يوجد بها تعريف دوال التعامل مع البيانات

<cctype>

تحتوي على دوال تقوم بالتعرف على خصائص المتغيرات وانواعها

 <cstring>

تحتوي على دوال التعامل مع المتغيرات من نوع string الخاصة بلغة السي وهي على هيئة نصوص طويلة

  <typeinfo>

تحتوي على دوال تقوم بالتعرف على نوع المتغير خلال تنفيذ البرنامج

<exception>, 
<stdexcept>

تحتوي على دوال تقوم بالتعرف على الاخطاء اثناء تنفيذ البرنامج

  <memory>

تحتوي على الدوال الخاصة بالتعامل مع الذاكرة

  <fstream>

تحتوي على الدوال الخاصة بالتعامل مع الملفات

  <string>

تحتوي على دوال التعامل مع المتغيرات من نوع string

  <sstream>

تحتوي على دوال التعامل مع المتغيرات من نوع string من خلال الذاكرة

  <functional>

تحتوي على بعض الالوجورثمات الحسابية

  <iterator>

تحتوي على دوال تستخدم للحصول على البيانات من وسائط الحفظ

  <algorithm>

يستخدم للتعامل مع البيانات الموجودة في وسائط الحفظ

  <cassert>

يحتوي على الدوال الخاصة بفحص البرنامج واكتشاف الاخطاء

  <cfloat>

يستخدم في حالة التعامل مع المتغيرات من النوع float

  <climits>

تحتوي على تعريف لأحجام المتغيرات الخاصة بلغة السي

  <cstdio>

تحتوي على تعريف لدوال الادخال والاخراج الخاصة بلغة السي

  <locale>

تحتوي على تعريف دوال للتعامل مع البيانات بلغات مختلفة

  <limits>

تحتوي على تعريف لأحجام المتغيرات

  <utility>

تحتوي على تعريف لبعض الدوال المفيدة

مثال

سنقوم الان بعمل برنامج يقوم بلعبة بسيطة، سنستخدم في هذا البرنامج الدالة rand لتوليد ارقام عشوائية موجبة من صفر الي RAND_MAX ويساوي 2147483647 في ++GNU C

وهذه الدالة موجودة في المكتبة ولذلك يجب علينا ادراجها في البرنامج

وإذا اردنا توليد ارقام عشوائية من ١ الي ٥ فقط فستخدم العلامة % الخاصة بباقي القسمة ونضيف ١ للناتج ويصبح البرنامج كالاتي

#include <iostream>
#include <cstdlib>

using namespace std;


int main (void)
{
   int number = 1 + rand() % 5;
   cout << "Random number = " << number << endl;

   return 0;
}

وعند تنفيذ هذا البرنامج فأن الدالة rand ستقوم بتوليد قيمة عشوائية واذا تم اعادة تشغيل البرنامج فالدالة rand ستعطي نفس القيمة وهذا لان عملية توليد الارقام العشوائية في الكمبيوتر ليس توليد عشوائي تماما كما في الطبيعة ويطلق عليها pseudorandom numbers فمثلا حركة جزيئات ذرية في حيز ما قد تكون عشوائية تماما ولا يمكن التنبؤ بها ولكن لوغاريتمات توليد الارقام العشوائية في الكمبيوتر يمكن التنبؤ بها ومن الممكن اختراقها

لجعل الدالة rand تعطينا رقم جديد كل مرة نقوم بتشغيل البرنامج سنقوم باستخدام الدالة srand وهو تعطي تعريف القيمة الاولية المستخدمة في التوليد ويمكن ان نستخدم معها وقت تنفيذ البرنامج فكل مرة سنقوم بتنفيذ البرنامج سيكون التوقيت مختلف وسنحصل على قيمة مختلفة، للتعامل مع دوال الوقت سيطلب ادراج المكتبة

كما يلي

srand(time(0));

ودالة الوقت هنا تقوم بإرجاع قيمة الثواني المنقضية منذ الاول من يناير سنة ١٩٧٠ ولذلك فان قيمتها لا تتكرر

وفكرة البرنامج ان يقوم بتوليد رقم عشوائي من ١ الي ٥ ويطلب من المستخدم تخمين الرقم

وسيكون البرنامج كما يلي

#include <iostream>
#include <cstdlib>
#include <ctime>

using namespace std;


int main (void)
{
   int randNumber = 0;
   int userInput = 0;
   bool isPlaying = true;

   // set random seed
   srand(time(0));

   while (isPlaying)
   {
      // generate new random number
      randNumber = 1 + rand() % 5;

      // get user input
      cout << "Guess the number: ";
      cin >> userInput;

      // check user input
      if (userInput == randNumber)
      {
         cout << "GOOD GUESS, YOU WIN";
         isPlaying = false;
      }
      else
      {
         cout << "Wrong, Try again" << endl;
      }
   }

   return 0;
}

ونتيجة التنفيذ كما يلي

Guess the number: 4
Wrong, Try again
Guess the number: 5
Wrong, Try again
Guess the number: 3
GOOD GUESS, YOU WIN

المتغيرات من النوع enum

تستخدم المتغيرات من النوع enum لتعريف مجموعة من الثوابت او الاحتمالات الممكنة لقيمة المتغير فمثلا إذا كان لدينا متغير عددي يحتوي على قيمة ترمز لحالة معينة.

فلنفرض ان لدينا متغير يحتوي على اللون وستكون قيمة احمر واخضر وازرق فقط اي قيم اخر ستكون غير مقبولة.

فسنقوم بعمل الاتي

اولا سنقوم بتعريف نوع خاص بنا وهو النوع enum وتحديد جميع القيم الممكنة لهذا النوع.

enum color 
   {
      RED,
      GREEN,
      BLUE
   };

سنقوم الان بتعريف المتغير myColor ونوعه color كالاتي

color myColor;

والان المتغير الذي قمنا بتعريفة myColor يمكن فقط ان يحتوي على أحد القيم الاتية RED GREEN BLUE

إذا حاولنا في البرنامج تخصيص أي قيمة لهذا المتغير فسيكون هناك خطأ من المجمع كالاتي

error: assigning to 'color' from incompatible type 'int'
   myColor = 10;
           ^ ~~

وللعلم فان هذه القيم RED GREEN BLUE هي ثوابت عددية تبدأ من الصفر والنوع الذى قمنا بتعريفه على اساس enum هو في الحقيقة متغير عددي ايضا من النوع int ولكن لا يقبل الا هذه القيم فقط.

الثوابت في enum يمكن اعادة تعريفيها كما يلي وفي حالة عدم تحديد القيمة سيأخذ قيمة الثابت الذى قبله مضافا اليه واحد

   enum color
   {
      RED = 10,
      GREEN = 20,
      BLUE
   };

ونلاحظ هنا ان BLUE ستكون قيمة 21

المثال الاتي يوضح كيفية استخدام enum

#include <iostream>
using namespace std;

int main (void)
{
   // define type color, each type we define
   // should be based on c++ fundamental types
   enum color
   {
      RED = 10,
      GREEN = 20,
      BLUE
   };

   // declare variable myColor with type color
   color myColor;

   // assign variable myColor to one of the allowed values
   myColor = BLUE;

   // variable myColor contains int value
   cout << "myColor = " << myColor << endl;

   return 0;
}

وإلى اللقاء في الدرس القادم


دورة أساسيات البرمجة بلغة السي بلس بلس
#2

الدرس في صورة PDF
الدرس الرابع الدوال في لغة السي بلس بلس C (ج١).pdf (408.3% u)