توابع سي بلس بلس

من ويكي الهندسة المعلوماتية
اذهب إلى: تصفح، ابحث

من المعروف أن الطريقة الأسهل لكتابة برنامج هي طريقة "فرق تسد"، من خلال تقسيمه إلى مهام جزئية يسهل تحويلها إلى تعليمات. تسمى هذه التعليمات التي تمثل مهمة جزئية بالتابع، ولها عدة فوائد، فهي تسهل فهم وكتابة وتعديل وإعادة استخدام التابع.... إلخ.

تعريف التابع

التصريح بالتابع

التصريح بالتابع (أو نموذجه البدائي) Function Declaration or Function Prototype هو عبارة وحيدة تنتهي بفاصلة منقوطة بالشكل:

Type_Returned Function_Name ( ParameterType_List );

حيث:

  • Type_Returned: النمط الذي يرده التابع.
  • Function_Name: اسم التابع.
  • ParameterType_List: قائمة بأنماط الوسطاء التي يردها التابع.


كما نعلم فإنه لا يمكن استدعاء التابع قبل تعريفه، ولكن عندما يكون لدينا الكثير من التوابع يصبح من الصعب ترتيبها بحيث "يرى" كل تابع التوابع التي يستدعيها، لتجنب ذلك ظهرت فكرة التصريح بالتابع، حيث نكتب تصريحات كل التوابع في بداية البرنامج (قبل كتابة كل التوابع) وبالتالي يمكننا استدعاء أي منها داخل أي منها ( أي كل تابع يرى كل التوابع ).

  • يقوم التصريح بتزويد المترجم بمعلومات عن اسم التابع والنمط الذي يرده وقائمة أنماط وسطائه.
  • وظيفة هذه القائمة هي تزويد البرنامج بمعلومات عن عدد وأنماط المتحولات التي يتلقاها التابع وترتيبها، ولا أهمية لأسماء هذه المتحولات في حال وضعها هنا.

بصمة التابع

لكل تابع بصمة signature خاصة به، ولا يمكن أن يكون لتابعين نفس البصمة، ونقصد بالبصمة اسم التابع وقائمة أنماط وسطائه فقط.


تفيد بصمة التابع في تعريف أكثر من تابع بنفس الاسم، ولكن تختلف أنماط الوسطاء التي يتلقاها أو ترتيبها أو عددها، كما في المثال التالي:

الشرح الكود

في الجوار تصريحات لخمسة توابع، كلها اسمها Power إلا أنها تختلف بقائمة الوسطاء التي تتلقاها، أي أن لكل منها بصمة وبالتالي لا يوجد أي خطأ في التصريحات، ولا يظهر المترجم أي رسالة خطأ

double Power(int , int);
double Power(double , double);
double Power(int , double);
double Power(double , int);
double Power(int , int , int);

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

double Power(int , int);
int Power(int , int);
float Power(int , int);
  • تسمى التوابع المتماثلة بالاسم والمختلفة بقائمة الوسطاء بالـ overloads (كل منها overload لذلك الاسم)، ففي المثال الأول يوجد 5 overloads للتابع Power.
  • لا يمكن أن يكون هنالك تابعان اسمهما main في برنامج واحد مهما كان الاختلاف كبيراً في بصمتيهما.

تعريف التابع

تعريف التابع Function Definition هو جسم التابع الذي اعتدنا على كتابته:

Type_Returned Function_Name (Parameter_List) 		// Header
{
Statement(S)
}

مثال عام :

#include <iostream.h>
 
double Power(int,double);		// Prototype
 
void main() {
	//MainStatement(s)
}
 
double Power(int Level, double Base)		// Header
{
	//Statement(s)
}
  • لا ينتهي السطر الأول (الترويسة Header) بفاصلة منقوطة بخلاف التصريح، ويجب أن يتطابق مع التصريح (في حال وجوده) من حيث اسم التابع والنمط الذي يرده وعدد الوسطاء وأنماطهم وترتيبهم (لاحظ التطابق في المثال العام) وإلا ظهر خطأ، ولكن هنا يفترض أن توجد أسماء الوسطاء لكي نتمكن من استخدامهم في التابع (علماً أنه يمكن ألا نضع الأسماء، ولكن حينها لا نتمكن من استخدامهم! ).
  • إذاً يتكون التعريف من نفس المعلومات الموجودة في التصريح إضافة لأسماء الوسطاء، وجسم التابع، أي التعليمات التي ينفذها محصورة بين قوسين { } مباشرة بعد الترويسة.
  • يفصل بين الوسطاء بفاصلة "," ، ويكتب نمط كل وسيط قبله بشكل منفصل (نكتب لكل وسيط نمطه الخاص، حتى ولو كان مماثلاً لما قبله)، وإذا لم يكن هنالك وسطاء، نترك مكان القائمة فارغاً أو نكتب void،
  • يعتبر الوسطاء متحولات محلية للتابع (سنرى معنى ذلك قريباً)، ولا يمكن تعريف متحولات داخل التابع لها نفس أسماء الوسطاء التي يتلقاها.

تطبيقات على المثال العام السابق: لنستبدل ترويسة التابع Power في المثال العام بما يلي:

الشرح الكود

صحيح

double Power(int Level, int Base)

خطأ، يجب أن يكون لكل متحول نمطه حتى ولو كان متحولان متعاقبان من نفس النمط.

double Power(int Level,Base ,double ThirdPar)

خطأ، لا يمكن تعريف متحول داخل التابع يحمل نفس اسم أحد الوسطاء.

double Power(int Level, int Base)
{
int Level = 5;
	//Statement(s)
}
  • يمكن للتابع ألا يرد أي قيمة بأن يكون نمطه void (اللاشيء – الفراغ) ويمكن أن يرد قيمة من أي نمط، ويفترض استخدامها في إسناد أو تمريرها كوسيط أو...، ويمكن استدعاء تابع بدون استخدام قيمته أيضاً.

في المثال العام السابق، لنضع داخل الـ main (مكان MainStatements):

الشرح الكود

هنا استخدمنا القيمة التي يردها التابع في عبارة رياضية، وcout، ومررناها كوسيط، واستخدمناها في إسناد.

