C++ In a Nutshell



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

الامر الذي اود تبشيركم به ، هو ان Qt تدعم المؤشرات ، كما في سي++ ، لكنها أضافت محرر للنفايات Garbage Collector يقوم بتحرير اي ذاكرة لكائن ابن ، في حالة تم حذف الكائن الاب لهذا الابن ، وذلك لانها تستخدم علاقة Parent-Child Relationship بين كائناتها

لاتنزعج !! فـ Qt تعطيك الحرية الكاملة في تحرير ذاكراتك ، سواءا كانت لكائن ابن او كائن اب ، كما في سي++ القياسية ، ولكنها فقط ستحرر ذاكرتك في حالة لم تحررها بنفسك .

###################

المتغيرات على المكدس Stack ، والمتغيرات على الـ Heap او المخزن الحر Free Space :

كما نعلم مسبقا من دراستنا لسي++ ، انه فقط يوجد طريقتين لحجز المتغيرات .

الطريقة الاولى هي الطريقة هي الاكثر استخداما ، فقط نعلن عن نوع المتغير ، ثم نكتب اسم المتغير.
مثلا :

  1.  
  2. int x;
  3. Car bmw;
  4. Vector v[5];


في الامثلة السابقة ، كل ما حدث هو ذهاب المترجم الى الذاكرة وحجز مساحة كافية للمتغير ، وذلك وقت ترجمة البرنامج ، اي قبل التشغيل .

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

اما الطريقة الثانية Dynamic Allocation ، والتي تتطلب تعاملا مع المؤشرات ، حيث يجب حجز كمية كافية من الذاكرة للمتغير 'تسمى هذه الذاكرة Heap ' و بعد الانتهاء من استخدام هذه الذاكرة يجب ان نحرر هذه الذاكرة بأنفسنا ، والا وقعنا في فخ تسرب الذاكرة Memory Leak !

امثلة توضيحية //
  1.  
  2. int* x=new int;
  3. Car* bmw=new Car;
  4. Vector* v=new Vector[5];


الفرق بين هذه الطريقة والطريقة الاولى ، هو ان الذاكرة في الطريقة الثانية Dynamic Allocation تحجز في وقت تنفيذ البرنامج، وبالتحديد عند الوصول الى الامر new.

الفرق الاخر والاهم ، هو انه في الحجز الديناميكي 'الطريقة الثانية' ، فان المتغير لن يحذف من الذاكرة حتى ولو خرج عن مداه ، الا عند التحرير باستخدام العبارة delete ، او الانتظار الى انتهاء البرنامج حيث يتم حذف جميع المتغيرات عند انتهاء عمل البرنامج ، وهذه الاخيرة قد تسبب تسرب للذاكرة !

امثلة لحذف المتغير :
  1. delete x;
  2. delete bmw;
  3. delete [] v;


##################

تسرب الذاكرة ، تسرب الذاكرة ! ما هو تسرب الذاكرة Memory Leak؟

هو تعبير عن مشكلة ، كان السبب فيها حجز احد المتغيرات على ال Heap 'اي حجز ديناميكي' ولم يتم تحرير الذاكرة ، ولا يوجد طريقة لتحريرها !

مثال بسيط يوضح تسربا للذاكرة :
  1.  
  2. int* p=new int;
  3. int* q=new int;
  4.  
  5. p=q;
  6.  
  7. // how I can delete the portion of Heap memory that allocated for p !


الان لا يوجد اي طريقة لتحرير الذاكرة الاولى ، لان المؤشران p,q يشيران الى الذاكرة الاخرى.
هذه الحالة تسمى تسرب للذاكرة ، وهذه ليست الحالة الوحيدة التي يحدث فيها تسرب للذاكرة ،
وانما توجد العديد من الحالات ، والقاسم المشترك فيها 'عدم القدرة على تحرير الذاكرة'.

###################

الفئات Classes :

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

هذ التعريف هو التعريف الشائع ، لكني لا احبذه تماما .
لذلك انظروا الى هذا التعريف :

الفئة class هي طريقة لانشاء انواع بيانات جديدة .
سي++ كاي لغة على وجه الارض ، تأتي مع انواع بيانات اساسية : عدد صحيح ، حرف ،...الخ.

الان لانشاء نوع بيانات جديد ، مثلا : Complex Number يتعامل مع الاعداد المركبة .
كل ما عليك كمبرمج هو كتابة :
  1.  
  2. class ComplexNumber
  3. {
  4. public:
  5. ComplexNumber();
  6. ComplexNumber(int,int);
  7.  
  8. double realPart()const;
  9. void setRealPart(int);
  10.  
  11. double imaginaryPart()const;
  12. void setImaginaryPart(int);
  13.  
  14. ComplexNumber operator+ (ComplexNumber& rhs);
  15.  
  16. ... //etc.
  17.  
  18. private:
  19. double x;
  20. double y;
  21. };


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