double i = (Power(4,3.2)*12)/5;
cout << Power(4,5);
i = Power( Power(2,2) , 8.3 );
cout << (i = Power(5,7.9));

وهنا استدعيناه بدون أن نستخدم قيمته

Power(3,4.2);
  • تستخدم التعليمة return من أجل تحديد القيمة التي يردها التابع بالشكل
    return Value;
    ، وعند تنفيذها يتم الخروج من التابع مباشرة، فهي تعليمة الخروج النظامي من التابع.
  • وظيفة return في التوابع من نمط void هي الخروج فقط، ويمكن ألا يحوي التابع على أي منها، وتكتب لوحدها وبعدها مباشرة ";" دون أي قيمة، وإلا ظهرت رسالة خطأ.
  • أما التوابع التي ترد قيمة فيجب أن تحوي على الأقل على return واحدة، وإلا ظهرت رسالة خطأ.
  • ينتهي تنفيذ التابع بشكل نظامي عند return، فإن لم يصادف أياً منها (كأن تكون في شرط غير محقق)، ينتهي عند القوس } ، فإذا كان يرد قيمة كانت قيمة عشوائية.
  • للتابع main خصوصية، فهو لا يرد إلا أنماطاً بسيطة، ويمكن ألا يحوي على أي return ولكن يظهر تحذير بذلك.

من المعروف أن الطريقة الأسهل لكتابة برنامج هي طريقة "فرق تسد"، من خلال تقسيمه إلى مهام جزئية يسهل تحويلها إلى تعليمات. تسمى هذه التعليمات التي تمثل مهمة جزئية بالتابع، ولها عدة فوائد، فهي تسهل فهم وكتابة وتعديل وإعادة استخدام التابع.... إلخ.

تعريف التابع

التصريح بالتابع

التصريح بالتابع (أو نموذجه البدائي) Function Declaration or Function Prototype هو عبارة وحيدة تنتهي بفاصلة منقوطة بالشكل:

Type_Returned Function_Name ( ParameterType_List );

حيث:

  • Type_Returned: النمط الذي يرده التابع.
  • Function_Name: اسم التابع.
  • ParameterType_List: قائمة بأنماط الوسطاء التي يردها التابع.


كما نعلم فإنه لا يمكن استدعاء التابع قبل تعريفه، ولكن عندما يكون لدينا الكثير من التوابع يصبح من الصعب ترتيبها بحيث "يرى" كل تابع التوابع التي يستدعيها، لتجنب ذلك ظهرت فكرة التصريح بالتابع، حيث نكتب تصريحات كل التوابع في بداية البرنامج (قبل كتابة كل التوابع) وبالتالي يمكننا استدعاء أي منها داخل أي منها ( أي كل تابع يرى كل التوابع ).

  • يقوم التصريح بتزويد المترجم بمعلومات عن اسم التابع والنمط الذي يرده وقائمة أنماط وسطائه.
  • وظيفة هذه القائمة هي تزويد البرنامج بمعلومات عن عدد وأنماط المتحولات التي يتلقاها التابع وترتيبها، ولا أهمية لأسماء هذه المتحولات في حال وضعها هنا.

بصمة التابع

لكل تابع بصمة signature خاصة به، ولا يمكن أن يكون لتابعين نفس البصمة، ونقصد بالبصمة اسم التابع وقائمة أنماط وسطائه فقط.


تفيد بصمة التابع في تعريف أكثر من تابع بنفس الاسم، ولكن تختلف أنماط الوسطاء التي يتلقاها أو ترتيبها أو عددها، كما في المثال التالي:

الشرح الكود

في الجوار تصريحات لخمسة توابع، كلها اسمها Power إلا أنها تختلف بقائمة الوسطاء التي تتلقاها، أي أن لكل منها بصمة وبالتالي لا يوجد أي خطأ في التصريحات، ولا يظهر المترجم أي رسالة خطأ

double Power(int , int);
double Power(double , double);
double Power(int , double);
double Power(double , int);
double Power(int , int , int);

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

double Power(int , int);
int Power(int , int);
float Power(int , int);
  • تسمى التوابع المتماثلة بالاسم والمختلفة بقائمة الوسطاء بالـ overloads (كل منها overload لذلك الاسم)، ففي المثال الأول يوجد 5 overloads للتابع Power.
  • لا يمكن أن يكون هنالك تابعان اسمهما main في برنامج واحد مهما كان الاختلاف كبيراً في بصمتيهما.

تعريف التابع

تعريف التابع Function Definition هو جسم التابع الذي اعتدنا على كتابته:

Type_Returned Function_Name (Parameter_List) 		// Header
{
Statement(S)
}

مثال عام :

#include <iostream.h>
 
double Power(int,double);		// Prototype
 
void main() {
	//MainStatement(s)
}
 
double Power(int Level, double Base)		// Header
{
	//Statement(s)
}
  • لا ينتهي السطر الأول (الترويسة Header) بفاصلة منقوطة بخلاف التصريح، ويجب أن يتطابق مع التصريح (في حال وجوده) من حيث اسم التابع والنمط الذي يرده وعدد الوسطاء وأنماطهم وترتيبهم (لاحظ التطابق في المثال العام) وإلا ظهر خطأ، ولكن هنا يفترض أن توجد أسماء الوسطاء لكي نتمكن من استخدامهم في التابع (علماً أنه يمكن ألا نضع الأسماء، ولكن حينها لا نتمكن من استخدامهم! ).
  • إذاً يتكون التعريف من نفس المعلومات الموجودة في التصريح إضافة لأسماء الوسطاء، وجسم التابع، أي التعليمات التي ينفذها محصورة بين قوسين { } مباشرة بعد الترويسة.
  • يفصل بين الوسطاء بفاصلة "," ، ويكتب نمط كل وسيط قبله بشكل منفصل (نكتب لكل وسيط نمطه الخاص، حتى ولو كان مماثلاً لما قبله)، وإذا لم يكن هنالك وسطاء، نترك مكان القائمة فارغاً أو نكتب void،
  • يعتبر الوسطاء متحولات محلية للتابع (سنرى معنى ذلك قريباً)، ولا يمكن تعريف متحولات داخل التابع لها نفس أسماء الوسطاء التي يتلقاها.

تطبيقات على المثال العام السابق: لنستبدل ترويسة التابع Power في المثال العام بما يلي:

الشرح الكود

صحيح

double Power(int Level, int Base)

خطأ، يجب أن يكون لكل متحول نمطه حتى ولو كان متحولان متعاقبان من نفس النمط.

double Power(int Level,Base ,double ThirdPar)

خطأ، لا يمكن تعريف متحول داخل التابع يحمل نفس اسم أحد الوسطاء.

double Power(int Level, int Base)
{
int Level = 5;
	//Statement(s)
}
  • يمكن للتابع ألا يرد أي قيمة بأن يكون نمطه void (اللاشيء – الفراغ) ويمكن أن يرد قيمة من أي نمط، ويفترض استخدامها في إسناد أو تمريرها كوسيط أو...، ويمكن استدعاء تابع بدون استخدام قيمته أيضاً.

في المثال العام السابق، لنضع داخل الـ main (مكان MainStatements):

الشرح الكود

هنا استخدمنا القيمة التي يردها التابع في عبارة رياضية، وcout، ومررناها كوسيط، واستخدمناها في إسناد.

double i = (Power(4,3.2)*12)/5;
cout << Power(4,5);
i = Power( Power(2,2) , 8.3 );
cout << (i = Power(5,7.9));

وهنا استدعيناه بدون أن نستخدم قيمته

Power(3,4.2);
  • تستخدم التعليمة return من أجل تحديد القيمة التي يردها التابع بالشكل
    return Value;
    ، وعند تنفيذها يتم الخروج من التابع مباشرة، فهي تعليمة الخروج النظامي من التابع.
  • وظيفة return في التوابع من نمط void هي الخروج فقط، ويمكن ألا يحوي التابع على أي منها، وتكتب لوحدها وبعدها مباشرة ";" دون أي قيمة، وإلا ظهرت رسالة خطأ.
  • أما التوابع التي ترد قيمة فيجب أن تحوي على الأقل على return واحدة، وإلا ظهرت رسالة خطأ.
  • ينتهي تنفيذ التابع بشكل نظامي عند return، فإن لم يصادف أياً منها (كأن تكون في شرط غير محقق)، ينتهي عند القوس } ، فإذا كان يرد قيمة كانت قيمة عشوائية.
  • للتابع main خصوصية، فهو لا يرد إلا أنماطاً بسيطة، ويمكن ألا يحوي على أي return ولكن يظهر تحذير بذلك.

إذا أجرينا التغييرات التالية على المثال العام:

الشرح الكود

الخرج

I can't Calculate
-1.#IND	لاحظ القيمة العشوائية    
Incorrect
Press any key to continue


الشرح

  • انتهى تنفيذ main عند return، وانتهى تنفيذ Power عند }الخاصة بعباراته.
  • الخروج من Power بغير عبارة return أدى إلى رد قيمة عشوائية.
  • لاحظ أن عبارة return في تابع من نمط void ليس بعدها قيمة.
void main() {
double i = Power(2,5);
cout << i << '\n';
if (i!=25) {
	cout << "Incorrect\n";
	return;   //No value with void
	}
else cout << "Correct!\n";
}
 
double Power(int Level, double Base)
{
if (Level=0) return 1;
else cout << "I can't Calculate\n";
لا يوجد return داخل// else 
}
  • خلافاً للباسكال، لا يمكن تعريف تابع داخل تابع آخر.
  • من المتعارف عليه كتابة تعليقات قبل أو بعد ترويسة و تصريح التابع لتوضيح وظيفته والوسطاء التي يتلقاها والقيمة التي يردها .... ، هذه التعليقات تسمى بتوثيق التابع Function documentation وهي تساعد جداً على فهم الكود المقروء.
  • يمكن كتابة التعليقات (بشكل عام) إما بوضع // قبل التعليق في كل سطر، أو بوضع التعليقات (سطر أو أكثر) ضمن /* */، فكل ما يأتي في أي سطر بينهما يعتبر تعليقاً وليس تعليمات.


المكتبات Libraries

كثيراً ما نحتاج إلى نفس التابع في برامج متعددة، كتابع القوة Power، فمن غير المنطقي أن نكتبه في كل برنامج!. وفي البرامج الكبيرة يكون لدينا عدد كبير جداً من التوابع، فهل من المعقول أن نكتب كل هذه التوابع في ملف واحد؟ كم سيكون حجم هذا الملف؟

لحل هذه المشاكل وغيرها، ظهرت فكرة المكتبات، فهي تساعد على خزن التوابع (وكثير من الأشياء الأخرى) لإعادة استخدامها بدون إعادة كتابتها أو حتى نسخها، كما تساعد على تقسيم البرامج الكبيرة إلى وحدات منطقية لتسهيل فهمها والتعامل معها، بالإضافة لفوائد عديدة أخرى.....

ليس هنالك وجود لملف نوعه "مكتبة" في لغة C++، ذلك لأن المكتبة مفهوم منطقي يدل على ملفين لهما نفس الاسم ولاحقتان مختلفتان، حيث تتألف كل مكتبة في لغة C++ من :

  1. ملف ترويسة Header file : لاحقته .h ويحوي على تصريحات التوابع Functions Declarations وتعريفات أنماط المعطيات والثوابت و......
  2. ملف مصدري Source file : لاحقته .cpp ويحوي على تعريفات التوابع Functions Definitions .

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


والمكتبات نوعان:

  1. مكتبات جاهزة PrePackaged: وهي مكتبات ما يسمى بـ المكتبة القياسية لـ C++ ( Standard C++ Library ) وهي في الواقع ليست مكتبة، بل مجموعة من المكتبات التي تمكن المبرمج من إجراء عمليات هامة كالعمليات الرياضية وعمليات والدخل والخرج و....، ولها خصوصية في التعامل معها
  2. مكتبات يقوم المبرمج بإنشائها، وهي ما ندرسه.....


لاستخدام مكتبة، نقوم بتضمين ملف الترويسة لها وذلك كالتالي:

الشرج الكود

لمكتبات المكتبة القياسية Standard Library : مثال:

#include <HeaderFileName>
#include <iostream.h>

للمكتبات الأخرى (من صنعنا مثلاً) : مثال:

#include "HeaderFileName"
#include "MyTestLibrary.h"

في الواقع #include هي derivative يفهمها الـ PreProcessor، كل الكلمات المحجوزة التي تبدأ بـ # هي تعليمات خاصة بالـ PreProcessor.


خطوات عملية الترجمة Compilation

  • أولاً يتم تنفيذ التعليمات الخاصة بالـ PreProcessor، وإن كانت إحداها خاطئة تتوقف العملية حالاً، ولا يظهر إلا خطأ وحيد هو الذي تم اكتشافه (يوصف بأنه مميت Fatal)، حتى ولو كان الملف مليئا بالأخطاء.
  • ثم يتم التحقق من صحة التعليمات الخاصة بالمترجم، وإذا كانت صحيحة يتم تحويلها إلى لغة تفهمها الآلة وتوضع في ملف من نمط obj ( Object file ).
  • ثم يتم جلب المكتبات القياسية المضمنة (أما غير القياسية فيتم جلبها في المراحل السابقة) ويتم دمج ما سبق من قبل الـ Linker الذي يدمجها في الملف التنفيذي exe.


تطبيق على استخدام المكتبات : توليد عدد عشوائي

تؤمن المكتبة cstdlib التابع rand() الذي يرد قيمة عشوائية صحيحة بين الصفر و RAND_MAX (عادة 32767 ).

الشرح الكود

صحيح أن النتائج ستكون على الأغلب مختلفة في كل دورة، ولكن في كل مرة نشغل فيها البرنامج سنحصل على نفس النتائج، فلماذا يحصل ذلك ؟؟؟

#include <iostream.h>
#include <cstdlib>
void main() {
	for (int i=0;i<10;i++)
		cout << rand() << '\n';
}

كما قلنا، فإن التابع rand() يرد قيمة عشوائية، ولكنه لا يولد هذه القيمة، بل يأخذها من PseudoRandom Number Sequence (سلسلة الأعداد شبه العشوائية) وهي سلسلة من القيم المولدة بواسطة خوارزمية مسماة PseudoRandom Number Generator (مولد الأعداد شبه العشوائية)، الخوارزمية تعتمد على "بذرة" (عدد) لتوليد هذه السلسلة بحيث تقوم بإجراء عمليات حسابية معقدة على هذه البذرة....، فما هي قيمة هذه البذرة؟

إذا لم يتم تحديد قيمة هذه البذرة فإن الخوارزمية تفترضها =1 ، وهذا يفسر ظهور نفس النتائج في كل مرة نشغل فيها البرنامج، فكيف إذاً نستطيع تحديد وتغيير قيمة هذه البذرة؟

يتم تحديد هذه البذرة بواسطة التابع (الإجرائية) srand(unsigned int)، فالعدد الصحيح الموجب الذي نمرره له يكون هو البذرة، وعند تشغيل البرنامج لأول مرة سنرى أننا حصلنا على نتائج مغايرة للنتائج التي نحصل عليها في حال عدم تغيير البذرة، ولكن عند تكرار تشغيل البرنامج نلاحظ أن النتائج الجديدة تتكرر، لماذا؟

سبب ذلك هو أن البذرة الجديدة بقيت نفسها عند كل تشغيل للبرنامج، إذاً للحصول على نتائج متغايرة في كل مرة نشغل فيها البرنامج علينا أن نجعل البذرة تتغير من تلقاء ذاتها عند كل تشغيل للبرنامج وتأخذ قيمة جديدة، أي تغيراً ديناميكياً وليس يدوياً، فيكف نستطيع فعل ذلك؟

نستطيع فعل ذلك بأن نربط قيمة البذرة بتابع متغير القيمة، أي نمرر لـ srand هذا التابع، ولكن من أين لنا بتابع كهذا؟

في الواقع التوابع المرتبطة بالـ Hardware هي توابع متغيرة القيمة باستمرار، فعند كل استدعاء نحصل على قيمة جديدة، مثلاً كتابع يقيس معدل استهلاك المعالج، أو كتابع يحفظ آخر قيمة تم اختزانها في الذاكرة..... وأشهر هذه التوابع وأكثرها استخداماً في مسألتنا هو تابع الوقت time الذي يرد قيمة صحيحة تعبر عن الوقت بالثواني، وهذا التابع موجود في المكتبة ctime فعلينا تضمينها لكي نتمكن من استخدامه، وهكذا يكون تعديل البرنامج السابق لتوليد قيمة عشوائية متغايرة عند كل تشغيل كالتالي :

الشرح الكود
  • النتائج ستكون متغايرة في كل دورة من دورات الحلقة، وستكون متغايرة عند كل تشغيل للبرنامج أيضاً.
  • لاحظ أنه لا حاجة لاستدعاء التابع srand أكثر من مرة (ولكن ذلك ليس خطأ، بل ربما يزيد من العشوائية)
  • لاحظ أننا مررنا 0 للتابع time، في الواقع هذا ليس عددا بل هو المؤشر Null حيث أن التابع يستقبل مؤشراً، ويفضل دائماً تمرير 0.
#include <iostream.h>
#include <cstdlib>
#include <ctime>
 
void main() {
	srand(time(0));
	for (int i=0;i<10;i++)
		cout << rand() << '\n';
}

مشكلة أخرى!، التابع rand يرد قيمة بين 0 و RAND_MAX وأنا أريد قيمة بين 0 و int x مثلاً، فكيف أحصل على ذلك؟

نحصل على ذلك بواسطة باقي القسمة، فنحن نعلم أن a % b هو حتماً عدد بين 0 و b-1، وعلى ذلك إذا أردنا قيمة بين 0 و x نكتب

 rand()%(x+1)

أي إذا أردنا عدداً ضمن مجال معين [0..X]، فإننا نأخذ باقي القسمة على عرض ذلك المجال (x+1).

ماذا لو أردت قيمة بين int y و int x؟ الحل : هنا اختلفت القيمة التي يبدأ منها المجال وأصبحت y بدلاً من 0، ولذلك نجمع للناتج الكلي قيمة البداية، ونعدل بعد المجال الذي أصبح (x+1-y)، أي:

 y + rand()%(x+1-y)