الان لو قمنا بكتابة العديد من الفئات ، ومزجناها باسلوب التوارث وتعدد الاوجه 'سنأتي عليه لاحقا'
سنحصل على لغة جديدة ! بها انواع بيانات جديدة ، لكنها تعتمد في الاخر على سي++.

وهذه احدى ميزات لغة سي++ ، انها لغة قابلة للتوسعة Extendability .
استفادت من هذه الخاصية مكتبة Qt ،حيث وسعت لغة سي++ في حدود اطار لغة سي++ :).

###################

التوارث الاحادي والتوارث المتعدد Single and Multiple Inheritance :

مفهوم التوارث بالرغم من بساطته الا انه يحل العديد من المشاكل ويجلب الكثير من المشاكل !
معادلة غير مفهومة ! طيب نوضح اكثر ..

التوارث يحل المشاكل التي يجب ان نستخدم فيها مفهوم التوارث ، مثلا علاقة الطالب بالانسان !
فالطالب هو انسان ، لذلك برث خصائص الانسان ، ثم يزيد عليها خصائص الطالب 'الرقم الاكاديمي ، التخصص ،....الخ.
في هذا النوع من المشاكل البرمجية ، فان التوارث يحل مشكلة كبيرة الا وهي كتابة شفرة تم كتابتها من قبل!

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

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

الحل لهذه المشكلة هو علاقة احتواء
جهاز الحاسوب يحوي معالج ، ويحوي ذاكرة و ....الخ.

وستكون الفئة التي تمثل الحاسوب بهذا الشكل :

  1. class Computer
  2. {
  3. public:
  4. Computer();
  5. //...etc
  6.  
  7. private:
  8. CPU* intel;
  9. Memory* ram;
  10. HD* myHardDisk;
  11. //...etc
  12. };



ويوجد نوع اخر من العلاقات اقوى من علاقة الاحتواء is Contain ، وهو علاقة has -a . ولكنها خارج اطار الموضوع.

لكن للاستزادة حول هذه المواضيع :
Applying UML:An Object-Oriented Analysis and Design 3rd Edition
Object Oriented Analysis and Design with Application 3rd Edition
Design Pattern for Dummies

###################

التوارث العام والتوراث الخاص :

يوجد نوعين من التوارث ، الاول وهو المستخدم بكثرة ، التوارث العام .
وفيه تكون كائنات الفئة المشتقة Derived Class Objects ، قادرة للوصول الى اي دوال موجودة سواءا داخل الفئة الرئيسية Base Class ، ام الفئة التي ينتمي اليها .

وطريقة الاعلان عنها كالاتي :
  1.  
  2. class Student::public Human
  3. {
  4. };


اما النوع الاخر ، التوارث الخاص Private Inheritance ، فمن خلاله تستطيع كائنات الفئة المشتقة الوصول الى الدوال الموجودة فقط داخل الفئة المشتقة .

مثلا:

  1. class Student::private Human
  2. {
  3. };


لو فرضنا ان الفئة Human تحوي دالة تسمى sleep ، هذه الدالة لم يتم تجاوزها Override it في الفئة المشتقة ، في هذه الحالة لن تستطيع استخدامها .

###################

التوارث المتعدد :

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

###################

تعدد الاوجه Polymorphism :

هذا المفهوم من اهم مفاهيم ال Object-Oriented Programming ، يفسره بعض المبرمجين بشئ والبعض الاخر بشئ مختلف تماما ..

في رأيي التفسير الاول لتعدد الاوجه ليس صحيحا تماما، فالاصح هو Static Polymorphism

الرأي الاول :: هو ان تأخذ الدالة عدة اشكال .كيف !
عن طريق اعادة تعريف الدالة Function Overloading .

مثلا لدي دالة رسم تسمى draw ، هذه الدالة تأخذ مرة مستطيل ومرة اخرى دائرة ، مثلث ..وهكذا.

  1. void draw(Rectangle r);
  2. void draw(Circle c);
  3. void draw(Triangle t);


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

هذا هو الـ Static Polymorphism .


نأتي الى الرأي الاخر ::
التعدد الشكلي Dynamic Polymorphism هو القدرة على ربط كائن من فئة رئيسية Base Class بكائن من فئة مشتقة Derived Class وقت تنفيذ البرنامج، عندها سيأخذ الكائن من الفئة الرئيسية عدة اشكال !!