والقاعدة العامة للحصول على عدد ضمن مجال معين : RangeStart + rand()%RangeWidth

حيث RangeStart بداية المجال و RangeWidth هو عرض هذا المجال.


خصائص جديدة للمتحولات

كما رأينا فإن للمتحول اسم ونمط و....، والآن سنتعرف على خصائص جديدة للمتحولات:

  1. نمط التخزين (storage class) : يحدد كم سيبقى المتحول في الذاكرة (كم سيبقى حياً).
  2. الارتباط (Linkage) : يحدد في أي ملفات البرنامج يمكن استخدام المتحول (في البرامج متعددة الملفات).
  3. المجال (Scope) : وهو المكان في البرنامج حيث يمكن أن يستخدم المتحول.

في لغة C++ أربع كلمات محجوزة تحدد كل منها خصائص معينة للمتحول، ولا يمكن استخدام كلمتين منها معاً لمتحول واحد بل كلمة واحدة فقط، وتوضع هذه الكلمات قبل نمط المعطيات:

auto

المتحول الذي يحمل هذه الخاصية يتم إنشاؤه فور الدخول إلى الـ Block المعرف فيه ويتم إلغاؤه فور الخروج من هذا الـ Block.

الشرح الكود

مثال خاطئ، لا يمكن لمتحول أن يحمل الخاصية auto ما لم يكن موجوداً داخل تابع (أو Block)

#include <iotream.h>
auto int j = 4;
void main()
{ cout << "Hello, World!\n"; }

لا يوجد أي فرق في الخصائص بين المتحولين i و j المعرفين في المثال المجاور، لأن المتحول المعرف داخل تابع يحمل الخاصية auto تلقائياً ما لم يحمل خاصية أخرى (من الخواص الثلاث التالية). وهذا سبب ندرة استخدام الكلمة auto (فهي الوضع الافتراضي)

#include <iotream.h>
void main()
{
	int i = 4;
	auto int j = 5;
	cout << "Hello, World!\n";
}

register

المتحول الذي يحمل هذه الخاصية يوضع في مكان ذي سرعة عالية في الذاكرة (في الـ Cache وريجسترات المعالج)، بحيث يمكن التعامل معه بسرعة كبيرة. لا يمكن الإكثار من المتحولات التي تحمل هذه الخاصية، وإلا فسيقوم المترجم بإهمالها ويحجز مكان المتحول في الذاكرة RAM (أي ليس في مكان ذي سرعة عالية بل بمكان ذي سرعة عادية)، ذلك لأن الأماكن ذات السرعة العالية قليلة جداً.

الشرح الكود

مثال خاطئ، لا يمكن لمتحول أن يحمل الخاصية register ما لم يكن موجوداً داخل تابع (أو Block)

#include <iotream.h>
register i = 4;
void main()
{ cout << "Hello, World!\n"; }

ينصح بهذه الخاصية لمتحولات الحلقات، بسبب كثرة استخدامها، فيصبح الوصول إليها أسرع وبالتالي اختبار شروط الحلقات وتنفيذها أسرع.

#include <iotream.h>
void main()
{ 
for (register int i=0;i<10;i++)
	cout << "register value is "
		<< i << '\n';
}

static

المتحول الذي يحمل هذه الخاصية يتم إنشاؤه مرة واحدة فقط عند أول دخول إلى تابعه (أو الـ Block المعرف فيه)، ويبقى ما بقي البرنامج، ويحافظ على قيمته بين استدعاءات التابع المعرف فيه.

الشرح الكود

يمكن أن يعرف خارج التوابع (أي يمكن أن يكون متحول Global )

#include <iotream.h>
static int i = 5;
void main()
{ cout << "hello, world\n"; }

الخرج:

5
10
15
Press any key to continue

الشرح:

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

هذه الخاصية مهمة جداً وخاصة في التوابع العودية.

#include <iostream.h>
void TestStatic()
{
	static int i = 5;
	cout << i << '\n';
	i = i+5;
}
void main()
{
	TestStatic();
	TestStatic();
	TestStatic();
}

خطأ فادح، رغم أن المتحول يبقى موجوداً في الذاكرة منذ تعريفه وحتى نهاية تنفيذ البرنامج، إلا أنه لا يمكن استخدامه إلا في المجال الذي هو معرف فيه، وهنا هو معرف داخل تابع، إذاً لا يمكن استخدامه إلا ضمن هذا التابع (رغم أنه لا يزال في الذاكرة)، أي أنه مرئي فقط داخل هذا التابع، أما خارجه فهو غير معروف.

#include <iostream.h>
void TestStatic() {
	static int i = 5;
	cout << i << '\n';
	i = i + 5; }
void main() {
	TestStatic();
	cout << i << '\n';
	TestStatic(); }

extern

يمكن للمتحولات الـ global فقط أن تحمل هذه الخاصية، فكما نعلم المتحول الـ global هو متحول يتم تعريفه في بداية الملف خارج التوابع ويكون معروفاً (مرئياً) لجميع التوابع، ولكن ليس مرئياً للملفات الأخرى في البرنامج. فإن عرفناه في الملفات الأخرى وأضفنا له الخاصية extern فسوف نتمكن من استخدامه فيها، كما في المثال التالي:

File1 File2
#include <iostream.h>
int i = 5;
#include <iostream.h>
extern int i;
void main()
{ cout<< "I = " <<i<<'\n'; }

الخرج:

I = 5
Press any key to continue

مجالات الرؤية Scopes

مجال الرؤية لمتحول هو مجموعة التعليمات التي يكون فيها هذا المتحول قابلا للاستخدام، ويوجد في لغة C++ نوعان لمجالات الرؤية:

  • Block or Local Scope : وهو عبارة عن مجموعة من التعليمات المحصورة ضمن قوسين كبيرين من الشكل { }، كما في جسم التابع وأجسام الحلقات وتعليمات if \ else.
  • Global or File Scope : وهو كل شيء موجود في الملف.

قواعد

  • لا يمكن استخدام المتحول قبل تعريفه ضمن نفس المجال.
  • المتحول الـ Global مرئي وقابل للاستخدام في كل التوابع التي بعده في الملف، وفترة حياته هي طوال تنفيذ البرنامج، ومن ميزاته أنه يحسن أداء البرنامج ويقلل من الوسطاء الممررة للتوابع، ولكنه يجعل التعامل مع البرنامج أصعب وغير مضمون، لذلك ينصح بتجنب هذا النوع من المتحولات.
  • المتحول الـ Local مرئي وقابل للاستخدام من قبل التعليمات والمجالات المحتواة في مجاله ولكن بعد مكان تعريفه، وفترة حياته منذ الدخول إلى الـ Block وحتى الخروج منه (هذا إذا كان auto أو register).
الشرح الكود

خطأ، لا يمكن استخدام المتحول قبل تعريفه حتى ولو كان الاستخدام في نفس المجال.

#include <iostream.h>
void main() {
	cout << i;
	int i = 6; }

الخرج:

Scope1: i= 5
Scope2: i= 8
Scope3: i= 7
Press any key to continue

الشرح:

  • يمكن تعريف متحولات بأسماء متماثلة في كل من المجالات المتداخلة.
  • وتكون الأسبقية في كل مجال للمتحول المعرف فيه، فإن لم يوجد فالمتحول الذي في المجال الأعلى بدرجة وهكذا...... كما في المثال التالي :
#include <iostream.h>
void main()
{
	int i = 5;
	cout<<"Scope1: i= "<<i<<'\n';
	{
		int i = 8;
		cout<<"Scope2: i= "<<i<<'\n';
		{
			int i = 7;
			cout<<"Scope3: i= "<<i<<'\n';
}	}	}

الخرج:

5
5
10
10
Press any key to continue
#include <iostream.h>
void main()
{	int i = 5;
	cout << i << '\n';
	{
		cout << i << '\n';
		{	int i = 10;
			cout << i << '\n';
			{
				cout << i << '\n';
}	}	}	}

الخرج:

local  i= 6
Global i= 4
Press any key to continue

الشرح: يمكن من أي مجال local الوصول إلى المتحول الـ Global (حتى بوجود متحول محلي بنفس الأسم) بواسطة العملية ::

#include <iostream.h>
int i = 4;
void main()
{
	int i = 6;
	cout << "local  i= "<< i<<'\n';
	cout << "Global i= "<< ::i<<'\n'; 
}

التوابع العودية (التراجعية) Recursive functions

  • كما نعلم فإن التابع العودي هو التابع الذي يحوي على استدعائه ضمن تعليماته.
  • لحوي جسم التابع العودي على الحالة العامة التي يتم فيها الاستدعاء العودي، وعلى شرط التوقف الذي يتم عنده توقف الاستدعاء العودي وإلا استمر إلى اللانهاية.
  • لكل استدعاء وسطاؤه ومتحولاته الخاصة به، ويتم إنشاؤهم عند استدعائه حيث يتوقف تنفيذ النسخة الحالية من التابع وينتقل التنفيذ إلى النسخة الجديدة، وهكذا تتراكم النسخ في الذاكرة حتى يتم الدخول إلى شرط التوقف، فعندها يرد التابع الذي تم خرق الشرط فيه قيمته ويسلم التنفيذ إلى النسخة السابقة له والتي استدعته، والتي بدورها ترد قيمتها وتسلم التنفيذ لمن قبلها وهكذا....
الشرح الكود

الخرج:

Call #0
Call #1
Call #2
Call #3
Call #4
Call #5
Press any key to continue
#include <iostream.h>
void Times(int i)
{	if (i>0)
		Times(i-1);
	cout <<"Call #"<<i<<'\n'; 
}
void main()
{ Times(5); }

الخرج:

Sum 10 = 55
Press any key to continue
#include <iostream.h>
long Sum(int i)
{	if (i>0)
		return i+Sum(i-1);
	else return i; }
void main()
{ cout << "Sum 10 = "<< Sum(10) 
	  << '\n';  }
long Factor(int i) //iteration
{
	int result = 1;
	for (;i>0;i--)
		result*=i;
	return result; }
long Factor(int i) //recursion
{
	if ((i==0)||(i==1))
		return 1;
	else return i*Factor(i-1);
}

نلاحظ في المثال الأخير نسختين للتابع Factor أحدهما يستخدم الطريقة العودية والآخر التكرارية، في العودية يتم انتهاء الدوران بواسطة شرط التوقف، أما في التكرار باختلال شرط الحلقة، وهنا يطرح السؤال : أيهما أفضل، العودية أم التكرارية؟ في الواقع لا يمكن الإجابة على هذا السؤال، ولكن يمكن وضع النقاط التالية في عين الاعتبار:

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

Function Inline

يمكن إضافة الخاصية inline إلى التوابع، والتابع الذي يحمل هذه الخاصية يتم نسخه ووضعه مكان الاستدعاء، وبهذا يتم التخفيف من كلف الاستدعاء (إذ أن استدعاء التابع له كلفة)، ويتم النسخ عند عملية الـ build (أي عند تحويل الملف إلى object)، وهو جيد للتوابع كثيرة الاستخدام، ولكن يجب عدم الإكثار منه حتى لا يصبح حجم الملف ضخماً جدا جراء النسخ، علماً بأن المترجم يمكن أن يهملها عند الإكثار منها.

مثال:

#include <iostream.h>
inline double Square(double n);
void main()
{	int j = 5;
	cout << Square(j) << '\n'; }
inline double Square(double n)
{ return n*n; }

التمرير بالقيمة (Call by Value) والتمرير بالعنوان (Call By Reference)

يتم تمرير المتحولات إلى الوسطاء عند استدعاء التوابع بأحد شكلين، وفي الجدول التالي شرح لهما:

Call by Value (التمرير بالقيمة) Call by Reference (التمرير بالعنوان)
FuncType FuncName(Type Name);
FuncType FuncName(Type &Name);

يتم إنشاء متحول جديد (حجز مكان جديد في الذاكرة) له اسم الوسيط ووضع قيمة المتحول الخارجي الممررة فيه.