تعريف غير قابل للهضم تماما لكنه صحيح 100% ، نوضحه بمثال ::

  1.  
  2. #include <iostream>
  3. using namespace std;
  4.  
  5. class Shape
  6. {
  7. public:
  8. virtual void draw()=0;
  9. };
  10.  
  11. class Rectangle:public Shape
  12. {
  13. public:
  14. void draw(){cout<<'Rectangle Drawn';}
  15.  
  16. private:
  17. int height;
  18. int width;
  19. };
  20.  
  21. class Circle:public Shape
  22. {
  23. public:
  24. void draw(){cout<<'Circle Drawn';};
  25. private:
  26. int radius;
  27. };
  28.  
  29. int main()
  30. {
  31. Shape* shape;
  32.  
  33. cout<<'n1- Rectangle';
  34. cout<<'n2- Circle';
  35.  
  36. int choice;
  37. cin>>choice;
  38.  
  39. if(choice == 1)
  40. shape=new Rectangle;
  41. else
  42. shape=new Circle;
  43.  
  44. shape->draw();
  45. return 0;
  46. }
  47.  


تحليل البرنامج :
لدينا فئة رئيسية مجردة وهي Shape ، وهي تمثل شكل غير معروف ، لذلك لا يمكنك انشاء كائن منها مباشرة .

الفئتان الاخرتان هما Rectangle و Circle ، وهما موروثتان من الفئة Shape ، ويجب عليهما اعادة تعريف الدالة draw.

ناتي الى الدالة main ، نلاحظ ان هناك اختيار رقم من اثنين ، في حالة اختيار 1 فان الشكل سيصبح مستطيل :

  1. shape=new Rectangle;


وفي الحالة الثانية فان الشكل سيصبح دائرة .
  1. shape=new Circle;


الان انظر الى تعدد الشكل هنا :
  1. shape->draw();


فقبل تنفيذ البرنامج ، لا نستطيع معرفة النتيجة من استدعاء الدالة draw ، وكذلك اثناء البدء في التنفيذ لا نستطيع معرفة النتيجة ، الا عند الاختيار .
وهذا هو جوهر التعدد الشكلي .

###################

عزل الواجهة عن التطبيق Separate Interface from Implementation:

لا تكاد تخلو برامج سي++ من فئة او عدة فئات ، مع شوية توارث على Polymorphism ، والا لاصبحت لغة سي++ نسخة طبق الاصل من لغة سي ، مع اختلاف دوال الادخال والاخراج فقط !

لذلك لا بد من طريقة موحدة لكتابة هذه البرامج بحيث تراعي مستخدمي الفئات .
نعم ! كاتب الفئة يختلف عن مستخدم الفئة 'في اغلب الاحوال ' .

فكاتب الفئة هو من كتبها وهو من كتب تطبيق الدوال Function Implementation .
اما مستخدم الفئة فهو من ينشيء كائن من الفئة ويستخدم الدوال ، بدون معرفة كيفية برمجتها .

لذا على كاتب الفئة ان يكتب تعريف الفئة في ملف منفصل ، والتطبيق في ملف اخر .
نأخذ مثال للفئة String .

ونفرض انك 'القارئ' من كتبت الفئة ، وانا مستخدمها .

نبدأ بك اولا ::
عليك ان تكتب تعريف الدالة في ملف منفصل ، مثلا string.h “لاحظ مطابقة اسم الملف مع اسم الفئة وهو امر متعارف عليه ، لكنه غير ضروري' بحيث تكون محتوياته :


  1.  
  2. #ifndef STRING_H
  3. #define STRING_H
  4.  
  5. class String
  6. {
  7. private:
  8. char* m_string;
  9.  
  10. public:
  11. String();
  12. String(char*);
  13. String(const String&);
  14.  
  15. void setString(char*);
  16. String string();
  17.  
  18. String operator+ (const String&);
  19.  
  20. //...etc
  21. };
  22.  
  23. #endif // STRING_H



ماذا فعلت انت حاليا ؟! كل ما فعلته انت هو كتابة الواجهة العامة للفئة فقط ، من غير اي تعريف لاي دالة !

لاني انا كمستخدم للفئة ، عند استخدامي للفئة String ، سارجع الى هذا الملف لاعرف جميع الدوال
التي ممكن استخدمها .

طيب ما فائدة هذه الاسطر :
  1. #ifndef STRING_H
  2. #define STRING_H
  3. ......
  4. #endif // STRING_H


هذه فقط لكي لا يتكرر تضمين الملف اكثر من مرة واحدة .
يعني انا كمستخدم الفئة ، مثلا لدي مشروع به عدة ملفات استخدمت الفئة String ، وحتى لا يصدر خطأ من المترجم مفاده تم تضمين الملف string.h اكثر من مرة ، فيجب عليك دائما استخدام هذه الاسطر .

السطر الاول يقول اذا لم يكن هذا المتغير معرفا 'لاحظ اسم المتغير هو نفسه اسم الفئة لكن بالاحرف الكبيرة ، وهذا ايضا امر متعارف عليه ، وتستطيع استخدام اي اسم مثلا EE “. فقم بتعريفه في السطر الثاني.

التنفيذ لن يصل الى السطر الثاني الا في حالة اذا كان الـ STRING_H غير معرف ، اما اذا كان معرف
فهذا دلالة على ان هذا الملف تم تضمينه في احد ملفات المشروع.

السطر الثاني هو لتعريف المتغير ، اما السطر الاخير فهو لانهاء جملة if .

الان ننتقل الى تعريف الفئة String ، والتي يجب ان تكون في ملف منفصل ايضا ، وذلك حتى يتم فصل الاعلان عن الفئة من تعريفها .

التعريف دائما يكون في ملف امتداده cpp ، وفي مثالنا هو string.cpp ، ومحتوياته هي :
  1.  
  2. #include 'string.h'
  3.  
  4. String::String()
  5. {}
  6.  
  7. String::String(char* str)
  8. {
  9. // definition goes here.
  10. }
  11.  
  12. void String::setString(char* str)
  13. {
  14. // definition goes here.
  15. }
  16.  
  17. ...// etc.


لاحظ انه يجب تضمين الملف string.h ، لانه هو الذي يحوي الاعلان عن الفئة .
وبعد ذلك يجب تعريف عمل كل دالة ، بالطريقة التي تحددها انت .

كل ما فعلته الى الان يسمى عزل الواجهة عن التطبيق Separate Interface from Implementation .
اي وضعت واجهت الفئة في ملف .h ، وتطبيق الفئة في ملف .cpp ، هذا الامر له عدة فوائد بالنسبة لك :
مثلا في حالة ما قررت عمل مكتبة للفئة ، سواءا ساكنة Static Lib ، او ديناميكية Dynamic Lib والتي تأخذ الامتداد .so في لينوكس او dll في ويندوز .

فان مشروعك سيكون به ملف string.h و string.dll او string.so ، وكذلك نشر الملف .cpp سيكون تحت ارادتك ، في حالة ما اردته مفتوح المصدر يجب عليك ارفاقه ، وفي حالة اذا اردته ملفا مغلقا فاحتفظ به في بيتك 'نظام ويندوز مثلا '

ميزة ملف .dll او .so عن ملف .cpp هو انه ملف مترجم مسبقا ، فقط كل ما علي هو ربط برنامجي معه.
كذلك في حالة تحديث المكتبة كل ما علي هو تحميل النسخة الحديثة من المكتبة واستبدالها مكان النسخة القديمة بدون حتى اعادة ترجمة برنامجي .

ثانيا :: بالنسبة لي كمستخدم للفئة :
افرض ان لدي ملف main.cpp واريد استخدام الفئة String ، والتي تفضلت انت بكتابتها ، كل ما علي
هو الاتي :

  1. #include <iostream>
  2. #include 'string.h'
  3.  
  4. using namespace std;
  5.  
  6. int main()
  7. {
  8. String str('Hello');
  9.  
  10. str.setString('C++');
  11.  
  12. str=str+' is a powerful Language';
  13.  
  14. //...etc
  15.  
  16. return 0;
  17. }



مع التأكد من ان الملف string.h موجود في نفس المجلد الذي يحوي المشروع ، و كذلك الامر بالنسبة للملف string.cpp .

طيب في حالة لم يكن هناك string.cpp فيجب ان يكون هناك مكتبة .so او .dll هذه المكتبة يجب ان تكون في نفس المجلد ، ويجب ربطها بالبرنامج بالامر :

  1. g++ main.cpp -lstring


ويوجد كذلك العديد من الاوامر الخاصة بالمترجم ، مثلا اذا كان الهيدر غير موجود في مجلد المشروع ، ووو...الخ . “راجع توثيق المترجم لمعرفة الاوامر '.

###################

ما الذي سنحتاجه في التعامل مع Qt ؟

اغلب التعامل هو :

انشاء المتغيرات ديناميكيا .
انشاء الفئات.
التوراث الاحادي والمتعدد.
عزل الواجهة عن التطبيق.


وان شاء الله سيتم اضافة بعض المواضيع الاخرى ..

دمتم بخير .



تمت طباعة الدرس من موقع مكتبة الدروس
http://qt-ar.org/lessons/show7.html