يتم إعطاء اسم جديد لنفس المتحول، فيتم التحكم بنفس خلية الذاكرة الخاصة بالمتحول الخارجي ولكن باسم مستعار.

تجري التغييرات على المتحول المحلي الجديد ولا يتأثر المتحول الأصلي الخارجي على الإطلاق

تجري التغييرات على المتحول الأصلي (تتغير قيمته)

ميزاته : الحماية من التأثيرات الجانبية المحتملة

ميزاته : لا يحجز أماكن جديدة بالذاكرة (توفير ذاكرة)

مثال للمقارنة:

#include <iostream.h>
double Square(int i);
void main()
{int n = 5;
cout << "before calling, n = " << 	n <<'\n';
cout << "Square = " << Square(n) <<'\n';
cout << "After calling, n = "<< n 	<<'\n'; }
double Square(int i) {
	i = i*i;
	return i; }
#include <iostream.h>
double Square(int &i);
void main()
{int n = 5;
cout << "before calling, n = " << 	n <<'\n';
cout << "Square = " << Square(n) 	<<'\n';
cout << "After calling, n = "<< n 	<<'\n'; }
double Square(int &i) {
	i = i*i;
	return i; }

الخرج:

before calling, n = 5
Square = 25
After calling, n = 5	//Here
Press any key to continue
before calling, n = 5
Square = 25
After calling, n = 25	//Here
Press any key to continue

القيمة الافتراضية للوسيط Default Argument

نعلم أنه عند استدعاء التابع فلا بد من تمرير عدد من القيم أو المتحولات مساوٍ لعدد وسطاء التابع، ولكن تتيح لغة C++ ميزة إعطاء قيمة افتراضية للوسيط، ويمكن لهذه القيمة أن تكون ثابتاً أو متحولاً Global أو استدعاء تابع. وعند تمرير عدد أقل من القيم، فإن القيم تذهب إلى المتحولات الأولى بالترتيب، فيما يأخذ المتحولات البقية قيمها الافتراضية.

الشرح الكود

خطأ، لا يمكن إعطاء القيم الافتراضية في تصريح وتعريف التابع معاً، بل في أحدهما، يجب إما أن نزيل القيم الافتراضية من الترويسة في التعريف (وهذا هو الأفضل –وضع القيم الافتراضية في التصريح-)، أو أن نزيلها من التصريح، ولكن هنا لا يمكن استدعاء التابع في التوابع التي قبله إلا مع تمرير عدد من الوسطاء مماثل لعددها في التصريح.

#include <iostream.h>
double Power(int = 1,int = 2);
void main()
{ cout << Power(); }
double Power(int x=1,int n=2)
{	double result = 1;
	for (int i =0;i<n;i++)
		result *= x;
	return x; }

الخرج:

No  arguments, Power = 1
One argument,  Power = 16
Two argumnets, Power = 64
Press any key to continue

الشرح:

  • عند عدم وجود قيم، تأخذ الوسطاء قيمها الافتراضية.
  • عند وجود قيم أقل من الوسطاء، تذهب القيم الموجودة إلى الوسطاء الأولى (المقابلة لها بالترتيب)، والوسطاء البقية تأخذ قيمها الافتراضية (هنا في الاستدعاء الثاني أخذ x القيمة 4 بينما أخذ n قيمته الافتراضية).
  • عند وجود قيم بعدد الوسطاء، يأخذ كل وسيط القيمة المقابلة له بالترتيب.
#include <iostream.h>
double Power(int = 1, int = 2);
void main()
{	cout <<  "No  arguments, 		Power = " << Power();
	cout <<"\nOne argument,  		Power = " << Power(4);
	cout <<"\nTwo argumnets, 		Power = " << Power(4,3) 	<<'\n'; }
double Power(int x ,int n)
{	int result = 1;
	for(int i=0;i<n;i++)
		result *= x;
	return result; }

يمكن أن يكون لجميع الوسطاء قيم افتراضية (Test0) أو لا يكون لهم جميعاً قيم افتراضية (Test3)، كما يمكن أن يكون بعضهم له وبعضهم ليس له، ولكن في هذه الحالة يجب أن تكون الوسطاء التي ليس لها قيم افتراضية قبل الوسطاء التي لها قيم افتراضية (Test2)، وإلا يكون التعريف أو التصريح خاطئاً (Test1) ويظهر Compiler error المجاور:

الكود:

double Test0(int i =1, int j =2);
double Test1(int i =1, int j);
double Test2(int i, int j =2);
double Test3(int i, int j);

الخطأ:

error C2548: 'Test1' : missing default parameter for parameter 2

النسخ المتعددة للتابع (Function Ovderloading)

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

الشرح الكود

خطأ، لا يمكن أن تختلف النسخ بالنمط الذي ترده فقط، بل يجب أن تختلف بقائمة الوسطاء (بالبصمة)

double Square(double i);
float Square(double i);
int Square(double i);

الخرج:

Float Square = 9
Double Square = 9
Integer Square = 9
Float Square = 1
Press any key to continue

الشرح:

لاحظ أن النسخ الثلاث لها نفس الاسم، ولكنها تختلف بقائمة الوسطاء. عند استدعاء التابع، يتم اختيار النسخة المناسبة بحسب نمط القيم الممررة وعددها وترتيبها، في المثال المجاور، تم اختيار النسخة الأولى التي تستقبل وسيطاً من نمط float عندما تم تمرير متحول من النمط float، وتم اختيار النسخة التي تستقبل وسيطًاً من النمط int عند تمرير متحول من النمط int، وتم اختيار النسخة التي تستقبل وسيطاً من النمط double عندما تم تمرير متحول من النمط double، وعند عدم تمرير أي قيمة، تم اختيار النسخة الوحيدة التي يمكن ألا تأخذ أي قيمة.

#include <iostream.h>
double Square(double);
double Square(int);
float Square(float = 1);
void main()
{
	int i = 3; double d = 3;
	float f = 3;
	cout << Square(f)<<'\n';
	cout << Square(d)<<'\n';
	cout << Square(i)<<'\n';
	cout << Square() <<'\n';
}
double Square(double i)
{	cout << "Double Square = ";
	return i*i; }
double Square(int i)
{	cout << "Integer Square = ";
	return i*i; }
float Square(float i)
{	cout << "Float Square = ";
	return i*i; }

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

//السابق المثال في التعليمات نفس
double Square(int = 1);
float Square(float = 1);
void main()
{//السابق المثال في التعليمات نفس
	cout << Square() <<'\n'; }
//السابق المثال في التعليمات نفس

الخرج:

Test(int,double)
Test(double,int)
Press any key to continue

الشرح:

لاحظ أن الاستدعاء يختلف باختلاف ترتيب المتحولات (أو القيم) الممررة له، يختلف الاستدعاء باختلاف أي شي متعلق بالبصمة.

#include <iostream.h>
void Test(int,double);
void Test(double,int);
void main()
{
	int i = 4; double d = 5;
	Test(i,d);
	Test(d,i);
}
void Test(int i,double j)
{ cout << "Test(int,double)\n"; }
void Test(double i,int j)
{ cout << "Test(double,int)\n"; }
  • وظيفة: جرب أن تستدعي وتمرر قيماً (أو متحولات) من أنماط غير الأنماط الموجودة في النسخ.

طرق البرمجة الشاملة Methods for Generic Programming

يمكن بواسطة البرمجة الشاملة إعادة استخدام الكود بدون إعادة كتابته أو نسخه، ويوجد طريقتان لذلك:


Macro

  • هي عبارات يقوم بتنفيذها الـ Pre-Processor بدلاً من الـ Compiler
  • يقوم الـ Pre-Processor باستبدال كل استدعاء بالعبارات.
double min(double a,double b)
{ return (a<b)?a:b; }
int min(int a,int b)
{ return (a<b)?a:b; }

يمكن استبدال التوابع المجاورة بالـ Macro التالي :

#define min(a,b) ((a<b)?a:b)
لماذا لا نستخدم الـ Macro كثيراً ولا ينصح باستخدامه؟
  • لأنه محدود الاستخدام (لا يمكن وضع كود أي تابع فيه)
  • لأنه يمكن أن يؤدي لنتائج غير متوقعة، إذ أن تنفيذه ليس كتنفيذ أي تابع، فهو يقوم بحساب وتنفيذ التعبير القادم إليه كل مرة، ويغير المتحولات التي يستقبلها، كما في المثال التالي:
الكود
#include <iostream.h>
#define min(a,b) ((a<b)?a:b)
void main()
{	int x=5,y=10;
	for (int i=0;i<3;i++)
	{	cout << "Round #" << i 			<< " : \n";
		Cout << "PreCall : ";
		cout << "x = " << x;
		cout << " ,y = "				<< y << '\n';
		cout << "Min = "				<< min(++x,--y);
		cout << '\n';
		cout << "AfterCall : ";
		cout << "x = " << x;
		cout << " ,y = " << y << 			"\n\n";
}	}
الخرج

Round #1 : PreCall : x = 5 ,y = 10 Min = 7 AfterCall : x = 7 ,y = 9

Round #2 : PreCall : x = 7 ,y = 9 Min = 7 AfterCall : x = 8 ,y = 7

Round #3 : PreCall : x = 8 ,y = 7 Min = 5 AfterCall : x = 9 ,y = 5 Press any key to continue


لماذا هذه النتائج الغريبة؟ الطريقة الوحيدة لفهم ذلك هو بواسطة الـ Debug، لذلك انسخ الكود ونفذه خطوة خطوة.


القوالب Templates

  • تعتبر القوالب Templates الوسيلة الأفضل للبرمجة الشاملة، يمكن من خلالها تعريف بنى معطيات وتوابع بدون عناء، فهي وسيلة موجزة ومحكمة لتعريف نسخ متعددة لتابع واحد.
  • تعتمد القوالب على الأنماط المستعارة، حيث نعرف نمطاً (أو نوعاً Class) أو أكثر في البداية، ثم نكتب التابع مستخدمين متحولات ووسطاء ونمط يرده التابع من تلك الأنماط المستعارة.
  • تحدد الأنماط المستعارة من خلال أنماط الوسطاء (ثوابت أو توابع أو متحولات) الممررة للتوابع، بحيث تطابق مع هذه الأنماط.
الشرح الكود

يتم تعريف القالب بالكلمة template وبعدها الأسماء المستعارة للأنماط المستخدمة داخل قوسين < > ، وقبل كل منها الكلمة typename أو الكلمة class.

template <typename X, class Y, typename Z>
//typename can be replaced with "class"
Z TemplateFunc(X a, Y b, Z c)
{	//statements; 
}

خطأ، يجب وضع كلمة typename أو class قبل كل اسم مستعار، هنا كتبنا Z بدون أن نضع إحداهما قبلها.

template <typename X,class Y, Z>
Z templatefunc(X a, Y b, Z c)
{ // statements
}

خطأ، لا يمكن أن نعرف نمطاً مستعاراً ولا نستخدمه كمنط لأحد الوسطاء، هنا عرفنا z ولم نستخدمه كنمط لأحد الوسطاء.

template <typename X,class Y,class Z>
Z templatefunc(X a, Y b)
{ // statements
}

إذا أردنا للتابع أن يحمل الخاصية inline نضع هذه الكلمة المحجوزة قبل نمط التابع بالضبط وليس قبل الكلمة Template

template <typename X, class Y>
inline X Func(X a, Y b)
//true is above, and not the following : 
inline template <typename X, class Y>
X Func(X a, Y b)

الخرج:

11      34
34      11
4.5     84.05
84.05   4.5
Press any key to continue

الشرح:

عند الاستدعاء Swap(i,j) تم إنشاء نسخة من القالب Swap واستبدلت كل T بـ int، وكذلك عند الاستدعاء Swap(d,q) تم إنشاء نسخة واستبدلت كل T بـ double.

#include <iostream.h>
template <typename T>
void Swap(T &a, T &b)
{	T temp = a;
	a = b;
	b = temp; }
void main()
{	int i,j;
	cin >> i >> j;
	Swap(i,j);
	cout<<i<<'\t'<<j<<'\n';
	double d, q;
	cin >> d >> q;
	Swap(d,q);
	cout<<d<<'\t'<<q<<'\n'; }